利用Visual Studio2010打包C++代码成动态链接库DLL文件

Dec 27,2020   5422 words   20 min

Tags: C/C++

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

本文作者原创,未经许可不得转载,谢谢配合

返回顶部