时间为友,记录点滴。
我们之前有一章,聊过色彩那一篇只是介绍了RGB和YUV,主要是为了介绍OpenCV的一些基础API的调用。时至今日,大家肯定对OpenCV不陌生了,那么让我们把色彩基础部分的内容补上吧。
RGB
我们已经知道了人眼中有三种视锥细胞,分别对波长为445nm/535nm/575nm左右的光最敏感,这些数字对应到物理世界分别为黄绿光、绿光和蓝紫光。这就是RGB三基色的生物基础。
国际照明委员会(EIC)定义,选择红色(波长λ=700.00nm),绿色(波长λ=546.1nm),蓝色(波长λ=438.8nm)三种单色光作为表色系统的三基色。其中R(red)、G(Green)、B(Blue)分别是红绿蓝的英文首字母。
所以,我们人眼所能看到的颜色,都可以通过调整RGB三种颜色的比例无限接近。在显示领域,RGB就被应用在各个场景中。
我们知道,在OpenCV Mat结构体中,type中的Cx就是代表了Channel的数量,我们可以通过一台例子来认识一下R/G/B通道。
注意:
- 对于OpenCV来说,一台RGB三通道的排布是BGR。
- 每个通道分离开来,其实就是一张C1的灰度图。
- 可以通过把其他通道置0的方式,再merge成C3的图来模拟只有B/G/R分量。
- 注意vector的使用
C++:
static void clearChannel(vector<Mat> &imgChannels)
{
for (vector<Mat>::iterator channel = imgChannels.begin(); channel != imgChannels.end(); channel++)
{
*channel = Scalar(0);
}
}
static void splitRGBTest(Mat imgOri)
{
Mat channelB, channelG, channelR;
split(imgOri, gImgRGBChannels);
vector<Mat> imgChannels(gImgRGBChannels);
/* Show original picture */
imshow(&#34;imgOri&#34;, imgOri);
/* Blue Vector */
channelB = imgChannels[0].clone();
imshow(&#34;Blue Channel&#34;, channelB);
/* Green Vector */
channelG = imgChannels[1].clone();
imshow(&#34;Green Channel&#34;, channelG);
/* Red Vector */
channelR = imgChannels[2].clone();
imshow(&#34;Red Channel&#34;, channelR);
/* G & R Channel is 0 */
Mat imgBlue;
clearChannel(imgChannels);
imgChannels[0] = channelB;
merge(imgChannels, imgBlue);
imshow(&#34;imgBlue&#34;, imgBlue);
/* B & R Channel is 0 */
Mat imgGreen;
clearChannel(imgChannels);
imgChannels[1] = channelG;
merge(imgChannels, imgGreen);
imshow(&#34;imgGreen&#34;, imgGreen);
/* B & G Channel is 0 */
Mat imgRed;
clearChannel(imgChannels);
imgChannels[2] = channelR;
merge(imgChannels, imgRed);
imshow(&#34;imgRed&#34;, imgRed);
}
int mAIn()
{
Mat imgOri = imread(&#34;Danxia.jpg&#34;);
if (imgOri.empty())
{
cout << &#34;Cannot find this picture!&#34; << endl;
}
splitRGBTest(imgOri);
waitKey(0);
return true;
}
原图
只有B通道
只有G通道
只有R通道
G和R分量置零后再merge
B和R分量置零后再merge
B和G分量置零后再merge
根据三原色制定的立方体直角坐标系色彩模型,是最常用的色彩模型。
不过用这种色彩模型选择色彩不是一件直观的事情,普通人很难知道(RGB:100%,50%,0%)混出来是指什么颜色,所以被称为对机器友好而对人不友好的色彩模型。
立方体直角坐标系色彩模型
基于RGB模型,除了立方体直角坐标系色彩模型,还有很多便于我们认识色彩的工具,其中CIE就值得我们了解一下:
HSV
很显然,RGB对显示友好,但是不管是人为或是计算机,如果涉及到定量的处理,这个模型就显得太吃力了。我们之前讲过YUV格式,今天我们再来聊一聊HSV模型。
色相
24阶色相环
色相又被称为色调,被称为是色彩的首要特征,通俗的来说就是把颜色分为红、橙、黄、绿…的特征。改变色相比同等程度改变饱和度或亮度感受到的色彩变化要更大,所以被称为颜色最重要的特征。
确切定义为颜色等效光谱峰值的位置,不过要知道的是不是所有色相都在光谱上,光谱红色端和紫色端之间还用洋红色段,所以色相用 2 段链接并补充紫色段的色相环表示。
由色相环可以想象,如果用数字表示,360个数字就可以把所有的色相表示出来,周而复始。在OpenCV中,一般用U8来表示一台channel,所以用0~180表示一台色相环。数值的大小,代表其对应颜色的变化。
饱和度
又被称为彩度、色彩浓度(Chroma),称为色彩浓度很好理解,从色彩最大浓度到无色彩(黑白或灰)的程度。
确切定义为颜色等效光谱分布集中于波峰(色相)的程度,越集中含其颜色越少,饱和度越高。
颜色越纯,饱和度越高
颜色纯=颜色的色相可识别性高
明度(我们的HSV中的V代表了Value)
颜色等效光谱各色相心理强度之和。可以从图中看到改变一台颜色的亮度,很可能会牵连到颜色光谱分布集中程度也就是饱和度,实际上不同的色彩体系对饱和度和明度的拆分是不同的。
色调看起来是最容易理解的概念,但事实上明度或者说亮度是最混乱的概念了:Brightness、Lightness、Value、Luma。虽然概括来说就只是颜色的明亮程度,但是明亮是指光的强度,或是人对光的感受?范围是从黑到白或是从黑到颜色能维持最大饱和度的亮度?在不同的色彩体系、标准、翻译下会有不同的意义 总模型
我们知道了HSV在各个分量上的含义,那么,我们可不可以像RGB那样把各分量统一到一张图或一台立体空间上呢?当然可以:
HSV 平面模型
HSV 立体模型
RGB和HSV的关系
如果想了解RGB和HSV之间的转换关系的话,可以参考下面的博文链接,本文章主要讨论在OpenCV中的应用。
OpenCV中的HSV
OpenCV中有现成的函数cvtColor函数可以将RGB空间转换为HSV,并设置参数为COLOR_BGR2HSV那么所得的H、S、V值范围分别是[0,180),[0,255),[0,255),而非[0,360],[0,1],[0,1];这时我们可以查下面的表格来确定颜色的大致区间:
HSV空间
当然,OpenCV也对cvtColor函数提供了COLOR_HSV2BGR参数可以把HSV再转回RGB显示。
下面你通过代码来看看HSV空间的和拆解吧:
注意点:
- 拆解后的HSV通道实际上跟RGB一样,是单通道,是一台灰度空间。
static void cover2HSVTest(Mat imgOri)
{
Mat hsv, channelH, channelS, channelV, imgBin;
/* Show original picture */
imshow(&#34;imgOri&#34;, imgOri);
/* Show HSV picture */
cvtColor(imgOri, hsv, COLOR_BGR2HSV);
imshow(&#34;HSV&#34;, hsv);
split(hsv, gImgHSVChannels);
split(hsv, gTempImgHSVChannels);
vector<Mat> imgChannels(gImgHSVChannels);
//gTempImgHSVChannels = gImgHSVChannels;
/* H Vector */
channelH = imgChannels[0].clone();
imshow(&#34;H Channel&#34;, channelH);
/* S Vector */
channelS = imgChannels[1].clone();
imshow(&#34;S Channel&#34;, channelS);
/* V Vector */
channelV = imgChannels[2].clone();
imshow(&#34;V Channel&#34;, channelV);
}
int main()
{
Mat imgOri = imread(&#34;Danxia.jpg&#34;);
if (imgOri.empty())
{
cout << &#34;Cannot find this picture!&#34; << endl;
}
cover2HSVTest(imgOri);
waitKey(0);
return true;
}
原图
HSV空间
H通道
S通道
V通道
应用
也许你会问,我费尽心机的把RGB转成HSV,有什么好处呢?我可以通过一台例子说明一下。
或是这张图(去年旅游在张掖拍的七彩丹霞),如果我想把丹霞和蓝天分开,如果在RGB空间中,是很难做到的,但是如果转换在HSV空间中,单对H向量做二值运算,就很容易。(这也很容易理解,蓝天虽然丹霞的在色彩空间相差比较大,可以转换到色相中,通过设置合理的Threshold分开)
H空间的二差值
附上所有代码:
- 支持RGB通道分离。
- 支持HSV通道分离,并且对H通道做了二差值显示。
- 支持修改HSV各个通道的值,并且在RGB空间内显示出来。
- 注意vector的赋值、copy问题。
#include <iostream>
#include <string>
//#include <cstdlib>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
static void splitRGBTest(Mat imgOri);
static void cover2HSVTest(Mat imgOri);
static void cover2OthersTest(Mat imgOri);
static void binHSVThreasholdCallBack(int, void*);
static void scaleHCallBack(int, void*);
static void scaleSCallBack(int, void*);
static void scaleVCallBack(int, void*);
static void binGrayThreasholdCallBack(int, void*);
static void showImgPara(Mat &img);
int gBinHSVThreshold = 64;
int gBinGrayThreshold = 100;
int gHScale = 100;
int gSScale = 100;
int gVScale = 100;
Mat gHImTemp, gGrayImTemp;
vector<Mat> gImgRGBChannels;
vector<Mat> gImgHSVChannels;
vector<Mat> gTempImgHSVChannels;
#define BIN_MAX 255
#define HSV_MAX 300
int main()
{
Mat imgOri = imread(&#34;Danxia.jpg&#34;);
if (imgOri.empty())
{
cout << &#34;Cannot find this picture!&#34; << endl;
}
//splitRGBTest(imgOri);
cover2HSVTest(imgOri);
//cover2OthersTest(imgOri);
waitKey(0);
return true;
}
static void clearChannel(vector<Mat> &imgChannels)
{
for (vector<Mat>::iterator channel = imgChannels.begin(); channel != imgChannels.end(); channel++)
{
*channel = Scalar(0);
}
}
static void splitRGBTest(Mat imgOri)
{
Mat channelB, channelG, channelR;
split(imgOri, gImgRGBChannels);
vector<Mat> imgChannels(gImgRGBChannels);
/* Show original picture */
imshow(&#34;imgOri&#34;, imgOri);
/* Blue Vector */
channelB = imgChannels[0].clone();
imshow(&#34;Blue Channel&#34;, channelB);
/* Green Vector */
channelG = imgChannels[1].clone();
imshow(&#34;Green Channel&#34;, channelG);
/* Red Vector */
channelR = imgChannels[2].clone();
imshow(&#34;Red Channel&#34;, channelR);
/* G & R Channel is 0 */
Mat imgBlue;
clearChannel(imgChannels);
imgChannels[0] = channelB;
merge(imgChannels, imgBlue);
imshow(&#34;imgBlue&#34;, imgBlue);
/* B & R Channel is 0 */
Mat imgGreen;
clearChannel(imgChannels);
imgChannels[1] = channelG;
merge(imgChannels, imgGreen);
imshow(&#34;imgGreen&#34;, imgGreen);
/* B & G Channel is 0 */
Mat imgRed;
clearChannel(imgChannels);
imgChannels[2] = channelR;
merge(imgChannels, imgRed);
imshow(&#34;imgRed&#34;, imgRed);
}
static void cover2HSVTest(Mat imgOri)
{
Mat hsv, channelH, channelS, channelV, imgBin;
/* Show original picture */
imshow(&#34;imgOri&#34;, imgOri);
/* Show HSV picture */
cvtColor(imgOri, hsv, COLOR_BGR2HSV);
imshow(&#34;HSV&#34;, hsv);
split(hsv, gImgHSVChannels);
split(hsv, gTempImgHSVChannels);
vector<Mat> imgChannels(gImgHSVChannels);
//gTempImgHSVChannels = gImgHSVChannels;
/* H Vector */
channelH = imgChannels[0].clone();
imshow(&#34;H Channel&#34;, channelH);
/* S Vector */
channelS = imgChannels[1].clone();
imshow(&#34;S Channel&#34;, channelS);
/* V Vector */
channelV = imgChannels[2].clone();
imshow(&#34;V Channel&#34;, channelV);
/* Bin Image */
gHImTemp = channelH;
threshold(gHImTemp, imgBin, gBinHSVThreshold, 255, THRESH_BINARY);
imshow(&#34;HSV Bin Channel&#34;, imgBin);
cv::createTrackbar(&#34;H&#34;, &#34;imgOri&#34;, &gHScale, HSV_MAX, scaleHCallBack);
cv::createTrackbar(&#34;S&#34;, &#34;imgOri&#34;, &gSScale, HSV_MAX, scaleSCallBack);
cv::createTrackbar(&#34;V&#34;, &#34;imgOri&#34;, &gVScale, HSV_MAX, scaleVCallBack);
cv::createTrackbar(&#34;Threshold&#34;, &#34;HSV Bin Channel&#34;, &gBinHSVThreshold, BIN_MAX, binHSVThreasholdCallBack);
}
static void cover2OthersTest(Mat imgOri)
{
Mat imgGray, imgBin;
cvtColor(imgOri, imgGray, COLOR_BGR2GRAY);
imshow(&#34;GRAY&#34;, imgGray);
/* Bin Image */
gGrayImTemp = imgGray;
threshold(gGrayImTemp, imgBin, gBinGrayThreshold, 255, THRESH_BINARY);
imshow(&#34;Bin Gray Channel&#34;, imgBin);
cv::createTrackbar(&#34;Threshold&#34;, &#34;Bin Gray Channel&#34;, &gBinGrayThreshold, BIN_MAX, binGrayThreasholdCallBack);
}
static void binHSVThreasholdCallBack(int, void*)
{
Mat imgBin;
threshold(gHImTemp, imgBin, gBinHSVThreshold, 255, THRESH_BINARY);
imshow(&#34;HSV Bin Channel&#34;, imgBin);
}
static void binGrayThreasholdCallBack(int, void*)
{
Mat imgBin;
threshold(gGrayImTemp, imgBin, gBinGrayThreshold, 255, THRESH_BINARY);
imshow(&#34;Bin Gray Channel&#34;, imgBin);
}
static void scaleHCallBack(int, void* )
{
#if 1
Mat fImgOriHChannel = gImgHSVChannels[0].clone();
Mat fImgOutHChannel = gImgHSVChannels[0].clone();
Mat imgOutHSV, imgOutShow;
fImgOriHChannel.convertTo(fImgOriHChannel, CV_32FC1);
fImgOutHChannel.convertTo(fImgOutHChannel, CV_32FC1);
Mat mask(fImgOriHChannel.rows, fImgOriHChannel.cols, CV_32FC1, Scalar(gHScale / 100.0));
multiply(fImgOriHChannel, mask, fImgOutHChannel);
fImgOutHChannel.convertTo(gTempImgHSVChannels[0], gTempImgHSVChannels[0].type());
merge(gTempImgHSVChannels, imgOutHSV);
cvtColor(imgOutHSV, imgOutShow, COLOR_HSV2BGR);
imshow(&#34;imgOri&#34;, imgOutShow);
//showImgPara(mask);
//showImgPara(fImgOutHChannel);
//cout << &#34;>>>>>>>>>>>>>&#34; << gHScale << &#34;<<<<<<<<<<<<<\n&#34; << endl;
#else
Mat imgHSVOutShow, imgOutShow;
Mat imgHChannel = gImgHSVChannels[0].clone();
for (int row = 0; row < imgHChannel.rows; row++)
{
uchar* pGImgHSVChannels = imgHChannel.ptr<uchar>(row);
uchar* pGTempImgHSVChannels = gTempImgHSVChannels[0].ptr<uchar>(row);
for (int col = 0; col < imgHChannel.cols; col++)
{
pGTempImgHSVChannels[col] = saturate_cast<uchar>((float)(pGImgHSVChannels[col]) * (float)gHScale / 100.0);
//cout << pGTempImgHSVChannels[col];
}
//cout << endl;
}
//cout << &#34;------------------------------END------------------------------\n&#34; << endl;
merge(gTempImgHSVChannels, imgHSVOutShow);
cvtColor(imgHSVOutShow, imgOutShow, COLOR_HSV2BGR);
imshow(&#34;imgOri&#34;, imgOutShow);
#endif
}
static void scaleSCallBack(int, void*)
{
Mat imgHSVOutShow, imgOutShow;
Mat imgSChannel = gImgHSVChannels[1].clone();
for (int row = 0; row < imgSChannel.rows; row++)
{
uchar* pGImgHSVChannels = imgSChannel.ptr<uchar>(row);
uchar* pGTempImgHSVChannels = gTempImgHSVChannels[1].ptr<uchar>(row);
for (int col = 0; col < imgSChannel.cols; col++)
{
pGTempImgHSVChannels[col] = saturate_cast<uchar>((float)(pGImgHSVChannels[col]) * (float)gSScale / 100.0);
}
}
merge(gTempImgHSVChannels, imgHSVOutShow);
cvtColor(imgHSVOutShow, imgOutShow, COLOR_HSV2BGR);
imshow(&#34;imgOri&#34;, imgOutShow);
}
static void scaleVCallBack(int, void*)
{
cout << gVScale << endl;
Mat fImgOriVChannel = gImgHSVChannels[2].clone();
Mat fImgOutVChannel = gImgHSVChannels[2].clone();
Mat imgOutHSV, imgOutShow;
fImgOriVChannel.convertTo(fImgOriVChannel, CV_32FC1);
fImgOutVChannel.convertTo(fImgOutVChannel, CV_32FC1);
Mat mask(fImgOriVChannel.rows, fImgOriVChannel.cols, CV_32FC1, Scalar(gVScale / 100.0));
multiply(fImgOriVChannel, mask, fImgOutVChannel);
fImgOutVChannel.convertTo(gTempImgHSVChannels[2], gTempImgHSVChannels[2].type());
merge(gTempImgHSVChannels, imgOutHSV);
cvtColor(imgOutHSV, imgOutShow, COLOR_HSV2BGR);
imshow(&#34;imgOri&#34;, imgOutShow);
//showImgPara(mask);
//showImgPara(fImgOutHChannel);
//cout << &#34;>>>>>>>>>>>>>&#34; << gHScale << &#34;<<<<<<<<<<<<<\n&#34; << endl;
}
static void showImgPara(Mat &img)
{
cout << &#34;sizeof(img) is: &#34; << sizeof(img) << &#34;, img size is: &#34; << img.size << endl;
cout << &#34;rows x cols: (&#34; << img.rows << &#34; x &#34; << img.cols << &#34;)&#34; << endl;
cout << &#34;dims: &#34; << img.dims << endl;
cout << &#34;channels: &#34; << img.channels() << endl;
cout << &#34;type: &#34; << img.type() << endl;
cout << &#34;depth:&#34; << img.depth() << endl;
cout << &#34;elemSize:&#34; << img.elemSize() << &#34; (Bytes per element)&#34; << endl;
cout << &#34;elemSize1:&#34; << img.elemSize1() << &#34;(Bytes per channel)&#34; << endl;
cout << &#34;step[0]: &#34; << img.step[0] << &#34; (Bytes per cows only when 2 dims)&#34; << endl;
cout << &#34;step[1]: &#34; << img.step[1] << &#34; (Bytes per element only when 2 dims)&#34; << endl;
cout << &#34;step1(0): &#34; << img.step1(0) << &#34;, step1(1): &#34; << img.step1(1) << &#34; (step / elemSize1)&#34; << endl;
cout << &#34;-------------- END --------------\n&#34; << endl;
}
运行结果
Python:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author:lowkeyway time:8/8/2019
import sys
import cv2 as cv
import numpy as np
hPos = 100
sPos = 100
vPos = 100
HSV_MAX = 300
def showImgPara(img):
print(&#34;Sizeof img is: &#34; + str(img.size))
print(&#34;rows x cols is: &#34; + str(np.size(img, 0)) + &#34; x &#34; + str(np.size(img, 1)))
print(&#34;dimension is: &#34;, img.ndim)
print(&#34;dtype is: &#34;, img.dtype)
print(&#34;itemsize is: &#34;, img.itemsize)
print(&#34;shape is(rows, cols, depth): &#34;, img.shape)
print(&#34;\n&#34;)
def splitRGBTest(imgOri):
cv.imshow(&#34;imgOri&#34;, imgOri)
B,G,R = cv.split(imgOri)
cv.imshow(&#34;B&#34;, B)
cv.imshow(&#34;G&#34;, G)
cv.imshow(&#34;R&#34;, R)
def hCallBack(pos):
hChannel = np.copy(HSV_MV[0])
print(&#34;---------Begain&#34;, pos, &#34;---------&#34;)
# showImgPara(hChannel)
hChannel = hChannel * (pos / 100.0)
# showImgPara(hChannel)
hChannel = hChannel.astype(HSV_TempMV[0].dtype)
# showImgPara(hChannel)
HSV_TempMV[0] = hChannel
imgHSVOut = np.zeros(imgOri.shape, imgOri.dtype)
imgHSVOut = cv.merge(HSV_TempMV)
imgOut = cv.cvtColor(imgHSVOut, cv.COLOR_HSV2BGR)
cv.imshow(&#34;imgOri&#34;, imgOut)
print(pos)
def sCallBack(pos):
sChannel = np.copy(HSV_MV[1])
print(&#34;---------Begain&#34;, pos, &#34;---------&#34;)
# showImgPara(sChannel)
sChannel = sChannel * (pos / 100.0)
# showImgPara(hChannel)
sChannel = sChannel.astype(HSV_TempMV[1].dtype)
# showImgPara(hChannel)
HSV_TempMV[1] = sChannel
imgHSVOut = np.zeros(imgOri.shape, imgOri.dtype)
imgHSVOut = cv.merge(HSV_TempMV)
imgOut = cv.cvtColor(imgHSVOut, cv.COLOR_HSV2BGR)
cv.imshow(&#34;imgOri&#34;, imgOut)
print(pos)
def vCallBack(pos):
vChannel = np.copy(HSV_MV[2])
print(&#34;---------Begain&#34;, pos, &#34;---------&#34;)
# showImgPara(hChannel)
vChannel = vChannel * (pos / 100.0)
# showImgPara(hChannel)
vChannel = vChannel.astype(HSV_TempMV[0].dtype)
# showImgPara(hChannel)
HSV_TempMV[2] = vChannel
imgHSVOut = np.zeros(imgOri.shape, imgOri.dtype)
imgHSVOut = cv.merge(HSV_TempMV)
imgOut = cv.cvtColor(imgHSVOut, cv.COLOR_HSV2BGR)
cv.imshow(&#34;imgOri&#34;, imgOut)
print(pos)
def cover2HSVTest(imgOri):
global HSV_MV, HSV_TempMV
cv.imshow(&#34;imgOri&#34;, imgOri)
HSV = cv.cvtColor(imgOri, cv.COLOR_BGR2HSV)
cv.imshow(&#34;HSV&#34;, HSV)
HSV_MV = cv.split(HSV)
HSV_TempMV = cv.split(HSV)
cv.imshow(&#34;H&#34;, HSV_MV[0])
cv.imshow(&#34;S&#34;, HSV_MV[1])
cv.imshow(&#34;V&#34;, HSV_MV[2])
cv.createTrackbar(&#34;H&#34;, &#34;imgOri&#34;, hPos, HSV_MAX, hCallBack)
cv.createTrackbar(&#34;S&#34;, &#34;imgOri&#34;, sPos, HSV_MAX, sCallBack)
cv.createTrackbar(&#34;V&#34;, &#34;imgOri&#34;, vPos, HSV_MAX, vCallBack)
def main_func(argv):
global imgOri
imgOri = cv.imread(&#34;Danxia.jpg&#34;)
# splitRGBTest(imgOri)
cover2HSVTest(imgOri)
cv.waitKey(0)
if __name__ == &#39;__main__&#39;:
main_func(sys.argv) |