OpenCV中的颜色空间
本文是根据 Color spaces in OpenCV (C++ / Python) 中对于OpenCV颜色空间的讨论整理而成,用于之后的学习和应用。
§01 前 言
本文将会讨论在计算机视觉中流行的颜色空间,并通过它来进行基于颜色的图像分割。我们也会分享C++和Python的演示代码。
1.1 自动求解魔方
在1975年,匈牙利的专利(专利号HU170062)介绍了一个智力玩具,它是从43,252,003,274,489,856,000(43×1030)种可能性中找出唯一正确的解。这个发明现在被称为三阶魔方(Rubik’ Cube),它风靡全球,截止到2009年一月份已经售出超过3.5亿只。
几天前我的朋友 Mark 回来告诉我他想编写一个计算机视觉程序来自动求出魔方的解,我被深深吸引了。他计划使用魔方上的颜色来进行颜色分割,进而获得当前魔方的状态。程序在他的房间环境中运行的很好,但在房间外的白天却无法工作了!
他向我寻求帮助,我立刻明白问题所在了。就像其他的计算机视觉爱好者那样,在进行颜色分割过程中他没有考虑到不同的光线环境。在很多的计算机视觉应用领域,比如皮肤颜色检测、交通等识别等,进行基于颜色图像分割时都会遇到这个问题。下面让我们看看如何能够帮助他构建一个更加鲁棒(可靠)的颜色检测系统,从而实现他的魔方求解机器人。
1.2 博文内容
下面博文的内容安排如下:
- 首先,看一下如何将图片通过OpenCV读入计算机并转换成不同的颜色空间。查看不同的颜色空间中的颜色通道可以提供我们什么图像信息;
- 我们放置Mark所做的简单的基于颜色分割算法并思考算法的缺点;
- 接着,我们转向一些分析,并使用系统的方法来选择:
- 正确的颜色空间;
- 正确的阈值来进行图像分割;
- 考察相应的结果;
§01 不同的颜色空间
本节中,将讨论一些在计算机视觉中的一些重要颜色空间。由于在WiKipedia中可以找到相关的理论,相关部分我们省略。取而代之的是建立一些直观概念,并学会一些重要的特征,用于后期决定。
2.1 载入图片
我们再入两张相同魔方照片,缺省情况下使用BGR格式。我们使用OpenCV中的 cvtColor() 函数将它们转换到不同的颜色空间,下面代码给出相应的示例:
- Python
#python
bright = cv2.imread('cube1.jpg')
dark = cv2.imread('cube8.jpg')
- 1
- 2
- 3
- C++
//C++
bright = cv::imread('cube1.jpg')
dark = cv::imread('cube8.jpg')
- 1
- 2
- 3
▲ 图2.1 同样的魔方在不同照明环境下的两张图片||左边:在室外;右边:在室内
2.2 RGB颜色空间
RGB颜色空间具有以下性质:
- 这是一个“叠加性”颜色空间,它是通过把 红、绿、蓝三种颜色线性叠加后获得值;
- 三个颜色通道信息有相关性,都包含有由于物体表面照射光强信息;
下面是将两幅图片分解成R,G,B三个颜色成分,通过观察了解颜色空间内部信息。
▲ 图2.2.1 将图片中的B,G,R不同颜色通道分别进行显示
2.2.1 观察结果
如果你观察蓝色通道的图片,你可以看到在第二张图像(室内拍摄)蓝色与白色非常相似,但在第一张图(室外拍摄)他们却相差很大。这种不一致性使得基于RGB颜色空间的图像分割变得非常困难。进一步,两张图偏重的数值存在整体差异。 下面给出了在RGB颜色空间中存在的固有问题:
- 严重的感知不一致性;
- 对色度(与颜色相关的信息)和亮度(与光强相关的信息)数据混合了;
2.3 LAB颜色空间
LAB 颜色空间有三个组成部分:
- L- 亮度(光强)
- a - 颜色组成部分,从绿色 到 品红;
- b - 颜色组成部分,从 蓝色 到 黄色;
LAB颜色空间与RGB 颜色空间有着很大的不同。 在RGB颜色空间中, 色彩信息吧分为R、G、B三个空间,但它们都包含着亮度信息。与其不同,Lab颜色空间 L通道与颜色通道不想管,仅仅对亮度信息编码。 其他两个通道对颜色进行编码。
它具有以下性质:
- 颜色感知统一,与我们感知色彩近似;
- 不依赖于对摄像和显示设备;
- 在Adobe Photoshop中被广泛是使用;
- 通过复杂的转换方程从RGB空间进行相互转换;
下面我们查看 一下在Lab 颜色空间中,上述两个图片的情况。
- Python
#python
brightLAB = cv2.cvtColor(bright, cv2.COLOR_BGR2LAB)
darkLAB = cv2.cvtColor(dark, cv2.COLOR_BGR2LAB)
- 1
- 2
- 3
- C++
//C++
cv::cvtColor(bright, brightLAB, cv::COLOR_BGR2LAB);
cv::cvtColor(dark, darkLAB, cv::COLOR_BGR2LAB);
- 1
- 2
- 3
▲ 图2.3.1 图像分割成L(亮度),A(颜色),B(颜色)通道成分显示
(1)观察结果
- 可以看的很清楚图像的照明改变主要影响亮度成分;
- A,B两个颜色通道则对于亮度的改变变化不大;
- 绿色、橙色以及红色(红色是A颜色成分中的主要组成)在B颜色通道中没有改变。蓝色和黄色(B颜色成分中的主要组成)在A颜色通道中没有改变。
2.4 YCrCb颜色通道
YCrCb颜色空间是从RGB颜色空间推导出来的,具有以下三个组成部分:
1. Y - 亮度部分,是通过伽马校正后的RGB获取;
2. Cr = R - Y (反映了 R与Y的偏差)
3. Cb = B - Y (反映了B与Y的偏差)
这个颜色空间具有以下特性:
-
将亮度和色彩成分分离到不同的通道;
-
在电视信息压缩传输中(Cr,Cb被降采样)被广泛使用;
-
与设想和显示设备有关系;
-
Python
#python
brightYCB = cv2.cvtColor(bright, cv2.COLOR_BGR2YCrCb)
darkYCB = cv2.cvtColor(dark, cv2.COLOR_BGR2YCrCb)
- 1
- 2
- 3
- C++
//C++
cv::cvtColor(bright, brightYCB, cv::COLOR_BGR2YCrCb);
cv::cvtColor(dark, darkYCB, cv::COLOR_BGR2YCrCb);
- 1
- 2
- 3
▲ 图2.4.1 YCrCb颜色空间中的亮度和色差(Cr,Cb)通道图像
2.4.1 观察结果
- 可以看到由于外部光线环境差异对于亮度和颜色成分的影响,这一点与LAB颜色空间相似;
- 相比于LAB颜色空间,室外拍摄的图像中的红色和橙色相差更小;
- 在三个组成份中,白色都有着变化;
2.5 HSV颜色空间
HSV颜色空间中的三个成分:
- H - 色度 (由光线波长决定)
- S - 饱和度 (纯色/颜色灰度)
- V - 亮度值(光强)
下面列举出它的特性:
- 它只使用 一个通道(H)来刻画颜色,这是它最好的一个特征,对于特定颜色非常直观;
- 与设想、显示设备相关;
下面给出了 两个魔方图片的H,S,V通道图像;
- Python
#python
brightHSV = cv2.cvtColor(bright, cv2.COLOR_BGR2HSV)
darkHSV = cv2.cvtColor(dark, cv2.COLOR_BGR2HSV)
- 1
- 2
- 3
- C++
//C++
cv::cvtColor(bright, brightHSV, cv::COLOR_BGR2HSV);
cv::cvtColor(dark, darkHSV, cv::COLOR_BGR2HSV);
- 1
- 2
- 3
▲ 图2.5.1 在HSV颜色空间中的三个通道的图像
2.5.1 观察结果
- 两幅不同光线下的图片中, 色度分量(H)非常一致,表明了颜色信息在两种光强下没有遭到破坏;
- 在两幅图中的饱和度(S)分量也很相似;
- 亮度(V)分量包含有落在魔方表面光线数量,所以在照明条件变化时该分量肯定发生变化。
- 在室内和室外两张拍摄的图片中,红色出现了明显的变化。这是因为色度是表示成一个圆形,而红色对应的起始角度。所以红色可能取值为(300,360),或者(0, 60)
§02 基于颜色空间分割
3.1 简单的方式
我们已经具备了颜色空间的基本概念,下面我们尝试着使用他们来检测魔方中的绿色。
- 第一步:提取特定颜色的颜色数值
在不同的颜色空间中,针对绿色获取它所对应的取值范围 。我编写了一个交互式GUI 可以检查每个像素在所有颜色空间中的取值,只要鼠标停留在图片上即可。下面显示了运行结果:
▲ 图3.1.1 演示程序显示了某一个像素在不同颜色空间中的取值。这是室外拍摄的图像
- 第二步: 使用阈值对图像进行分割
把图片中所有取值与绿色接近的像素抽出,使用在所有的颜色空间中,取值范围都都以为±40
,看看结果如何。 使用 OpenCV
中的 inRange
函数来获得绿色像素的遮掩图, 使用bitsiwze_and
函数 利用 遮掩图 获得图像中的绿色像素。
请注意为了将每一个像素转换成另外的颜色空间,我们需要吧 1D 数组转换成 3D数组。
- Python
#python
bgr = [40, 158, 16]
thresh = 40
minBGR = np.array([bgr[0] - thresh, bgr[1] - thresh, bgr[2] - thresh])
maxBGR = np.array([bgr[0] + thresh, bgr[1] + thresh, bgr[2] + thresh])
maskBGR = cv2.inRange(bright,minBGR,maxBGR)
resultBGR = cv2.bitwise_and(bright, bright, mask = maskBGR)
#convert 1D array to 3D, then convert it to HSV and take the first element
# this will be same as shown in the above figure [65, 229, 158]
hsv = cv2.cvtColor( np.uint8([[bgr]] ), cv2.COLOR_BGR2HSV)[0][0]
minHSV = np.array([hsv[0] - thresh, hsv[1] - thresh, hsv[2] - thresh])
maxHSV = np.array([hsv[0] + thresh, hsv[1] + thresh, hsv[2] + thresh])
maskHSV = cv2.inRange(brightHSV, minHSV, maxHSV)
resultHSV = cv2.bitwise_and(brightHSV, brightHSV, mask = maskHSV)
#convert 1D array to 3D, then convert it to YCrCb and take the first element
ycb = cv2.cvtColor( np.uint8([[bgr]] ), cv2.COLOR_BGR2YCrCb)[0][0]
minYCB = np.array([ycb[0] - thresh, ycb[1] - thresh, ycb[2] - thresh])
maxYCB = np.array([ycb[0] + thresh, ycb[1] + thresh, ycb[2] + thresh])
maskYCB = cv2.inRange(brightYCB, minYCB, maxYCB)
resultYCB = cv2.bitwise_and(brightYCB, brightYCB, mask = maskYCB)
#convert 1D array to 3D, then convert it to LAB and take the first element
lab = cv2.cvtColor( np.uint8([[bgr]] ), cv2.COLOR_BGR2LAB)[0][0]
minLAB = np.array([lab[0] - thresh, lab[1] - thresh, lab[2] - thresh])
maxLAB = np.array([lab[0] + thresh, lab[1] + thresh, lab[2] + thresh])
maskLAB = cv2.inRange(brightLAB, minLAB, maxLAB)
resultLAB = cv2.bitwise_and(brightLAB, brightLAB, mask = maskLAB)
cv2.imshow("Result BGR", resultBGR)
cv2.imshow("Result HSV", resultHSV)
cv2.imshow("Result YCB", resultYCB)
cv2.imshow("Output LAB", resultLAB)
- 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
- C++
//C++ code
cv::Vec3b bgrPixel(40, 158, 16);
// Create Mat object from vector since cvtColor accepts a Mat object
Mat3b bgr (bgrPixel);
//Convert pixel values to other color spaces.
Mat3b hsv,ycb,lab;
cvtColor(bgr, ycb, COLOR_BGR2YCrCb);
cvtColor(bgr, hsv, COLOR_BGR2HSV);
cvtColor(bgr, lab, COLOR_BGR2Lab);
//Get back the vector from Mat
Vec3b hsvPixel(hsv.at<Vec3b>(0,0));
Vec3b ycbPixel(ycb.at<Vec3b>(0,0));
Vec3b labPixel(lab.at<Vec3b>(0,0));
int thresh = 40;
cv::Scalar minBGR = cv::Scalar(bgrPixel.val[0] - thresh, bgrPixel.val[1] - thresh, bgrPixel.val[2] - thresh)
cv::Scalar maxBGR = cv::Scalar(bgrPixel.val[0] + thresh, bgrPixel.val[1] + thresh, bgrPixel.val[2] + thresh)
cv::Mat maskBGR, resultBGR;
cv::inRange(bright, minBGR, maxBGR, maskBGR);
cv::bitwise_and(bright, bright, resultBGR, maskBGR);
cv::Scalar minHSV = cv::Scalar(hsvPixel.val[0] - thresh, hsvPixel.val[1] - thresh, hsvPixel.val[2] - thresh)
cv::Scalar maxHSV = cv::Scalar(hsvPixel.val[0] + thresh, hsvPixel.val[1] + thresh, hsvPixel.val[2] + thresh)
cv::Mat maskHSV, resultHSV;
cv::inRange(brightHSV, minHSV, maxHSV, maskHSV);
cv::bitwise_and(brightHSV, brightHSV, resultHSV, maskHSV);
cv::Scalar minYCB = cv::Scalar(ycbPixel.val[0] - thresh, ycbPixel.val[1] - thresh, ycbPixel.val[2] - thresh)
cv::Scalar maxYCB = cv::Scalar(ycbPixel.val[0] + thresh, ycbPixel.val[1] + thresh, ycbPixel.val[2] + thresh)
cv::Mat maskYCB, resultYCB;
cv::inRange(brightYCB, minYCB, maxYCB, maskYCB);
cv::bitwise_and(brightYCB, brightYCB, resultYCB, maskYCB);
cv::Scalar minLAB = cv::Scalar(labPixel.val[0] - thresh, labPixel.val[1] - thresh, labPixel.val[2] - thresh)
cv::Scalar maxLAB = cv::Scalar(labPixel.val[0] + thresh, labPixel.val[1] + thresh, labPixel.val[2] + thresh)
cv::Mat maskLAB, resultLAB;
cv::inRange(brightLAB, minLAB, maxLAB, maskLAB);
cv::bitwise_and(brightLAB, brightLAB, resultLAB, maskLAB);
cv2::imshow("Result BGR", resultBGR)
cv2::imshow("Result HSV", resultHSV)
cv2::imshow("Result YCB", resultYCB)
cv2::imshow("Output LAB", resultLAB)
- 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
▲ 图3.1.2 在RGB颜色空间中分割效果很好,也许讨论颜色空间实在浪费时间
3.1.1 更多的一些结果
上面的结果显示,利用RGB,LAB颜色空间可以获得足够好的绿色分割效果,似乎前面我们想多了。下面让我们看看更多的结果。
▲ 图3.1.3 使用相同的阈值处理室内拍摄的魔方图片,在所有的色彩空间效果都很差
因此,对于暗的图像,使用相同的阈值恐怕不行。对于黄色使用相同的方法进行检测,下面是相应的结果:
▲ 图3.1.4 使用相同的方法对于黄色进行检测,图片是亮(室外)图片,可以看到 HSV , YCrCb颜色空间表现不错
▲ 图3.1.5 对于室内拍摄的暗的图片检测黄色,所有的颜色空间结果都不好
3.1.2 为什么呢?
为什么检测结果如此不好呢?这是由于我们粗暴的采用40作为分隔阈值。下面通过另外一个交互式的演示程序,你可以调整某些数值来对所有的图片进行测试来寻找一个合适的数值。下面的程序截图中,显示应用到其它照片效果还是不好。我们不能够这样盲目的通过试凑的方式确定检测阈值。现在我们还没有使用到颜色空间的强大威力。
我们需要寻找确定合适阈值有条理的方法。
▲ 图3.1.6 对于给定的图片在所有颜色空间中寻找检测特定颜色的演示程序截屏图片
§03 数据分析
4.1 处理方法
- 步骤1: 数据搜集
我搜集了在不同光线环境下的10张魔方的照片,然后把每个颜色裁剪下来组成六种颜色的六个数据集合。你可以看到他们的变化情况。
▲ 图4.1 不同光线环境下的颜色变化
- 步骤2: 计算密度图表
检查特定颜色(比如蓝色,或者黄色)在不同颜色空间中的分布。 密度图表(Density Plot)或者2D直方图可以对给定的颜色的变化范围有个直观的了解。比如,理想情况下,蓝色在蓝色通道里对应的数值应该为255 ,但实际上,它的分布在 0 ~ 255。
下面的代码仅仅对BGR颜色空间中显示密度图表,你可以尝试其他的颜色空间。
1. 将所有蓝色,或者黄色图片读入;
- Python
#python
B = np.array([])
G = np.array([])
R = np.array([])
im = cv2.imread(fi)
- 1
- 2
- 3
- 4
- 5
-
分割成不同的颜色通道,为每个通道建立一个数组,添加每个图像中的颜色数值。
-
Python
#python
b = im[:,:,0]
b = b.reshape(b.shape[0]*b.shape[1])
g = im[:,:,1]
g = g.reshape(g.shape[0]*g.shape[1])
r = im[:,:,2]
r = r.reshape(r.shape[0]*r.shape[1])
B = np.append(B,b)
G = np.append(G,g)
R = np.append(R,r)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
使用matplotlib中的直方图绘制函数显示 2D 直方图。
-
Python
#python
nbins = 10
plt.hist2d(B, G, bins=nbins, norm=LogNorm())
plt.xlabel('B')
plt.ylabel('G')
plt.xlim([0,255])
plt.ylim([0,255])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
▲ 图4.2 密度图表显示了蓝色在亮度相近图像中的色彩通道数值的变化
▲ 图4.3 密度图表显示了黄色在亮度相近的图像中色彩通道取值的变化分布
可以看出,在相同的外部光线条件下 ,所有的分布都非常紧凑。但需要注意的是:
- YCrCb,LAB颜色空间聚集程度更高;
- HVS颜色空间中, S方向上存在差异;但H方向上变化很小。
4.2 数据分析
▲ 图4.4 蓝色在亮度不同的图片中的色彩通道数值变化分布
▲ 图4.5 黄色在亮度不同的图像中色彩通道数值变化分布
在外部光线环境变化较大的时候,我们发现:
- 理想情况下,我们希望在一个颜色空间使得密度图表显得紧凑/聚集中来完成图像分割工作;
- RGB颜色空间中的密度图表变化剧烈。这说明颜色通道数值变化大,采用阈值分割图像会产生很大的问题。大的阈值范围将会把期望检测颜色相近的颜色也误检测出来(False Positive),但小的阈值范围则会在不同的亮度图片中遗漏期望检测的颜色(False Negatives).
- HVS颜色空间中只有H分量包含有绝对颜色。所以它成为我的首选颜色空间。因为我只需使用H滑动杆调整,而不用在两个滑动杆(在YCrCb空间,LAB空间),这样调整简单。
- 比较一下 YCrCb,LAB两个颜色看吗,可以看到 LAB颜色空间中的分布更加的紧凑,所以下一个选择是LAB颜色空间。
4.3 最后处理结果
本文最后一部分,我们现实采用通过密度图表中所选择的阈值来对蓝色和黄色进行分割的结果。在后面我们也会采用相同的方法对其它颜色进行分割。在HSV,YCrCb,LAB 颜色空间中我们不需要担心亮度成分的影响。我们只需要在颜色成分中确定相应的分隔阈值即可。下面就是我用于分割蓝色和黄色所采取的的阈值以及分割的结果。
▲ 图4.3.1 测试图像1
▲ 图4.3.2 演示图像中的黄色分割结果
▲ 图4.3.3 演示图像中的蓝色分割结果
▲ 图4.3.4 测试图像2
▲ 图4.3.5 测试图像2 中的 黄色分割结果
▲ 图4.3.6 测试图像中蓝色分割结果
▲ 图4.3.7 测试图像3
▲ 图4.3.8 测试图像3 中的黄色分割结果
▲ 图4.3.9 测试图像3中的蓝色分割结果
在上面的分割结果中,我是直接从密度图表中选取的阈值。我们还可以选择在密度图表中最大的密度区域数值御用分割,这样可以更紧凑控制颜色范围。这会导致在分割图像中出现空洞,可以通过后期的滤波、膨胀和腐蚀来解决。
§04 颜色空间应用程序
下面是一些其它在颜色空间比较有用方法:
- 在灰度图像中进行直方图均衡化。你也可以吧彩色图像转换成YCrCb颜色空间后,针对Y通道进行直方图均衡化。
- 将图像转换到LAB颜色空间,可以实现两个图像之间色彩转移;
- 在一些手机APP中,比如Google Camera 或 Histagram,使用了颜色空间转换来实现很多炫酷的特效。
5.1 求解魔方
如果你对于求解三阶魔方感兴趣, 这个链接 中会给出求解步骤指导。
■ 相关文献链接:
● 相关图表链接:
- 图2.1 同样的魔方在不同照明环境下的两张图片||左边:在室外;右边:在室内
- 图2.2.1 将图片中的B,G,R不同颜色通道分别进行显示
- 图2.3.1 图像分割成L(亮度),A(颜色),B(颜色)通道成分显示
- 图2.4.1 YCrCb颜色空间中的亮度和色差(Cr,Cb)通道图像
- 图2.5.1 在HSV颜色空间中的三个通道的图像
- 图3.1.1 演示程序显示了某一个像素在不同颜色空间中的取值。这是室外拍摄的图像
- 图3.1.2 在RGB颜色空间中分割效果很好,也许讨论颜色空间实在浪费时间
- 图3.1.3 使用相同的阈值处理室内拍摄的魔方图片,在所有的色彩空间效果都很差
- 图3.1.4 使用相同的方法对于黄色进行检测,图片是亮(室外)图片,可以看到 HSV , YCrCb颜色空间表现不错
- 图3.1.5 对于室内拍摄的暗的图片检测黄色,所有的颜色空间结果都不好
- 图3.1.6 对于给定的图片在所有颜色空间中寻找检测特定颜色的演示程序截屏图片
- 图4.1 不同光线环境下的颜色变化
- 图4.2 密度图表显示了蓝色在亮度相近图像中的色彩通道数值的变化
- 图4.3 密度图表显示了黄色在亮度相近的图像中色彩通道取值的变化分布
- 图4.4 蓝色在亮度不同的图片中的色彩通道数值变化分布
- 图4.5 黄色在亮度不同的图像中色彩通道数值变化分布
- 图4.3.1 测试图像1
- 图4.3.2 演示图像中的黄色分割结果
- 图4.3.3 演示图像中的蓝色分割结果
- 图4.3.4 测试图像2
- 图4.3.5 测试图像2 中的 黄色分割结果
- 图4.3.6 测试图像中蓝色分割结果
- 图4.3.7 测试图像3
- 图4.3.8 测试图像3 中的黄色分割结果
- 图4.3.9 测试图像3中的蓝色分割结果
文章来源: zhuoqing.blog.csdn.net,作者:卓晴,版权归原作者所有,如需转载,请联系作者。
原文链接:zhuoqing.blog.csdn.net/article/details/122750434
- 点赞
- 收藏
- 关注作者
评论(0)