Java使用OpenCV计算两张图片相似度

业务:找出两个表的重复的图片。

图片在表里存的是二进制值,存在大量由于一些特殊情况例如扫描有差异,导致图片存的二进制值不同,但图片其实是一样来的。

所以找出两个表重复相同的图片,不可能只是单纯的比较二进制值相等。

方法:针对这种情况,使用OpenCV直方图算法可以比较两张图片的相似度,测试发现完全相同的图片相似度等于1(表里存的二进制值不相等)

实操:Java引入使用opencv步骤详解

1.引入opencv依赖

java 复制代码
<!-- https://mvnrepository.com/artifact/org.openimaj/core -->
<dependency>
	<groupId>org.openpnp</groupId>
	<artifactId>opencv</artifactId>
	<version>4.5.5-1</version>
</dependency>

2.代码Demo

opencv提供了均方差算法(MSE)、结构相似性指数算法(SSIM)、峰值信噪比算法(PSNR)、直方图算法(SSIM-WH),其中使用直方图算法来比较图片相似效果最好。

java 复制代码
    public static void main(String[] args) {
        // 加载OpenCV库
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

        // 读取两张图像。准备比对的图片
        Mat image1 = Imgcodecs.imread("D:\\work\\testdata\\psc_1716260008343.jpg");
        Mat image2 = Imgcodecs.imread("D:\\work\\testdata\\psc_1716260008345.jpg");

        // 将图片处理成一样大
        Imgproc.resize(image1, image1, image2.size());
        Imgproc.resize(image2, image2, image1.size());

        // 计算均方差(MSE)
        double mse = calculateMSE(image1, image2);
        System.out.println("均方差(MSE): " + mse);

        // 计算结构相似性指数(SSIM)
        double ssim = calculateSSIM(image1, image2);
        System.out.println("结构相似性指数(SSIM): " + ssim);

        // 计算峰值信噪比(PSNR)
        double psnr = calculatePSNR(image1, image2);
        System.out.println("峰值信噪比(PSNR): " + psnr);

        // 计算直方图
        final double similarity = calculateHistogram(image1, image2);
        System.out.println("图片相似度(直方图): " + similarity);

        // 计算归一化交叉相关(NCC)
//        double ncc = calculateNCC(image1, image2);
//        System.out.println("归一化交叉相关(NCC): " + ncc);
    }

    // 计算均方差(MSE)
    private static double calculateHistogram(Mat image1, Mat image2) {
        // 计算直方图
        Mat hist1 = calculateHistogram(image1);
        Mat hist2 = calculateHistogram(image2);

        // 计算相似度
        final double similarity = Imgproc.compareHist(hist1, hist2, Imgproc.CV_COMP_CORREL);

        // 手动释放内存
//        if (hist1 != null) {
//            hist1.release();
//        }
//        if (hist2 != null) {
//            hist2.release();
//        }

        return similarity;
    }

    // 计算均方差(MSE)
    private static double calculateMSE(Mat image1, Mat image2) {
        Mat diff = new Mat();
        Core.absdiff(image1, image2, diff);
        Mat squaredDiff = new Mat();
        Core.multiply(diff, diff, squaredDiff);
        Scalar mseScalar = Core.mean(squaredDiff);
        return mseScalar.val[0];
    }

    // 计算结构相似性指数(SSIM)
    private static double calculateSSIM(Mat image1, Mat image2) {
        Mat image1Gray = new Mat();
        Mat image2Gray = new Mat();
        Imgproc.cvtColor(image1, image1Gray, Imgproc.COLOR_BGR2GRAY);
        Imgproc.cvtColor(image2, image2Gray, Imgproc.COLOR_BGR2GRAY);
        MatOfFloat ssimMat = new MatOfFloat();
        Imgproc.matchTemplate(image1Gray, image2Gray, ssimMat, Imgproc.CV_COMP_CORREL);
        Scalar ssimScalar = Core.mean(ssimMat);
        return ssimScalar.val[0];
    }

    // 计算峰值信噪比(PSNR)
    private static double calculatePSNR(Mat image1, Mat image2) {
        Mat diff = new Mat();
        Core.absdiff(image1, image2, diff);
        Mat squaredDiff = new Mat();
        Core.multiply(diff, diff, squaredDiff);
        Scalar mseScalar = Core.mean(squaredDiff);
        double mse = mseScalar.val[0];
        double psnr = 10.0 * Math.log10(255.0 * 255.0 / mse);
        return psnr;
    }

    // 计算归一化交叉相关(NCC)
