用策略设计模式比较颜色
【实现】
#pragma once
#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
#include<vector>
#include<iostream>
#include<random>
using namespace std;
using namespace cv;
class ColorDetector {
private:
//允许的最小差值
int maxDist;
//目标颜色
cv::Vec3b target;
//存储二值影响结果的图像
cv::Mat result;
public:
//空构造函数
//在此初始化默认参数
ColorDetector();
//另一种构造函数,使用目标颜色和颜色距离作为参数
//有参构造1
ColorDetector(uchar blue, uchar green, uchar red, int distance);
//有参构造2
ColorDetector(cv::Vec3b color, int distance);
//设置目标颜色函数1
void setTargetColor(uchar blue, uchar green, uchar red);
//设置目标颜色函数2
void setTargetColor(cv::Vec3b color);
//设置颜色公差
void setColorDistanceThreshold(int distance);
//取颜色公差
int getColorDistanceThreshold() const;
//取目标颜色
cv::Vec3b getTargetColor() const;
//用城区距离计算颜色差距以供类内调用
int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const;
//计算与目标颜色差距
int getDistanceToTargetColor(const cv::Vec3b& color) const;
//用迭代器遍历并返回二值图像
cv::Mat process(const cv::Mat& image);
};
ColorDetector::ColorDetector()
{
this->maxDist = 100;
this->target = cv::Vec3b(0, 0, 0);
}
ColorDetector::ColorDetector(uchar blue, uchar green, uchar red, int distance)
{
this->maxDist = distance;
this->target = cv::Vec3b(blue, green, red);
}
ColorDetector::ColorDetector(cv::Vec3b color, int distance)
{
this->maxDist = distance;
this->target = color;
}
//设置需要检测的颜色
void ColorDetector::setTargetColor(uchar blue,
uchar green,
uchar red) {
//次序为BGR
target = cv::Vec3b(blue, green, red);
}
//设置颜色差距的阈值
//阈值必须是正数,否则就设为0
void ColorDetector::setColorDistanceThreshold(int distance) {
if (distance < 0) {
distance = 0;
this->maxDist = distance;
}
}
//取得颜色差距的阈值
int ColorDetector::getColorDistanceThreshold() const {
return maxDist;
}
//设置需要检测的颜色
void ColorDetector::setTargetColor(cv::Vec3b color) {
target = color;
}
//取得需要检测的颜色
cv::Vec3b ColorDetector::getTargetColor() const {
return target;
}
//计算与目标颜色的差距
int ColorDetector::getDistanceToTargetColor(const cv::Vec3b& color)const {
return getColorDistance(color, target);
}
//计算两个颜色直接的城区距离
int ColorDetector::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::Mat ColorDetector::process(const cv::Mat& image) {
//必要时重新分配二值映像
//与输入图像的尺寸相同,不过是单通道
cv::Mat result;
result.create(image.size(), CV_8U);
//取得迭代器
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 (; it != itend; ++it, ++itout) {
//比较与目标颜色的差距
if (ColorDetector::getDistanceToTargetColor(*it) <= maxDist) {
*itout = 255;
}
else {
*itout = 0;
}
}
return result;
}
int main()
{
// 1.创建图像处理器对象
ColorDetector cdetect;
// 2.读取输入的图像
cv::Mat image = cv::imread("bluesky.jpg");
if (image.empty()) return 0;
// 3.设置输入参数
cdetect.setTargetColor(230, 190, 130); // 这里表示蓝天
// 4.处理图像并显示结果
cv::namedWindow("result");
cv::Mat result = cdetect.process(image);
cv::imshow("result", result);
cv::waitKey();
return 0;
}
【实现原理】
首先,这个算法的核心过程非常简单,只是对每个像素进行循环扫描,把它的颜色和目标颜色做比较。如果<=则为白色,否则为黑色。
本次的代码要封装一个ColorDetector,这里涉及到C++的知识。在这个类中,private里自然不必多说,public里的函数我将逐个进行说明。
(1)ColorDetector(); 初始化默认参数。将最小差值maxDist设为100,目标颜色设为(0,0,0)即为黑色。这一步只是初始化了一下参数,相当于int i=1;
(2)ColorDetector(uchar blue, uchar green, uchar red, int distance);有参构造1。这里将最小差值设置为目标输入的distance,将目标颜色设置为用户输入的颜色。
(3)ColorDetector(cv::Vec3b color, int distance);有参构造2。这里的意义和有参构造1一致。
(4)void setTargetColor(uchar blue, uchar green, uchar red);设置目标颜色函数1。这里将目标颜色设置为用户输入的BGR。
(5)void setTargetColor(cv::Vec3b color);设置目标颜色函数2。作用同上。只是用户输入的形式不同。
(6)void setColorDistanceThreshold(int distance);设置颜色公差。如果输入的distance<0则最小差值设为0。
(7)int getColorDistanceThreshold() const;取颜色公差。返回最小差值。
(8)cv::Vec3b getTargetColor() const;取目标颜色。返回目标颜色。
(9)int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const;用城区距离计算颜色差距以供类内调用。返回BGR的距离之和。
(10)int getDistanceToTargetColor(const cv::Vec3b& color) const;计算与目标颜色差距。用(9)的函数进行计算,计算用户输入颜色与目标颜色的差值。
(11)cv::Mat process(const cv::Mat& image);用迭代器遍历并返回二值图像。这里首先要建立一个和image同样大小的图像:result。然后取得迭代器,将image的开始位置赋予it,结束位置赋予itend,将result的开始位置赋予itout。接下来就开始循环,比较图像中颜色与目标颜色的差距,<=最小差值的赋予白色,>的赋予黑色。
说明完这些后,我们来看main函数。首先创建图像处理器对象,接着读取图像,设置一个输入参数。这里的意思是,将目标颜色变为(230,190,130),然后用process进行处理。
【扩展阅读】
例子中的算法可识别出图像中与指定目标颜色足够接近的像素。过程中已经完成了计算步骤。OpenCV 中有一个具有类似功能的函数,可以从图像中提取出与特定颜色相关联的部件。另外,我们也可以用函数对象来补充策略设计模式。OpenCV 中定义了一个基类 cv::Algorithm,实现策略设计模式的概念。
(1)计算两个颜色向量间的距离
在本例中我们用的方法是返回BGR的距离之和,OpenCV 中也有计算向量的欧几里得范数的函数,因此也可以这样计算距离。
return static_cast<int>(
cv::norm<int,3>(cv::Vec3i(color[0]-target[0],
color[1]-target[1],
color[2]-target[2])));
这样得到的结果与原来的非常接近。这里之所以使用 cv::Vec3i(三个向量的整型数组),是因为减法运算得到的是整数值。
还可以这样计算距离。
cv::Vec3b dist;
cv::absdiff(color,target,dist);
return cv::sum(dist)[0];
不过在计算三个数组间距离时调用这两个函数的效率并不高。
(2)使用opencv函数
调用 OpenCV 的系列函数也可以检测颜色
cv::Mat ColorDetector::process(const cv::Mat &image) {
cv::Mat output;
// 计算与目标颜色的距离的绝对值
cv::absdiff(image,cv::Scalar(target),output);
// 把通道分割进 3 幅图像
std::vector<cv::Mat> images;
cv::split(output,images);
// 3 个通道相加(这里可能出现饱和的情况)
output= images[0]+images[1]+images[2];
// 应用阈值
cv::threshold(output, // 相同的输入/输出图像
output,
maxDist, // 阈值(必须<256)
255, // 最大值
cv::THRESH_BINARY_INV); // 阈值化模式
return output;
}
absdiff 函数用来计算图像的像素与标量值之间差距的绝对值。该函数的第二个参数也可以不用标量值,而是改用另一幅图像,这样就可以逐个像素地计算差距。因此两幅图像的尺寸必须相同。然后,用 split 函数提取出存放差距的图像的单个通道以便求和。注意,累加值有可能超过 255,但因为饱和度对值范围有要求,所以最终结果不会超过 255。这里的 maxDist 参数也必须小于 256。如果你觉得这样不合理, 可以进行修改。
最后一步是用 cv::threshold 函数创建一个二值图像。此函数通常用于将所有像素与某阈值(第3个参数)比较,并且在常规阈值化模式(cv::THRESH_BINARY)下,将所有>指定阈值的像素赋值为预定的最大值(第4个参数),将其他像素赋值为 0。这里用相反模式(cv::THRESH_BINARY_INV)把<=阈值的像素赋值为预定的最大值。还有 cv::THRESH_TOZERO 和 cv::THRESH_TOZERO_INV 模式,它们使大于或小于阈值的像素保持不变。
一般来说,最好直接使用 OpenCV 函数。它可以快速建立复杂程序,减少潜在的错误,而且 程序的运行效率通常也比较高(得益于 OpenCV 项目参与者做的优化工作)。不过这样会执行很多的中间步骤,消耗更多内存。
(3)floodFill 函数
cv::floodFill 函数的做法与ColorDetector 类 类似,但有一个很大的区别:它在判断 一个像素时,还要检查附近像素的状态,这是为了识别某种颜色的相关区域。用户只需指定一个起始位置和允许的误差,就可以找出颜色接近的连续区域。
首先根据亚像素确定搜寻的颜色,并检查它旁边的像素,判断它们是否为颜色接近的像素; 然后,继续检查它们旁边的像素,并持续操作。这样就可以从图像中提取出特定颜色的区域。例如要从图中提取出蓝天,可以执行以下语句
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); // 与起始点像素比
这种算法重绘了一个独立的连续区域(这里是把天空画成白色)。即使其他地方有颜色接近 的像素(例如水面),除非它们与天空相连,否则也不会被识别出来。
(4)仿函数或函数对象
利用 C++的操作符重载功能,我们可以让类的实例表现得像函数。它的原理是重载 operator()方法,让调用类的处理方法就像调用纯粹的函数一样。这种类的实例被称为函数对 象或者仿函数(functor)。一个仿函数通常包含一个完整的构造函数,因此能够在创建后立即使 用。例如,可以在 ColorDetector 类中添加完整的构造函数:
// 完整的构造函数
ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100):
maxDist(maxDist) {
// 目标颜色
setTargetColor(blue, green, red);
}
//很显然,前面定义的获取方法和设置方法仍然可以使用。可以这样定义仿函数方法:
cv::Mat operator()(const cv::Mat &image) {
// 这里放检测颜色的代码
}
//若想用仿函数方法检测指定的颜色,只需要用这样的代码片段:
ColorDetector colordetector(230,190,130, // 颜色
100); // 阈值
cv::Mat result= colordetector(image); // 调用仿函数
//可以看到,这里对颜色检测方法的调用类似于对某个函数的调用。
(5)openCV的算法基类
为实现计算机视觉的各项功能,OpenCV 提供了很多算法。为方便使用,大多数算法都被封装成了通用基类 cv::Algorithm 的子类。这体现了策略设计模式的一些概念。首先,所有算法都在专门的静态方法中动态地创建,以确保创建的算法总是有效的(即每个缺少的参数都有有效的默认值)。来看一个例子,即它的其中一个子类 cv::ORB。这里只把它作为一个算法示例。
//用下面的方法创建一个算法实例:
cv::Ptr<cv::ORB> ptrORB = cv::ORB::create(); // 默认状态
算法一旦创建完毕,就可以开始使用,例如通用方法 read 和 write 可用于装载或存储算 法的状态值。算法也有一些专用方法(例如 ORB 的方法 detect 和 compute 用于触发它的主体 计算单元),也有专门用来设置内部参数的设置方法。需要注意的是,你可以把指针类型定为 cv::Ptr,但那样就无法使用它的专用方法了。