网上现有资料大多基于 OpenCV 1.x 的,其实基于 C++ 的 OpenCV 2.x 使用面向对象的思想,对各种函数库进行了更好的封装,加入了自动内存管理,相比基于 C 的 1.x 版本要好用的多。当然,2.x 版本的 OpenCV (大多数时候)也能向下兼容 1.x 版。1.x 版本的头文件包含在文件夹“includeopencv”中,而 2.x 版本的头文件包含在文件夹“includeopencv2”中。
OpenCV 2.x 同时提供了 C 语言版和 C++ 版,可根据项目需求选择。C++ 版的头文件扩展名一般为“.hpp”,如“core.hpp”,对应的 C 语言版一般扩展名为“.h”,同时文件名末尾一般会加上“_c”,如“core_c.h”。
C++ 版本所有类和函数等都包含在命名空间“cv”中。函数命名规则上,一般 C 语言版本以“cv”开头,按照驼峰规则命名,如“cvNamedWindow”,对应的 C++ 版本一般去掉了前面的“cv”(因为使用了命名空间),如“namedWindow”。同样,在一些枚举类型上也有类似规则,如 C 版本中的“CV_WINDOW_AUTOSIZE”,对应 C++ 版本为“WINDOW_AUTOSIZE”,当然,这两个版本下的值一般是相同的,所以即使混用,关系也不大。
1.x 版本到 2.x 版本还有一些变化较大的是,在老版本上,图像的处理一般用 IplImage,矩阵处理一般用 cvMat,新版本上全部统一到了 cv::Mat 类,使用更加方便。凡此种种,不一而足。
使用配置
以 32位系统下 Visual Studio 2010 为例,这里假设 OpenCV 安装在“E:opencv”目录下。
建好项目之后,在项目上右键选择属性,在“配置属性”->“VC++目录”选项中的包含目录添加“E:opencvopencv2.4.5opencvbuildinclude”,“库目录”添加“E:opencvopencv2.4.5opencvbuildx86vc10lib”。如果没有把 OpenCV 目录加入到环境变量的话,运行时会提示缺少 dll,可前往“E:opencvopencv2.4.5opencvbuildx86vc10bin”目录下选择所需的 dll 复制到运行目录下。
下面列举一些 OpenCV 的最基本用法,主要使用 OpenCV 2.4.5 C++ 版本。
显示图像
这个相当于 OpenCV 的 hello world。
首先要包含头文件,然后引用命名空间:
1 #include "opencv2/opencv.hpp" 2 using namespace cv;
然后需要引入两个库文件:
1 const string wnd_name = "Test OpenCV"; 2 namedWindow(wnd_name); 3 4 const string file_name = "D:\a.jpg"; 5 Mat img = imread(file_name); 6 imshow(wnd_name, img);
程序能够运行还需要两个对应的 dll:opencv_core245d.dll 和 opencv_highgui245d.dll,将这两个 dll 拷到 Debug 目录下即可。
要显示图像首先需要创建一个窗口,OpenCV 中使用函数 namedWindow() 创建,其原型如下:
void namedWindow(const string& winname, int flags = WINDOW_AUTOSIZE);
第一个参数为标准库 string 类型,表示该窗口的名称,该名称将显示在窗口栏上,OpenCV 用该名称唯一标识一个窗口;
第二个参数表示窗口类型,只有三个可选值:WINDOW_NORMAL、WINDOW_AUTOSIZE、WINDOW_OPENGL,其中 WINDOW_NORMAL 允许用户拖动窗口改变其大小,WINDOW_AUTOSIZE 则自动根据图像尺寸调整大小,不运行用户改变,WINDOW_OPENGL 使用 OpenGL 进行显示。
然后需要读入图像。使用 Mat imread( const string& filename, int flags=1 ); 函数从文件读入图像数据。该函数的第二个参数 flag 有多个取值,默认的为 IMREAD_COLOR,常用的还有 IMREAD_GRAYSCALE 读入为灰度图像。该函数返回值是一个 Mat 类型,具体像素数据存储在 Mat.data 中。
这里有必要说一下 Mat.data。首先,其中的数据是按行存储的,每行占多少个字节由图像的宽度和通道数决定。Mat 中有一个 step 数组,对于二维的图像来说,Mat.step[0] 存储的就是一行(第一维)的步长,也就是一行占用的字节数,Mat.step[1] 存储的为第二维,即一个元素(像素)的宽度,对于 RGB 图像,step[1] 为 3,step[0] 为图像宽度乘以step[1]。其次,Mat.data 中的数据是按照 BGR 存储的。最后,每个像素的数据最好不要直接使用 data 访问,可使用 Mat.at() 等函数访问,这个后面再说。
最后使用 imshow() 函数将读入的数据显示在上面创建好的窗口即可。
在 MFC 控件中显示图像
事实上只是把 OpenCV 自动生成的窗口绑定到 MFC 窗口的控件上,如果只是需要显示图像文件的话,建议使用 GDI++ 实现,效果更佳,这里用 OpenCV 实现主要是为了方便在图像处理过程中直接在界面上看到效果。
1 CWnd* ctl = GetDlgItem(IDC_IMG); 2 CRect rc; 3 ctl->GetClientRect(&rc); 4 5 namedWindow(wnd_name, flags); 6 resizeWindow(wnd_name, rc.Width(), rc.Height()); 7 8 HWND hWnd = (HWND)cvGetWindowHandle(wnd_name.c_str()); 9 HWND hParent = ::GetParent(hWnd); 10 ::SetParent(hWnd, ctl->m_hWnd); 11 ::ShowWindow(hParent, SW_HIDE); 12 13 imshow(wnd_name, img);
随便放一个控件,比如 STATIC_TEXT 控件 IDC_IMG,使用 OpenCV 的方法创建一个名为 wnd_name 的窗口,然后将其大小调整为控件相同大小,再将其绑定到主窗口上,然后隐藏掉 OpenCV 的窗口即可。图像的显示跟平常一样,使用 imshow() 即可。
此法有两个弊端,一是在 OpenCV 创建的窗口隐藏的时候可以看到它闪一下,这个相当不爽,目前还没有找到解决办法;另一个是由于控件大小固定,显示的图像不会自动缩放而是裁剪了,这个在 imshow() 和 namedWindow() 里没有看到解决办法,只能自己进行缩放了。
图像数据修改
这里的修改指的是直接修改像素数据,比如:需要在指定位置画一个绿色的十字叉。
以上面读入的 img 为例,在 cv::Point pt(300, 200) 处画一个单边长度为 len=8 的绿色十字叉,操作如下:
1 Point pt(300, 200); 2 const int len = 8; 3 4 int rows = img.rows; 5 int cols = img.cols; 6 7 for(int i=0; i<len; i++){ 8 // 画横 9 if((pt.x) + i < cols){ 10 img.at<BYTE>(pt.y, ((pt.x) + i) * img.step[1]+0) = 0; 11 img.at<BYTE>(pt.y, ((pt.x) + i) * img.step[1]+1) = 255; 12 img.at<BYTE>(pt.y, ((pt.x) + i) * img.step[1]+2) = 0; 13 } 14 15 if((pt.x) - i >= 0){ 16 img.at<BYTE>(pt.y, ((pt.x) - i) * img.step[1]+1) = 0; 17 img.at<BYTE>(pt.y, ((pt.x) - i) * img.step[1]+1) = 255; 18 img.at<BYTE>(pt.y, ((pt.x) - i) * img.step[1]+2) = 0; 19 } 20 21 // 画竖 22 if((pt.y) + i < rows){ 23 img.at<BYTE>((pt.y) + i, (pt.x) * img.step[1]+0) = 0; 24 img.at<BYTE>((pt.y) + i, (pt.x) * img.step[1]+1) = 255; 25 img.at<BYTE>((pt.y) + i, (pt.x) * img.step[1]+2) = 0; 26 } 27 28 if((pt.y) - i >= 0){ 29 img.at<BYTE>((pt.y) - i, (pt.x) * img.step[1]+0) = 0; 30 img.at<BYTE>((pt.y) - i, (pt.x) * img.step[1]+1) = 255; 31 img.at<BYTE>((pt.y) - i, (pt.x) * img.step[1]+2) = 0; 32 } 33 } 34 imshow(wnd_name, img);
首先,Point 的 x 对应于图像的列,y 对应于行,而 Mat 的访问是按照 (行, 列) 的方式访问,不要搞反了。其次,要注意边界问题。
img.at(pt.y, ((pt.x) – i) * img.step[1]+1) 指按字节访问 img.data 中的第 pt.y 行,第 ((pt.x) – i) * img.step[1]+1 列。列为什么会是这个,在上面 Mat 部分已经讲了,RGB 图像,第 pt.y 行第 pt.x 个像素占了 img.step[1] 个字节,其中第 2 个字节表示“G”通道,将其值修改为 255,其他两个通道修改为 0,则该像素显示为绿色。