《Opencv计算机视觉编程攻略(第二版)》书中例子只给出了部分关键代码,将实现例子的笔记整理出来做为学习笔记。
本文是书中3.3节:用控制器设计模式实现功能模块间的通信的内容。
主要步骤
①创建一个基于对话框的MFC工程并绘制所需按钮
②在工程中添加3.2节编写的算法类头文件colordetector.h
③在MFC工程头文件中定义控制器类ColorDetectController
④在对话框类的定义中添加控制器类变量controller(书中变量名为colordetect)
⑤添加两个按钮open image和process的响应代码
具体步骤
1.创建工程并绘制对话窗
新建基于对话框的MFC工程opencv-ColorDetectController。并配置好opencv。
在对话框中添加按钮如下图所示,将open和process按钮的ID分别修改为ID_OPEN和ID_PORCESS。
2.添加算法类头文件colordetector.h
(1)添加算法类头文件至工程
将第3.2节中编写的算法类头文件colordetector.h拷贝到本MFC工程目录下(该头文件中封装了ColorDetector算法类代码),并在解决方案资源管理器中添加该头文件。此步骤相当于将编写好的算法以类的方式添加进MFC工程。
将算法类头文件添加进工程
(2)在MFC工程头文件中包含算法类头文件
因为用到之前编写的ColorDetector算法类,因此需要在MFC工程的头文件opencv-ColorDetectController.h中添加对该算法类头文件colordetector.h的包含。
#if !defined CD_CNTRLLR #define CD_CNTRLLR #include "stdafx.h" #include <opencv2/highgui/highgui.hpp> #include "colordetector.h" //添加对算法类头文件的包含3.在MFC工程头文件中定义Controller类
利用Controller类,开发人员可以很容易的构建执行算法的程序接口,既不需要理解类与类是如何连接的,也不需要知道为了让所有的类正确运行需要调用哪个类的哪个方法。所有的这些工作都由Controller类来完成。唯一需要做的就是定义一个Controller类的实例。下文将介绍如何在应用程序中定义Controller类。
在本MFC工程头文件opencv-colorDetectController.h中定义Controller类。Controller类的首要任务是创建执行程序所需的类。本例中只有一个控制器类ColorDetectController,但更复杂的程序会有多个类。
以下将逐步讲解头文件中的ColorDetectController类定义。
(1)定义控制器类私有变量:算法和图像缓存
该类中首先添加对算法类ColorDetector的使用。另外需要有两个成员变量,作为输入输出结果的引用。class ColorDetectController { //定义控制器类(开头部分代码) private: // the algorithm class ColorDetector *cdetect; cv::Mat image; // The image to be processed cv::Mat result; // The image result(2)创建控制器类构造函数
创建ColorDetectController类的构造函数。这里采用动态分配类的方式,也可以简单地声明类的变量。public: ColorDetectController() { // private constructor //setting up the application cdetect= new ColorDetector();(3)在控制器类中定义所有设置和获取方法
为了部署算法,需要在控制器类ColorDetectController中定义用于控制程序的所有设置方法和获取方法。通常,这些方法都只需要简单的调用相关算法类的中对应的方法。这个例子只用到一个算法类ColorDetector,实际开发中会遇到多个。因此,controller的作用就是讲请求重新定向到相关类(在面想对象的编程中,这种机制称为委托)。
本例中需要定义的方法包括(均在控制器类定义的public下):
// Sets the colour distance threshold设置颜色差距的阈值 void setColorDistanceThreshold(int distance) { cdetect->setColorDistanceThreshold(distance); } // Gets the colour distance threshold获取颜色差距阈值 int getColorDistanceThreshold() const { return cdetect->getColorDistanceThreshold(); } // Sets the colour to be detected设置目标颜色的方法 void setTargetColor(unsigned char red, unsigned char green, unsigned char blue) { cdetect->setTargetColor(blue,green,red); } // Gets the colour to be detected获取目标颜色的方法 void getTargetColour(unsigned char &red, unsigned char &green, unsigned char &blue) const { cv::Vec3b colour= cdetect->getTargetColor(); red= colour[2]; green= colour[1]; blue= colour[0]; }(4)在控制器类中定义输入图像和获得该图像的方法
在控制器类的定义中,需要定义如何读取图像的方法,以及如何获取输入的图像的方法。// Sets the input image. Reads it from file.输入图像,从文件中读取 bool setInputImage(std::string filename) { image= cv::imread(filename); return !image.empty(); } // Returns the current input image.返回当前的输入图像 const cv::Mat getInputImage() const { return image; }(5)在控制器类中定义启动处理过程方法
定义一个启动处理过程的方法,供以后调用。// Performs image processing.启动处理过程的方法 void process() { result= cdetect->process(image); }(6)在控制器类中定义获取处理结果的方法
需要一个方法来获取处理结果
// Returns the image result from the latest processing. const cv::Mat getLastResult() const { return result; }(7)在控制器类中定义析构函数
最后,非常重要的一步是在程序结束时做内存清理,即释放Controller类,在程序结束时清理内存
// Deletes all processor objects created by the controller.删除由控制器创建的对象 ~ColorDetectController() { delete cdetect; // 释放动态分配的实例的内存 } }; // 控制器类定义的结束 #endif // 接最前面的预编译指令4.在MFC工程对话框类中添加Controller类成员变量
现在只需要在对话框类(这里为Copencv-ColorDetectControllerDlg类)中添加一个ColorDetectController成员变量controller,即可创建一个最基本的使用了控制器的对话框程序了。
(1)向对话框类Copencv-ColorDetectControllerDlg中添加Controller成员变量
通过类向导向Copencv-ColorDetectControllerDlg类中添加ColorDetectController类型的成员变量controller。添加完控制器变量后,该对话框类定义代码的最后将自动添加了如下图红框所示的代码。
void CopencvColorDetectControllerDlg::OnBnClickedOpen() { // TODO: 在此添加控件通知处理程序代码 // 选择bmp或jpg类型文件的MFC对话框 CFileDialog dlg(TRUE,_T("*.bmp"),NULL, OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST|OFN_HIDEREADONLY, _T("image file(*.bmp;*,jpg)|*.bmp;*.jpg|ALL Files(*.*)|*.*||"),NULL); dlg.m_ofn.lpstrTitle=_T("Open Image"); //如果选中了一个文件名 if(dlg.DoModal()==IDOK) { //取得选定文件名的完整路径 //Cstring->std::string CString cstrfilename = dlg.GetPathName();//.GetPathName返回的是CString类型的字符串 std::string filename; setlocale(LC_ALL, "chs"); char *p = new char[256]; wcstombs( p, cstrfilename, 256 ); filename = p; //设置并显示输入的图像,注意controller变量已经在前文向对话框类中添加过了 controller.setInputImage(filename); //.setInputImage方法输入的为std::string类型的字符串 cv::imshow("Input Image",controller.getInputImage()); } }这里要注意的是,书中使用.GetPathName()方法,返回的是CString类型的字符串表示的文件路径,而.setInputImage方法输入要求为std::string类型的字符串,因此需要转换。转化方法为网上下载,应该有更简单的方法。
打开图片的效果
(3)process按钮的处理程序
双击process按钮进入其处理程序代码位置。添加处理代码如下:
void CopencvColorDetectControllerDlg::OnBnClickedProcess() { // TODO: 在此添加控件通知处理程序代码 //这里颜色采用硬编码 //controller.setTargetColor(130,190,230);//源代码的目标颜色 controller.setTargetColor(140,174,209); //根据读入城堡图像调整了一下目标颜色 //处理输入图像并显示结果 controller.process(); cv::imshow("Output Result",controller.getLastResult()); }注意,书上ColorDetectController类型的成员变量定义为colordetect,而本文中定义的控制器实例名称为controller(即控制器变量名称)(见3.3.4节(1)小节),因此,注意将书中的名称colordetect改为controller。通过process按钮的颜色检测结果5.使用MVC架构为程序增加肤色检测功能
书中的扩展阅读介绍了MVC架构。MVC架构的目的是生成一个能把程序逻辑与用户接口清晰地隔离的程序。MVC主要包含三个组件:模型、视图、控制器。
模型存放与应用程序有关的信息。他控制着程序的所有数据,当产生新数据时,它会通知控制器,然后控制器通知视图显示新结果。
视图相当于用户接口。它由不同的窗口组成,窗口可以向用户展示数据并且允许用户与程序交互。视图的功能之一就是将用户发出的命令发送给控制器。当有新数据时,视图会刷新以显示新的信息。
控制器是连接视图和模型的模块。它从视图接收请求,并转发给模型中对应的方法。模型状态发生变化时会通知控制器,然后控制器通知视图进行刷新,以显示新信息。
下面我们将扩展阅读介绍的不同的色彩空间的知识,以及在HSV色彩空间中搜寻特定颜色物体的例子添加到MFC程序中,为程序增加一项检测肤色的功能。
注:在对特定物体做初步检测时,颜色信息非常有用。例如在辅助驾驶程序中检测路标,就要凭借标准路标的颜色快速地提取可能是路标的信息。另一个例子是检测皮肤的颜色,检测到的皮肤区域可作为图像中有人存在的标志。在手势识别中经常使用这个方法,用肤色检测来确定手的位置。
下面将书中的肤色检测的例子按照MVC模式,添加到刚才编写的MFC程序中。
(1)模型层:在算法类中定义肤色检测方法
为了给算法添加肤色检测功能,需要在ColorDetecor算法类中定义一个肤色检测方法。打开包含算法类的头文件colordetector.h,在类定义中添加如下肤色检测代码:
//增加肤色检测方法 void detectHScolor(const cv::Mat& image, // input image double minHue, double maxHue, // Hue interval double minSat, double maxSat, // saturation interval cv::Mat& mask) { // output mask输出肤色掩码 // convert into HSV space cv::Mat hsv; cv::cvtColor(image, hsv, CV_BGR2HSV); // split the 3 channels into 3 images std::vector<cv::Mat> channels; cv::split(hsv, channels); // channels[0] is the Hue 色调 // channels[1] is the Saturation 饱和度 // channels[2] is the Value 亮度 // Hue masking cv::Mat mask1; // under maxHue cv::threshold(channels[0], mask1, maxHue, 255, cv::THRESH_BINARY_INV); cv::Mat mask2; // over minHue cv::threshold(channels[0], mask2, minHue, 255, cv::THRESH_BINARY); cv::Mat hueMask; // hue mask if (minHue < maxHue) hueMask = mask1 & mask2; else // if interval crosses the zero-degree axis hueMask = mask1 | mask2; // Saturation masking // under maxSat cv::threshold(channels[1], mask1, maxSat, 255, cv::THRESH_BINARY_INV); // over minSat cv::threshold(channels[1], mask2, minSat, 255, cv::THRESH_BINARY); cv::Mat satMask; // saturation mask satMask = mask1 & mask2; // combined mask组合掩码 mask = hueMask&satMask; }(2)视图层:在对话框中添加detect skin按钮
在对话框中使用工具添加按钮,增加一个处理按钮,并名称和ID分别改为detect skin和ID_DETECTSKIN。
添加肤色检测按钮
添加完按钮后,双击按钮进入响应函数编写,添加如下按钮响应代码:
void CopencvColorDetectControllerDlg::OnBnClickedDetectskin() { // TODO: 在此添加控件通知处理程序代码 controller.detectHScolor();//调用检测肤色的算法 cv::imshow("Output detected skin",controller.getLastResult());//调用获取检测结果的方法显示结果 }上面的代码表示,视图层通过按钮来调用控制器层运行检测肤色的方法controller.detectHScolor(),按照上文在控制器中定义方法的命名规则,我们也将该方法命名为detectHScolor()。该方法的算法实现为算法类中添加的detectHScolor()方法,控制器只是将视图层的请求重新定向到算法类(即实现委托)。
(3)控制器层:在控制器类中定义肤色检测方法
如何将算法层与视图层连接起来呢?这就要通过控制器层,下面将介绍控制器层如何实现它在MVC架构中的作用。
控制器是连接视图和模型的模块。它从视图接收请求,并转发给模型中对应的方法。模型状态发生变化时会通知控制器,然后控制器通知视图进行刷新,以显示新信息。
这里我们在控制器类中添加检测肤色的方法供视图层调用,而实际的处理则是由控制器层通知模型层的算法来处理。
在控制器类ColorDetectController中定义肤色检测方法,代码如下:
void detectHScolor() { cv::Mat mask; cdetect->detectHScolor(image,160,10,25,166,mask);//调用算法类中的肤色检测算法,返回mask //显示掩码后的图像 image.copyTo(result,mask); }控制器类中的肤色检测方法detectHScolor将输入图像image传递给模型层的算法cdetect->detectHScolor(…),由算法检测肤色后返回检测结果掩码mask。接下来,控制器的detectHScolor方法再将mask应用到输入图像上image.copyTo(result,mask),生成只有肤色部分的图像result。这时,视图层调用方法controller.getLastResult()就可以从控制器层获得最终的肤色检测结果了。
打开一幅有人物的图像
显示肤色检测结果
肤色检测的参数设置在控制器层的controller.detectHScolor()方法中,下一步还可以将参数设置改到视图层作为用户与程序的交互功能。