OpenCV实战(6)——OpenCV策略设计模式
- 0. 前言
- 1. 策略设计模式颜色识别
- 1.1 颜色比较
- 1.2 策略设计模式
- 1.3 实现颜色比较
- 1.4 ColorDetector 类
- 1.4 计算两个颜色向量之间的距离
- 2. 使用 OpenCV 函数
- 3. 函子或函数对象
- 4. OpenCV 算法的基类
- 小结
- 系列链接
0. 前言
良好的计算机视觉程序始于良好的编程实践,构建无错误的应用程序只是一个开始。我们真正想要的是一个能够随着新需求的出现而轻松适应和发展的应用程序。本节将介绍如何充分利用一些面向对象的编程原则以构建高质量的软件程序,我们将学习一些重要的设计模式,帮助我们使用易于测试、维护和可重用的组件构建应用程序。
设计模式是软件工程中的一个常见概念,设计模式是针对软件设计中经常出现的通用问题的可靠的、可重用的解决方案。当前有许多设计模式在软件设计中被引入,我们应该对现有设计模式有所了解。
1. 策略设计模式颜色识别
1.1 颜色比较
假设我们想要构建一个简单的算法来识别图像中具有给定颜色的所有像素。为了达到目的,算法需要接受图像和颜色作为输入,并返回二值图像,其中在输入图像中与指定颜色相同的像素位置值为 1
,否则为 0
,例如,输入图像中位置 (1,1)
处的像素值与指定颜色相同,则在二值图像的 (1,1)
位置处像素值为 1
。同时,函数也可以接受颜色容差作为参数。
1.2 策略设计模式
为了实现颜色比较,本节将使用策略设计模式,这种面向对象的设计模式将算法封装在类中。这比用另一种算法替换给定算法或将几种算法链接在一起以构建更复杂的过程更容易。此外,这种模式通过隐藏尽可能多的复杂性来简化算法的部署。
一旦使用策略设计模式将算法封装在一个类中,就可以通过创建该类的实例来部署它。通常,实例在程序初始化时创建。在构造的时候,类实例会用它们的默认值初始化算法的不同参数,也可以使用合适的方法读取和设置算法的参数值。对于具有 GUI
的应用程序,可以使用不同的小部件(文本、滑块等)来显示和修改这些参数。
1.3 实现颜色比较
接下来,我们将介绍 Strategy
类的结构。在此之前,我们编写一个简单的 main
函数来运行上述颜色检测算法。
(1) 首先,在 main
函数中,我们必须为类 ColorDetector
创建一个实例(对于类的具体代码,我们将在下一节中介绍):
// 创建图像处理器对象
ColorDetector cdetect;
(2) 读取图像进行处理:
// 读取输入图像
cv::Mat image = cv::imread("1.png");
(3) 用 empty()
函数检查我们是否正确加载了图像,如果图像为空,则退出应用程序:
if (image.empty()) return 0;
(4) 使用 ColorDetector
的新实例设置目标颜色(该函数是在类中定义的):
cdetect.setTargetColor(230, 190, 130);
(5) 创建一个窗口显示图像处理结果。因此,我们必须使用 ColorDetector
实例的 process
函数:
// 处理图像并显示结果
cv::namedWindow("Result");
cv::Mat result = cdetect.process(image);
cv::imshow("Result", result);
6. 最后,在退出之前等待用户按键操作:
cv::waitKey();
return 0;
运行此程序,可以得到以下输出:
在上图中,白色像素表示图像中与给定颜色相同的像素,黑色表示图像中与给定颜色不同的像素。我们封装在 ColorDetector
类中的算法比较简单(仅由一个扫描循环和一个容差参数组成)。当要实现的算法更复杂、步骤较多且包含多个参数时,策略设计模式也会变得更加有用。
1.3.1 完整代码
完整代码 (colorDetector.cpp
) 如下所示:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "colordetector.h"
#include <vector>
int main() {
// 创建图像处理器对象
ColorDetector cdetect;
// 读取输入图像
cv::Mat image = cv::imread("1.png");
if (image.empty()) return 0;
cv::namedWindow("Original Image");
cv::imshow("Original Image", image);
// 设置输入参数
cdetect.setTargetColor(230, 190, 130);
// 处理图像并显示结果
cv::namedWindow("Result");
cv::Mat result = cdetect.process(image);
cv::imshow("Result", result);
// 或者使用函子
ColorDetector colordetector(230, 190, 130, 45, true);
cv::namedWindow("Result (functor)");
result = colordetector(image);
cv::imshow("Result (functor)", result);
// floodfill函数
cv::floodFill(image, // 输入/输出图像
cv::Point(100, 50), // 种子位置
cv::Scalar(255, 255, 255), // 重绘制的颜色
(cv::Rect*)0, // 重绘制的像素集的边框
cv::Scalar(35, 35, 35), // 低差异阈值
cv::Scalar(35, 35, 35), // 高差异阈值
cv::FLOODFILL_FIXED_RANGE // 像素与种子位置颜色进行比较
);
cv::namedWindow("Flood fill result");
result = colordetector(image);
cv::imshow("Flood fill result", image);
// 创建图像,演示颜色空间属性
cv::Mat colors(100, 300, CV_8UC3, cv::Scalar(100, 200, 150));
cv::Mat range = colors.colRange(0, 100);
range = range + cv::Scalar(10, 10, 10);
range = colors.colRange(200, 300);
range = range + cv::Scalar(-10, -10, -10);
cv::namedWindow("3 colors");
cv::imshow("3 colors", colors);
cv::Mat labImage(100, 300, CV_8UC3, cv::Scalar(100, 200, 150));
cv::cvtColor(labImage, labImage, cv::COLOR_BGR2Lab);
range = colors.colRange(0, 100);
range = range + cv::Scalar(10, 10, 10);
range = colors.colRange(200, 300);
range = range + cv::Scalar(-10, -10, -10);
cv::cvtColor(labImage, labImage, cv::COLOR_Lab2BGR);
cv::namedWindow("3 colors (Lab)");
cv::imshow("3 colors (Lab)", colors);
cv::Mat grayLevels(100, 256, CV_8UC3);
for (int i=0; i<256; i++) {
grayLevels.col(i) = cv::Scalar(i, i, i);
}
range = grayLevels.rowRange(50, 100);
cv::Mat channels[3];
cv::split(range, channels);
channels[1] = 128;
channels[2] = 128;
cv::merge(channels, 3, range);
cv::cvtColor(range, range, cv::COLOR_Lab2BGR);
cv::namedWindow("Luminance vs Brightness");
cv::imshow("Luminance vs Brightness", grayLevels);
cv::waitKey();
return 0;
}
1.4 ColorDetector 类
颜色比较算法的核心过程很容易实现,通过一个简单的扫描循环,遍历每个像素,将其颜色与给定目标颜色进行比较:
// 迭代器
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>();
if (useLab) {
it = converted.begin<cv::Vec3b>();
itend = converted.end<cv::Vec3b>();
}
for (; it != itend; ++it, ++itout) {
if (getDistanceToTargetColor(*it) < maxDist) {
*itout = 255;
} else {
*itout = 0;
}
}
cv::Mat
变量 image
指输入图像,而 result
是指二值输出图像。因此,第一步需要设置所需的迭代器,使扫描循环易于实现。在每次迭代时评估当前像素颜色和给定目标颜色之间的距离,以检查它们之间的距离是否在 maxDist
定义的容差参数范围内。如果在容差范围内,则将输出图像像素值设为 255
(白色);否则,将其值设为 0
(黑色)。我们可以使用 getDistanceToTargetColor
方法计算当前像素颜色与给定目标颜色之间的距离,OpenCV
中也有许多不同的方法来计算距离。例如,可以计算包含 RGB
颜色值的向量之间的欧几里得距离,或对 RGB
值之间差值的绝对值求和(这也称为城市街区距离)。在现代架构中,浮点欧几里得距离的计算速度比城市街区距离更快。为了获得更高的灵活性,我们根据 getColorDistance
方法编写 getDistanceToTargetColor
方法:
// 计算与目标颜色的距离
int getDistanceToTargetColor(const cv::Vec3b& color) const {
return getColorDistance(color, target);
}
// 计算两种颜色之间的城市街区距离
int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const {
return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);
}
我们使用 cv::Vec3d
保存表示 RGB
值的三个无符号字符。target
变量是指给定的目标颜色,它在我们定义的类算法中定义为类 (class
) 变量。对于类方法 process
,其根据提供的输入图像,扫描图像完成后返回结果:
cv::Mat ColorDetector::process(const cv::Mat& image) {
result.create(image.size(), CV_8U);
// 循环处理过程
...
return result;
}
每次调用此方法时,需要检查包含结果二值图的输出图像是否需要重新分配内存以匹配输入图像的大小,这就是我们使用 cv::Mat
的 create
方法的原因。需要注意的是,只有在指定的大小和深度(图像深度是指存储每个像素所用的位数)与当前图像结构不对应时,此方法才会进行重新分配。
定义了核心处理方法之后,我们需要继续添加一些额外的类方法来部署算法。由于我们已经确定了算法需要哪些输入和输出数据,因此,我们首先定义保存这些数据的类属性:
class ColorDetector {
private:
// 容差
int maxDist;
// 目标颜色
cv::Vec3b target;
// 结果二值图像
cv::Mat result;
为了创建封装算法的类的实例(命名为 ColorDetector
),我们需要定义一个构造函数,策略设计模式的目标之一是使算法部署尽可能简单。可以定义的最简单的构造函数是空构造函数,它将创建一个有效的类算法实例。然后,我们令构造函数将所有输入参数初始化为其默认值,在此算法中,我们将通常是可接受的容差参数设为 100
;此外,我们还需要设置默认的目标颜色。用于确保我们总是从可预测和有效的输入值开始测试算法:
// 空构造函数
// 默认参数初始化
ColorDetector() : maxDist(100), target(0, 0, 0) {}
创建类算法实例的后,我们可以使用有效图像调用 process
方法并获得有效输出,这是策略模式的另一个目标,即确保算法始终以有效参数运行。显然,使用这个类时,我们会需要使用自定义参数,这可以通过提供相应的 getter
和 setter
方法实现。以颜色容差参数为例:
// 设置颜色阈值距离
void setColorDistanceThreshold(int distance) {
if (distance < 0) distance = 0;
maxDist = distance;
}
// 获取颜色阈值距离
int getColorDistanceThreshold() const {
return maxDist;
}
需要注意的是,我们首先需要检查输入的有效性,这是为了确保我们的算法永远不会在无效状态下运行。我们也可以用类似的方式设置给定目标颜色参数:
// 设置颜色阈值距离
void setColorDistanceThreshold(int distance) {
if (distance < 0) distance = 0;
maxDist = distance;
}
// 获取颜色阈值距离
int getColorDistanceThreshold() const {
return maxDist;
}
在以上代码中,我们提供了 setTargetColor
方法的两个定义。在定义的第一个版本中,三个颜色分量被指定为三个参数,而在第二个版本中,使用 cv::Vec3b
保存颜色值,目标是便于类算法的使用,以方便在使用时选择最适合的 setter
。
本节介绍了如何使用策略设计模式将算法封装在类中,本节我们使用颜色比较算法识别足够接近指定目标颜色的图像像素。此外,策略设计模式的实现可以通过使用函数对象进行补充。
1.3.1 完整代码
完整代码 (colordetector.h
) 如下所示:
#if !defined COLORDETECT
#define COLORDETECT
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
class ColorDetector {
private:
// 容差
int maxDist;
// 目标颜色
cv::Vec3b target;
// 转换色彩后的图像
cv::Mat converted;
bool useLab;
// 结果二进制图像
cv::Mat result;
public:
// 空构造函数
// 默认参数初始化
ColorDetector() : maxDist(100), target(0, 0, 0), useLab(false) {}
// Lab颜色空间的额外构造函数
ColorDetector(bool useLab) : maxDist(100), target(0, 0, 0), useLab(true) {}
// 完整构造函数
ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100, bool useLab=false) : maxDist(maxDist), useLab(useLab) {
// 目标颜色
setTargetColor(blue, green, red);
}
// 计算与目标颜色的距离
int getDistanceToTargetColor(const cv::Vec3b& color) const {
return getColorDistance(color, target);
}
// 计算两种颜色之间的城市街区距离
int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const {
return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);
// 或者
// return static_cast<int>(cv::norm<int,3>(cv::Vec3i(color1[0]-color2[0],color1[1]-color2[1],color1[2]-color2[2])));
// 或者
// cv::Vec3b dist;
// cv::absdiff(color1, color2, dist);
// return cv::sum(dist)[0];
}
// 处理图像,返回单通道二进制图像
cv::Mat process(const cv::Mat& image);
cv::Mat operator()(const cv::Mat& image) {
cv::Mat input;
if (useLab) {
cv::cvtColor(image, input, cv::COLOR_BGR2Lab);
} else {
input = image;
}
cv::Mat output;
cv::absdiff(input, cv::Scalar(target), output);
// 分割图像通道
std::vector<cv::Mat> images;
cv::split(output, images);
// 对三个通道进行加法运算
output = images[0] + images[1] + images[2];
// 应用阈值
cv::threshold(output, // 输入图像
output, // 输出图像
maxDist, // 阈值
255, // 最大值
cv::THRESH_BINARY_INV // 阈值运算类型
);
return output;
}
// 设置颜色阈值距离
void setColorDistanceThreshold(int distance) {
if (distance < 0) distance = 0;
maxDist = distance;
}
// 获取颜色阈值距离
int getColorDistanceThreshold() const {
return maxDist;
}
// 设置BGR颜色空间中给定的待检测颜色
void setTargetColor(uchar blue, uchar green, uchar red) {
target = cv::Vec3b(blue, green, red);
if (useLab) {
cv::Mat tmp(1, 1, CV_8UC3);
tmp.at<cv::Vec3b>(0, 0) = cv::Vec3b(blue, green, red);
// 将目标颜色转换到 Lab 色彩空间
cv::cvtColor(tmp, tmp, cv::COLOR_BGR2Lab);
target = tmp.at<cv::Vec3b>(0, 0);
}
}
// 设置待检测颜色
void setTargetColor(cv::Vec3b color) {
target = color;
}
// 获取待检测颜色
cv::Vec3b getTargetColor() const {
return target;
}
};
cv::Mat ColorDetector::process(const cv::Mat& image) {
result.create(image.size(), CV_8U);
// 转换色彩空间
if (useLab) cv::cvtColor(image, converted, cv::COLOR_BGR2Lab);
// 迭代器
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>();
if (useLab) {
it = converted.begin<cv::Vec3b>();
itend = converted.end<cv::Vec3b>();
}
for (; it != itend; ++it, ++itout) {
if (getDistanceToTargetColor(*it) < maxDist) {
*itout = 255;
} else {
*itout = 0;
}
}
return result;
}
#endif
1.4 计算两个颜色向量之间的距离
为了计算两个颜色向量之间的距离,我们可以使用以下公式:
return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);
但是,OpenCV
中也包含了用于计算向量欧几里得范数的函数。因此,我们可以按如下方式计算距离:
return static_cast<int>(cv::norm<int,3>(cv::Vec3i(color1[0]-color2[0],color1[1]-color2[1],color1[2]-color2[2])));
根据以上方式使用 getDistance
方法,可以得到获得相似的结果。在以上代码中,我们使用 cv::Vec3i
(三向量整数数组),因为减法的结果是整数值。
由于 OpenCV
矩阵和向量数据结构包括基本算术运算符。因此,我们也可以使用以下方法计算距离:
return static_cast<int>(cv::norm<uchar,3>(color-target)); // wrong!
以上语法初看之下很容易误以为是正确的,但并非如此。这是因为 OpenCV
中运算符包含对 saturate_cast
的调用以确保结果保持在输入类型的域内(以上代码中为 uchar
)。因此,在给定目标颜色大于当前图像颜色值的情况下,结果为 0
而非预期的负值。正确的计算方法如下:
cv::Vec3b dist;
cv::absdiff(color1, color2, dist);
return cv::sum(dist)[0];
但是,使用两个函数来计算两个三元向量数组之间的距离的效率较低。
2. 使用 OpenCV 函数
在上一节中,我们将使用带有迭代器的循环来执行颜色比较计算。我们也可以通过调用一系列 OpenCV
函数来得到相同的结果,使用 OpenCV
函数重写以上颜色检测方法:
cv::ColorDetector::process(const cv::Mat& image) {
cv::Mat output;
// 计算与目标颜色的绝对差值
cv::absdiff(image, cv::Scalar(target), output);
// 分割图像通道
std::vector<cv::Mat> images;
cv::split(output, images);
// 对通道进行加法运算
output = images[0] + images[1] + images[2];
// 应用阈值
cv::threshold(output, output, maxDist, 255, cv::THRESH_BINARY_INV);
return output;
}
此方法使用 absdiff()
函数计算图像像素之间的绝对差,以上代码计算图像与标量值 cv::Scalar(target)
之间的距离,除了标量值外,我们也可以提供另一个图像作为 absdiff
函数的第二个参数,在这种情况下,将逐像素计算像素差;因此,两个图像必须具有相同的尺寸大小。使用 split
函数提取图像的各个通道,以便将各通道像素值相加,当计算结果结果大于 255
时,由于应用饱和运算 cv::saturate_cast
,结果将被限制为 255
。最后,使用 threshold()
函数创建二值图像,该函数通常用于将所有像素与阈值(第三个参数,必须小于 256
)进行比较,在常规阈值模式 (cv::THRESH_BINARY
) 下,将所有大于阈值的像素设为定义的最大值(第四个参数),而其他值设为 0
;在逆模式 (cv::THRESH_BINARY_INV
) 下,将所有低于或等于阈值的像素设为定义的最大值;而 cv::THRESH_TOZERO
和 cv::THRESH_TOZERO_INV
模式令大于或小于阈值的像素保持不变。
使用 OpenCV
函数可以快速构建复杂的应用程序并减少出错的可能性,并且通常更高效。但是,当使用多个中间步骤时,可能会消耗较多内存。
3. 函子或函数对象
使用 C++
运算符重载,可以创建一个类,其实例的行为类似于函数。为此,我们可以重载 operator()
方法,这样对类处理方法的调用就像函数调用一样简单。生成的类实例称为函数对象或函子 (functor
),通常,函子包含一个完整的构造函数,这样它就可以在创建后立即使用。例如,我们可以将以下构造函数添加到 ColorDetector
类中:
// 完整构造函数
ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100, bool useLab=false) : maxDist(maxDist), useLab(useLab) {
// 目标颜色
setTargetColor(blue, green, red);
}
显然,我们仍然可以使用之前定义的 setter
和 getter
。函子方法可以定义如下:
cv::Mat operator()(const cv::Mat& image) {
// 色彩检测代码
...
}
要使用此函子方法检测给定颜色,只需使用以下代码:
ColorDetector colordetector(230, 190, 130, 100);
cv::Mat result= colordetector(image); // 调用函子
如上所示,对颜色检测方法的调用与函数调用一样。事实上,colordetector
变量可以像函数一样使用。
4. OpenCV 算法的基类
OpenCV
提供了许多执行各种计算机视觉任务的算法。为了方便它们的使用,这些算法中的大多数属于通用基类 cv::Algorithm
的子类,这实现了策略设计模式所规定的一些概念。所有这些算法都是动态创建的,使用专门的静态方法来确保算法始终在有效状态下创建(即未指定参数时应具有有效的默认值)。例如,子类 cv::ORB
是一个兴趣点算子(本节我们将其用作算法的说明性示例,关于此算子的详细用法将在之后的学习中详细介绍)。cv::ORB
算法的实例创建如下:
cv::Ptr<cv::ORB> ptrORB = cv::ORB::create();
创建完成后,就可以使用该算法。例如,通用方法 read
和 write
可用于加载或存储算法的状态。这些算法也有专门的方法(例如,在 ORB
中,可以使用 detect
和 compute
方法来触发其主要计算单元);算法也有专门的 setter
方法指定它们的内部参数。我们可以将指针声明为 cv::Ptr<cv::Algorithm>
,但在这种情况下,我们无法使用其专用方法。
小结
设计模式是软件工程中的一个常见概念,设计模式是针对软件设计中经常出现的通用问题的可靠的、可重用的解决方案。良好的计算机视觉程序始于良好的编程实践,我们需要应用程序能够随着新需求的出现而轻松适应和扩展。本节中,我们介绍了如何充分利用一些面向对象的编程原则以构建高质量的软件程序,我们学习了一些重要的设计模式,帮助我们使用易于测试、维护和可重用的组件构建应用程序。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解