这个方案不使用复杂的向量化存储,而是采用简单的颜色直方图作为特征,适合小规模应用和入门学习。

这个方案不使用复杂的向量化存储,而是采用简单的颜色直方图作为特征,适合小规模应用和入门学习。

1. 数据库设计 (MySQL)

CREATE TABLE `image_features` (
  `id` int NOT NULL AUTO_INCREMENT,
  `image_name` varchar(255) NOT NULL,
  `file_path` varchar(512) NOT NULL,
  `red_histogram` varchar(255) NOT NULL,
  `green_histogram` varchar(255) NOT NULL,
  `blue_histogram` varchar(255) NOT NULL,
  `upload_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 2. Java实现代码

2.1 添加依赖 (pom.xml)

 <dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.0</version>
    </dependency>
    
    <!-- 数据库连接 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>
    
    <!-- 图片处理 -->
    <dependency>
        <groupId>org.imgscalr</groupId>
        <artifactId>imgscalr-lib</artifactId>
        <version>4.2</version>
    </dependency>
    
    <!-- 文件上传 -->
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.4</version>
    </dependency>
</dependencies>

 2.2 图片特征提取工具类

 import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;

public class ImageFeatureExtractor {
    
    // 提取RGB颜色直方图特征
    public static ImageFeatures extractFeatures(File imageFile) throws IOException {
        BufferedImage img = ImageIO.read(imageFile);
        if (img == null) {
            throw new IOException("无法读取图片文件");
        }
        
        // 简化处理:缩放图片到统一大小
        img = resizeImage(img, 100, 100);
        
        int[] redHistogram = new int[16];   // 红色分量直方图 (0-15)
        int[] greenHistogram = new int[16]; // 绿色分量直方图
        int[] blueHistogram = new int[16];  // 蓝色分量直方图
        
        // 计算颜色直方图
        for (int y = 0; y < img.getHeight(); y++) {
            for (int x = 0; x < img.getWidth(); x++) {
                int rgb = img.getRGB(x, y);
                int r = (rgb >> 16) & 0xFF;
                int g = (rgb >> 8) & 0xFF;
                int b = rgb & 0xFF;
                
                redHistogram[r / 16]++;
                greenHistogram[g / 16]++;
                blueHistogram[b / 16]++;
            }
        }
        
        // 归一化处理
        int totalPixels = img.getWidth() * img.getHeight();
        double[] normalizedRed = normalizeHistogram(redHistogram, totalPixels);
        double[] normalizedGreen = normalizeHistogram(greenHistogram, totalPixels);
        double[] normalizedBlue = normalizeHistogram(blueHistogram, totalPixels);
        
        return new ImageFeatures(
                arrayToString(normalizedRed),
                arrayToString(normalizedGreen),
                arrayToString(normalizedBlue)
        );
    }
    
    private static BufferedImage resizeImage(BufferedImage originalImage, int width, int height) {
        BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        resizedImage.getGraphics().drawImage(originalImage, 0, 0, width, height, null);
        return resizedImage;
    }
    
    private static double[] normalizeHistogram(int[] histogram, int total) {
        double[] normalized = new double[histogram.length];
        for (int i = 0; i < histogram.length; i++) {
            normalized[i] = (double) histogram[i] / total;
        }
        return normalized;
    }
    
    private static String arrayToString(double[] array) {
        StringBuilder sb = new StringBuilder();
        for (double d : array) {
            sb.append(String.format("%.4f", d)).append(",");
        }
        return sb.substring(0, sb.length() - 1);
    }
    
    public static class ImageFeatures {
        public final String redHistogram;
        public final String greenHistogram;
        public final String blueHistogram;
        
        public ImageFeatures(String redHistogram, String greenHistogram, String blueHistogram) {
            this.redHistogram = redHistogram;
            this.greenHistogram = greenHistogram;
            this.blueHistogram = blueHistogram;
        }
    }
}

 2.3 数据库访问层

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class ImageRepository {
    
    private final JdbcTemplate jdbcTemplate;
    
    public ImageRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    public void saveImage(String imageName, String filePath, 
                        String redHist, String greenHist, String blueHist) {
        String sql = "INSERT INTO image_features " +
                    "(image_name, file_path, red_histogram, green_histogram, blue_histogram) " +
                    "VALUES (?, ?, ?, ?, ?)";
        
        jdbcTemplate.update(sql, imageName, filePath, redHist, greenHist, blueHist);
    }
    
    public List<ImageMatch> findSimilarImages(String redHist, String greenHist, String blueHist, 
                                            int limit) {
        String sql = "SELECT id, image_name, file_path, " +
                    "(ABS(red_histogram - ?) + ABS(green_histogram - ?) + ABS(blue_histogram - ?)) " +
                    "AS similarity " +
                    "FROM image_features " +
                    "ORDER BY similarity ASC " +
                    "LIMIT ?";
        
        return jdbcTemplate.query(sql, (rs, rowNum) -> 
            new ImageMatch(
                rs.getLong("id"),
                rs.getString("image_name"),
                rs.getString("file_path"),
                rs.getDouble("similarity")
            ),
            redHist, greenHist, blueHist, limit
        );
    }
    
    public static class ImageMatch {
        public final long id;
        public final String imageName;
        public final String filePath;
        public final double similarity;
        
        public ImageMatch(long id, String imageName, String filePath, double similarity) {
            this.id = id;
            this.imageName = imageName;
            this.filePath = filePath;
            this.similarity = similarity;
        }
    }
}

2.4 控制器实现

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@RestController
@RequestMapping("/api/images")
public class ImageController {
    
    @Value("${upload.directory}")
    private String uploadDirectory;
    
    private final ImageRepository imageRepository;
    
    public ImageController(ImageRepository imageRepository) {
        this.imageRepository = imageRepository;
    }
    
    @PostMapping("/upload")
    public String uploadImage(@RequestParam("file") MultipartFile file) throws IOException {
        // 确保上传目录存在
        Path uploadPath = Paths.get(uploadDirectory);
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }
        
        // 保存文件
        String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
        Path filePath = uploadPath.resolve(fileName);
        file.transferTo(filePath);
        
        // 提取特征
        ImageFeatureExtractor.ImageFeatures features = 
            ImageFeatureExtractor.extractFeatures(filePath.toFile());
        
        // 存入数据库
        imageRepository.saveImage(
            file.getOriginalFilename(),
            filePath.toString(),
            features.redHistogram,
            features.greenHistogram,
            features.blueHistogram
        );
        
        return "图片上传成功! ID: " + fileName;
    }
    
    @PostMapping("/search")
    public List<ImageRepository.ImageMatch> searchSimilarImages(@RequestParam("file") MultipartFile file) 
            throws IOException {
        // 临时保存查询图片
        Path tempFile = Files.createTempFile("query-", file.getOriginalFilename());
        file.transferTo(tempFile);
        
        // 提取查询图片特征
        ImageFeatureExtractor.ImageFeatures queryFeatures = 
            ImageFeatureExtractor.extractFeatures(tempFile.toFile());
        
        // 删除临时文件
        Files.delete(tempFile);
        
        // 搜索相似图片
        return imageRepository.findSimilarImages(
            queryFeatures.redHistogram,
            queryFeatures.greenHistogram,
            queryFeatures.blueHistogram,
            5
        );
    }
}

2.5 配置文件 (application.properties)

# 服务器配置
server.port=8080

# 文件上传目录
upload.directory=uploads/

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/image_search?useSSL=false
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

3. 使用说明

3.1. 上传图片:

curl -X POST -F "file=@test.jpg" http://localhost:8080/api/images/upload

3.2. 搜索相似图片:

curl -X POST -F "file=@query.jpg" http://localhost:8080/api/images/search

4. 方案特点

  1. 简单易实现:使用颜色直方图代替复杂向量
  2. 无需深度学习:纯Java实现,不依赖外部模型
  3. 轻量级:适合小型应用和教学演示
  4. 可扩展:可以在此基础上添加更复杂的特征

5. 改进建议

  1. 添加更多特征:如纹理特征、形状特征等
  2. 优化相似度算法:使用更精确的直方图比较方法
  3. 添加缓存机制:提高搜索性能
  4. 支持批量导入:一次上传多张图片

这个方案虽然不如深度学习方案精确,但对于小型应用和入门学习已经足够,且实现简单、运行速度快。

 


 

在基础方案上添加更复杂的图像特征

在原有颜色直方图的基础上,我们可以添加纹理特征和形状特征来提升相似图片搜索的准确性。以下是逐步增强的实现方案:

1. 扩展数据库设计

ALTER TABLE `image_features` 
ADD COLUMN `texture_feature` VARCHAR(255) AFTER `blue_histogram`,
ADD COLUMN `shape_feature` VARCHAR(255) AFTER `texture_feature`;

2. 纹理特征实现 (灰度共生矩阵)

import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;

public class TextureFeatureExtractor {
    
    // 使用灰度共生矩阵(GLCM)提取纹理特征
    public static String extractGLCMFeature(BufferedImage image) {
        // 转换为灰度图像
        BufferedImage grayImage = convertToGray(image);
        
        // 定义GLCM参数
        int distance = 1;  // 像素对距离
        int[] angles = {0, 45, 90, 135}; // 四个方向
        int levels = 16;   // 灰度级数
        
        // 计算四个方向的GLCM
        double[][] glcm = new double[levels][levels];
        for (int angle : angles) {
            accumulateGLCM(grayImage, glcm, distance, angle, levels);
        }
        
        // 归一化并提取特征值
        normalizeGLCM(glcm);
        double contrast = calculateContrast(glcm);
        double energy = calculateEnergy(glcm);
        double homogeneity = calculateHomogeneity(glcm);
        
        return String.format("%.4f,%.4f,%.4f", contrast, energy, homogeneity);
    }
    
    private static BufferedImage convertToGray(BufferedImage colorImage) {
        BufferedImage grayImage = new BufferedImage(
            colorImage.getWidth(), 
            colorImage.getHeight(), 
            BufferedImage.TYPE_BYTE_GRAY);
        
        grayImage.getGraphics().drawImage(colorImage, 0, 0, null);
        return grayImage;
    }
    
    private static void accumulateGLCM(BufferedImage image, double[][] glcm, 
                                     int distance, int angle, int levels) {
        int width = image.getWidth();
        int height = image.getHeight();
        
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int x2 = x, y2 = y;
                
                // 根据角度计算相邻像素坐标
                switch (angle) {
                    case 0:   x2 = x + distance; break;
                    case 45:  x2 = x + distance; y2 = y - distance; break;
                    case 90:  y2 = y - distance; break;
                    case 135: x2 = x - distance; y2 = y - distance; break;
                }
                
                // 检查边界
                if (x2 < 0 || x2 >= width || y2 < 0 || y2 >= height) continue;
                
                // 获取灰度值并量化
                int gray1 = (image.getRGB(x, y) & 0xFF) * levels / 256;
                int gray2 = (image.getRGB(x2, y2) & 0xFF) * levels / 256;
                
                glcm[gray1][gray2]++;
            }
        }
    }
    
    private static void normalizeGLCM(double[][] glcm) {
        double sum = 0;
        for (double[] row : glcm) {
            for (double val : row) {
                sum += val;
            }
        }
        
        for (int i = 0; i < glcm.length; i++) {
            for (int j = 0; j < glcm[i].length; j++) {
                glcm[i][j] /= sum;
            }
        }
    }
    
    private static double calculateContrast(double[][] glcm) {
        double contrast = 0;
        for (int i = 0; i < glcm.length; i++) {
            for (int j = 0; j < glcm[i].length; j++) {
                contrast += glcm[i][j] * (i - j) * (i - j);
            }
        }
        return contrast;
    }
    
    private static double calculateEnergy(double[][] glcm) {
        double energy = 0;
        for (double[] row : glcm) {
            for (double val : row) {
                energy += val * val;
            }
        }
        return Math.sqrt(energy);
    }
    
    private static double calculateHomogeneity(double[][] glcm) {
        double homogeneity = 0;
        for (int i = 0; i < glcm.length; i++) {
            for (int j = 0; j < glcm[i].length; j++) {
                homogeneity += glcm[i][j] / (1 + Math.abs(i - j));
            }
        }
        return homogeneity;
    }
}

3. 形状特征实现 (Hu矩)

import java.awt.image.BufferedImage;

public class ShapeFeatureExtractor {
    
    // 计算Hu矩作为形状特征
    public static String extractHuMoments(BufferedImage image) {
        // 转换为二值图像
        BufferedImage binaryImage = convertToBinary(image);
        
        // 计算几何矩
        double m00 = calculateMoment(binaryImage, 0, 0);
        double m10 = calculateMoment(binaryImage, 1, 0);
        double m01 = calculateMoment(binaryImage, 0, 1);
        
        // 计算质心
        double xc = m10 / m00;
        double yc = m01 / m00;
        
        // 计算中心矩
        double u11 = calculateCentralMoment(binaryImage, 1, 1, xc, yc);
        double u20 = calculateCentralMoment(binaryImage, 2, 0, xc, yc);
        double u02 = calculateCentralMoment(binaryImage, 0, 2, xc, yc);
        double u30 = calculateCentralMoment(binaryImage, 3, 0, xc, yc);
        double u03 = calculateCentralMoment(binaryImage, 0, 3, xc, yc);
        double u12 = calculateCentralMoment(binaryImage, 1, 2, xc, yc);
        double u21 = calculateCentralMoment(binaryImage, 2, 1, xc, yc);
        
        // 归一化中心矩
        double n20 = u20 / Math.pow(m00, 2);
        double n02 = u02 / Math.pow(m00, 2);
        double n11 = u11 / Math.pow(m00, 2);
        double n30 = u30 / Math.pow(m00, 2.5);
        double n03 = u03 / Math.pow(m00, 2.5);
        double n12 = u12 / Math.pow(m00, 2.5);
        double n21 = u21 / Math.pow(m00, 2.5);
        
        // 计算Hu矩
        double h1 = n20 + n02;
        double h2 = Math.pow(n20 - n02, 2) + 4 * Math.pow(n11, 2);
        double h3 = Math.pow(n30 - 3 * n12, 2) + Math.pow(3 * n21 - n03, 2);
        double h4 = Math.pow(n30 + n12, 2) + Math.pow(n21 + n03, 2);
        
        return String.format("%.4f,%.4f,%.4f,%.4f", h1, h2, h3, h4);
    }
    
    private static BufferedImage convertToBinary(BufferedImage image) {
        BufferedImage binary = new BufferedImage(
            image.getWidth(), 
            image.getHeight(), 
            BufferedImage.TYPE_BYTE_BINARY);
        
        binary.getGraphics().drawImage(image, 0, 0, null);
        return binary;
    }
    
    private static double calculateMoment(BufferedImage image, int p, int q) {
        double moment = 0;
        for (int y = 0; y < image.getHeight(); y++) {
            for (int x = 0; x < image.getWidth(); x++) {
                if ((image.getRGB(x, y) & 0xFF) > 128) { // 白色像素
                    moment += Math.pow(x, p) * Math.pow(y, q);
                }
            }
        }
        return moment;
    }
    
    private static double calculateCentralMoment(BufferedImage image, int p, int q, 
                                               double xc, double yc) {
        double moment = 0;
        for (int y = 0; y < image.getHeight(); y++) {
            for (int x = 0; x < image.getWidth(); x++) {
                if ((image.getRGB(x, y) & 0xFF) > 128) { // 白色像素
                    moment += Math.pow(x - xc, p) * Math.pow(y - yc, q);
                }
            }
        }
        return moment;
    }
}

4. 更新特征提取服务

public class EnhancedImageFeatureExtractor {
    
    public static EnhancedImageFeatures extractAllFeatures(File imageFile) throws IOException {
        BufferedImage img = ImageIO.read(imageFile);
        if (img == null) {
            throw new IOException("无法读取图片文件");
        }
        
        // 统一大小
        img = resizeImage(img, 256, 256);
        
        // 颜色特征
        ImageFeatureExtractor.ImageFeatures colorFeatures = 
            ImageFeatureExtractor.extractFeatures(img);
        
        // 纹理特征
        String textureFeature = TextureFeatureExtractor.extractGLCMFeature(img);
        
        // 形状特征
        String shapeFeature = ShapeFeatureExtractor.extractHuMoments(img);
        
        return new EnhancedImageFeatures(
            colorFeatures.redHistogram,
            colorFeatures.greenHistogram,
            colorFeatures.blueHistogram,
            textureFeature,
            shapeFeature
        );
    }
    
    private static BufferedImage resizeImage(BufferedImage originalImage, int width, int height) {
        BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        resizedImage.getGraphics().drawImage(originalImage, 0, 0, width, height, null);
        return resizedImage;
    }
    
    public static class EnhancedImageFeatures {
        public final String redHistogram;
        public final String greenHistogram;
        public final String blueHistogram;
        public final String textureFeature;
        public final String shapeFeature;
        
        public EnhancedImageFeatures(String redHistogram, String greenHistogram, 
                                   String blueHistogram, String textureFeature, 
                                   String shapeFeature) {
            this.redHistogram = redHistogram;
            this.greenHistogram = greenHistogram;
            this.blueHistogram = blueHistogram;
            this.textureFeature = textureFeature;
            this.shapeFeature = shapeFeature;
        }
    }
}

5. 更新数据库访问层

@Repository
public class EnhancedImageRepository {
    
    private final JdbcTemplate jdbcTemplate;
    
    public EnhancedImageRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
    
    public void saveEnhancedImage(String imageName, String filePath, 
                                EnhancedImageFeatureExtractor.EnhancedImageFeatures features) {
        String sql = "INSERT INTO image_features " +
                    "(image_name, file_path, red_histogram, green_histogram, blue_histogram, " +
                    "texture_feature, shape_feature) VALUES (?, ?, ?, ?, ?, ?, ?)";
        
        jdbcTemplate.update(sql, 
            imageName, 
            filePath,
            features.redHistogram,
            features.greenHistogram,
            features.blueHistogram,
            features.textureFeature,
            features.shapeFeature
        );
    }
    
    public List<EnhancedImageMatch> findSimilarImages(EnhancedImageFeatureExtractor.EnhancedImageFeatures queryFeatures, 
                                                    int limit) {
        String sql = "SELECT id, image_name, file_path, " +
                    "( " +
                    "  0.3 * (ABS(red_histogram - ?) + ABS(green_histogram - ?) + ABS(blue_histogram - ?)) + " +
                    "  0.4 * ABS(texture_feature - ?) + " +
                    "  0.3 * ABS(shape_feature - ?) " +
                    ") AS similarity " +
                    "FROM image_features " +
                    "ORDER BY similarity ASC " +
                    "LIMIT ?";
        
        return jdbcTemplate.query(sql, (rs, rowNum) -> 
            new EnhancedImageMatch(
                rs.getLong("id"),
                rs.getString("image_name"),
                rs.getString("file_path"),
                rs.getDouble("similarity")
            ),
            queryFeatures.redHistogram,
            queryFeatures.greenHistogram,
            queryFeatures.blueHistogram,
            queryFeatures.textureFeature,
            queryFeatures.shapeFeature,
            limit
        );
    }
    
    public static class EnhancedImageMatch {
        public final long id;
        public final String imageName;
        public final String filePath;
        public final double similarity;
        
        public EnhancedImageMatch(long id, String imageName, String filePath, double similarity) {
            this.id = id;
            this.imageName = imageName;
            this.filePath = filePath;
            this.similarity = similarity;
        }
    }
}

6. 更新控制器

@RestController
@RequestMapping("/api/enhanced-images")
public class EnhancedImageController {
    
    @Value("${upload.directory}")
    private String uploadDirectory;
    
    private final EnhancedImageRepository imageRepository;
    
    public EnhancedImageController(EnhancedImageRepository imageRepository) {
        this.imageRepository = imageRepository;
    }
    
    @PostMapping("/upload")
    public String uploadEnhancedImage(@RequestParam("file") MultipartFile file) throws IOException {
        Path uploadPath = Paths.get(uploadDirectory);
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }
        
        String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
        Path filePath = uploadPath.resolve(fileName);
        file.transferTo(filePath);
        
        // 提取所有特征
        EnhancedImageFeatureExtractor.EnhancedImageFeatures features = 
            EnhancedImageFeatureExtractor.extractAllFeatures(filePath.toFile());
        
        // 存入数据库
        imageRepository.saveEnhancedImage(
            file.getOriginalFilename(),
            filePath.toString(),
            features
        );
        
        return "图片上传成功! 文件名: " + fileName;
    }
    
    @PostMapping("/search")
    public List<EnhancedImageRepository.EnhancedImageMatch> searchSimilarEnhancedImages(
            @RequestParam("file") MultipartFile file) throws IOException {
        
        Path tempFile = Files.createTempFile("query-", file.getOriginalFilename());
        file.transferTo(tempFile);
        
        // 提取查询图片特征
        EnhancedImageFeatureExtractor.EnhancedImageFeatures queryFeatures = 
            EnhancedImageFeatureExtractor.extractAllFeatures(tempFile.toFile());
        
        Files.delete(tempFile);
        
        // 搜索相似图片
        return imageRepository.findSimilarImages(queryFeatures, 5);
    }
}

7. 特征权重优化建议

在实际应用中,可以通过以下方式优化特征权重:

  1. 手动调整权重:根据测试结果调整不同特征的权重比例
  2. 机器学习优化:收集用户反馈数据,训练权重模型
  3. 动态权重:根据图片类型自动调整权重(如风景图侧重颜色,纹理图侧重纹理)

8. 性能优化

  1. 特征预处理:在上传时计算并存储所有特征
  2. 索引优化:为常用搜索特征创建数据库索引
  3. 缓存机制:缓存热门图片的搜索结果
  4. 并行计算:使用多线程并行计算不同特征

这个增强版方案通过添加纹理和形状特征,显著提升了图片搜索的准确性,同时保持了相对简单的实现方式。