基于OpenCV的SSIM算法实现

简介

最近接到了一个需求,需要对比图片并自动生成对比报表,核心功能就是获取图片相似度,生成表格。
这里仅介绍如何实现的图片相似度获取;

思路

相似度计算的算法选择的是SSIM算法,具体算法原理参考的是SSIM 的原理和代码实现,算法中涉及了卷积运算,还有图片的矩阵运算,决定选用OpenCV库来实现。因为后台使用的是C#写的,OpenCV使用的是C++,所以决定用C++封装图像相似度处理的函数,通过dll导出接口到C#中使用;(C#中有已经封装的OpenCV库,OpencvSharp和Emgu都是很好的,但是这次功能简单,没有必要使用)

实现

VS2019下的OpenCV环境搭建

OpenCV源代码编译

  1. 从https://opencv.org/releases/下载源代码,如果不介意官方打包的dll和lib体积太大的话,也可以下载exe版本,双击即可,可以省去后面的编译步骤
  2. 从https://cmake.org/download/下载cmake,用于源码编译
  3. 下载完cmake后解压,运行 解压目录/bin/cmake-gui.exe ,通过Browse Source找到第一步下载解压的OpenCV源码目录,然后选择一个结果输出路径比如我这里分别是:E:/Program Zip/opencv-4.4.0/opencv-4.4.0和 E:/Program Zip/opencv-4.4.0/opencv-4.4.0/build
    基于OpenCV的SSIM算法实现_第1张图片
  4. 点击左下角的Configure按钮,选择编译选项如下,然后点finish;这里我只配置了64位的,其他平台可以自行选择
    -基于OpenCV的SSIM算法实现_第2张图片
  5. 待配置完成后,点击generate,等待生成完成,点击project按钮打开解决方案
  6. 选择Debug或Releas编译选项,在 INSTALL项目上右键,Build
  7. 漫长的Build之后,你会在输出目录下(比如我的是E:\Program Zip\opencv-4.4.0\opencv-4.4.0\build\install)看到, 如果中间报了python相关的错误,可以忽略
    基于OpenCV的SSIM算法实现_第3张图片
  8. 到这里,opencv的编译就成功完成了

项目配置

  1. 把上一步编译好的opencv库中的 include,x64目录拷贝到合适的地方,最好是release合debug版本的分目录存放,下面是我的目录结构,因为暂时不考虑多个vs版本的,所以x64下面的vc目录去掉了
    基于OpenCV的SSIM算法实现_第4张图片

  2. 新建一个空的C++项目

  3. 在项目上右键,跳转到: 属性页->Configuration Properties->General->Configuration Type,修改为Dynamic Library (.dll)

  4. 跳转到:属性页 ->C/C++->General->Additional Include Directories,填入存放opencv库的include目录的路径
    基于OpenCV的SSIM算法实现_第5张图片

  5. 跳转到:属性页->Linker->General->Additional Library Directories,填入存放opencv库的 路径中对应版本的lib路径,我这里使用了相应的编译选项宏以及编译平台宏,可以视情况选择
    基于OpenCV的SSIM算法实现_第6张图片

  6. 跳转到:属性页->Linker->Input->Additioinal Dependencies;release编译选项下填入opencv_core440.lib opencv_imgcodecs440.lib opencv_imgproc440.lib opencv_gapi440.lib opencv_calib3d440.lib,debug选项下记得名字后面要加d,因为我的代码中只用到了这些lib,所以只填了这些lib,如果不介意大小的话,可以把opencv所有的lib都加上,这样可以避免出现 找不到定义的报错

  7. 到此,项目配置就完成了,dll我们就直接拷贝到项目的输出路径去即可,可以按需拷贝,运行若报错说某个dll找不到就拷贝过去即可

SSIM的算法实现

算法的原理可以参考SSIM 的原理和代码实现,讲的非常清晰,同时也建议阅读下python下的实现

下面是计算图片差异的核心函数,返回的Mat中 包含了两幅图片各个像素点的相似度,如果需要整张图的,求一下均值即可;完整代码放在末尾

