最近,图像处理组之前专门做MFC的同事离职了,这开发界面的事情就落到了我们自己的头上。
这几乎是大家的共识:用C++调用OpenCV写函数底层库很方便;但是用MFC写上层的GUI程序却很麻烦。因为MFC太不C++了。我成为一只程序猿以来,从来没有任何一刻喜欢上MFC。而我家那位一直用的是C#开发界面的,再加上另一位基友的推荐,因此我便想尝试这用C#来做界面的开发。但底层还是得用C/C++来做图像处理啊,这就牵扯到了C#调用非托管的C/C++动态库的问题。
C#,我是小白一只。但好在各路大神的帮忙以及它本身的便捷性,编写个界面啥的也是没有什么问题。但用C#来调用C/C++ 动态库,朋友们用的不多,我就只能自己艰苦奋斗啦~但是我是很有信心的。因为我知道,OpenCV有一个C#的版本,叫Emgu,它实际上也是调用的OpenCV的C++ DLL。Emgu在这个动态库之上又封装了一层,形成了C#的动态库,使得在C#界面上可以轻松调用OpenCV。既然Emgu能做到,我就也能做到。
我也不打算把Emgu全部解析一遍,我既没有这个能力,也没有这个时间,咱们就实用主义,来看看Emgu是如何调用OpenCV的动态库的吧。
拿一个我们最常用的函数作为例子吧。cvCreateImage
。
在Emgu的教程里,第一个例子就是说,
The CvInvoke class provides a way to directly invoke OpenCV function within .NET languages. Each method in this class corresponds to a function in OpenCV of the same name. For example, a call to
IntPtr image = CvInvoke.cvCreateImage(new System.Drawing.Size(400, 300), CvEnum.IPL_DEPTH.IPL_DEPTH_8U, 1);
is equivalent to the following function call in C
IplImage* image = cvCreateImage(cvSize(400, 300), IPL_DEPTH_8U, 1);
在Emgu的源码里面,可以看到CvInvoke.cvCreateImage是这样子的:
#region Initialization
/// <summary>
/// Creates the header and allocates data.
/// </summary>
/// <param name="size">Image width and height.</param>
/// <param name="depth">Bit depth of image elements</param>
/// <param name="channels">
/// Number of channels per element(pixel). Can be 1, 2, 3 or 4. The channels are interleaved, for example the usual data layout of a color image is:
/// b0 g0 r0 b1 g1 r1 ...
/// </param>
/// <returns>A pointer to IplImage </returns>
[DllImport(OPENCV_CORE_LIBRARY, CallingConvention = CvInvoke.CvCallingConvention)]
public static extern IntPtr cvCreateImage(
Size size,
CvEnum.IPL_DEPTH depth,
int channels);
下面通过一些官方的文档或渠道来注解这段代码。首先是DllImport
来自MSDN
直接从 C# 调用 DLL 导出:若要声明一个方法使其具有来自 DLL 导出的实现,请执行下列操作:
- 使用 C# 关键字
static
和extern
声明方法。 - 将 DllImport 属性附加到该方法。DllImport 属性允许您指定包含该方法的 DLL 的名称。通常的做法是用与导出的方法相同的名称命名 C# 方法,但也可以对 C# 方法使用不同的名称。
- 还可以为方法的参数和返回值指定自定义封送处理信息,这将重写 .NET Framework 的默认封送处理。
示例:
// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
public static void Main()
{
puts("Test");
_flushall();
}
}
输出:
Test
代码讨论
前面的示例显示了声明在非托管 DLL 中实现的 C# 方法的最低要求。PlatformInvokeTest.puts 方法用 static 和 extern 修饰符声明并且具有 DllImport 属性,该属性使用默认名称 puts 通知编译器此实现来自 msvcrt.dll。若要对 C# 方法使用不同的名称(如 putstring),则必须在 DllImport 属性中使用 EntryPoint 选项,如下所示:
[DllImport("msvcrt.dll", EntryPoint="puts")]
官网上没有详细说的是CallingConvention。这个是调用惯例。可以直接看源代码:
public enum CallingConvention
{
// 摘要:
// 此成员实际上不是调用约定,而是使用了默认平台调用约定。例如,在 Windows 上默认为 System.Runtime.InteropServices.CallingConvention.StdCall,在
// Windows CE.NET 上默认为 System.Runtime.InteropServices.CallingConvention.Cdecl。
Winapi = 1,
//
// 摘要:
// 调用方清理堆栈。这使您能够调用具有 varargs 的函数(如 Printf),使之可用于接受可变数目的参数的方法。
Cdecl = 2,
//
// 摘要:
// 被调用方清理堆栈。这是使用平台 invoke 调用非托管函数的默认约定。
StdCall = 3,
//
// 摘要:
// 第一个参数是 this 指针,它存储在寄存器 ECX 中。其他参数被推送到堆栈上。此调用约定用于对从非托管 DLL 导出的类调用方法。
ThisCall = 4,
//
// 摘要:
// 不支持此调用约定。
FastCall = 5,
}
支持的有Winapi
, Cdecl
, StdCall
, ThisCall
, FastCall
。我一般使用的都是Stdcall
。调用惯例要保持一致,C++的代码上用的什么调用惯例,则C#上就使用什么调用惯例。
至此,DllImport(OPENCV_CORE_LIBRARY, CallingConvention = CvInvoke.CvCallingConvention)
,这一行也就差不多明白了。再来往下看。
当从 C# 代码中调用非托管函数时,公共语言运行库必须封送参数和返回值。
对于每个 .NET Framework 类型均有一个默认非托管类型,公共语言运行库将使用此非托管类型在托管到非托管的函数调用中封送数据。例如,C# 字符串值的默认封送处理是封送为 LPTSTR(指向 TCHAR 字符缓冲区的指针)类型。可以在非托管函数的 C# 声明中使用 MarshalAs 属性重写默认封送处理。
示例 2
本示例使用 DllImport 属性输出一个字符串。它还显示如何通过使用 MarshalAs 属性重写函数参数的默认封送处理。
// Marshal.cs
using System;
using System.Runtime.InteropServices;
class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(
[MarshalAs(UnmanagedType.LPStr)]
string m);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();
public static void Main()
{
puts("Hello World!");
_flushall();
}
}
输出
Hello World!
代码讨论
在前面的示例中,puts 函数的参数的默认封送处理已从默认值 LPTSTR 重写为 LPSTR。
MarshalAs属性可以放置在方法参数、方法返回值以及结构和类的字段上。若要设置方法返回值的封送处理,请将 MarshalAs 属性与返回属性位置重写一起放置在方法上的属性块中。例如,若要显式设置 puts 方法返回值的封送处理:
...
[DllImport("msvcrt.dll")]
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
...
再来看一下,在OpenCV中,cvCreateImage
是这样的签名:
CVAPI(IplImage*) cvCreateImage( CvSize size, int depth, int channels );
```
而在C#中,是这样的:
```.cs
public static extern IntPtr cvCreateImage(Size size, CvEnum.IPL_DEPTH depth, int channels);
```
光从类型来看,最好理解的是`int channels`,完全一样。
比较难理解的是`CvEnum.IPL_DEPTH`和`IntPtr`。我们看一下`CvEnum.IPL_DEPTH`的定义:
```.cs
public enum IPL_DEPTH : uint{
...
}
```
这我们就知道了,其实是一致的。
而指针类型,都可以用`IntPtr`来代替,也没有什么问题。事实上,我们可以在网上找到一张C#和C++之间的mapping[表格](http://blog.csdn.net/yangzhenping/article/details/6549150)。
最为费解的是Size。在C++的版本中,是用的CvSize
```.c
typedef struct CvSize
{
int width;
int height;
}
CvSize;
```
而C#中,用的却是Size,这是System.Drawing里面的一个类。Emgu的[教程](http://www.emgu.com/wiki/index.php/Tutorial)上面一句话带过:
Emgu CV also borrows some existing structures in .Net to represent structures in OpenCV:
| .Net Structure | OpenCV structure |
| ------------------ | ----------------------- |
| System.Drawing.Point | CvPoint |
| System.Drawing.PointF | CvPoint2D32f |
| System.Drawing.Size | CvSize |
| System.Drawing.Rectangle | CvRect |
可是,究竟为什么可以这样借用呢?查了很久,也没查出个所以然来,但是最后我用Marshal.SizeOf函数测试Size的大小,得到的结果是8。但这也无法窥得Mashal机制的全貌来~或许问题就在于文中提到过的一句话:
> .NET Framework 类型均有一个默认非托管类型。
那么,`System.Drawing.Size`所对应的非托管类型是什么,又是在哪里定义这个非托管类型的呢?就无从知晓了。