介绍如何为 OpenCV 程序搭建 MVC(模型-视图-控制器) 架构。

最近在读《OpenCV 2 Computer Vision Application Programming Cookbook》,书中的第三章介绍了如何了在进行 OpenCV 开发时常用的几种设计模式,包括策略模式、单例模式,以及如何搭建 MVC(模型-视图-控制器)架构。内容精彩,让人受益良多。

是否应该在程序中应用设计模式,这一直以来都是一个饱受争议的话题。但这并非本文的讨论重点。笔者的观点是,如果你需要在程序开发中使用面向对象的思想,那就不得不应用设计模式进行设计和重构,以期让自己的程序更加合理。

应该说,OpenCV 和设计模式并没有什么必然的联系,在 OpenCV 程序中实现 MVC 也没有什么特别的魔法。但在这之前,我已经读完了被称为 Gang of Four 的《设计模式:可复用面向对象软件的基础》,感觉内容有点偏学术,不太好读。而通过 OpenCV 的实际案例来掌握几种设计模式,比起直接去阅读各种设计模式的定义要容易让人接受得多。

好东西当然要拿出来分享,所以我结合自己的理解,重新组织了这部分的内容(本文源代码可以在 这里 找到)。

什么是 MVC 架构

MVC 架构是一种将用户界面和内部逻辑清晰地分离的手段,它由三个部分构成: OpenCV 2 Computer Vision Application Programming Cookbook附图 1 OpenCV 2 Computer Vision Application Programming Cookbook

  • 模型(Model) 保存着与应用相关的数据信息。模型保存着所有需要交给应用程序处理的数据。当产生新的数据时,模型就通知它的视图,而视图将与模型通信以展示这些新数据。
  • 视图(View) 与用户的界面对应。视图由多个不同的窗口元素组成,用于将数据展现给用户,并与用户交互。视图的一个作用是将用户的指令发送给控制器。当有可用的新数据时,视图将得到刷新,并展现新内容给用户。
  • 控制器(Controller) 是连接模型和视图的桥梁。控制器接收从视图发送过来的请求,并将之交付给相应的视图进行处理。当模型的状态发生变化时,控制器也会得到通知,并与视图通信以展示新信息。

下图显示了一个模型和三个视图(姑且可以把中间的线条当做控制器)。模型包含一些数据,视图通过电子表格、柱状图、饼图这些不同的方式来显示这些数据。数据在从模型到视图的过程中是由控制器来控制的,不同的控制器决定着不同的呈现方式。

MVC架构
MVC架构

下面将通过实际案例,一步步讲解搭建 MVC 架构的方法。

案例需求

现在我们需要编写一个程序 ColorDetector ,用于检测一幅图片中是否含有与目标颜色相近的颜色,并生成一幅和源图像一样大的二值图像,用白色填充这些像素所对应的位置,用黑色填充其他位置。如下图所示,左边的图像为源图像,中间的色块为待检测的目标颜色,右边的图像为最终得到的结果图。我们可以很明显的看出源图像中色调稍红的部分都变成了白色:

颜色检测算法
颜色检测算法

颜色检测算法的思路很简单:对图像做一次遍历,并计算每个像素的颜色与目标颜色的欧氏距离,下面给出一个未经重构的简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/** 
* detectColor - detect color with target
*
* @param image - the color to be detected
* @param target - the target color
* @param minDist - minimum distance
*
* @return the detected result
*/
cv::Mat detectColor(const cv::Mat &image,
const cv::Vec3b &target,
const int minDist = 100) {
// create a Mat container for storing result
cv::Mat result;
// same size as input image, but 1-channel
result.create(image.rows,
image.cols,
CV_8U);

// get the iterators
cv::Mat_<cv::Vec3b>::const_iterator it =
image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::const_iterator itend =
image.end<cv::Vec3b>();

cv::Mat_<uchar>::iterator itout =
result.begin<uchar>();

// for each pixel
for ( ; it!=itend; ++it, ++itout) {
// process each pixel
// compute distance from target color
if (getDistance(*it, target) < minDist) {
*itout = 255;
} else {
*itout = 0;
}
} // end of pixel precessing
return result;
}

编写模型(Model)

对于图像处理程序,模型是若干个算法模块的组合,所有这些算法模块都是模型的一部分。这些模块往往采取 策略(Strategy)模式 来实现。 Design Patterns: Elements of Reusable Object-Oriented Software附图 2 Design Patterns: Elements of Reusable Object-Oriented Software GoF 将策略模式定义为:

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

