在本教程中,我们将学习Computer Vision中使用的流行色彩空间,并将其用于基于颜色的分割。
1975年,匈牙利专利HU170062引入了一种难题,在43,252,003,274,489,856,000(43亿亿)种可能性中,只有一种正确的解决方案。到2009年1月,这项被称为“魔方”的发明席卷全球,销量超过3.5亿。
因此,有位同学又建立基于计算机视觉的自动Rubik立方体求解器的想法,我对此很感兴趣。他试图使用颜色分割来查找多维数据集的当前状态。尽管他的颜色分割代码在他的房间晚上工作得很好,但是白天在他的房间外面却消失了!
他向我寻求帮助,我立即了解他出了问题。像许多其他计算机视觉业余爱好者一样,他在进行颜色分割时没有考虑不同照明条件的影响。我们在许多涉及基于颜色的分割(例如肤色检测,交通信号灯识别)的计算机视觉应用中都遇到了这个问题。让我们看看如何帮助他为他的机器人构建一个强大的颜色检测系统。
文章的结构安排如下:
首先,我们将了解如何在OpenCV中读取图像并将其转换为不同的色彩空间,并了解每个色彩空间的不同通道为我们提供了哪些新信息。
我们将应用Mark所做的一种简单的颜色分割算法,并思考其缺点。
然后,我们将进入一些分析并使用系统的方法进行选择:
正确的色彩空间。
正确的细分阈值。
查看结果
不同的色彩空间
在本节中,我们将介绍计算机视觉中使用的一些重要色彩空间。我们不会描述它们背后的理论,因为可以在Wikipedia上找到它。相反,我们将发展一个基本的直觉并学习一些重要的属性,这些属性对于以后的决策很有用。
让我们加载2个相同立方体的图像。默认情况下,它将以BGR格式加载。我们可以使用OpenCV函数cvtColor()在不同的色彩空间之间进行转换,如下所示。
#python
bright = cv2.imread('cube1.jpg')
dark = cv2.imread('cube8.jpg')
//C++
bright = cv::imread('cube1.jpg')
dark = cv::imread('cube8.jpg')
第一张图像是在室外环境下在明亮的阳光下拍摄的,第二张图像是在正常照明条件下在室内拍摄的。
RGB色彩空间
RGB色彩空间具有以下属性
它是一种加色空间,其中颜色是通过红色,绿色和蓝色值的线性组合获得的。
这三个通道与撞击表面的光量相关。
让我们将两个图像分为R,G和B分量,然后观察它们,以更深入地了解色彩空间。
图2:分别显示的RGB颜色空间的不同通道蓝色(B),绿色(G),红色(R)
建议
如果您查看蓝色通道,可以看到在室内照明条件下,第二张图像中的蓝色和白色部分看起来相似,但是第一张图像中有明显的不同。这种不均匀性使得在这种颜色空间中基于颜色的分割非常困难。此外,两个图像的值之间存在总体差异。下面我们总结了与RGB颜色空间相关的固有问题:
重大的感知不一致性。
色度(颜色相关信息)和亮度(强度相关信息)数据的混合。
LAB颜色空间
Lab颜色空间包含三个组成部分。
L –亮度(强度)。
a –颜色成分从绿色到洋红色。
b –颜色分量,从蓝色到黄色。
Lab颜色空间与RGB颜色空间完全不同。在RGB颜色空间中,颜色信息分为三个通道,但是相同的三个通道也编码亮度信息。另一方面,在Lab颜色空间中,L通道与颜色信息无关,并且仅编码亮度。其他两个通道对颜色进行编码。
它具有以下属性。
感知均匀的色彩空间,近似我们对色彩的感知方式。
与设备无关(捕获或显示)。
在Adobe Photoshop中广泛使用。
通过复杂的转换方程与RGB颜色空间相关。
让我们查看Lab颜色空间中的两个图像,这些图像分为三个通道。
#python
brightLAB = cv2.cvtColor(bright, cv2.COLOR_BGR2LAB)
darkLAB = cv2.cvtColor(dark, cv2.COLOR_BGR2LAB)
//C++
cv::cvtColor(bright, brightLAB, cv::COLOR_BGR2LAB);
cv::cvtColor(dark, darkLAB, cv::COLOR_BGR2LAB);
图3:LAB颜色空间中的亮度(L)和颜色分量(A,B)。
建议
从图中可以很明显地看出,照度的变化主要影响了L分量。
包含颜色信息的A和B组件没有发生大的变化。
在B分量中绿色,橙色和红色的相应值(A分量的极值)不变,在蓝色分量中蓝色和黄色(B分量的极值)的相应值不变。组件。
YCrCb颜色空间
YCrCb颜色空间是从RGB颜色空间派生的,并且具有以下三个组件。
Y –伽玛校正后从RGB获得的亮度或亮度分量。
Cr = R – Y(距离Luma的红色分量多远)。
Cb = B – Y(距离Luma的蓝色分量多远)。
此色彩空间具有以下属性。
将亮度和色度分量分成不同的通道。
通常用于电视传输的压缩(Cr和Cb成分)。
取决于设备。
YCrCb颜色空间中的两个图像分为两个通道,如下所示
#python
brightYCB = cv2.cvtColor(bright, cv2.COLOR_BGR2YCrCb)
darkYCB = cv2.cvtColor(dark, cv2.COLOR_BGR2YCrCb)
//C++
cv::cvtColor(bright, brightYCB, cv::COLOR_BGR2YCrCb);
cv::cvtColor(dark, darkYCB, cv::COLOR_BGR2YCrCb);
图4:YCrCb颜色空间中的亮度(Y)和色度(Cr,Cb)分量。
建议
关于亮度变化,可以对强度和颜色分量进行与LAB类似的观察。
与LAB相比,即使在室外图像中,红色和橙色之间的感知差异也较小。
白色的所有三个部分均发生了变化。
HSV色彩空间
HSV颜色空间具有以下三个组成部分
H –色相(主波长)。
S –饱和度(纯度/颜色阴影)。
V –值(强度)。
让我们枚举其一些属性。
最好的事情是,它仅使用一个通道来描述颜色(H),从而非常直观地指定颜色。
取决于设备。
这两个图像的H,S和V分量如下所示。
#python
brightHSV = cv2.cvtColor(bright, cv2.COLOR_BGR2HSV)
darkHSV = cv2.cvtColor(dark, cv2.COLOR_BGR2HSV)
//C++
cv::cvtColor(bright, brightHSV, cv::COLOR_BGR2HSV);
cv::cvtColor(dark, darkHSV, cv::COLOR_BGR2HSV);
图5:HSV颜色空间中的色相(H),饱和度(S)和值(V)分量
建议
两个图像中的H分量非常相似,这表明即使在光照变化下颜色信息也完整无缺。
在两个图像中,S分量也非常相似。
V分量捕获落在其上的光量,因此由于照明的变化而发生变化。
室外红色图像和室内图像的红色值之间存在巨大差异。这是因为色相表示为圆圈,红色表示起始角度。因此,它可能取[300,360]到[0,60]之间的值。
如何使用这些色彩空间进行细分
最简单的方法
现在我们对不同的色彩空间有了一些了解,让我们首先尝试使用它们从多维数据集中检测绿色。
步骤1:获取特定颜色的颜色值
找到每个颜色空间的绿色值的近似范围。为此,我制作了一个交互式GUI,您可以通过将鼠标悬停在图像上来检查每个像素的所有颜色空间的值,如下所示:
图6:演示显示了户外图像在不同颜色空间中的像素及其值。
第2步:应用细分阈值
从图像中提取值接近绿色像素值的所有像素。我们可以为每个色彩空间采用+/- 40的范围,并检查结果的外观。我们将使用inRange中的opencv函数查找绿色像素的蒙版,然后使用bitwise_and操作使用该蒙版从图像中获取绿色像素。
另请注意,要将一个像素转换为另一种颜色空间,我们首先需要将1D阵列转换为3D阵列。
#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)
//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
(0,0)); Vec3b ycbPixel(ycb.at
(0,0)); Vec3b labPixel(lab.at
(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)
图7:RGB看起来不错,也许我们只是在浪费时间
因此,似乎RGB和LAB足以检测颜色,我们不需要花太多时间。让我们看看更多结果
图8:对室内图像应用相同的阈值无法检测所有颜色空间中的绿色立方体。
因此,相同的阈值不适用于深色图像。进行相同的实验以检测黄色,得出以下结果。
图9:使用相同的技术和从明亮图像获得的阈值(对于黄色)检测黄色碎片。HSV和YCrCb仍然表现不佳。
室内图像上黄色的初始结果
图10:尝试使用从明亮的立方体获得的阈值检测黄色碎片。所有颜色空间再次失败。
但是为什么结果这么差呢?这是因为我们对阈值进行了40个疯狂的猜测。我制作了另一个交互式演示,您可以在其中演示这些值,并尝试找到一个适用于所有图像的演示。查看屏幕截图。但是,在某些情况下,还会有其他图像出现而无法再次使用。我们不能只是盲目地尝试和尝试一些门槛。我们这样做并不是在利用色彩空间的力量。
我们需要有一些系统的方法来找到正确的阈值。
图11:演示的屏幕快照,用于处理不同的值以检测给定图像在所有颜色空间中的特定颜色。
一些数据分析以寻求更好的解决方案
步骤1:数据收集
我收集了10个在不同照明条件下的立方体图像,并分别裁剪每种颜色以获得6种不同颜色的6个数据集。您可以直观地看到颜色的变化。
图片:显示由于光照条件变化而引起的颜色变化
第2步:计算密度图
检查特定颜色(例如蓝色或黄色)在不同颜色空间中的分布。密度图或2D直方图给出了有关给定颜色的值变化的想法。例如,理想情况下,蓝色图像的蓝色通道应始终具有255的值。但是实际上,它的分布范围是0到255。
我只显示BGR颜色空间的代码。您需要针对所有颜色空间进行此操作。
我们将首先加载所有蓝色或黄色的图像
#python
B = np.array([])
G = np.array([])
R = np.array([])
im = cv2.imread(fi)
分隔通道,并通过附加每个图像的值为每个通道创建和排列。
#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)
使用matplotlib中的直方图绘制2D直方图
#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])
观察结果:类似的照明
图13:密度图显示了2个相似的蓝色明亮图像在彩色通道中的值变化
图14:密度图显示了黄色的2张相似的明亮图像在彩色通道中的值变化
可以看出,在相似的照明条件下,所有地块都非常紧凑。需要注意的几点是:
YCrCb和LAB比其他紧凑得多
在HSV中,S方向(色纯度)有变化,但H方向几乎没有变化。
观察结果:不同的照明
图15:密度图显示了在蓝色变化的光照下颜色通道中值的变化
图16:密度图显示了在黄色变化的光照下颜色通道中值的变化
随着光照变化很大,我们可以看到:
理想情况下,我们希望使用色彩空间具有最紧凑/最密集的颜色通道的色彩空间。
RGB的密度图急剧增大。这意味着通道值的变化非常大,固定阈值是一个大问题。固定较高的范围将检测到与所需颜色相似的颜色(假阳性),而较低的范围将无法在不同的照明下检测所需颜色(假阴性)。
在HSV中,由于仅H分量包含有关绝对颜色的信息。因此,与YCrCb(Cr和Cb)和LAB(A和B)中的2个旋钮相比,我只能微调一个旋钮(H)来指定颜色,这成为我色彩空间的首选。
比较YCrCb和LAB的图,在LAB的情况下显示出更高的紧密度。因此,对我而言,下一个最佳选择是LAB颜色空间。
最终结果
在最后一部分中,我将展示通过检测密度图中的阈值并将其应用到各个颜色空间中来检测蓝色和黄色部分的结果,方法与第二部分相同。在HSV,YCrCb和LAB颜色空间中工作时,我们不必担心强度分量。我们只需要指定颜色分量的阈值即可。图中显示了我为生成结果所采用的值。
图17:演示图像1
图18:演示图像1上的黄色检测结果
图19:演示图像1上的蓝色检测结果
图20:演示图像2
图21:演示图像2上的黄色检测结果
图22:演示图像2上的蓝色检测结果
图23:演示图像3
图24:演示图像3上的黄色检测结果
图25:演示图像3上的蓝色检测结果
在以上结果中,我直接从密度图中获取了值。我们还可以选择采用属于密度图中最密集区域的值,这将有助于更严格地控制颜色范围。这将留下一些孔和杂散的像素,可以使用“侵蚀和扩散”以及“过滤”将其清洁。
相关源码图片下载地址:关注“图像算法”微信公众号,回复颜色空间
色彩空间的其他有用应用
直方图均衡化通常在灰度图像上完成。但是,可以通过将RGB图像转换为YCbCr并仅对Y通道进行直方图均衡来对彩色图像进行均衡。
通过将图像转换为Lab色彩空间,在两个图像之间进行颜色转移。
智能手机相机应用程序(例如Google相机或Instagram)中的许多滤镜都利用这些颜色空间转换来创建这些炫酷效果!
相关源码图片下载地址:关注“图像算法”微信公众号,回复颜色空间