static const double C1 = 6.5025, C2 = 58.5225; 
Mat Compare_SSIM(Mat image1, Mat image2)
{
    Mat validImage1, validImage2;
    image1.convertTo(validImage1, CV_32F); //数据类型转换为 float,防止后续计算出现错误
    image2.convertTo(validImage2, CV_32F);

    Mat image1_1 = validImage1.mul(validImage1); //图像乘积
    Mat image2_2 = validImage2.mul(validImage2);
    Mat image1_2 = validImage1.mul(validImage2);

    Mat gausBlur1, gausBlur2, gausBlur12;
    GaussianBlur(validImage1, gausBlur1, Size(11, 11), 1.5); //高斯卷积核计算图像均值
    GaussianBlur(validImage2, gausBlur2, Size(11, 11), 1.5);
    GaussianBlur(image1_2, gausBlur12, Size(11, 11), 1.5);

    Mat imageAvgProduct = gausBlur1.mul(gausBlur2); //均值乘积
    Mat u1Squre = gausBlur1.mul(gausBlur1); //各自均值的平方
    Mat u2Squre = gausBlur2.mul(gausBlur2);

    Mat imageConvariance, imageVariance1, imageVariance2;
    Mat squreAvg1, squreAvg2;
    GaussianBlur(image1_1, squreAvg1, Size(11, 11), 1.5); //图像平方的均值
    GaussianBlur(image2_2, squreAvg2, Size(11, 11), 1.5);
    
    imageConvariance = gausBlur12 - gausBlur1.mul(gausBlur2);// 计算协方差
    imageVariance1 = squreAvg1 - gausBlur1.mul(gausBlur1); //计算方差
    imageVariance2 = squreAvg2 - gausBlur2.mul(gausBlur2); 

    auto member = ((2 * gausBlur1 .mul(gausBlur2) + C1).mul(2 * imageConvariance + C2));
    auto denominator = ((u1Squre + u2Squre + C1).mul(imageVariance1 + imageVariance2 + C2));

    Mat ssim;
    divide(member, denominator, ssim);
    return ssim;
}

dll导出接口

dll导出接口给C#使用,主要有以下几点需要关注

  • 接口导出的参数传递和命名规则 ,这里用的是 __stdcall, 还有其他几种传递规则,可以参考官方文档参数传递和命名约定,这里我是直接使用的默认约定,其他几种规则没有尝试,有兴趣可以研究一下;
  • extern "C",这个在导出接口前一定要加上,不然C#会找不到对应的函数; 因为C++编译默认会给C++函数添加名字修饰,就是按照一定的规则加上一些修饰字符,而C#中是通过函数名称调用dll中的函数的,加上 extern "C"的目的就让编译器不要加修饰
  • 参数传递;如何从C++传递参数到C#,基本数据类型可以直接传递,字符串,结构体什么的传递就很麻烦了,我是用的是在C++中new内存,传递指针到C#中,然后C#中调用C++导出的release函数释放之前new的内存,具体代码可以参考下文

C++头文件部分

#pragma once
#include "stdafx.h"

#define Export_Dll extern "C" _declspec(dllexport) 
#define Dll_API __stdcall

#pragma pack(1) //必须写,防止出现字节补齐,导致C#中读到错误的字节
struct Mat_Struct
{
    int width;
    int height;
    int channels;
    uchar data[0]; //必须这么写,使用指针的话,C#中读取数据太麻烦,得读两次指针
};

//所有导出接口均使用malloc进行内存分配,不许使用new
Export_Dll uchar* Dll_API SSIM_Image(const char* imgPath1, const char* imgPath2);

Export_Dll double Dll_API SSIM_Percent(const char* imgPath1, const char* imgPath2);

Export_Dll void Dll_API Release(void* ptr);

C++源文件部分

#include "DllExport.h"
#include "ImageCompare.h"

uchar* Dll_API SSIM_Image(const char* imgPath1, const char* imgPath2)
{
	auto img1 = imread(_imagePath1);
    auto img2 = imread(_imagePath2);
    auto result = Compare_SSIM(img1,img2); //这个就是上面的ssim的核心函数了
    int width = result.size().width;
    int height = result.size().height;
    int channels = result.channels();
    int length = width * height * channels;
    Mat_Struct* res = static_cast<Mat_Struct*>(malloc(sizeof(Mat_Struct) + length)); //传递给dll外部,不要在这里释放

    res->width = width;
    res->height = height;
    res->channels = channels;
    memcpy(res->data, result.data, length);

    return (uchar*)res;
}

