learn opencv-OpenCV中的颜色空间(C ++ / Python)

参考:
https://github.com/spmallick/learnopencv


在本教程中,我们将学习计算机视觉中使用的流行颜色空间,并将其用于基于颜色的分割。我们还将以C ++和Python分享演示代码。

1975年,匈牙利专利HU170062在432,003,274,489,856,000(43万亿)的可能性中引入了一个正确的解决方案。现在被称为“魔方”的这项发明在2009年1月份之前风靡全球,销量超过3.5亿。

所以,几天前我的朋友马克告诉我关于构建一个基于计算机视觉的自动化魔方求解器的想法,我很好奇。他试图使用颜色分割来查找立方体的当前状态。虽然他的颜色分段代码在晚上在他的房间里工作得很好,但是在白天他的房间外面,他的颜色分解代码却崩溃了!

他问我求助,我立即明白他哪里出了问题。像许多其他业余电脑视觉爱好者一样,他在做色彩分割时没有考虑不同照明条件的影响。我们在许多计算机视觉应用中遇到了这个问题,这些计算机视觉应用涉及肤色检测,交通灯识别等基于颜色的分割。

文章的结构安排如下:

  • 首先,我们将看到如何在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分量,并观察它们以获得更多的色彩空间。

这里写图片描述

如果你看蓝色的通道,可以看到在室内照明条件下,第二张图像中的蓝色和白色的部分看起来相似,但是在第一张图像中有明显的差别。 这种不均匀性使得在这个色彩空间中基于颜色的分割非常困难。 此外,两个图像的值之间存在总体差异。 下面我们总结了与RGB颜色空间相关的固有问题:

  • 显着的感知不均匀性。
  • 色度(颜色相关信息)和亮度(强度相关信息)数据的混合。

LAB色彩空间

Lab色彩空间有三个组件。

1、L - 亮度(强度)。
2、a-从绿色到品红色的一种颜色成分。
3、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);

这里写图片描述

  • 从图中可以清楚地看出,照度的变化主要影响L分量。
  • 包含颜色信息的A和B成分没有经历大的变化。
  • 在B分量中,绿色,橙色和红色(它们是A分量的极值)的相应值没有改变,类似地,蓝色和黄色(它们是B分量的极值)的相应值在 一个组件。

YCrCb色彩空间

YCrCb颜色空间是从RGB颜色空间导出的,并具有以下三个组件。

1、Y - 伽马校正后从RGB获得的亮度或亮度(Luma )分量。
2、Cr = R - Y(的红色成分距离Luma有多远)。
3、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);

这里写图片描述

  • 对于照度变化,可以针对强度和颜色分量对LAB进行类似的观察。
  • 与LAB相比,红色和橙色之间的感知差异甚至在户外图像中较小。
  • 所有3个部件中的白色都发生了变化。

HSV色彩空间

HSV色彩空间有以下三个组成部分

1、H - 色调(主波长)。
2、S - 饱和度(纯度/颜色的阴影)。
3、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);

这里写图片描述

  • 在这两个图像中,H分量非常相似,即使在光照变化下,颜色信息也是完整的。
  • S组件在两幅图像中也非常相似。
  • V分量捕捉到的光线量因此会随着光照的变化而变化。
  • 红色的室外和室内图像的价值之间存在着巨大的差异。 这是因为色调表现为一个圆形,红色是在起始角度。所以,可能需要[300,360]和[0,60]之间的值。

如何使用这些色彩空间进行分割

最简单的方法

现在我们已经对不同的色彩空间有了一些了解,我们先尝试用它们来检测立方体中的绿色。

第1步:获取特定颜色的颜色值

为每个色彩空间查找绿色值的近似范围。 为此,我创建了一个交互式GUI,您可以通过将鼠标悬停在图像上来检查每个像素的所有颜色空间的值,如下所示:
这里写图片描述

步骤2:应用阈值进行分割

从图像中提取具有与绿色像素值接近的所有像素。 我们可以为每个色彩空间取+/- 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的门槛。 我做了另一个交互式演示,在这里你可以玩这些值,并试图找到一个适用于所有图像的演示。 看看截图。 但是,那么会出现另一个图像出现的情况,并且不能再次工作。 我们不能盲目地通过试错来取得一些门槛。 我们没有这样做,而是利用色彩空间的力量。

我们需要有一些有条不紊的方法来找到正确的阈值。
这里写图片描述

一些数据分析为更好的解决方案

第1步:数据收集

我在变化的照明条件下收集了10个立方体的图像,并分别裁剪每种颜色,以获得6种不同颜色的6个数据集。 你可以看到多少变化的颜色进行视觉。

第2步:计算密度图

检查不同颜色空间中特定颜色的分布,如蓝色或黄色。 密度图或二维直方图给出了一个给定颜色值的变化的想法。 例如,理想情况下,蓝色图像的蓝色通道应始终具有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中的直方图绘制二维直方图
#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])

可以看出,在相似的照明条件下,所有的地块都非常紧凑。 有一点要注意的是:

  • YCrCb和LAB比其他更紧凑
  • 在HSV中,S方向(颜色纯度)有变化,但H方向变化很小。

当照明变化很大时,我们可以看到:

  • 理想情况下,我们希望使用色彩通道最密集/最密集的色彩空间。
  • RGB的密度图剧烈地爆炸。 这意味着渠道价值的变化是非常高的,固定一个门槛是一个大问题。固定更高的范围将检测与所需颜色(假阳性)相似的颜色,而低范围将不会检测到不同照明下的所需颜色(假阴性)。
  • 在HSV中,由于只有H分量包含有关绝对颜色的信息。因此,与YCrCb(Cr和Cb)和LAB(A和B)中的2个旋钮相比,我可以调整一个旋钮(H)来指定颜色,因此它成为我的第一个色彩空间选择。
  • 比较YCrCb和LAB的图表,在LAB的情况下表现出更高的紧凑性。 所以,下一个最好的选择就是LAB色彩空间。

最终结果

在最后一节中,我将通过从密度图中获取阈值来显示检测蓝色和黄色片段的结果,并以与我们在第二部分中所做的相同的方式将其应用于各个色彩空间。 当我们在HSV,YCrCb和LAB色彩空间工作时,我们不必担心Intensity分量。 我们只需要指定颜色分量的阈值。 图中显示了我用来生成结果的值。

色彩空间的其他有用的应用

  • 通常在灰度图像上进行直方图均衡。 但是,您可以通过将RGB图像转换为YCbCr并仅对Y通道进行直方图均衡来执行彩色图像的均衡。
  • 通过将图像转换为Lab色彩空间,在两幅图像之间进行色彩转换。
  • 智能手机相机应用程序(如Google相机或Instagram)中的许多滤镜都使用这些色彩空间转换来创建那些很酷的效果!

P.S:如果你对解决魔方有兴趣,可以参考这个循序渐进的指南。

代码下载

你可能感兴趣的:(opencv)