OpenCV实战(7)——OpenCV色彩空间转换
0. 前言
为了更好的进行图像处理,我们有时会使用不同的色彩空间。色彩空间是一个抽象的数学模型概念,色彩是人的眼睛对于不同频率的光线的不同感受,为了更好的表示色彩,人们建立了多种色彩模型以一维、二维、三维等坐标系来描述不同色彩,这种坐标系所能定义的色彩范围即色彩空间。
在 OpenCV 策略设计模式一节中,我们已经学习了如何将算法封装到类中,利用封装,算法通过简化的界面变得更易于使用。封装还允许我们在不影响使用它的类的情况下修改算法的实现。在本节中,我们通过修改 ColorDetector 类算法以使用另一颜色空间,通过实战介绍如何实现 OpenCV
的色彩空间转换。
1. RGB 色彩空间
RGB
颜色空间使用红色、绿色和蓝色加色原色。选择这些颜色是因为当它们组合在一起时,可以产生不同颜色的宽色域。事实上,人类视觉系统也是基于对颜色的三原色感知,视锥细胞的敏感性位于红、绿、蓝光谱周围。RGB
颜色空间通常是数字图像中的默认色彩空间,这也正是获取数字图像的方式(捕获的光通过红色、绿色和蓝色滤光片)。此外,在数字图像中,红色、绿色和蓝色通道会被调整,以便在等量组合时获得灰度级强度,即从黑色 (0,0,0)
到白色 (255,255,255)
。
但是,使用 RGB
颜色空间计算颜色之间的距离并不是衡量两种给定颜色之间相似性的最佳方法,这是由于 RGB
并不是一个感知上均匀的色彩空间。这意味着在给定距离处的两种颜色可能看起来非常相似,而具有相同距离的其他两种颜色可能看起来非常不同。
为了解决这个问题,引入了具有感知均匀特性的其他颜色表示,例如,CIE L*a*b*
颜色空间就是这样一种颜色模型,通过将图像转换为这种表示,图像像素和目标颜色之间的欧几里德距离将成为两种颜色之间视觉相似性的度量。在本节中,我们将学习如何将图像颜色转换到 CIE L*a*b*
色彩空间。
2. 色彩空间转换
通过使用 cv::cvtColor
函数可以轻松完成不同颜色空间之间的图像转换。
2.1 CIE Lab* 色彩空间
(1) 首先,在 process
方法开始时将输入图像转换为 CIE L*a*b*
颜色空间:
cv::Mat ColorDetector::process(const cv::Mat& image) {
result.create(image.size(), CV_8U);
// 转换色彩空间
cv::cvtColor(image, converted, cv::COLOR_BGR2Lab);
// 迭代器
cv::Mat_<cv::Vec3b>::const_iterator it = converted.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::const_iterator itend = converted.end<cv::Vec3b>();
cv::Mat_<uchar>::iterator itout = result.begin<uchar>();
for (; it != itend; ++it, ++itout) {
// 像素处理
...
}
return result;
}
(2) converted
变量包含颜色转换后的图像。在 ColorDetector
类中,它被定义为一个类属性:
class ColorDetector {
private:
// 转换色彩后的图像
cv::Mat converted;
(3) 我们还需要转换输入的目标颜色,可以通过创建仅包含一个像素的临时图像来实现此目的。需要注意的是,需要使用相同的签名 setTargetColor
,即用户仍然以 RGB
提供目标颜色:
// 设置待检测颜色
void setTargetColor(uchar blue, uchar green, uchar red) {
target = cv::Vec3b(blue, green, red);
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);
}
}
如果使用修改后的类编译应用程序,将使用 CIE L*a*b*
颜色模型检测目标颜色的像素。
2.2 其它色彩空间
当图像从一种颜色空间转换到另一种颜色空间时,会对每个输入像素应用线性或非线性变换以产生输出像素,输出图像的像素类型与其中一个输入图像相同。即使我们大部分时间使用 8
位像素,我们也可以对浮点图像(像素值通常在 0
和 1.0
之间变化)或整数图像(像素通常在 0
到 65535
之间变化)执行转换。但是,像素值的确切域取决于特定的色彩空间和目标图像类型。例如,在 CIE L*a*b*
颜色空间中,代表每个像素亮度的 L
通道在 0
到 100
之间变化,在 8
位图像的情况下,它会重新缩放至 0
到 255 之间。a
和 b
通道对应于色度分量,这些通道包含有关像素颜色的信息,与其亮度无关,它们的值在 -127
和 127
之间变化;对于 8
位图像,每个值都加上 128
,以使其缩放到 0
到 255
区间内。但是,颜色转换会引入舍入误差,因此转换不完全可逆。
我们也可以使用其他常见的色彩空间。只需向 OpenCV
函数提供正确的色彩空间转换代码,例如对于 CIE L*a*b*
,代码为 cv::COLOR_BGR2Lab
。接下来我们介绍一些常见的色彩空间。
-
YCrCb
是JPEG
压缩中使用的色彩空间,要将颜色空间从BGR
转换为YCrCb
,需要使用cv::COLOR_BGR2YCrCb
-
CIE L*u*v*
颜色空间是另一个感知上均匀的颜色空间,可以使用cv::COLOR_BGR2Luv
将图像从BGR
转换为CIE L*u*v
,L*a*b*
和L*u*v*
对亮度通道使用相同的转换公式,但对色度通道使用不同的表示;需要注意的是,由于这两个颜色空间会扭曲RGB
颜色域以使其与人类感知保持一致,因此这些变换是非线性的,计算成本很高; -
CIE XYZ
颜色空间是一种标准色彩空间,用于以独立于设备的方式表示任何可感知的颜色;可以使用cv::COLOR_BGR2XYV
将图像从BGR
转换为CIE XYZ
空间,在进行图像处理时通常不直接使用此空间;在L*u*v
和L*a*b
颜色空间的计算中,XYZ
颜色空间通常用作中间表示,RGB
和XYZ
之间的转换是线性的;另外,Y
通道对应于图像的灰度版本 -
HSV
和HLS
是另外两种色彩空间,因为它们可以将颜色分解为色调和饱和度分量,再加上颜色的值或亮度分量,这是人类描述颜色的更自然的方式
我们还可以将彩色图像转换为灰度图像,得到的输出是一个单通道图像:
cv::cvtColor(color, gray, cv::COLOR_BGR2Gray);
我们也可以进行反向转换,生成的彩色图像的三个通道使用灰度图像中的相应值进行填充,即得到的彩色图像三个通道的值完全相同。
3. 用色调、饱和度和亮度表示颜色
3.1 直觉色彩空间
我们已经学习了如何使用不同的色彩空间并尝试识别具有特定颜色的图像区域。例如,RGB
颜色空间虽然是电子成像系统中颜色捕获和显示的有效表示,但这种表示不是很直观,因为,这并不是人类观察颜色的方式。我们通常根据色调、亮度或色彩(即颜色是鲜艳的还是柔和的)来谈论颜色,因此,引入了基于色调、饱和度和亮度概念的直觉色彩空间 (phenomenal color spaces
),以使用更直观的属性指定颜色。在本节中,我们将介绍并利用色相、饱和度和亮度作为描述颜色的一种方式。将 BGR 图像转换为直觉色彩空间同样需要使用 cv::cvtColor
函数完成。
(1) 使用 cv::COLOR_BGR2HSV
转换 BGR
色彩空间:
// 转换色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
(2) 我们可以使用 cv::COLOR_HSV2BGR
将图像转换回到 BGR
空间。我们可以通过将转换后的图像通道拆分为三个独立的图像来可视化 HSV
图像的每个组成通道:
// 分割图像通道
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
由于处理的图像为 8
位图像,OpenCV
会重新调整通道值为 0
到 255
区间范围内(色调除外,色调值在 0
和 180
之间)。这便于我们将这些通道显示为灰度图像,图像的值通道可视化结果如下所示:
图像的饱和度通道通道可视化结果如下所示:
最后,图像的色调通道通道可视化结果如下所示:
引入直觉色彩空间是因为它们与人类观察色彩的方式相对应,事实上,人类更喜欢用直观的属性来描述颜色,比如色调、色彩和亮度,这三个属性是大多数直觉色彩空间的基础。色调 (Hue
) 指定主色,不同颜色(如绿色、黄色、蓝色和红色)对应于不同的色调值;饱和度 (saturation
) 指示颜色的鲜艳程度,柔和的颜色饱和度较低,而艳丽的颜色饱和度较高;亮度 (brightness
) 是一种主观属性,指的是颜色的亮度。其他直觉色彩空间使用颜色值 (value
) 或颜色亮度 (lightness
) 作为表征相对颜色强度的一种方式。
这些颜色成分试图模拟人类对颜色的直觉感知,因此,它们没有标准定义。在不同资料中,我们可能会看到色相、饱和度和亮度的不同定义和公式。OpenCV
提出了两种直觉色彩空间的实现:HSV
和 HLS
色彩空间。转换公式略有不同,但它们得到的结果非常相似。
在 HSV
空间的 OpenCV
实现中,值 (value
) 属性被定义为 BGR
三个通道中的最大值,这是亮度概念的一个简单实现。为了更好地匹配人类视觉系统的定义,我们应该使用 L*a*b*
或 L*u*v*
颜色空间中的 L
通道表示亮度概念。
为了计算饱和度 (saturation
),OpenCV
使用基于 BGR
通道的最小值和最大值进行计算:
S = ( m a x ( R , G , B ) − m i n ( R , G , B ) ) m a x ( R , G , B ) S=\frac {(max(R,G,B)-min(R,G,B))} {max(R,G,B)} S=max(R,G,B)(max(R,G,B)−min(R,G,B))
在以上公式中,R
、G
和 B
分量都相等的灰度颜色对应于完全不饱和的颜色;因此,饱和度值为 0
。饱和度是一个介于 0
和 1.0
之间的值,对于 8
位图像,饱和度会被重新调整为 0
到 255
之间的值,使用灰度图像显示时,较亮的区域对应具有较高饱和度。根据定义,不同灰度的饱和度值为零(因为在这种情况下,所有三个 BGR
分量都相等);同时,在饱和度图像中,可以看到一些白色像素位于与原始图像非常暗的区域相对应的位置,这是由于根据以上公式可知,饱和度仅衡量最大和最小 BGR
值之间的相对差异,所以 (1,0,0)
之类的三元组可以得到完全饱和度值 1.0
,即使这种颜色三元组可能为黑色,因此,在暗区测量的饱和度值是不可靠的。
颜色的色调 (hue
) 一般用 0
到 360
度之间的角度值表示,红色为 0
度。在 8 位图像的情况下,OpenCV
将此角度除以 2
,以适应单字节范围。因此,每个色调值对应于给定的色彩,而与其亮度和饱和度无关。需要注意的是,在评估饱和度非常低的颜色时,色调并不太可靠。HSB
颜色空间通常由一个锥体表示,其中内部的每个点都对应一种特定的颜色。角位置对应颜色的色调,饱和度是与中心轴的距离,亮度由高度表示,锥体的尖端对应于未定义色调和饱和度的黑色:
通过使用 HSV
值可以创建有趣的效果。例如,我们可以通过为图像的所有像素分配恒定亮度而不改变色调和饱和度来修改图像:
// 转换色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 分割图像通道
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
channels[2] = 255;
cv::merge(channels, hsv);
// 转换回 BGR 色彩空间
cv::Mat newImage;
cv::cvtColor(hsv, newImage, cv::COLOR_HSV2BGR);
程序执行结果如下图所示:
4. 基于颜色信息进行肤色检测
颜色信息对于特定对象的初始检测非常有用。例如,辅助驾驶应用程序中的道路标志检测可以依靠标准标志的颜色来快速提取潜在的候选道路标志。皮肤颜色的检测是另一个例子,其中检测到的皮肤区域可以用作图像中是否有人的指标;这种方法经常用于手势识别,使用肤色检测来检测手部位置。
通常,要使用颜色进行对象检测,首先需要收集包含从不同环境条件获取的对象的大型图像样本数据库。需要使用这些图像样本定义分类器参数,并用于分类的颜色表示。对于肤色检测,研究表明,来自不同种族的肤色在色调饱和度空间中能够很好地聚集,本节我们将使用色调和饱和度值来识别下图中的肤色:
定义函数 detectHScolor
,该函数基于值的区间(最小和最大色调,以及最小和最大饱和度值)将图像的像素分类为皮肤或非皮肤:
void detectHScolor(const cv::Mat& image, // 输入图像
double minHue, double maxHue, // Hue 区间
double minSat, double maxSat, // Saturation 区间
cv::Mat& mask) { // 输出掩码
// 转换色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 分割图像通道
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
cv::Mat mask1;
cv::threshold(channels[0], mask1, maxHue, 255, cv::THRESH_BINARY_INV);
cv::Mat mask2;
cv::threshold(channels[0], mask2, minHue, 255, cv::THRESH_BINARY);
cv::Mat hueMask;
if (minHue < maxHue) hueMask = mask1 & mask2;
else hueMask = mask1 | mask2;
cv::threshold(channels[1], mask1, maxSat, 255, cv::THRESH_BINARY_INV);
cv::threshold(channels[1], mask2, minSat, 255, cv::THRESH_BINARY);
cv::Mat satMask;
satMask = mask1 & mask2;
// 组合掩码
mask = hueMask & satMask;
}
有大量包含皮肤(和非皮肤)的图像样本可供使用,我们可以使用概率方法来确定在皮肤类中观察给定颜色的可能性与在非皮肤类中观察到相同颜色的可能性。在本节中,我们根据经验为测试图像定义了一个可接受的色调饱和度间区间(色调从 0
到 180
,饱和度从 0
到 255
)。执行以上程序,结果得到如下检测图像:
为简单起见,我们没有在检测中考虑颜色饱和度。实际上,排除具有高饱和度的颜色会降低将亮红色错误检测为皮肤的可能性。显然,可靠且准确的肤色检测需要基于对大量皮肤样本更加精细的分析。仅使用色调信息很难保证对不同图像的均具有良好的检测效果,因为有许多因素会影响摄影中的色彩效果,例如白平衡和照明条件。尽管如此,仅使用色调信息作为初始检测器也可以得到在可接受范围内的结果。
5. 完整代码
完整代码 (huesaturation.cpp
) 如下所示:
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <vector>
void detectHScolor(const cv::Mat& image, // 输入图像
double minHue, double maxHue, // Hue 区间
double minSat, double maxSat, // Saturation 区间
cv::Mat& mask) { // 输出掩码
// 转换色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 分割图像通道
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
cv::Mat mask1;
cv::threshold(channels[0], mask1, maxHue, 255, cv::THRESH_BINARY_INV);
cv::Mat mask2;
cv::threshold(channels[0], mask2, minHue, 255, cv::THRESH_BINARY);
cv::Mat hueMask;
if (minHue < maxHue) hueMask = mask1 & mask2;
else hueMask = mask1 | mask2;
cv::threshold(channels[1], mask1, maxSat, 255, cv::THRESH_BINARY_INV);
cv::threshold(channels[1], mask2, minSat, 255, cv::THRESH_BINARY);
cv::Mat satMask;
satMask = mask1 & mask2;
// 组合掩码
mask = hueMask & satMask;
}
int main() {
cv::Mat image = cv::imread("1.png");
if (!image.data) return 0;
cv::namedWindow("Original image");
cv::imshow("Original image", image);
// 转换色彩空间
cv::Mat hsv;
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 分割图像通道
std::vector<cv::Mat> channels;
cv::split(hsv, channels);
cv::namedWindow("Value");
cv::imshow("Value", channels[2]);
cv::namedWindow("Saturation");
cv::imshow("Saturation", channels[1]);
cv::namedWindow("Hue");
cv::imshow("Hue", channels[0]);
cv::Mat newImage;
cv::Mat tmp(channels[2].clone());
channels[2] = 255;
cv::merge(channels, hsv);
cv::cvtColor(hsv, newImage, cv::COLOR_HSV2BGR);
cv::namedWindow("Fixed Value Image");
cv::imshow("Fixed Value Image", newImage);
channels[1] = 255;
channels[2] = tmp;
cv::merge(channels, hsv);
cv::cvtColor(hsv, newImage, cv::COLOR_HSV2BGR);
cv::namedWindow("Fixed saturation");
cv::imshow("Fixed saturation", newImage);
channels[1] = 255;
channels[2] = 255;
cv::merge(channels, hsv);
cv::cvtColor(hsv, newImage, cv::COLOR_HSV2BGR);
cv::namedWindow("Fixed saturation/value");
cv::imshow("Fixed saturation/value", newImage);
// 显示所有可能的HS色彩
cv::Mat hs(128, 360, CV_8UC3);
for (int h=0; h<360; h++) {
for (int s=0; s<128; s++) {
hs.at<cv::Vec3b>(s,h)[0] = h/2;
hs.at<cv::Vec3b>(s,h)[1] = 255-s*2;
hs.at<cv::Vec3b>(s,h)[2] = 255;
}
}
cv::cvtColor(hs, newImage, cv::COLOR_HSV2BGR);
cv::namedWindow("Hue/Saturation");
cv::imshow("Hue/Saturation", newImage);
// 皮肤分割
image = cv::imread("1.png");
if (!image.data) return 0;
cv::namedWindow("Original image");
cv::imshow("Original image", image);
cv::Mat mask;
detectHScolor(image, 140, 10, 20, 166, mask);
cv::Mat detected(image.size(), CV_8UC3, cv::Scalar(0, 0, 0));
image.copyTo(detected, mask);
cv::imshow("Detection result", detected);
// 对比luminance和brightness
cv::Mat linear(100, 256, CV_8U);
for (int i=0; i<256; i++) linear.col(i) = i;
linear.copyTo(channels[0]);
cv::Mat constante(100, 256, CV_8U, cv::Scalar(128));
constante.copyTo(channels[1]);
constante.copyTo(channels[2]);
cv::merge(channels, image);
cv::Mat brightness;
cv::cvtColor(image, brightness, cv::COLOR_Lab2BGR);
cv::split(brightness, channels);
cv::Mat combined(200, 256, CV_8U);
cv::Mat half1(combined, cv::Rect(0, 0, 256, 100));
linear.copyTo(half1);
cv::Mat half2(combined, cv::Rect(0, 100, 256, 100));
channels[0].copyTo(half2);
cv::namedWindow("Luminance vs Brightness");
cv::imshow("Luminance vs Brightness", combined);
cv::waitKey();
return 0;
}
小结
为了更好的表示色彩,建立了多种色彩模型以一维、二维、三维等坐标系来描述不同色彩,这种坐标系所能定义的色彩范围即色彩空间。本节中,我们介绍了多种常见的色彩空间,包括 RBG
、HSV
HSL
和 CIE L*a*b*
等,使用 cv::cvtColor()
可以将一种色彩空间的输入图像转换为另一种色彩空间,并且通过皮肤检测应用展示了颜色信息在图像处理中的重要作用。
系列链接
OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式