double Dll_API SSIM_Percent(const char* imgPath1, const char* imgPath2)
{
    double sum = 0.0f;
	auto img1 = imread(_imagePath1);
    auto img2 = imread(_imagePath2);
    auto result = Compare_SSIM(img1,img2); 
    auto meanResult = mean(result);
    int channels = result.channels();
    
    for (auto depthIndex = 0; depthIndex < channels; ++depthIndex)
    {
        sum += meanResult[depthIndex];
    }
    sum /= channels;

    return sum;
}

void Dll_API Release(void* ptr)
{
    if (ptr)
    {
        free(ptr);
        ptr = nullptr;
    }
}

C#调用C++的dll

    [StructLayout(LayoutKind.Sequential, Pack = 1), Serializable] //防止字节补齐,如果不需要C#往C++传递,不写也可以
    struct Mat_Struct
    {
        public int width;
        public int height;
        public int channels;
        public byte[] data;

        public Mat_Struct(byte[] byteArray) //这里是原先想通过字节数组传递,发现行不通,放弃了,留这里当二进制读取的例子吧
        {
            MemoryStream ms = new MemoryStream(byteArray);
            BinaryReader br = new BinaryReader(ms);
            try
            {
                width = br.ReadInt32();
                height = br.ReadInt32();
                channels = br.ReadInt32();
                data = br.ReadBytes(width * height * channels);
            }
            catch (EndOfStreamException eofEx)
            {
                width = 0;
                height = 0;
                channels = 0;
                data = new byte[0];
                LogHelper.Error(eofEx);
            }
        }

		//核心解析函数,从非托管C++的指针指向的内存中读取结构体中的数据
		//参考: https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.interopservices.marshal?view=netcore-3.1
		//https://docs.microsoft.com/zh-cn/dotnet/api/system.intptr.add?view=netcore-3.1
        public Mat_Struct(IntPtr ptr)
        {
            IntPtr iter = ptr;
            width = Marshal.ReadInt32(iter);
            iter = IntPtr.Add(iter, sizeof(int)); //需要注意,这一步不能少,读取操作是没有自动偏移的

            height = Marshal.ReadInt32(iter);
            iter = IntPtr.Add(iter, sizeof(int));

            channels = Marshal.ReadInt32(iter);
            iter = IntPtr.Add(iter, sizeof(int));

            var length = width * height * channels;
            data = new byte[length];
            Marshal.Copy(iter, data, 0, length);
        }
    };

    public class OpencvAdapter
    {
    	//dll名称和函数名称声明,注意不要写错名字了
        [DllImport("OpencvInterface.dll", EntryPoint = "SSIM_Image", CharSet = CharSet.Ansi)]
        public static extern IntPtr SSIM_Image([MarshalAs(UnmanagedType.LPStr)] string path1, [MarshalAs(UnmanagedType.LPStr)] string path2);

        [DllImport("OpencvInterface.dll", EntryPoint = "SSIM_Percent", CharSet = CharSet.Ansi)]
        public static extern double SSIM_Percent([MarshalAs(UnmanagedType.LPStr)] string path1, [MarshalAs(UnmanagedType.LPStr)] string path2);

        [DllImport("OpencvInterface.dll", EntryPoint = "Release", CharSet = CharSet.Ansi)]
        public static extern void Release(IntPtr ptr);

		public static Example()
		{
			//注意,不要使用 Marshal.FreeHGlobal(IntPtr)来释放内存,因为内存是从非托管C++中申请的,要用导出的Release函数释放
		    IntPtr res = new IntPtr(0);
            try
            {
                res = SSIM_Image(img1Path, img2Path);
                var matData = new Mat_Struct(res);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            finally
            {
                Release(res); //一定要用Release释放
            }
		}
    }

C#调用C++的dll是一个比较常见的场景,在实现的过程中,难点就是参数传递,返回值传递,因为涉及到了托管内存和非托管内存的交互,好在C#中是有指针的,实现起来没有想象中的复杂(实在不行,用unsafe和裸指针强行读内存也可以解决)

C++核心代码github: https://github.com/luochanganz/ImageDiff

参考文档

  1. SSIM 的原理和代码实现
  2. 参数传递和命名约定
  3. Marshal MSDN官方文档
  4. IntPtr的函数文档

你可能感兴趣的:(C++基础知识,拓展学习,opencv,c++,dll)