GoF

策略模式的目标是将一个算法封装成类。应用这种模式后,我们可以很方便的将一个算法替换成另一个算法,或将几个算法串起来以形成一个更加复杂的流程。另外,通过只暴露程序接口而隐藏算法实现细节,应用该模式可以很有效的降低算法的使用难度。

下面使用策略模式,将原始的 detectColor() 函数封装成类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class ColorDetector
{
public:

// empty constructor
ColorDetector() : minDist(100) {
// default parameter initialization here
target[0] = target[1] = target[2] = 0;
}
// Set the color distance threshold.
void setColorDistanceThreshold(int distance);
// Get the color distance threshold.
int getColorDistanceThreshold() const;
// Set the target color.
void setTargetColor(unsigned char red,
unsigned char green,
unsigned char blue);
// Get the target color.
const cv::Vec3b getTargetColor();
// Core process of color detect algorithm.
cv::Mat process(const cv::Mat &image);

private:
// minimum acceptable distance
int minDist;
// target color
cv::Vec3b target;
// image containing resulting binary map
cv::Mat result;
// Computes the distance from target color
int getDistance(const cv::Vec3b &color) const;

};

/**
* getDistance - Computes the distance from target color.
*
* @param color - the color to be compared to
* @param target - target color
*
* @return the Euclidean distance
*/
int ColorDetector::getDistance(const cv::Vec3b &color) const
{
return abs(color[0] - target[0]) +
abs(color[1] - target[1]) +
abs(color[2] - target[2]);
}

/**
* setColorDistanceThreshold - Set the color distance threshold.
*
* Threshold must be positive.
* otherwise distance threshold is set to 0.
*
* @param distance - threshold.
*/
void ColorDetector::setColorDistanceThreshold(int distance)
{
if (distance < 0)
distance = 0;
minDist = distance;
}

/**
* getColorDistanceThreshold - Get the color distance threshold.
*
* @return the color distance
*/
int ColorDetector::getColorDistanceThreshold() const
{
return minDist;
}

/**
* setTargetColor - Set the target color.
*
* @param red - value of red channel
* @param green - value of green channel
* @param blue - value of blue channel
*/
void ColorDetector::setTargetColor(unsigned char red,
unsigned char green,
unsigned char blue)
{
// BGR order
target[2] = red;
target[1] = green;
target[0] = blue;
}

/**
* getTargetColor - Get the target color.
*
* @return the target color
*/
const cv::Vec3b ColorDetector::getTargetColor()
{
return target;
}

/**
* process - Core process of color detect algorithm.
*
* @param image - image for detecting color
*
* @return - detect result
*/
cv::Mat ColorDetector::process(const cv::Mat &image) {
// re-allocate binary map if neccessary
// same size as input image, but 1-channel
result.create(image.rows, image.cols, CV_8U);

// get the iterators
cv::Mat_<cv::Vec3b>::const_iterator it =
image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::const_iterator itend =
image.end<cv::Vec3b>();
cv::Mat_<uchar>::iterator itout =
result.begin<uchar>();

// for each pixel
for ( ; it!=itend; ++it, ++itout) {
// process each pixel
// compute distance from target color
if (getDistance(*it) < minDist) {
*itout = 255;
} else {
*itout = 0;
}
} // end of pixel precessing
return result;
}

其中:

  • process() 函数是核心的处理函数,和 detectColor() 函数作用相同;
  • setColorDistanceThreshold()getColorDistanceThreshold() 函数分别用于设置和获取颜色阈值,即 minDist ;
  • setTargetColor()getTargetColor() 函数分别用于设置和获取目标颜色;
  • getDistance() 函数用于计算两个颜色向量的欧氏距离。

至此,模型所需要用到的算法已经编写完成。我们可以写程序测试使用这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main()
{
cv::Mat img = cv::imread("./lena.png");

if (!img.data) {
perror("Open file failed!");
return 1;
}

cv::namedWindow("image", CV_WINDOW_AUTOSIZE);
cv::imshow("image", img);

ColorDetector *processor = new ColorDetector();
processor->setTargetColor(190, 80, 80);
cv::Mat result = processor->process(img);

cv::namedWindow("result", CV_WINDOW_AUTOSIZE);
cv::imshow("result", result);

char k;

while(1){
k = cv::waitKey(0);
if (k == 27)
break;
}
return 0;
}

