参考:
https://github.com/spmallick/learnopencv
在本教程中,我们将学习计算机视觉中使用的流行颜色空间,并将其用于基于颜色的分割。我们还将以C ++和Python分享演示代码。
1975年,匈牙利专利HU170062在432,003,274,489,856,000(43万亿)的可能性中引入了一个正确的解决方案。现在被称为“魔方”的这项发明在2009年1月份之前风靡全球,销量超过3.5亿。
所以,几天前我的朋友马克告诉我关于构建一个基于计算机视觉的自动化魔方求解器的想法,我很好奇。他试图使用颜色分割来查找立方体的当前状态。虽然他的颜色分段代码在晚上在他的房间里工作得很好,但是在白天他的房间外面,他的颜色分解代码却崩溃了!
他问我求助,我立即明白他哪里出了问题。像许多其他业余电脑视觉爱好者一样,他在做色彩分割时没有考虑不同照明条件的影响。我们在许多计算机视觉应用中遇到了这个问题,这些计算机视觉应用涉及肤色检测,交通灯识别等基于颜色的分割。
文章的结构安排如下:
在本节中,我们将介绍计算机视觉中使用的一些重要色彩空间。 我们不会在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色彩空间具有以下属性
让我们把这两幅图像分解成它们的R,G和B分量,并观察它们以获得更多的色彩空间。
如果你看蓝色的通道,可以看到在室内照明条件下,第二张图像中的蓝色和白色的部分看起来相似,但是在第一张图像中有明显的差别。 这种不均匀性使得在这个色彩空间中基于颜色的分割非常困难。 此外,两个图像的值之间存在总体差异。 下面我们总结了与RGB颜色空间相关的固有问题:
Lab色彩空间有三个组件。
1、L - 亮度(强度)。
2、a-从绿色到品红色的一种颜色成分。
3、b-颜色分量从蓝色到黄色。
Lab色彩空间与RGB色彩空间大不相同。 在RGB色彩空间中,色彩信息被分成三个通道,但相同的三个通道也编码亮度信息。 另一方面,在Lab色彩空间中,L通道独立于色彩信息,仅对亮度进行编码。 另外两个通道编码颜色。
它具有以下属性。
让我们看看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);
YCrCb颜色空间是从RGB颜色空间导出的,并具有以下三个组件。
1、Y - 伽马校正后从RGB获得的亮度或亮度(Luma )分量。
2、Cr = R - Y(的红色成分距离Luma有多远)。
3、Cb = B - Y(蓝色分量距离Luma的有多远)。
此颜色空间具有以下属性。
下面显示了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);
HSV色彩空间有以下三个组成部分
1、H - 色调(主波长)。
2、S - 饱和度(纯度/颜色的阴影)。
3、V值(强度)。
我们列举一些属性。
两幅图像的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);
现在我们已经对不同的色彩空间有了一些了解,我们先尝试用它们来检测立方体中的绿色。
为每个色彩空间查找绿色值的近似范围。 为此,我创建了一个交互式GUI,您可以通过将鼠标悬停在图像上来检查每个像素的所有颜色空间的值,如下所示:
从图像中提取具有与绿色像素值接近的所有像素。 我们可以为每个色彩空间取+/- 40的范围,并检查结果是怎样的。 我们将使用inRange中的opencv函数查找绿色像素的遮罩,然后使用bitwise_and操作使用遮罩从图像中获取绿色像素。
另外请注意,为了将一个像素转换为另一个色彩空间,我们首先需要将一维数组转换为三维数组。
#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)
所以,RGB和LAB似乎足以检测颜色,我们不需要考虑太多。 让我们看看更多的结果。
所以,相同的门槛不适用于黑暗的形象。 做相同的实验来检测黄色给出以下结果。
但为什么结果如此糟糕呢? 这是因为我们猜测了40的门槛。 我做了另一个交互式演示,在这里你可以玩这些值,并试图找到一个适用于所有图像的演示。 看看截图。 但是,那么会出现另一个图像出现的情况,并且不能再次工作。 我们不能盲目地通过试错来取得一些门槛。 我们没有这样做,而是利用色彩空间的力量。
我们需要有一些有条不紊的方法来找到正确的阈值。
我在变化的照明条件下收集了10个立方体的图像,并分别裁剪每种颜色,以获得6种不同颜色的6个数据集。 你可以看到多少变化的颜色进行视觉。
检查不同颜色空间中特定颜色的分布,如蓝色或黄色。 密度图或二维直方图给出了一个给定颜色值的变化的想法。 例如,理想情况下,蓝色图像的蓝色通道应始终具有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)
#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])
可以看出,在相似的照明条件下,所有的地块都非常紧凑。 有一点要注意的是:
当照明变化很大时,我们可以看到:
在最后一节中,我将通过从密度图中获取阈值来显示检测蓝色和黄色片段的结果,并以与我们在第二部分中所做的相同的方式将其应用于各个色彩空间。 当我们在HSV,YCrCb和LAB色彩空间工作时,我们不必担心Intensity分量。 我们只需要指定颜色分量的阈值。 图中显示了我用来生成结果的值。
P.S:如果你对解决魔方有兴趣,可以参考这个循序渐进的指南。
代码下载