处理图像的颜色
1:用策略设计模式比较颜色;面向对象的设计模式
2:用GrabCut 算法分割图像;
3:转换颜色表示法;
4:用色调、饱和度和亮度表示颜色。
调用颜色检测算法:
int main()
{
// 1.创建图像处理器对象
ColorDetector cdetect;
// 2.读取输入的图像
cv::Mat image= cv::imread("boldt.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;
}
这里的白色像素表示检测到指定的颜色,黑色表示没有检测到。
很明显,封装进这个类的算法相对简单(下面会看到它只是组合了一个扫描循环和一个公差
参数)。当算法的实现过程变得更加复杂、步骤繁多并且包含多个参数时,策略设计模式才会真
正展现出强大的威力。
颜色算法检测的核心:
// 取得迭代器
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 (getDistanceToTargetColor(*it)<=maxDist) {
*itout= 255;
} else {
*itout= 0;
}
}
cv::Mat 类型的变量image 表示输入图像,result 表示输出的二值图像
。这里用getDistance
ToTargetColor 方法来计算与目标颜色的差距
也有其他可以计算这个差距的方法,例如计算包含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::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)创建实例,就需要定义一个构造函数。
使用策略设计模式的原因之一,就是让算法的部署尽可能简单
// 空构造函数
// 在此初始化默认参数
ColorDetector() : maxDist(100), target(0,0,0) {}
也可以不使用空的构造函数,而是采用复杂的构造函数,要求用户输入目标颜色和颜色距离:
// 另一种构造函数,使用目标颜色和颜色距离作为参数
ColorDetector(uchar blue, uchar green, uchar red, int mxDist);
创建该算法类的用户此时可以立即调用处理方法并传入一个有效的图像,然后得到一个有效
的输出。这是策略设计模式的另一个目的,即只要保证参数正确,算法就能正常运行。用户显然
希望使用个性化设置,我们可以用相应的设置方法和获取方法来实现这个功能。首先要实现
color 公差参数的定制:
// 设置颜色差距的阈值
// 阈值必须是正数,否则就设为0
void setColorDistanceThreshold(int distance) {
if (distance<0)
distance=0;
maxDist= distance;
}
// 取得颜色差距的阈值
int getColorDistanceThreshold() const {
return maxDist;
}
注意,我们首先检查了输入的合法性。再次强调,这是为了确保算法运行的有效性。可以用
类似的方法设置目标颜色:
// 设置需要检测的颜色
void setTargetColor(uchar blue,
uchar green,
uchar red) {
// 次序为BGR
target = cv::Vec3b(blue, green, red);
}
// 设置需要检测的颜色
void setTargetColor(cv::Vec3b color) {
target= color;
}
// 取得需要检测的颜色
cv::Vec3b getTargetColor() const {
return target;
}
这次我们提供了setTargetColor 方法的两种定义,第一个版本用三个参数表示三个颜色
组件,第二个版本用cv::Vec3b 保存颜色值。再次强调,这么做是为了让算法类更便于使用,
使用户只需要选择最合适的设置函数。
计算两个颜色向量间的距离:
可使用这个简单的公式:
return abs(color[0]-target[0])+
abs(color[1]-target[1])+
abs(color[2]-target[2]);
然而,OpenCV 中也有计算向量的欧几里得范数的函数,因此也可以这样计算距离:
return static_cast<int>(
cv::norm<int,3>(cv::Vec3i(color[0]-target[0],
color[1]-target[1],
color[2]-target[2])));
改用这种方式定义getDistance 方法后,得到的结果与原来的非常接近。这里之所以使用
cv::Vec3i(三个向量的整型数组),是因为减法运算得到的是整数值。
我们发现OpenCV 中矩阵和向量等数据结构
定义了基本的算术运算符。因此,有人会想这样计算距离:
return static_cast( cv::norm<uchar,3>(color-target));// 错误!!!!!!
这种做法看上去好像是对的,但实际上是错误的,因为为了确保结果在输入数据类型的范围
之内(这里是uchar),这些运算符通常都调用了saturate_cast(详情请参见2.6 节)。因此
在target 的值比color 大的时候,结果就会是0 而不是负数。正确的做法应该是:
cv::Vec3b dist;
cv::absdiff(color,target,dist);
return cv::sum(dist)[0];
不过在计算三个数组间距离时调用这两个函数的效率并不高。
使用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 函数创建一个二值图像。这个函数通常用于将所有像素与某
个阈值(第三个参数)进行比较,并且在常规阈值化模式(cv::THRESH_BINARY)下,将所有
大于指定阈值的像素赋值为预定的最大值(第四个参数),将其他像素赋值为0。这里使用相反
的模式(cv::THRESH_BINARY_INV)把小于或等于阈值的像素赋值为预定的最大值。此外还
有cv::THRESH_TOZERO 和cv::THRESH_TOZERO_INV 模式,它们使大于或小于阈值的像素保持
不变。
一般来说,最好直接使用OpenCV 函数。它可以快速建立复杂程序,减少潜在的错误,而且
程序的运行效率通常也比较高(得益于OpenCV 项目参与者做的优化工作)。不过这样会执行很
多的中间步骤,消耗更多内存。
floodFill 函数
ColorDetector 类可以在一幅图像中找出与指定颜色接近的像素,它的判断方法是对像素
进行逐个检查。cv::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); // 与起始点像素比较
图像中亚像素(100, 50)所处的位置是天空。函数会检查所有的相邻像素,颜色接近的像素会
被重绘成第三个参数指定的新颜色。为了判断颜色是否接近,需要分别定义比参考色更高或更低
的值作为阈值。这里使用固定范围模式,即所有像素都与亚像素的颜色进行对比,默认模式是将
每个像素与和它邻近的像素进行对比。得到的结果如下图所示。
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); // 调用仿函数
可以看到,这里对颜色检测方法的调用类似于对某个函数的调用。
OpenCV 的算法基类
为实现计算机视觉的各项功能,OpenCV 提供了很多算法。为方便使用,大多数算法都被封
装成了通用基类cv::Algorithm 的子类。这体现了策略设计模式的一些概念。首先,所有算法
都在专门的静态方法中动态地创建,以确保创建的算法总是有效的(即每个缺少的参数都有有效
的默认值)。来看一个例子,即它的其中一个子类cv::ORB(用于兴趣点运算,详情请参见8.5
节)。这里只把它作为一个算法示例。
用下面的方法创建一个算法实例:
cv::Ptrcv::ORB ptrORB = cv::ORB::create(); // 默认状态
算法一旦创建完毕,就可以开始使用,例如通用方法read 和write 可用于装载或存储算
法的状态值。算法也有一些专用方法(例如ORB 的方法detect 和compute 用于触发它的主体
计算单元),也有专门用来设置内部参数的设置方法。需要注意的是,你可以把指针类型定为
cv::Ptrcv::Algorithm,但那样就无法使用它的专用方法了。
用GrabCut 算法分割图像
OpenCV 提供了一种常用的图像分割算法,即GrabCut 算法。GrabCut 算法比较复杂,计算量也很大,但结果通常很精确。如果要
从静态图像中提取前景物体(例如从图像中剪切一个物体,并粘贴到另一幅图像),最好采用
GrabCut 算法。
cv::grabCut 函数的用法非常简单,只需要输入一幅图像,并对一些像素做上“属于背景”
或“属于前景”的标记即可。根据这个局部标记,算法将计算出整幅图像的前景/背景分割线。
一种指定输入图像局部前景/背景标签的方法是定义一个包含前景物体的矩形:
// 定义一个带边框的矩形
// 矩形外部的像素会被标记为背景
cv::Rect rectangle(5,70,260,120);
注意,我们在函数的中用cv::GC_INIT_WITH_RECT 标志作为最后一个参数,表示将使用
带边框的矩形模型,输入/输出的分割图像可以是以下四个值之一。
cv::GC_BGD:这个值表示明确属于背景的像素(例如本例中矩形之外的像素)。
cv::GC_FGD:这个值表示明确属于前景的像素(本例中没有这种像素)。
cv::GC_PR_BGD:这个值表示可能属于背景的像素。
cv::GC_PR_FGD:这个值表示可能属于前景的像素(即本例中矩形之内像素的初始值)。
通过提取值为cv::GC_PR_FGD 的像素,可得到包含分割信息的二值图像,实现代码为:
// 取得标记为“可能属于前景”的像素
cv::compare(result,cv::GC_PR_FGD,result,cv::CMP_EQ);
// 生成输出图像
cv::Mat foreground(image.size(),CV_8UC3,cv::Scalar(255,255,255));
image.copyTo(foreground, result); // 不复制背景像素
要提取全部前景像素,即值为cv::GC_PR_FGD 或cv::GC_FGD 的像素,可以检查第一位
的值,代码如下所示:
// 用“按位与”运算检查第一位
result= result&1; // 如果是前景像素,结果为1
这可能是因为这几个常量被定义的值为1 和3,而另外两个(cv::GC_BGD 和cv::GC_PR_BGD)
被定义为0 和2。本例因为分割图像不含cv::GC_FGD 像素(只输入了cv::GC_BGD 像素),所
以得到的结果是一样的。
转换颜色表示法:
RGB 色彩空间的基础是对加色法三原色(红、绿、蓝)的应用选用
这三种颜色作为三原色,是因为将它们组合后可以产生色域很宽的各种颜色,与人类视觉系统对
应。这通常是数字成像中默认的色彩空间,因为这就是用红绿蓝三种滤波器生成彩色图像的方式。
红绿蓝三个通道还要做归一化处理,当三种颜色强度相同时就会取得灰度,即从黑色(0, 0, 0)到白
色(255, 255, 255)。
但利用RGB 色彩空间计算颜色之间的差距并不是衡量两个颜色相似度的最好方式。实际上,RGB 并不是感知均匀的色彩空间。也就是说,两种具有一定差距的颜色可能看起来非常接近,
而另外两种具有同样差距的颜色看起来却差别很大。
为解决这个问题,引入了一些具有感知均匀特性的颜色表示法。CIE Lab*就是一种这样的
颜色模型。把图像转换到这种表示法后,我们就可以真正地使用图像像素与目标颜色之间的欧几
里得距离,来度量颜色之间的视觉相似度。本节将介绍如何转换颜色表示法,以便使用其他色彩
空间。
使用OpenCV 的函数cv::cvtColor 可以轻松转换图像的色彩空间。回顾一下3.2 节提到的
ColorDetector 类。在process 方法中先把输入图像转换成CIE Lab*色彩空间:
cv::Mat ColorDetector::process(const cv::Mat &image) {
// 必要时重新分配二值图像
// 与输入图像的尺寸相同,但用单通道
result.create(image.rows,image.cols,CV_8U);
// 转换成Lab 色彩空间
cv::cvtColor(image, converted, CV_BGR2Lab);
// 取得转换图像的迭代器
cv::Mat_cv::Vec3b::iterator it= converted.begincv::Vec3b();
cv::Mat_cv::Vec3b::iterator itend= converted.endcv::Vec3b();
// 取得输出图像的迭代器
cv::Mat_::iterator itout= result.begin();
// 针对每个像素
for ( ; it!= itend; ++it, ++itout) {
转换后的变量包含颜色转换后的图像,被定义为类ColorDetector 的一个属性:
class ColorDetector {
private:
// 颜色转换后的图像
cv::Mat converted;
输入的目标颜色也需要进行转换——通过创建一个只有单个像素的临时图像,可以实现这种
转换。注意,需要让函数保持与前面几节一样的签名,即用户提供的目标颜色仍然是RGB 格式:
// 设置需要检测的颜色
void setTargetColor(unsigned char red, unsigned char green,
unsigned char blue) {
// 临时的单像素图像
cv::Mat tmp(1,1,CV_8UC3);
tmp.atcv::Vec3b(0,0)= cv::Vec3b(blue, green, red);
// 将目标颜色转换成Lab 色彩空间
cv::cvtColor(tmp, tmp, CV_BGR2Lab);
target= tmp.atcv::Vec3b(0,0);
在将图像从一个色彩空间转换到另一个色彩空间时,会在每个输入像素上做一个线性或非线
性的转换,以得到输出像素。输出图像的像素类型与输入图像是一致的。即使你经常使用8 位像
素,也可以用浮点数图像(通常假定像素值的范围是0~1.0)或整数图像(像素值范围通常是
0~65 535)进行颜色转换。但是,实际的像素值范围取决于指定的色彩空间和目标图像的类型。
比如说CIE Lab色彩空间中的L 通道表示每个像素的亮度,范围是0~100;在使用8 位图像时,
它的范围就会调整为0~255。a 通道和b 通道表示色度组件,这些通道包含了像素的颜色信息,
与亮度无关。它们的值的范围是127~127;对于8 位图像,为了适应0~255 的区间,每个值会加
上128。但是要注意,进行8 位颜色转换时会产生舍入误差,因此转换过程并不是完全可逆的。
大多数常用的色彩空间都是可以转换的。你只需要在OpenCV 函数中指定正确的色彩空间转
换代码(CIE Lab的代码为CV_BGR2Lab),其中就有YCrCb,它是在JPEG 压缩中使用的色
彩空间。把色彩空间从BGR 转换成YCrCb 的代码为CV_BGR2YCrCb。注意,所有涉及三原色(红、
绿、蓝)的转换过程都可以用RGB 和BGR 的次序。
CIE Luv是另一种感知均匀的色彩空间。若想从BGR 转换成CIE Luv,可使用代码
CV_BGR2Luv。Lab和Luv对亮度通道使用同样的转换公式,但对色度通道则使用不同的表
示法。另外,为了实现视觉感知上的均匀,这两种色彩空间都扭曲了RGB 的颜色范围,所以这
些转换过程都是非线性的(因此计算量巨大)。
此外还有CIE XYZ 色彩空间(用代码CV_BGR2XYZ 表示)。它是一种标准色彩空间,用与设
备无关的方式表示任何可见颜色。在Lab和Luv色彩空间的计算中,用XYZ 色彩空间作
为一种中间表示法。RGB 与XYZ 之间的转换是线性的。还有一点非常有趣,就是Y 通道对应着
图像的灰度版本。
HSV 和HLS 这两种色彩空间很有意思,它们把颜色分解成加值的色调和饱和度组件或亮度
组件。人们用这种方式来描述的颜色会更加自然。
你可以把彩色图像转换成灰度图像,输出是一个单通道图像:
cv::cvtColor(color, gray, CV_BGR2Gray);
也可以进行反向的转换,但是那样得到的彩色图像的三个通道是相同的,都是灰度图像中对
应的值。
色调、饱和度和亮度表示颜色:
cv::cvtColor 函数把BGR 图像转换成另一种色彩空间。这里使用转换
代码CV_BGR2HSV:
// 转换成HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);
我们可以用代码CV_HSV2BGR 把图像转换回BGR 色彩空间。通过把图像的通道分割到三个
独立的图像中,我们可以直观地看到每一种HSV 组件,方法如下所示:
// 把3 个通道分割进3 幅图像中
std::vectorcv::Mat channels;
cv::split(hsv,channels);
// channels[0]是色调
// channels[1]是饱和度
// channels[2]是亮度
注意第三个通道表示颜色值,即颜色亮度的近似值。因为处理的是8 位图像,所以OpenCV
会把通道值的范围重新调节为0255(色调除外,它的范围被调节为0180)
我们可以把这几个通道作为灰度图像进行显示
城堡图的亮度通道显示
图像的饱和度通道显示
图像的色调通道
实现原理
之所以要引入色调/饱和度/亮度的色彩空间概念,是因为人们喜欢凭直觉分辨各种颜色,而
它与这种方式吻合。实际上,人类更喜欢用色彩、彩度、亮度等直观的属性来描述颜色,而大多
数直觉色彩空间正是基于这三个属性。色调(hue)表示主色,我们使用的颜色名称(例如绿色、
黄色和红色)就对应了不同的色调值;饱和度(saturation)表示颜色的鲜艳程度,柔和的颜色饱
和度较低,而彩虹的颜色饱和度就很高;最后,亮度(brightness)是一个主观的属性,表示某种
颜色的光亮程度。其他直觉色彩空间使用颜色明度(value)或颜色亮度(lightness)的概念描述
有关颜色的强度。
利用这些颜色概念,能尽可能地模拟人类对颜色的直观感知。因此,它们没有标准的定义。
根据文献资料,色调、饱和度和亮度都有多种不同的定义和计算公式。OpenCV 建议的两种直觉
色彩空间的实现是HSV 和HLS 色彩空间,它们的转换公式略有不同,但是结果非常相似。
亮度成分可能是最容易解释的。在OpenCV 对HSV 的实现中,它被定义为三个BGR 成分中
的最大值,以非常简化的方式实现了亮度的概念。为了让定义更符合人类视觉系统,应该使用均
匀感知的色彩空间Lab和Luv的L 通道。举个例子,L 通道已经考虑到了,在强度相同的
情况下,人们会觉得绿色比蓝色等颜色的亮度更高。
我们还可以人为生成一幅图像,用来说明各种色调/饱和度组合。
cv::Mat hs(128, 360, CV_8UC3);
for (int h = 0; h < 360; h++) {
for (int s = 0; s < 128; s++) {
hs.atcv::Vec3b(s, h)[0] = h/2; // 所有色调角度
// 饱和度从高到低
hs.atcv::Vec3b(s, h)[1] = 255-s*2;
hs.atcv::Vec3b(s, h)[2] = 255; // 常数
}
}
使用HSV 的值可以生成一些非常有趣的效果。一些用照片编辑软件生成的色彩特效就是用
这个色彩空间实现的。你可以修改一幅图像,把它的所有像素都设置为一个固定的亮度,但不改
变色调和饱和度。可以这样实现:
// 转换成HSV 色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);
// 将3 个通道分割到3 幅图像中
std::vectorcv::Mat channels;
cv::split(hsv,channels);
// 所有像素的颜色亮度通道将变成255
channels[2]= 255;
// 重新合并通道
cv::merge(channels,hsv);
// 转换回BGR
cv::Mat newImage;
cv::cvtColor(hsv,newImage,CV_HSV2BGR);
得到的结果如下图所示,看起来像是一幅绘画作品。