通过这样的封装,原来对算法函数 detectColor() 的调用被抽象成了更加清晰可读的 ColorDetector 类,调用该算法只需要实例化该类,并调用 process() 函数,而具体的参数则可以通过一系列的 get 、set 函数来设置。这种接口风格可以被其他算法函数沿用,从而减少应用开发者的记忆负担,有利于提高可用性。

编写控制器(Controller)

编写 ColorDetectController 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class ColorDetectController {
private:
// pointer to the singleton
static ColorDetectController *singleton;
// the algorithm class
ColorDetector *cdetect;
// private constructor
ColorDetectController() {
// setting up the application
cdetect = new ColorDetector();
}
cv::Mat image; // The image to be processed
cv::Mat result; // The image result
public:
// Deletes processor objects created by the controller.
~ColorDetectController() {
delete cdetect;
}
// Gets access to Singleton instance
static ColorDetectController *getInstance() {
// Creates the instance at first call
if (singleton == 0)
singleton = new ColorDetectController;
return singleton;
}
// Release the singleton instance of this controller.
static void destroy() {
if (singleton != 0) {
delete singleton;
singleton = 0;
}
}
// Sets the color distance threshold
void setColorDistanceThreshold(int distance) {
cdetect->setColorDistanceThreshold(distance);
}
// Gets the color distance threshold
int getColorDistanceThreshold() const {
return cdetect->getColorDistanceThreshold();
}
// Sets the color to be detected
void setTargetColor(unsigned char red,
unsigned char green, unsigned char blue) {
cdetect->setTargetColor(red,green,blue);
}
// Gets the color to be detected
void getTargetColor(unsigned char &red,
unsigned char &green, unsigned char &blue) const {
cv::Vec3b color= cdetect->getTargetColor();
red= color[2];
green= color[1];
blue= color[0];
}
// Sets the input image. Reads it from file.
bool setInputImage(std::string filename) {
image = cv::imread(filename);
if (!image.data)
return false;
else
return true;
}
// Returns the current input image.
const cv::Mat getInputImage() const {
return image;
}
// Performs image processing.
void process() {
result = cdetect->process(image);
}
// Returns the image result from the latest processing
const cv::Mat getLastResult() const {
return result;
}
};

ColorDetectController 将起到一个连接模型和视图的作用。例如,当用户发送处理指令时,可以通过 ColorDetectController::getInstance()->process() 来执行处理操作。

实现 ColorDetectController 的过程中用到了 单例(Singleton)模式 ,让类自身负责保存它的唯一实例,用于确保该控制器类在任何时刻都只能有一个实例,并提供一个用于访问该实例的全局指针。GoF 对单例模式定义如下:

Ensure a class only has one instance, and provide a global point of access to it.

GoF

为什么建议控制器使用单例模式来设计呢?其实,像本文这么简单的例子,用不用单例模式都无所谓。然而,对于复杂的系统,可能有多个视图需要访问和使用同一个控制器,比如存在多个对话框以修改同个算法模块的参数,这个时候,单例模式就显得非常有用了——它可以保证这种唯一性和同一性。

上面的代码中:

  • 静态指针成员 *singleton 指向类本身的唯一实例;
  • 构造函数 ColorDetectController() 被设成了私有函数,因此不能供外部直接调用 (出于三法则,最好也将赋值操作符和复制构造函数定义为私有函数);
  • 公有函数 getInstance() 用于创建或获取该唯一实例的指针。调用该函数时, 如果该类已经被实例化过,则直接返回该实例。否则先创建该实例,然后将其返回(注意这种创建方式并非线程安全的);
  • 公有函数 destroy() 用于销毁该实例。由于该实例是动态创建的,当用户不需要用到它时,应该调用这个函数将它销毁。

编写视图(View)

完成模型和控制器的编写后,视图的编写就显得轻而易举了。我们可以设计如下的界面:

程序界面
程序界面

