赞
踩
最近接到了一个需求,需要对比图片并自动生成对比报表,核心功能就是获取图片相似度,生成表格。
这里仅介绍如何实现的图片相似度获取;
相似度计算的算法选择的是SSIM算法,具体算法原理参考的是SSIM 的原理和代码实现,算法中涉及了卷积运算,还有图片的矩阵运算,决定选用OpenCV库来实现。因为后台使用的是C#写的,OpenCV使用的是C++,所以决定用C++封装图像相似度处理的函数,通过dll导出接口到C#中使用;(C#中有已经封装的OpenCV库,OpencvSharp和Emgu都是很好的,但是这次功能简单,没有必要使用)
INSTALL
项目上右键,BuildE:\Program Zip\opencv-4.4.0\opencv-4.4.0\build\install
)看到, 如果中间报了python相关的错误,可以忽略把上一步编译好的opencv库中的 include,x64
目录拷贝到合适的地方,最好是release合debug版本的分目录存放,下面是我的目录结构,因为暂时不考虑多个vs版本的,所以x64下面的vc目录去掉了
新建一个空的C++项目
在项目上右键,跳转到: 属性页->Configuration Properties->General->Configuration Type
,修改为Dynamic Library (.dll)
跳转到:属性页 ->C/C++->General->Additional Include Directories
,填入存放opencv库的include目录的路径
跳转到:属性页->Linker->General->Additional Library Directories
,填入存放opencv库的 路径中对应版本的lib路径,我这里使用了相应的编译选项宏以及编译平台宏,可以视情况选择
跳转到:属性页->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都加上,这样可以避免出现 找不到定义的报错
到此,项目配置就完成了,dll我们就直接拷贝到项目的输出路径去即可,可以按需拷贝,运行若报错说某个dll找不到就拷贝过去即可
算法的原理可以参考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导出接口给C#使用,主要有以下几点需要关注
__stdcall
, 还有其他几种传递规则,可以参考官方文档参数传递和命名约定,这里我是直接使用的默认约定,其他几种规则没有尝试,有兴趣可以研究一下;extern "C"
,这个在导出接口前一定要加上,不然C#会找不到对应的函数; 因为C++编译默认会给C++函数添加名字修饰,就是按照一定的规则加上一些修饰字符,而C#中是通过函数名称调用dll中的函数的,加上 extern "C"
的目的就让编译器不要加修饰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; } }
[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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。