开启辅助访问
 找回密码
 立即注册

(十七)再谈色彩(RGB通道和HSV)

阳仔 回答数3 浏览数1047
时间为友,记录点滴。

我们之前有一章,聊过色彩那一篇只是介绍了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("imgOri", imgOri);
       
        /* Blue Vector */
        channelB = imgChannels[0].clone();
        imshow("Blue Channel", channelB);

        /* Green Vector */
        channelG = imgChannels[1].clone();
        imshow("Green Channel", channelG);

        /* Red Vector */
        channelR = imgChannels[2].clone();
        imshow("Red Channel", channelR);

        /* G & R Channel is 0 */
        Mat imgBlue;
        clearChannel(imgChannels);
        imgChannels[0] = channelB;
        merge(imgChannels, imgBlue);
        imshow("imgBlue", imgBlue);

        /* B & R Channel is 0 */
        Mat imgGreen;

        clearChannel(imgChannels);
        imgChannels[1] = channelG;
        merge(imgChannels, imgGreen);
        imshow("imgGreen", imgGreen);

        /* B & G Channel is 0 */
        Mat imgRed;
        clearChannel(imgChannels);
        imgChannels[2] = channelR;
        merge(imgChannels, imgRed);
        imshow("imgRed", imgRed);
}

int mAIn()
{
        Mat imgOri = imread("Danxia.jpg");
        if (imgOri.empty())
        {
                cout << "Cannot find this picture!" << 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模型。

  • 颜色三属性-色相 Hue



色相



24阶色相环

色相又被称为色调,被称为是色彩的首要特征,通俗的来说就是把颜色分为红、橙、黄、绿…的特征。改变色相比同等程度改变饱和度或亮度感受到的色彩变化要更大,所以被称为颜色最重要的特征。
确切定义为颜色等效光谱峰值的位置,不过要知道的是不是所有色相都在光谱上,光谱红色端和紫色端之间还用洋红色段,所以色相用 2 段链接并补充紫色段的色相环表示。
由色相环可以想象,如果用数字表示,360个数字就可以把所有的色相表示出来,周而复始。在OpenCV中,一般用U8来表示一台channel,所以用0~180表示一台色相环。数值的大小,代表其对应颜色的变化。


  • 颜色三属性-饱和度 Saturation



饱和度

又被称为彩度、色彩浓度(Chroma),称为色彩浓度很好理解,从色彩最大浓度到无色彩(黑白或灰)的程度。
确切定义为颜色等效光谱分布集中于波峰(色相)的程度,越集中含其颜色越少,饱和度越高。
颜色越纯,饱和度越高
颜色纯=颜色的色相可识别性高


  • 颜色三属性-色调 Value




明度(我们的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("imgOri", imgOri);

        /* Show HSV picture */
        cvtColor(imgOri, hsv, COLOR_BGR2HSV);
        imshow("HSV", hsv);

        split(hsv, gImgHSVChannels);
        split(hsv, gTempImgHSVChannels);
        vector<Mat> imgChannels(gImgHSVChannels);
        //gTempImgHSVChannels = gImgHSVChannels;

        /* H Vector */
        channelH = imgChannels[0].clone();
        imshow("H Channel", channelH);

        /* S Vector */
        channelS = imgChannels[1].clone();
        imshow("S Channel", channelS);

        /* V Vector */
        channelV = imgChannels[2].clone();
        imshow("V Channel", channelV);
}

int main()
{
        Mat imgOri = imread("Danxia.jpg");
        if (imgOri.empty())
        {
                cout << "Cannot find this picture!" << 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("Danxia.jpg");
        if (imgOri.empty())
        {
                cout << "Cannot find this picture!" << 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("imgOri", imgOri);
       
        /* Blue Vector */
        channelB = imgChannels[0].clone();
        imshow("Blue Channel", channelB);

        /* Green Vector */
        channelG = imgChannels[1].clone();
        imshow("Green Channel", channelG);

        /* Red Vector */
        channelR = imgChannels[2].clone();
        imshow("Red Channel", channelR);

        /* G & R Channel is 0 */
        Mat imgBlue;
        clearChannel(imgChannels);
        imgChannels[0] = channelB;
        merge(imgChannels, imgBlue);
        imshow("imgBlue", imgBlue);

        /* B & R Channel is 0 */
        Mat imgGreen;

        clearChannel(imgChannels);
        imgChannels[1] = channelG;
        merge(imgChannels, imgGreen);
        imshow("imgGreen", imgGreen);

        /* B & G Channel is 0 */
        Mat imgRed;
        clearChannel(imgChannels);
        imgChannels[2] = channelR;
        merge(imgChannels, imgRed);
        imshow("imgRed", imgRed);
}


static void cover2HSVTest(Mat imgOri)
{
        Mat hsv, channelH, channelS, channelV, imgBin;

        /* Show original picture */
        imshow("imgOri", imgOri);

        /* Show HSV picture */
        cvtColor(imgOri, hsv, COLOR_BGR2HSV);
        imshow("HSV", hsv);

        split(hsv, gImgHSVChannels);
        split(hsv, gTempImgHSVChannels);
        vector<Mat> imgChannels(gImgHSVChannels);
        //gTempImgHSVChannels = gImgHSVChannels;

        /* H Vector */
        channelH = imgChannels[0].clone();
        imshow("H Channel", channelH);

        /* S Vector */
        channelS = imgChannels[1].clone();
        imshow("S Channel", channelS);

        /* V Vector */
        channelV = imgChannels[2].clone();
        imshow("V Channel", channelV);


        /* Bin Image */
        gHImTemp = channelH;
        threshold(gHImTemp, imgBin, gBinHSVThreshold, 255, THRESH_BINARY);
        imshow("HSV Bin Channel", imgBin);

        cv::createTrackbar("H", "imgOri", &gHScale, HSV_MAX, scaleHCallBack);
        cv::createTrackbar("S", "imgOri", &gSScale, HSV_MAX, scaleSCallBack);
        cv::createTrackbar("V", "imgOri", &gVScale, HSV_MAX, scaleVCallBack);

        cv::createTrackbar("Threshold", "HSV Bin Channel", &gBinHSVThreshold, BIN_MAX, binHSVThreasholdCallBack);
}

static void cover2OthersTest(Mat imgOri)
{
        Mat imgGray, imgBin;

        cvtColor(imgOri, imgGray, COLOR_BGR2GRAY);
        imshow("GRAY", imgGray);

        /* Bin Image */
        gGrayImTemp = imgGray;
        threshold(gGrayImTemp, imgBin, gBinGrayThreshold, 255, THRESH_BINARY);
        imshow("Bin Gray Channel", imgBin);

        cv::createTrackbar("Threshold", "Bin Gray Channel", &gBinGrayThreshold, BIN_MAX, binGrayThreasholdCallBack);
}


static void binHSVThreasholdCallBack(int, void*)
{
        Mat imgBin;
        threshold(gHImTemp, imgBin, gBinHSVThreshold, 255, THRESH_BINARY);
        imshow("HSV Bin Channel", imgBin);
}

static void binGrayThreasholdCallBack(int, void*)
{
        Mat imgBin;
        threshold(gGrayImTemp, imgBin, gBinGrayThreshold, 255, THRESH_BINARY);
        imshow("Bin Gray Channel", 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("imgOri", imgOutShow);

        //showImgPara(mask);
        //showImgPara(fImgOutHChannel);
        //cout << ">>>>>>>>>>>>>" << gHScale << "<<<<<<<<<<<<<\n" << 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 << "------------------------------END------------------------------\n" << endl;

        merge(gTempImgHSVChannels, imgHSVOutShow);
        cvtColor(imgHSVOutShow, imgOutShow, COLOR_HSV2BGR);

        imshow("imgOri", 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("imgOri", 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("imgOri", imgOutShow);

        //showImgPara(mask);
        //showImgPara(fImgOutHChannel);
        //cout << ">>>>>>>>>>>>>" << gHScale << "<<<<<<<<<<<<<\n" << endl;
}

static void showImgPara(Mat &img)
{
        cout << "sizeof(img) is: " << sizeof(img) << ", img size is: " << img.size << endl;
        cout << "rows x cols: (" << img.rows << " x " << img.cols << ")" << endl;
        cout << "dims: " << img.dims << endl;
        cout << "channels: " << img.channels() << endl;
        cout << "type: " << img.type() << endl;
        cout << "depth:" << img.depth() << endl;
        cout << "elemSize:" << img.elemSize() << " (Bytes per element)" << endl;
        cout << "elemSize1:" << img.elemSize1() << "(Bytes per channel)" << endl;
        cout << "step[0]: " << img.step[0] << " (Bytes per cows only when 2 dims)" << endl;
        cout << "step[1]: " << img.step[1] << " (Bytes per element only when 2 dims)" << endl;
        cout << "step1(0): " << img.step1(0) << ", step1(1): " << img.step1(1) << " (step / elemSize1)" << endl;
        cout << "--------------        END --------------\n" << endl;
}


运行结果

Python:

  • 或是要注意数据类型的转换。这里用了astype
#!/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("Sizeof img is: " + str(img.size))
    print("rows x cols is: " + str(np.size(img, 0)) + " x " + str(np.size(img, 1)))
    print("dimension is: ", img.ndim)
    print("dtype is: ", img.dtype)
    print("itemsize is: ", img.itemsize)
    print("shape is(rows, cols, depth): ", img.shape)
    print("\n")

def splitRGBTest(imgOri):
    cv.imshow("imgOri", imgOri)
    B,G,R = cv.split(imgOri)

    cv.imshow("B", B)
    cv.imshow("G", G)
    cv.imshow("R", R)

def hCallBack(pos):
    hChannel = np.copy(HSV_MV[0])
    print("---------Begain", pos, "---------")
    # 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("imgOri", imgOut)
    print(pos)

def sCallBack(pos):
    sChannel = np.copy(HSV_MV[1])
    print("---------Begain", pos, "---------")
    # 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("imgOri", imgOut)
    print(pos)

def vCallBack(pos):
    vChannel = np.copy(HSV_MV[2])
    print("---------Begain", pos, "---------")
    # 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("imgOri", imgOut)
    print(pos)

def cover2HSVTest(imgOri):
    global HSV_MV, HSV_TempMV
    cv.imshow("imgOri", imgOri)
    HSV = cv.cvtColor(imgOri, cv.COLOR_BGR2HSV)
    cv.imshow("HSV", HSV)

    HSV_MV = cv.split(HSV)
    HSV_TempMV = cv.split(HSV)


    cv.imshow("H", HSV_MV[0])
    cv.imshow("S", HSV_MV[1])
    cv.imshow("V", HSV_MV[2])

    cv.createTrackbar("H", "imgOri", hPos, HSV_MAX, hCallBack)
    cv.createTrackbar("S", "imgOri", sPos, HSV_MAX, sCallBack)
    cv.createTrackbar("V", "imgOri", vPos, HSV_MAX, vCallBack)



def main_func(argv):
    global  imgOri
    imgOri = cv.imread("Danxia.jpg")
    # splitRGBTest(imgOri)
    cover2HSVTest(imgOri)

    cv.waitKey(0)


if __name__ == '__main__':
    main_func(sys.argv)
使用道具 举报
| 来自北京 用Deepseek满血版问问看
3600s | 来自北京
写的很棒,用心了
用Deepseek满血版问问看
回复
使用道具 举报
nuxgod | 来自上海
非常感谢
回复
使用道具 举报
jjcz | 来自北京
请问R通道与V通道归一化后是不是相同了?
回复
使用道具 举报
快速回复
您需要登录后才可以回帖 登录 | 立即注册

当贝投影