这个界面包含了如下一些窗口元素:

  • “Open Image” 按钮,用于打开图像。点击它将执行 onOpen() 函数,该函数将调用 ColorDetectController 控制器的 setInputImage() 函数;
  • “Select Color”按钮,用于选择目标颜色。点击它将执行 onSelectColor() 函数,该函数将调用 ColorDetectController 控制器的 setTargetColor() 函数;
  • 竖直滑动选择块,用于选择最小距离阈值。滑动它后将执行 onSelectThreshold() 函数,该函数将调用 ColorDetectController 控制器的 setColorDistanceThreshold() 函数;
  • “Process”按钮,用于执行颜色检测。点击它将执行 onProcess() 函数,该函数将调用 ColorDetectController 控制器的 process() 函数;
  • “Output Image”按钮,用于输出图像。点击它将执行 onSave() 函数。

相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/** 
* showImage - display an image on label
*
* @param image - image for displaying
*/
void MainWindow::showImage(cv::Mat image)
{
cv::Mat tmp;
QImage img;
if (image.channels() == 3) { // color image
cv::cvtColor(image, tmp, CV_BGR2RGB);
img = QImage((const unsigned char*)(tmp.data),
tmp.cols, tmp.rows, QImage::Format_RGB888);
} else if (image.channels() == 1) { // gray scale image
cv::cvtColor(image, tmp, CV_GRAY2RGB);
img = QImage((const unsigned char*)(tmp.data),
tmp.cols, tmp.rows, tmp.step, QImage::Format_RGB888);
}
// display on label
ui->imageLabel->setPixmap(QPixmap::fromImage(img));
ui->imageLabel->repaint();
}

/**
* onOpen() - open image
*
*/
void MainWindow::onOpen()
{
// Callback method of "Open" button.
QString fileName = QFileDialog::getOpenFileName(
this,
tr("Open Image"),
".",
tr("Image Files (
*.png *.jpg *.jpeg *.bmp)")
);
if(fileName.isEmpty()) {
return;
}

// change the cursor
QApplication::setOverrideCursor(Qt::WaitCursor);

// set and display the input image
ColorDetectController::getInstance()->setInputImage(
fileName.toStdString());

showImage(ColorDetectController::getInstance()->getInputImage());

// restore the cursor
QApplication::restoreOverrideCursor();

// get the file's standard location
curFile = QFileInfo(fileName).canonicalPath();

ui->processBtn->setEnabled(true);
ui->thresholdSlider->setEnabled(true);
ui->colorBtn->setEnabled(true);
ui->saveBtn->setEnabled(true);
}

/**
* onSelectColor() - select target color
*
*/
void MainWindow::onSelectColor()
{
QColor color = QColorDialog::getColor(Qt::green, this);
if (color.isValid()) {
ColorDetectController::getInstance()
->setTargetColor(color.red(),
color.green(),
color.blue()
);
}
}

/**
* onSelectThreshold - select threshold
*
*/
void MainWindow::onSelectThreshold()
{
ui->thresholdLabel->setText(tr("Color Distance Threshold: %1")
.arg(ui->thresholdSlider->value()));
ColorDetectController::getInstance()
->setColorDistanceThreshold(
ui->thresholdSlider->value());
}

/**
* saveImage - Save the file to a specified location
*
* @param fileName - the target location
*
* @return true if the file is successfully saved
*/
bool MainWindow::saveImage(const QString &fileName)
{
QFile file(fileName);

if (!file.open(QFile::WriteOnly)) {
QMessageBox::warning(this, tr("ImageViewer"),
tr("Enable to save %1: \n %2")
.arg(fileName).arg(file.errorString()));
return false;
}

// change the cursor
QApplication::setOverrideCursor(Qt::WaitCursor);

// save all the contents to file
cv::imwrite(fileName.toStdString(),
ColorDetectController::getInstance()->
getLastResult());

// restore the cursor
QApplication::restoreOverrideCursor();

return true;
}

/**
* onProcess - process image
*
*/
void MainWindow::onProcess()
{
ColorDetectController::getInstance()->process();
cv::Mat resulting =
ColorDetectController::getInstance()->getLastResult();
if (!resulting.empty())
showImage(resulting);
}

/**
* onSave - save the image
*
*/
void MainWindow::onSave()
{
QString fileName = QFileDialog::getSaveFileName(this,
tr("Save as"),
curFile);
if (!fileName.isEmpty())
saveImage(fileName);
}

/**
* onClose - destroy the instance
*
*/
void MainWindow::onClose()
{
ColorDetectController::getInstance()->destroy();
}

本文源代码

本文的源代码可以在 这里 找到。

Comments