众所周知,OpenCV是一个图形图像处理方面的库,里边封装了许多有用的函数。网上也有许多很实用的教程。但是,大部分都是使用C++进行开发。这段时间一直在做一个项目,过程中踩了不少坑,谨以此文提醒自己,留下记录。首先,上某度查找“C#调用OpenCv”得到的结果大部分是关于EmguCv的使用。不得不说,EmguCv也是一个很好的工具,我在编码过程中也有用到。但是今天不是讲这个。——如果读者想要在C#中使用类似OpenCv的函数,强烈建议先下载一个EmguCv,具体的下载、配置、使用教程不在此说明,请自行查找。如果在EmguCv中找不到想要用的函数,再继续看下去。
本人在编码过程中,使用的是EmguCv3.0版本。使用过其中的二值化、腐蚀、膨胀等方法。但是想要进行线段检测时,却找不到LSD(lineSegmentDetector)方法。在Emgucv的文档中进行搜索,也找不到这个方法。而OpenCv3.0中有一个LineSegmentDetector的类,其中提供了LSD方法。
这里说明一下,其实EmguCv提供了Hough检测的方法,但是我想要的是LSD,所以才要继续研究怎么去调用OpenCv,如果读者想要用的方法已经有了,完全不必再去费心思调用openCv。
查找了某度,实在无果,想到opencv是用C++进行编程的,因此便想利用C# C++混合编程来进行调用:先使用C++编写dll(动态链接库)文件,然后在C#中进行引用。
相关资料:点击打开链接
上述资料讲的是如何编写dll并在C#中调用;要调用openCv还需要进行配置。
具体配置方法也可以参考:点击打开链接
这个配置方法很容易搜到,只是要注意现在要编写的是dll。
然后在dll项目中定义好头文件和源文件:
这里我定义了一个存储结构的头文件myVec.h,一个声明函数的头文件cvRefClass.h。因为要考虑传递数据的类型,而OpenCv中大部分都是InputArray,OutputArray之类的,所以我定义的是一个vector进行存储——因为我要调用的是LSD,结果其实是所有线段的两个端点的横纵坐标,所以采用这种形式。
然后在cvRefClass.cpp中进行方法的实现。
然后对工程生成解决方案。成功之后,可以在Debug目录下看到有dll文件生成。
生成dll文件之后,即可在C#中添加引用。
public ArrayList LsdUsingOpenCv(string path)
{
unsafe
{
//ArrayList res = new ArrayList();
cvRefClass cvClass = new cvRefClass();
sbyte[] sbArray = (sbyte[])((Array)System.Text.Encoding.Default.GetBytes(path));
fixed (sbyte* psb = sbArray) //涉及指针必须在unsafe使用
{
cvClass.lsd(psb); //在这里会对cvClass的数组进行赋值
}
int[] nums = new int[cvClass.arrayLen];
IntPtr datas = (IntPtr)cvClass.datas;
Marshal.Copy(datas, nums, 0, cvClass.arrayLen); //将datas的内容全部复制到nums
ArrayList numList = new ArrayList();
for (int i = 0; i < cvClass.arrayLen; i++)
{
numList.Add(nums[i]);
}
ArrayList list = new ArrayList();
for (int i = 0; i < numList.Count / 4; i++)
{
int x1 = (int)numList[i * 4];
int y1 = (int)numList[i * 4 + 1];
int x2 = (int)numList[i * 4 + 2];
int y2 = (int)numList[i * 4 + 3];
LineSegment2D line = new LineSegment2D(new Point(x1, y1), new Point(x2, y2));
list.Add(line);
}
return list;
}
}
上述代码涉及了一些指针操作,和数据类型的传递问题有关,在此不做赘述,可以参考其他资料获得更完整的数据类型传递说明。
实现了上述方法之后,在C#中是可以进行调用的,传入图片的路径,即可产生相应的ArrayList。
至此,C#中调用openCv的问题算是告一段落。
但是!!!这仍然留着坑。并且在最近坑了我一次。
C#项目中引用了许多dll文件,比如我用了EmguCv,还有自己写的这个dll,在移植到其他主机上时可能会有问题:
首先,如果引用的时候使用的是绝对路径,那么移植到其他主机上,就会因为找不到dll而报错。
这个问题,网上也有许多解决方法。有人说引用了之后会复制dll到debug目录下,这没错,如果引入dll的属性中的“复制本地”(localCopy)选择true,确实会自动复制。但是仍然没用,因为我打包发给别人是整个项目发过去,也就是在debug中也把dll发过去了。网上还说了一些其他的方法,比如反射之类的,但是我没有尝试。这里说一下我的解决方法:
在app.config文件中,增加配置:
比如我的配置是
这个是我之前使用C#调用matlab的时候从网上查到的,但具体网址忘记了,因此没法放出来。若读者调用的是这些第三方工具提供的(matlab能够把.m文件的函数编译成dll)dll,使用这种方式,并对项目重新编译,应该就能移植成功了。即在其他主机上使用是不会再找不到dll库。
但是!!!还是个坑。大家读到这里应该知道,CVRef.dll是我自己编写的dll文件,也是我在自己机器上编译的,移植到其他人的电脑上时,发现其他dll都找得到,就是CVRef.dll找不到。
************** 异常文本 **************
System.IO.FileNotFoundException: 未能加载文件或程序集“CVRef.dll”或它的某一个依赖项。找不到指定的模块。
文件名:“CVRef.dll”
当时我也很纳闷,因为其他都找得到,没理由这个找不到。想了一段时间,注意到“或它的某一个依赖项”,顿时茅塞顿开。赶紧查查他依赖项是什么。
====>打开visual studio->工具->visual studio命令提示
输入dumpbin -dependents d:\CVRef.dll
读者如果不了解这个命令可以查一查,后边跟着的是dll文件的路径。回车之后,输出了几个dll:
openCv的dll是指编译时使用的那个版本的动态链接库文件,另外几个都是放在C盘的Windows下。因为我使用的是VS2012,所以是msvcp110d和msvcr110d。
然后我把这几个放到了debug文件中,在本机测试,发现没问题了(即使我把openCV和CVRef.dll项目都放到其他地方,也可以照常运行)。
但是!!!还是个坑。
我把整个项目重新生成解决方案之后发给其他人,到其他机器上,又提示:“不是win32应用程序。”
这个问题又是困扰了我一段时间。个中辛酸不表,说说如何解决:
将CVRef工程发到那台机器上,在那台机器上进行编译,并把编译生成的CVRef.dll替换掉原有的CVRef.dll(可能还要替换掉依赖)即可运行,C#项目无需在其他机器上重新编译。
事实上,我的那个小伙伴电脑上是只有VS2010,而我只有VS2012,他的VS打不开高版本的解决方案。
到某度上查了之后,用notepad++打开sln文件对其进行替换:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
即可用vs2010打开解决方案文件。
这时候还不能成功编译项目,会提示平台工具集错误。
但要编译的时候,还需要设置:项目->属性->配置属性->常规->平台工具集:选择v100(对于VS2010)
如果是VS2012,其平台工具集是v110.
但这样还不够,如果是直接编译,会提示error LNK2038: 检测到“_MSC_VER”的不匹配项: 值“1700”不匹配值“1600”
想了一下,是因为我在配置项目的时候,选择opencv下的x86->v11->lib文件夹中的dll,这个也是对应VS2012(版本号是11)。而同学的VS2010应该对应v10.
但是下载的opencv中没有提供v10,因此需要自己编译。
使用cmake工具编译:
参考资料:点击打开链接
之后参考v11中,看需要依赖哪些dll,重新指定依赖路径,并重新生成解决方案。
解决方案生成成功之后,将新的dll替换掉旧的dll,并查看其依赖,发现此时依赖的msvcr和msvcp后缀都变成了100,便将这两个也放入debug文件夹下。
运行->成功。
-------------------------2017.05.26更新-------------------------------
最近又遇到一点问题,因为是同一个项目的问题,就写在这了。
我使用一个C#写的exe去调用上面的exe,发现窗口打得开,但是一进行图像处理就报错误找不到组件。
后来查了一些资料,好像和运行时环境有关,不过没有深究,我改为开启一个进程去启动上边的exe,就没问题了。
private void startExe(string path, string name)
{
ProcessStartInfo psi = new ProcessStartInfo();
// 设置启动进程的初始目录
psi.WorkingDirectory = path;
// 设置启动进程的应用程序或文档名
psi.FileName = name;
// 设置启动进程的参数
psi.Arguments = "";
//启动由包含进程启动信息的进程资源
try
{
Process.Start(psi);
}
catch (System.ComponentModel.Win32Exception ex)
{
MessageBox.Show(ex.Message);
return;
}
}
这是启动一个新进程的方法