在MFC中使用OpenCV
目录[隐藏] |
例程下载
OpenCV与MFC
OpenCV是计算机视觉自由软件的宝库。但是,由于历史的原因它的软件主要采用类似DOS操作系统的命令行方式,使用十分不便,这也影响了它的推广。如果能将它应用到VC++文档结构中就好了。OpenCV程序在MFC中实现的方法通常是采用CvvImage类,这个类的成员函数DrawToHDC可将位图整体经缩放后显示到视图窗口中,解决了位图的显示问题,也就解决了OpenCV在MFC中的使用问题。但是,也有两个致命的弱点,一是显示方式不合图像处理使用习惯,二是位图必须采用CvvImage类。显示方面,虽然已经能够察看图像,但当位图与窗口的长宽比不一致时会造成图像失真,这是浏览器的显示习惯,并不适用于图像处理应用。图像采集与处理的使用习惯是显示比例1:1,图像未经缩放,显示画面可按窗口大小进行裁剪并可使用滚动条选择显示部位。数据结构方面,采用CvvImage类以后程序中所有位图必须修改成这个类,这对于利用大量OpenCV现成软件来说是十分不方便的。显然,在VC++文档结构中使用OpenCV,其关键还是在于OpenCV位图在MFC中的显示。因此,有必要先比较一下两者的位图结构,然后寻找新的解决方法。
StretchDIBits 函数
由于OpenCV位图结构中的像素数据与DIB中的像素具有类似的存储结构,可以考虑直接用来在视图窗口中显示。知道位图像素的存放地址直接往视图窗口显示的函数虽然不多,但还是有Windows API中的StretchDIBits函数可以利用,由下面列出的StretchDIBits函数原型中可知,只要为它构造一个DIB的位图信息就行了。
int StretchDIBits( HDC hdc, // 显示设备句柄 int XDest, YDest, nDestWidth, nDestHeight, // 目标矩形区域参数 int XSrc, YSrc, nSrcWidth, nSrcHeight, // 源矩形区域参数 // 源区域与目标区域参数相同时为 1:1 比例显示 CONST VOID *lpBits, // 位图的像素存放首地址 CONST BITMAPINFO *lpBitsInfo, // 位图信息存放地址 UINT iUsage, // 位图中的颜色类型,RGB模式用DIB_RGB_COLORS DWORD dwRop // 像素操作码,简单复制用SRCCOPY );
为此,演示程序CCVMFC中增加了3个函数,即CtreateMapInfo、imageClone与imageReplace。其中,CtreateMapInfo 函数用于建立OpenCV位图的位图信息,其特点是可以为单通道位图设置黑白灰阶调色板。ImageClone函数使用OpenCV函 数实现位图的复制,自动释放老的指针所指向的存储单元以防止内存泄漏,同时返回的m_dibFlag标志可以用于激发刷新工 作位图workImg的位图信息m_lpBmi (见后面说明)。imageReplace与ImageClone相似,但不建立新位图,只用输入位图替 换输出位图。
LPBITMAPINFO CtreateMapInfo(IplImage* workImg) // 建立位图信息 { BITMAPINFOHEADER BIH={40,1,1,1,8,0,0,0,0,0,0}; LPBITMAPINFO lpBmi; int wid, hei, bits, colors,i; RGBQUAD ColorTab[256]; wid =workImg->width; hei =workImg->height; bits=workImg->depth*workImg->nChannels; if (bits>8) colors=0; else colors=1<<bits; lpBmi=(LPBITMAPINFO) malloc(40+4*colors); BIH.biWidth =wid; BIH.biHeight =hei; BIH.biBitCount=(BYTE) bits; memcpy(lpBmi,&BIH,40); // 复制位图信息头 if (bits==8) { // 256 色位图 for (i=0;i<256;i++) { // 设置灰阶调色板 ColorTab[i].rgbRed=ColorTab[i].rgbGreen=ColorTab[i].rgbBlue=(BYTE) i; } memcpy(lpBmi->bmiColors, ColorTab, 1024); } return(lpBmi); }
int imageClone(IplImage* pi, IplImage** ppo) // 复制 IplImage 位图 (OpenCV) { if (*ppo) cvReleaseImage(ppo); // 释放原来位图 (*ppo) = cvCloneImage(pi); // 复制新位图 return(true); }
演示程序
演示程序CVMFC采用VC++多文档带滚动条结构,图像的存放与处理则采用OpenCV的位图结构与函数,图像的显示通过建立相应的位图信息m_lpBmi来实现,为了便于管理对m_lpBmi的操作集中在OnDraw程序中。待显示位图结构发生改变时用m_dibFlag标志激发m_lpBmi的刷新。除了文件结构与图像显示外,其馀部分基本上都是OpenCV程序。
位图数据:
CVMFCDoc中, pImg (读入图像文件所得原始位图)
CVMFCView中, workImg (工作位图)、saveImg (备份位图)、m_lpBmi (工作位图的位图信息)
CVDSCap中, m_Frame (视频采集所得位图)
视图的显示管理集中在OnDraw函数中:
void CCVMFC0View::OnDraw(CDC* pDC) { …… // 其馀部分内容 if (m_dibFlag) { // 刷新 DIB位图信息 if (m_lpBmi) free(m_lpBmi); m_lpBmi = CtreateMapInfo(workImg); m_dibFlag = 0; } if (workImg) { // 刷新视图窗口 StretchDIBits(pDC->m_hDC, 0, 0, workImg->width, workImg->height, 0, 0, workImg->width, workImg->height, workImg->imageData, m_lpBmi, DIB_RGB_COLORS, SRCCOPY); } }
像素数据类型
图像处理主要是对像素数据的处理。需要注意的是,在OpenCV中像素存放地址imageData为char* 类型。因此,在图像处理时必须转换成BYTE* 类型才可以使用。下面以识别图像类型函数imageType为例来作说明。
int imageType(IplImage* p) // 识别图像类型 { int i,j,k,bpl,n,pg[256]; BYTE *buf; k=p->nChannels; // 1 与 3 分别表示灰阶图像与彩色图像 if ( k==1 ) { // 检查二值图像 for ( i = 0; i < 256; i++ ) pg[i] = 0; buf = (BYTE*) p->imageData; // 修改像素数据类型 bpl = p->widthStep; for ( i = 0; i < p->height; i++ ) { for ( j = 0; j < p->width; j++) pg[buf[j]]++; // 直方图统计 buf += bpl; } for ( i = 0, n = 0; i < 256; i++ ) if (pg[i]) n++; // 统计使用色阶数 if (n == 2) k = -1; // -1 表示二值图像 } return(k); }
图像镜像
在位图的像素方面,除了imageData的类型问题外,DIB位图与IplImage结构间还有一个显着的不同,那就是坐标原点位置的不同。前者的坐标原点在位图底部左侧,而后者在顶部左侧。因此,当在OpenCV中需要使用MFC的函数显示时位图应作垂直镜像,反之亦然。典型的例子是在OpenCV中调用DirectShow视频采集程序CameraDS中的获取当前帧函数QueryFrame,其程序如下:
IplImage* CCameraDS::QueryFrame() { …… m_pSampleGrabber->GetCurrentBuffer(&m_nBufferSize, (long*) m_pFrame->imageData); cvFlip(m_pFrame); return m_pFrame; }
演示程序CVMFC的图像输入输出采用OpenCV的cvLoadImage与cvSaveImage函数实现,而显示采用Windows API中的 StretchDIBits函数。为了能正常工作,图像读入后需作垂直镜像,图像存盘前也需作垂直镜像。也就是说,内存中存放的是 经过垂直镜像的OpenCV位图。同样,因为结构相同,它也是DIB位图的像素数据。
BOOL CCVMFCDoc::Load(IplImage** pp,LPCTSTR csFileName) { IplImage* pImg=NULL; pImg = cvLoadImage(csFileName,-1); // 读图像文件 if (!pImg) return(false); cvFlip(pImg); // 使与 DIB 像素结构一致 if (*pp) cvReleaseImage(pp); (*pp)=pImg; m_Display=0; return(true); }
BOOL CCVMFCDoc::Save(LPCTSTR csFileName,IplImage* pImg) { int bl; cvFlip(pImg); // 恢复原 OpenCV 位图结构 bl=cvSaveImage(csFileName,pImg); // 图像存盘 return(bl); }
对于大多数的图像处理算法来说位图的镜像与否没有什么影响。但是,对于某些OpenCV函数,例如涉及旋转方向以及需要往IplImage结构的位图上绘制图形、显示文字时就会使位置出错。这时就需要与存盘时一样先作垂直镜像,旋转角度反向,操作结束返回Windows视图显示处理结果时再转换回来。
驱动模式与人机交互
众所周知,DOS操作系统采用过程驱动与文本模式,Windows操作系统采用事件驱动与图形模式,它们分别使用键盘与鼠标器作为主要输入工具。OpenCV编程环境基于DOS操作系统,因此显示图像需要调用cvNamedWindow函数建立专门的窗口,然后调用cvShowImage函数进行显示。画面的保持则使用cvWaitKey(0)语句,或使用内含cvWaitKey语句的死循环来实现。为了避免出现死机故障,出现OpenCV函数所开窗口时,务必使用ESCAPE键关闭窗口并退出。注意:菜单中带 (ESC) 字样的命令必须使用ESCAPE键退出。
同时,OpenCV编程环境人机交互的功能有限,只有鼠标、键盘与滑动条。其中,鼠标与滑动条属于事件驱动,而键盘属于过程驱动。由于没有菜单功能,只能使用键盘命令控制程序走向并等待键盘输入选择不同功能。
在驱动模式方面为了便于比较,提供了两个[点集凸包]命令,一个采用过程驱动,另一个采用事件驱动,不妨比较具体功能及其实现的程序。
程序移植例
演示程序CVMFC中待处理图像与处理结果都放在工作位图workImg中。同时,工作位图workImg也是窗口画面显示的内容。所以,处理程序中仅源图像的获取与结果图像的显示与OpenCV中的程序有所不同,中间处理部分可以完全相同,现以《学习OpenCV》一书中的例2-6为例来作修改说明。
void CCVMFC0View::OnCannyBorddetec() // Canny 边缘检测 { // 根据《学习OpenCV》例 2-6 改编 // 定义工作位图 IplImage* pImage; IplImage* pImg8u = NULL; IplImage* pImg8uSmooth = NULL; IplImage* pImgCanny = NULL; //** 输入待处理图像 ** // 修改部分 1 pImage = workImg; // 建立辅助位图 pImg8u =cvCreateImage(cvGetSize(pImage),IPL_DEPTH_8U,1); pImg8uSmooth=cvCreateImage(cvGetSize(pImage),IPL_DEPTH_8U,1); pImgCanny =cvCreateImage(cvGetSize(pImage),IPL_DEPTH_8U,1); // 图像处理 cvCvtColor(pImage, pImg8u,CV_BGR2GRAY); cvSmooth(pImg8u, pImg8uSmooth,CV_GAUSSIAN,3,0,0); cvCanny(pImg8uSmooth, pImgCanny,100,200,3); // 释放位图 cvReleaseImage(&pImg8u); cvReleaseImage(&pImg8uSmooth); //** 输出处理结果 ** // 修改部分 2 m_dibFlag = ImageReplace(pImgCanny, &workImg); //** 设置标志及刷新窗口 ** m_ImageType=1; Invalidate(); }
形参书写顺序
演示程序CVMFC是Windows API系统与OpenCV两种体系函数的混合使用,而它们的函数参数的书写习惯是不同的,极容易引起混淆。这里作一简要说明,请看下面例子:
目标参数Dest <-- 源参数Src
C: memcpy( pDest, pSrc, size); Windows API: BitBlt( hDestDC, 0, 0, wid, hei, hSrcDC, 0, 0, SRCCOPY); VC++: pDestDC->BitBlt( 0, 0, p->wid, p->hei, pSrcDC, 0, 0, SRCCOPY);
源参数Src --> 目标参数Dest
OpenCV: cvCvtColor( pSrcImg, pDestcImg, CV_BGR2GRAY); cvSmooth( pSrcImg, pDestcImg, CV_GAUSSIAN, 3, 0, 0);
演示程序中除了OpenCV函数采用后一书写形式外,其他函数都采用前一书写形式,千万注意不能混淆。OpenCV函数很 容易辨认,都以前缀cv-开头,如cvCvtColor、cvSmooth。新增函数ImageClone与imageReplace因为调用OpenCV函数, 故也采用后一书写形式。
CVMFC 1.1版菜单
[文件]:打开图像、恢复图像、关闭当前窗口、保存当前位图、恢复原始图像、当前画面存盘、退出
[点处理]:彩色变灰阶、图像反相、垂直镜像、水平镜像、180 度旋转、30 度旋转、仿射变换、透视变换、亮度变换、灰阶图像直方图、直方图均衡化
[邻域处理]:邻域平均、Gauss 滤波、中值滤波、Sobel 边缘检测、Laplace 边缘检测
[二值化]:选择阈值、选择阈值(原位图)、自适应阈值法、基本全局阈值法
[二值图像处理]:点集最小区域、外接矩形、最小面积矩形、多边形逼近、点集凸包、点集凸包 (事件驱动)、区域凸包、区域凹差、轮廓跟踪、距离变换
[形态学处理]:腐蚀、膨胀、开运算、闭运算、形态学梯度、顶帽变换、波谷检测
[彩色图像处理]:RGB 分量、HSV 分量、Lab 分量、RGB 分量 (C)、XYZ 分量、YCrCb 分量、Luv 分量、二维直方图、邻域平均、Gauss 滤波、中值滤波、Sobel 边缘检测、Laplace 边缘检测
[综合处理例]:图像缩小、径向梯度、Canny 算法、Hough 变换 (直线)、"Hough 变换 (圆)、平行四边形检测、连通区域填充、金字塔法图像分割、椭圆曲线拟合、Snake 原理、分水岭原理、角点检测、点集聚类、分割二维点集、旋转点跟踪、人脸检测
[动态检测]:动态边缘检测、L_K 算法光流跟踪、背景建模检测、运动目标检测、彩色目标跟踪、人脸检测二
[视频采集播放]:启动摄像头、打开 AVI 文件、视频解冻、视频冻结、多图像平均、关闭视频、选择采样分辨率
[图形及其他]:绘制图形、绘制 Delaunay 图形、极坐标变换、DFT