winform中c#调用第三方、opencv原生dll库图像处理
winform使用相较于较MFC,拖拖拽拽实现基本的交互功能极其的简单。
这里不比较winform和mfc的其他优劣,单从编程语言来说,winform使用c#编程语言,使用c/c++的dll库时不能直接使用,需要使用P/Invoke来导入dll库。
本文内容包含以下内容实现,简单dll库导入使用,复杂dll库(简单dll又引用了其他dll库)导入使用, 利用c#封装c/++功能,图像数据传递处理等。
项目源代码 https://github.com/wanggao1990/LearningCodeLanguage/tree/c#
1、P/Invoke
P/Invoke全称是Platform Invoke (平台调用) ,是一种函数调用机制。通过P/Invoke我们就可以调用非托管DLL中的函数。只需要添加DllImport特性即可以导入C/C++的函数,但是问题是PInvoke不能简单的实现对C++类的调用。
这种机制类似于android中通过jni调用c/c++代码库,需要一个中间层,并且需要保证数据传递保持一致。
导入函数示例说明如下
[DllImport("NativeDll.dll", EntryPoint="?fnNativeDll@@YAHXZ")]
static extern int func() ;
其中,NativeDll.dll
是导入的库的路径,入口点?fnNativeDll@@YAHXZ
是c++函数原型int fnNativeDll()
的导出符号,导入函数别名为int func()
,名称可以不一样。
注意两点:
- 函数返回值都是int,不需要做额外处理,当是c/c++使用
指针
时,这里对应的类型为IntPtr
。函数必须使用static extern
说明。 - 导出符号使用c++,所以看着像乱码,对与c代码可以使用extern c 的方式导出,保证符号简单唯一。c++符号导出可以用vs命令行 dumpbin /exports xxx.lib/dll 查看。
1、简单库导入
以上面的例子为例,这里的简单库只有一个函数。
1.1 生成一个dll简单库
使用vs新建一个dll库项目,修改仅保留部分代码,h和cpp如下
/* --- NativeDll.h --- */
#ifdef NATIVEDLL_EXPORTS
#define NATIVEDLL_API __declspec(dllexport)
#else
#define NATIVEDLL_API __declspec(dllimport)
#endif
NATIVEDLL_API int fnNativeDll();
/* --- NativeDll.cpp --- */
#include "NativeDll.h"
NATIVEDLL_API int fnNativeDll()
{
return 42;
}
编译生成后,查看函数导出符号,为“?fnNativeDll@@YAHXZ”。
1.2 c#中测试
c#控制台程序,直接上代码
using System;
using System.Runtime.InteropServices;
namespace WinformTest
{
class NativeDllTest
{
[DllImport("NativeDll.dll", EntryPoint = "?fnNativeDll@@YAHXZ")]
private static extern int fnNativeDll();
public static void Main()
{
System.Console.WriteLine("%d", fnNativeDll());
}
}
}
编译成功运行,直接会输出42。
- 导入库可以写绝对路径或相对路径,也可以直接放入执行目录不加路径(这里使用的方式)。
- 平台使用Any Cpu、x84、x64, dll项目和测试项目保证一致即可。
2、复杂库
在上一节中的代码中,引入opencv库(64位,因此平台都要设置为64)添加一个处理图像的类,类中仅包含灰度化的一个函数,目前输入的rgb图像,不做额外的处理。
2.1 图像处理库导出
函数 grayScale(uint8_t* pSrc, int channel, int width, int height, uint8_t** pDst)
传递的是图像的数据指针,通道,宽,高,输出是目标指针的指针。
#include "opencv2\opencv.hpp"
class NATIVEDLL_API CNativeDll {
public:
CNativeDll();
static void grayScale(uint8_t* pSrc, int channel, int width, int height, uint8_t** pDst);
};
#include "NativeDll.h"
CNativeDll::CNativeDll()
{
return;
}
void CNativeDll::grayScale(uint8_t * pSrc, int channel, int width, int height, uint8_t ** pDst)
{
FILE *pFile = fopen("log.txt", "w");
fprintf(pFile,
"input dat: \n"
" pSrc %p, channle %d, width %d, height %d\n"
" ppDst %p, pDst %p\n",
pSrc, channel, width, height, pDst, *pDst
);
fclose(pFile);
int inStride = 0; //4字节对齐
int outStride = (width + 3) / 4 * 4;
cv::Mat gray;
if(pDst != nullptr) {
gray = cv::Mat(height, outStride, CV_8UC1, *pDst);
} else {
gray = cv::Mat(height, outStride, CV_8UC1);
pDst = &(gray.data);
}
if(channel == 3) {
inStride = (width*channel + 3) / 4 * 4;
for(int i = 0; i < height; i++) {
cv::Mat rgbRow(1, width, CV_8UC3, pSrc);
cv::Mat grayRow(1, width, CV_8UC1, gray.ptr<char>(i));
cv::cvtColor(rgbRow, grayRow, cv::COLOR_BGR2GRAY);
pSrc += inStride; // 越过间隙部分
}
}
}
BitMap的内存数据要求四字节对齐。因此,不论彩色图还是灰度图,对于图像的一行,其stride并不等于width*channel, 图像处理时要越过后面的间隙部分。
导出符号如下,其中有c++类生成的两个默认赋值函数。
2.2 winform 测试代码
首先c#封装导入的库函数,对应导入函数为 void grayScale(IntPtr pSrc, int channel, int width, int height, ref IntPtr pDst)
,c/c++指针对应c#为IntPtr, 指针的指针可对应ref IntPtr.
灰度化函数原型为void grayScale(Bitmap srcBitmap, out Bitmap dstBitmap)
,代码如下:
[DllImport("x64/Debug/NativeDll.dll", EntryPoint = "?grayScale@CNativeDll@@SAXPEAEHHHPEAPEAE@Z")]
private static extern void grayScale(IntPtr pSrc, int channel, int width, int height, ref IntPtr pDst);
public void grayScale(Bitmap srcBitmap, out Bitmap dstBitmap)
{
if (srcBitmap.PixelFormat == PixelFormat.Format8bppIndexed) {
dstBitmap = null;
return;
}
BitmapData srcBmpData = srcBitmap.LockBits(new Rectangle(0, 0, srcBitmap.Width, srcBitmap.Height), ImageLockMode.ReadOnly, srcBitmap.PixelFormat);
// 分配目标bmp数据
Bitmap resBitmap = new Bitmap( srcBmpData.Width, srcBmpData.Height, PixelFormat.Format8bppIndexed);
BitmapData resBmpData = resBitmap.LockBits(new Rectangle(0, 0, resBitmap.Width, resBitmap.Height), ImageLockMode.WriteOnly, resBitmap.PixelFormat);
// bmp 需要4字节对齐
System.IntPtr dstPtr = resBmpData.Scan0;
NativeDllTest.grayScale(srcBmpData.Scan0, 3, srcBitmap.Width, srcBmpData.Height, ref dstPtr);
srcBitmap.UnlockBits(srcBmpData);
resBitmap.UnlockBits(resBmpData);
// 创建索引表
ColorPalette palette = resBitmap.Palette;
for (int i = 0; i < palette.Entries.Length; i++) {
palette.Entries[i] = Color.FromArgb(i, i, i);
}
resBitmap.Palette = palette;
dstBitmap = resBitmap;
}
灰度化按钮测试部分代码
public partial class OpencvForm : Form
{
private NativeDllTest instance;
public OpencvForm()
{
InitializeComponent();
instance = new NativeDllTest();
}
private void grayScaleToolStripMenuItem_Click(object sender, EventArgs e)
{
Bitmap res;
instance.grayScale((Bitmap)image, out res);
if(null == res)
{
return;
}
Form grayForm = new Form();
grayForm.BackgroundImage = res;
grayForm.MdiParent = this;
grayForm.Show();
}
2.3 测试结果
可能失败的提示处理, 当前NativeDll.dll库需要依赖opencv_worldxxx.dll库,运行前必须要将所有依赖库放入运行目录下,否则只会提示无法加载NativeDll.dll的错误,并且在项目大时不好排查
。
3、修改c++类使用
在上面c++代码中,灰度化函数 static void grayScale(uint8_t* pSrc, int channel, int width, int height, uint8_t** pDst);
是用static声明的,调用时实际是加了一个命名空间.
当我们去掉static作为一个成员函数时,导出符号会发生改变,重新修改入口点,之后运行会报错。
出错原因是我们使用了 NativeDllTest.grayScale()调用静态方法,而这里已经修改为了成员函数。因此这里必须要先实例化一个c++对象,再调用成员函数。
这里可以借用前面博客Android安卓中封装opencv jni代码为Java类安卓中的思想,在c#中线导入构造函数,返回实例化的对象指针,之后调用功能函数时加上传入指针对象即可。
这样的场景适合一个类初始化之后,加载一次资源,之后需要反复调用其私有或保护成员函数的情况。简单情况直接导出函数即可。
3.1 重写c++代码
我们修改c++代码,添加的2个外部函数直接在头文件中,简单期间使用懒加载形式,如下
class NATIVEDLL_API CNativeDll {
public:
CNativeDll();
// 外部方法
static void* GetInstance()
{
static CNativeDll instace = CNativeDll();
return &instace;
}
// 外部方法
static void GrayScale(void *instance, uint8_t* pSrc, int channel, int width, int height, uint8_t** pDst)
{
((CNativeDll*)instance)->grayScale(pSrc, channel, width, height, pDst);
}
//内部方法
static void grayScale(uint8_t* pSrc, int channel, int width, int height, uint8_t** pDst);
};
导出符号如下,仅关注我们需要的两个函数。
3.2 c#中导入函数测试
[DllImport("x64/Debug/NativeDll.dll", EntryPoint = "?GetInstance@CNativeDll@@SAPEAXXZ")]
private static extern IntPtr GetInstance();
[DllImport("x64/Debug/NativeDll.dll", EntryPoint = "?GrayScale@CNativeDll@@SAXPEAXPEAEHHHPEAPEAE@Z")]
private static extern void GrayScale(IntPtr instance, IntPtr pSrc, int channel, int width, int height, ref IntPtr pDst);
测试时,将
NativeDllTest.grayScale(srcBmpData.Scan0, 3, srcBitmap.Width, srcBmpData.Height, ref dstPtr); // static 方法调用
修改为
IntPtr instance = GetInstance(); // 可以作为全局对象,避免c++中反复构造、释放
GrayScale(instance, srcBmpData.Scan0, 3, srcBitmap.Width, srcBmpData.Height, ref dstPtr);
实测,可以正常运行。