//    private static double calculateNCC(Mat image1, Mat image2) {
//        Mat image1Gray = new Mat();
//        Mat image2Gray = new Mat();
//        Imgproc.cvtColor(image1, image1Gray, Imgproc.COLOR_BGR2GRAY);
//        Imgproc.cvtColor(image2, image2Gray, Imgproc.COLOR_BGR2GRAY);
//        MatOfInt histSize = new MatOfInt(256);
//        MatOfFloat ranges = new MatOfFloat(0, 256);
//        Mat hist1 = new Mat();
//        Mat hist2 = new Mat();
//
//        Core.normalize(hist1, hist1, 0, 1, Core.NORM_MINMAX);
//        Core.normalize(hist2, hist2, 0, 1, Core.NORM_MINMAX);
//        double ncc = Core.compareHist(hist1, hist2, Imgproc.CV_COMP_CORREL);
//        return ncc;
//    }

    private static Mat calculateHistogram(Mat image) {
        Mat hist = new Mat();

        // 设置直方图参数
        MatOfInt histSize = new MatOfInt(256);
        MatOfFloat ranges = new MatOfFloat(0, 256);
        MatOfInt channels = new MatOfInt(0);
        List<Mat> images = new ArrayList<>();
        images.add(image);

        // 计算直方图
        Imgproc.calcHist(images, channels, new Mat(), hist, histSize, ranges);

        return hist;
    }

3.运行遇到的报错问题以及解决方法

Exception in thread "main" java.lang.UnsatisfiedLinkError: no opencv_java455 in java.library.path

报错原因:

在JDK bin 目录下找不到 opencv_java455.dll 文件

解决方法:

官网下载地址:Releases - OpenCV

找到对应的版本下载opencv(如果下载不起很慢,可以复制链接用迅雷下载)

双击打开安装包选择安装提取目录

等待解压

在目录找到dll文件

然后复制到jdk bin目录中

再重新运行程序即可解决

4.运行

均方差算法(MSE):

计算两幅图片每个像素之间的差异,并计算它们的平均值。MSE值越小,表示两幅图片越相似。

结构相似性指数(SSIM):

通过比较两幅图片的亮度、对比度和结构信息来评估它们的相似性。值越大,越相似。

峰值信噪比(PSNR):

通过计算两幅图片的MSE值,并将其转换为对数尺度,来评估它们的相似性。PSNR值越大,表示两幅图片越相似。

图片相似度(直方图):

通过将SSIM指数和直方图相似性组合起来,来评估两幅图片的相似性。返回的相似性度量值越接近1,表示两幅图像越相似。

5.结合业务实现代码片段

注:务必手动释放Mat内存,亲测不写手动释放内存,随着循环量越多,创建Mat越多,就会导致内存崩溃泄露(按理说Java有回收机制,但我经过测试发现并没有触发回收内存,即使是没用的Mat)

java 复制代码
byte[] ecf2Image = bsImage.getImage();
byte[] upsImage = upsPage.getScanPage();
// 1.先直接对比ecf2和ups图片的二进制值
if (Arrays.equals(ecf2Image, upsImage)) {
    // 二进制值相等则给ecf2图片状态更新为重复的
    updateAndRecord(shipmentNo, filename, upsPage, 1);
    // 然后跳出scanPageList的循环,已经确认为重复就不用再去匹配
    break;
}

// 2.byte值不等,再用OpenCV来比较
// 将图片二进制数据转换为Mat对象
Mat image1 = Imgcodecs.imdecode(new MatOfByte(ecf2Image), Imgcodecs.IMREAD_COLOR);
Mat image2 = Imgcodecs.imdecode(new MatOfByte(upsImage), Imgcodecs.IMREAD_COLOR);
try {
    // 将图片处理成一样大
    Imgproc.resize(image1, image1, image2.size());
    Imgproc.resize(image2, image2, image1.size());
    // 计算直方图
    final double similarity = calculateHistogram(image1, image2);
    if (similarity == 1) {
        // 更新状态为重复的
        updateAndRecord(shipmentNo, filename, upsPage, 2);
        break;
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    // 手动释放内存
    if (image1 != null) {
        image1.release();
    }
    if (image2 != null) {
        image2.release();
    }
}
相关推荐
lingRJ7771 分钟前
微服务架构下的抉择:Consul vs. Eureka,服务发现该如何选型?
java·eureka·springcloud·consul·backend·microservices·servicediscovery
RainbowSea1 分钟前
问题:后端由于字符内容过长,前端展示精度丢失修复
java·spring boot·后端
C182981825754 分钟前
OOM电商系统订单缓存泄漏,这是泄漏还是溢出
java·spring·缓存
风象南18 分钟前
SpringBoot 控制器的动态注册与卸载
java·spring boot·后端
我是一只代码狗1 小时前
springboot中使用线程池
java·spring boot·后端
hello早上好1 小时前
JDK 代理原理
java·spring boot·spring
PanZonghui1 小时前
Centos项目部署之Java安装与配置
java·linux
沉着的码农2 小时前
【设计模式】基于责任链模式的参数校验
java·spring boot·分布式
Mr_Xuhhh2 小时前
信号与槽的总结
java·开发语言·数据库·c++·qt·系统架构
纳兰青华2 小时前
bean注入的过程中,Property of ‘java.util.ArrayList‘ type cannot be injected by ‘List‘
java·开发语言·spring·list