1.背景
这篇博客同样是源自一个实际项目的需求。具体就是需要将我写好的裂缝检测的C++代码打包成DLL以便于给公司他们那边测试。但是不得不吐槽一下这个代码写的时候还是一波三折的。因为原版RPCA代码是Matlab的,裂缝判别代码是用Python写的。一开始说是要统一成C++代码,所以只好全部重写一遍在Ubuntu下改成C++版本可以运行了。可是后来又说他们并不关心C++,他们的平台是Windows的,Ubuntu下的程序运行不了。无奈,只好又把Ubuntu下的C++代码转到Windows下,用Visual Studio 2010重新写一遍。写完了之后再要打包成DLL。所以比较乱。当时还给自己设了一个个小目标,逐个突破,如下。 当然在逐个击破的过程中也遇到和解决了很多新的问题。这篇博客主要记录一下如何用Visual Studio打包写好的C++代码,以及如何调用和测试打包好的DLL。这里我们以一个实际的案例,也就是调用OpenCV和Eigen库,实现一个小功能,也是裂缝检测处理流程里的一步。也就是给定一张灰度图像,计算该图像的灰度众数A(也就是像素数量最多的灰度级数),然后把所有灰度值大于A的像素都赋成A。之所以用到OpenCV是因为涉及到灰度直方图统计、图像的读写等操作,用到Eigen是因为涉及到逐像素的遍历操作,用Eigen会更高效一点。说起来比较简单。话不多说,直接开始吧。
关于Windows下使用OpenCV和Visual Studio进行开发的环境配置与使用方法,可见这篇博客,这里就不赘述了。
2.实现功能代码
常规操作在Visual Studio中建立一个Win32控制台应用程序,然后还是常规操作,配置项目包含目录、库目录等。这里建议把一些库的目录以相对路径的形式进行配置,这样在不同的电脑上不用做任何修改就可以直接用了,如下所示。如果不知道如何配置,参考上面提到的那篇博客。
然后就可以开始写代码了。头文件的代码如下:
// 基础功能相关引用
#include<iostream>
// Eigen相关引用
#include <Eigen/Core>
#include <Eigen/Dense>
// OpenCV相关引用
#include <opencv2/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv/cv.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/core/eigen.hpp>
using namespace std;
using namespace Eigen;
using namespace cv;
// 将大于阈值的元素全部设为该阈值(Eigen MatrixXd)
MatrixXd setBiggerNum2Th(MatrixXd in_Mat, int threshold);
// 对于输入图像,自动寻找灰度众数,并且修改影像内容
Mat stretchImg(Mat img, int & mode_gray);
然后是CPP的代码文件,如下:
#include"DLLExport.h"
// 将大于阈值的元素全部设为该阈值(Eigen MatrixXd)
MatrixXd setBiggerNum2Th(MatrixXd in_Mat, int threshold) {
MatrixXd out_Mat = MatrixXd::Zero(in_Mat.rows(), in_Mat.cols());
for (int i = 0; i < in_Mat.rows(); i++) {
for (int j = 0; j < in_Mat.cols(); j++) {
if (in_Mat(i, j) > threshold) {
out_Mat(i,j) = threshold;
}else{
out_Mat(i, j) = in_Mat(i, j);
}
}
}
return out_Mat;
}
// 对于输入图像,自动寻找灰度众数,并且修改影像内容
Mat stretchImg(Mat img, int & mode_gray){
// 直方图统计
int histSize = 256;
float range[] = {0,255};
const float * histRanges = {range};
Mat hist;
calcHist(&img,1,0,Mat(),hist,1,&histSize,&histRanges,true,false);
// 获取灰度众数(像素数量最多的灰度级数)
double min_v,max_v;
Point min_loc,max_loc;
minMaxLoc(hist, &min_v, &max_v,&min_loc,&max_loc);
mode_gray = max_loc.y;
// 对于大于该值的像素重新赋值
MatrixXd tmp_mat_eigen;
cv2eigen(img, tmp_mat_eigen);
MatrixXd tmp_img_filter = setBiggerNum2Th(tmp_mat_eigen,mode_gray);
Mat img_filter;
eigen2cv(tmp_img_filter, img_filter);
img_filter.convertTo(img_filter,CV_8U);
return img_filter;
}
然后我们可以写一段测试代码用于测试,如下。
#include"DLLExport.h"
int main(){
// 读取影像
Mat img = imread("E:\\Imgs\\test.jpg",IMREAD_GRAYSCALE);
// 进行处理
int mode_gray;
Mat img_processed = stretchImg(img, mode_gray);
// 展示结果
cout<<"Mode gray:"<<mode_gray<<endl;
imshow("Original",img);
imshow("Processed",img_processed);
waitKey(0);
return 0;
}
这里需要注意几点,一是利用OpenCV计算直方图的方法,二是利用OpenCV获取一个矩阵中最大最小值以及对应位置的方法,三是Eigen的Matrix和OpenCV的Mat相互转换的方法。在写完上面的代码以后,编译应该就是没问题的了。当然如果要运行,记得把一些依赖的DLL拷贝到可执行文件目录下,不然会报各种找不到依赖的错误。 上述代码运行测试的效果如下。 这也就说明我们的代码本身是没有问题的,接下来就是对代码进行DLL打包。
3.打包DLL
其实VS打包DLL也非常简单,首先还是常规操作,打开VS,然后选择新建Win32控制台应用程序,然后在弹出的对话框中选择DLL,如下所示。
然后还是和上面一样,配置一下项目的平台(Win32 or x64,Debug or Release)以及项目的包含目录、库目录等。这里就不再赘述,因为和上面是一模一样的。配置完成后,我们可以将刚刚写的头文件和实现文件都拷贝到DLL项目的对应目录下,然后以“添加现有项”的方式添加到项目中,如下图所示。
当然记得,输出的DLL的文件名是和头文件与实现文件的名称保持一致的,所以如果有需求的话记得修改。然后就是修改我们之前写的头文件,也非常简单,就是在要导出的函数名称前面增加extern "C" _declspec(dllexport)
,这里我们只导出stretchImg()
这个函数,如下所示。
最后,点击生成就可以生成DLL文件了。如下图所示。
当然,记得生成DLL的不同模式Debug和Release是不同的,不能混用。不同模式生成的文件如下图所示。
由图中也可以看到,Debug生成的DLL和Release生成的DLL大小差别还是非常大的。至此,DLL的打包就完成了。另外需要注意的是DLL项目本身是不支持运行的(因为不是可执行文件)。
4.测试
最后就是如何使用我们生成的DLL。首先,还是常规操作,新建一个Win32控制台项目,然后,将生成好的DLL文件拷贝到可执行文件目录下(注意Debug和Release别弄混了)。测试项目需要用到OpenCV读取影像,因此还是和上面一样配置一下OpenCV依赖。输入以下代码,即可动态加载DLL使用了。
// 基础功能相关引用
#include<iostream>
// 加载DLL需要用到Windows的API
#include<Windows.h>
// OpenCV相关引用
#include <opencv2/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv/cv.hpp>
#include <opencv2/highgui.hpp>
using namespace std;
using namespace cv;
// 根据提供的接口定义一个函数对象
typedef Mat(*stretchImg)(Mat img, int &mode_gray);
int main(){
// 读取影像
Mat img = imread("E:\\Imgs\\test.jpg",IMREAD_GRAYSCALE);
// 加载DLL文件
HMODULE hDll = LoadLibrary(TEXT("ImgProcesser.dll"));
if(hDll!=NULL){
cout<<"DLL file found."<<endl;
// 新建一个函数对象
stretchImg stretcher = (stretchImg)GetProcAddress(hDll,"stretchImg");
if(stretcher!=NULL){
cout<<"Function found."<<endl;
int mode_gray;
// 调用函数
Mat img_processed = stretcher(img,mode_gray);
// 结果展示
cout<<"Mode gray:"<<mode_gray<<endl;
imshow("Original",img);
imshow("Processed",img_processed);
waitKey(0);
}else{
cout<<"Function not found."<<endl;
system("pause");
}
}else{
cout<<"File not found."<<endl;
system("pause");
}
return 0;
}
可以看到,整体使用还是比较简单的,只要知道DLL的接口(函数名和输入输出的数据类型)就可以了,关于实现完全不用关心。当然需要注意的是,因为我们的代码还依赖OpenCV,因此还需要把OpenCV的DLL连同我们生成的DLL一并拷贝到可执行文件目录下才可以,否则还是会报依赖找不到的错误。正常测试效果如下。 当然如果你明明将生成的DLL文件放到可执行文件夹下了,但还是提示找不到文件的话。这个时候你需要注意,它指的并不是找不到你的这个DLL,而是你这个DLL依赖的其它DLL文件。DLL的查找是一层层进行的,只要有一个文件找不到,就会提示找不到文件。这点需要注意。在实际使用的时候,只要随身携带够了DLL文件,一般情况下都是可以正常运行的,即使没有配置相应的环境。我已经在很多电脑上进行了测试,这种方法生成的DLL都是OK的。
另外再提一点,对于Debug和Release的差别。在之前我是知道在性能上会有一定差别,但是经过实际测试,发现比想象的要大很多,如下所示,可以看到,非常夸张。所以如果你的代码效率不尽如人意,不妨先考虑一下是不是Debug的问题,然后再考虑如何从代码层面优化。
至此,将C++代码打包成DLL并进行测试的任务就完成了。
5.参考资料
- [1] https://blog.csdn.net/zhang_qing_yu/article/details/77141449
- [2] https://www.cnblogs.com/holyprince/p/4236818.html
- [3] https://blog.csdn.net/ywhputx0802/article/details/80667919
- [4] https://blog.csdn.net/xz1308579340/article/details/84335487
- [5] https://blog.csdn.net/whu_zs/article/details/80344822
本文作者原创,未经许可不得转载,谢谢配合