iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0

一、介紹

1. 什麼是色彩模型

色彩模型是一種數學和視覺模型,用於描述和表示顏色的方式。這些模型基於不同的原理和特性,可以幫助我們理解、分類、比較和操控顏色。不同的色彩模型將顏色表示為不同的數值或屬性,並在不同的應用中有不同的用途。

從事電腦繪圖或印刷會非常注意繪圖軟體內的畫布是使用何種色彩模型,因為他們非常注重印刷出來的色差。如果你使用RGB的圖片輸出到印表機做印刷,因為彩色印表機使用的是CMYK這四個基底顏色進行噴墨,即使可以透過特定的數學公式將RGB轉換為CMYK,有可能會出現色差,最好的方式還是使用CMYK的圖片進行輸出。

常見的色彩空間有:

  • RGB:RGB 色彩模型是基於顏色的發光原理,它將顏色表示為紅色(Red)、綠色(Green)和藍色(Blue)三個通道的強度或亮度值的組合。每個通道的值通常在 0 到 255 之間,可以建立出各種不同的顏色。這種模型常用於顯示器、電視和電腦圖形。
  • CMYK:CMYK主要用於印刷和印刷媒體。它將顏色表示為青色(Cyan)、洋紅色(Magenta)、黃色(Yellow)和黑色(Key plate)四個油墨的量。
  • HSV:HSV 色彩模型將顏色分為色相(Hue)、飽和度(Saturation)和明度(Value)三個參數。色相表示顏色的種類,飽和度表示顏色的純度或鮮艷度,明度表示顏色的明亮度。這種模型常用於影像處理和圖形設計中。

2. HSV色彩模型

在之前的文章中,我們介紹了一張彩色圖片是由三原色紅(Red)、綠(Green)、藍(Blue)組成,廣泛用於顯示器、感光元件。RGB 色彩空間可以表示為一個三維立方體。這個立方體的每個座標點分別代表了紅、綠和藍的通道的不同亮度值。紅色、綠色和藍色通道的值分別對應於 X、Y 和 Z 軸的坐標位置。

HSV 是一種用來描述和表示顏色的色彩模型,它的名稱代表著色相(Hue)、飽和度(Saturation)、明度(Value)。HSV 是一個非常直觀和常用的色彩模型,通常用於影像處理、圖形設計、電腦繪圖和藝術創作等領域。HSV 色彩空間可以表示為一個圓錐或圓柱體。色相(H)環繞著中心軸,飽和度(S)從中心向外延伸,明度(V)則從底部向頂部延伸。

  1. 色相(Hue)
    • 色相指的是顏色的種類或在彩虹光譜上的位置。以0到360度的角度值表示,其中0度對應紅色,60度對應黃色,120度對應綠色,240度對應藍色,以此類推。這種方式讓我們能夠直觀地區分不同的顏色,並控制顏色的變化。
  2. 飽和度(Saturation)
    • 飽和度表示顏色的純度或鮮艷度。以0%到100%之間的值來表示。較高的飽和度表示顏色更鮮艷,而較低的飽和度則會使顏色變得更加褪色,看起來更白。
  3. 明度(Value)
    • 明度表示顏色的明亮度或亮度。同樣是以0%到100%之間的值表示。明度值較高的顏色看起來更明亮,而明度值較低的顏色看起來更昏暗。

https://ithelp.ithome.com.tw/upload/images/20230925/201617321jxZ6Y92wO.png

由 SharkD - 自己的作品 Source-code available at the POV-Ray Object Collection., CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=3375025

https://ithelp.ithome.com.tw/upload/images/20230925/2016173215Hq4EQdlH.png

由 Jacob Rus - 自己的作品, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9445469

3. 顏色辨識

顏色辨識用於識別影像中特定顏色的區域。以辨識膚色為例,由於光源通常不太可能平均分布在皮膚上,每個在皮膚上的顏色值不會完全一樣,因此需要定義一個特定顏色範圍。這個顏色範圍區間可以規定在特定顏色範圍內的任何顏色都會被視為皮膚色彩,即使顏色稍微變化。這樣可以增加顏色辨識的穩定性,使其在不同光線環境下正確識別膚色的機率也提升。

顏色辨識可以使用HSV(色相、飽和度、明度)色彩模型。這種模型將顏色的屬性分解為色相(描述顏色類型)、飽和度(描述顏色的鮮艷度)和明度(描述顏色的明暗度)。通過在HSV色彩模型中設置顏色範圍,我們可以更好地應對不同光線和皮膚色調變化,以實現準確的膚色辨識。

4. 為什麼顏色辨識要用HSV

在影像處理中,RGB 色彩模型雖然能夠透露出顏色,但它無法提供有關色相、飽和度和明度等其他關鍵資訊。使用 RGB 色彩模型進行影像處理時會面臨一個棘手的問題。RGB 值會受到照明環境的影響,這意味著只要光源的顏色或強度發生變化,RGB三個通道的值都有可能改變,造成顏色誤判。

這就是為什麼在進行顏色辨識時,特別是在光源不穩定的環境中,HSV 色彩模型變得非常有用。HSV 模型將顏色描述為色相、飽和度和明度三個獨立的參數,當光源的強度改變時對色相的影響較小,因此我們可以單獨辨識色相進行顏色辨識。

二、原理

1. RGB轉換成HSV

RGB可以透過計算轉換成HSV,為了方便計算,需要先將r、g、b這三個值做正規化,讓值域分布在0~1之間。

以下是各個參數的值域定義:

  • h 代表色相(Hue),範圍從 0 度到 360 度。
  • s 代表飽和度(Saturation),範圍在 0 到 1 之間。
  • v 代表明度(Value),同樣在範圍 0 到 1 之間。

https://ithelp.ithome.com.tw/upload/images/20230925/20161732bXqO541EZe.png

接下來取出r、g、b這三個值中的最大值以及最小值,將前者命名為max,後者命名為min。

https://ithelp.ithome.com.tw/upload/images/20230925/20161732rDi3Q7xprh.png

透過下列的公式即可計算出h、s、v三個通道值:
https://ithelp.ithome.com.tw/upload/images/20230925/20161732SHJbxBzMEp.png

2. 顏色辨識

  1. 轉換為HSV色彩空間
    • 將影像從其原始的色彩空間轉換為HSV色彩空間。
  2. 定義目標顏色範圍
    • 定義要識別的目標顏色的HSV值範圍。依照你要辨識的具體顏色定義色相(H)的最小值Hmin最大值Hmax
  3. 目標區域的檢測
    • 對圖片的每個像素進行顏色辨識,如果色相(H)落在我們上一個步驟定義的最大值和最小值,我們就會將它視為目標的顏色,視為前景。

https://ithelp.ithome.com.tw/upload/images/20230925/20161732xzwAUfWHUn.png

三、程式碼

這個程式碼提供了兩種運算方式,一個是使用OpenCV內建的函式,另一個是使用蜂巢迴圈實現的演算法,兩者效果一樣。你可以透過設定USE_OPENCV10來決定你要使用前者還是後者的實現方式。

#define USE_OPENCV 1

setMouseCallback 中設置了滑鼠點擊事件處理函數,以便用戶可以點擊原始影像來獲取像素的HSV值。onClick 函數是一個滑鼠點擊事件處理函數。當在原始影像視窗上點擊滑鼠左鍵時,這個函數會被觸發。它會從HSV影像中獲取點擊位置的像素值,然後計算並顯示該點的HSV值。

cv::setMouseCallback("Original", onClick);

通過 createTrackbar 函數,建立了三個滑條,用於調整HSV範圍的值。當滑條改變的時候,滑條的值會被分別分配到h_ranges_rangev_range這三個變數。

cv::createTrackbar("H Range", "Original", &h_range, 100,NULL);
cv::createTrackbar("S Range", "Original", &s_range, 100,NULL);
cv::createTrackbar("V Range", "Original", &v_range, 100,NULL);

這段程式碼是一個滑鼠點擊事件處理函數 onClick,用於處理在原始影像上點擊滑鼠左鍵的事件。

  1. 這個函數首先檢查觸發事件是否是滑鼠左鍵按下事件。
  2. 如果事件是滑鼠左鍵按下事件,它會獲取在點擊位置 (x, y) 的HSV值,這些值存儲在 h(色相)、s(飽和度)和 v(明度)變數中。
  3. 然後,將這些HSV值顯示在Console上,以便用戶可以看到點擊位置的具體HSV值。
  4. 接下來,根據滑塊上設定的HSV範圍調整值(h_ranges_rangev_range)計算了HSV範圍,這些值將在後續的遮罩操作中使用。
  5. 然後,使用 cv::inRange 函數基於這些HSV範圍計算遮罩 mask,這個遮罩將標記出位於指定HSV範圍內的像素。
  6. 最後,使用 cv::bitwise_and 函數將原始影像 image 和遮罩 mask 進行遮罩運算,生成輸出影像 dst,這個影像只包含符合指定HSV範圍的像素。
void onClick(int event, int x, int y, int flags, void* param)
{
    if (event & cv::EVENT_LBUTTONDOWN)
    {
		cv::Vec3b vec = hsv_image.at<cv::Vec3b>(y, x);
		uint8_t h = vec[0];
		uint8_t s = vec[1];
		uint8_t v = vec[2];
		printf("f(%d,%d) = [%.2f* %.2f%% %.2f%%]\n", x,y,360.0 * (vec[0] / 255.0),100*vec[1]/255.0,100*vec[2]/255.0);
		cv::Mat mask,dst;
		
		uint8_t h_norm = 255.0 * (h_range / 100.0);
		uint8_t s_norm = 255.0 * (s_range / 100.0);
		uint8_t v_norm = 255.0 * (v_range / 100.0);

		cv::inRange(hsv_image,
			cv::Scalar(max(h-h_norm,0),max(s-s_norm,0),max(v-v_norm,0)),
			cv::Scalar(min(h+h_norm,255),min(s+s_norm,255),min(v+v_norm,255)),
			mask);
		cv::imshow("Mask",mask);
		cv::bitwise_and(image,image, dst, mask);
		cv::imshow("Output",dst);
    }
}

1. 逐行講解

1) 轉換色彩空間

使用OpenCV中的 cv::cvtColor 函數,將一張BGR格式的影像轉換為HSV格式。讓我解釋這行程式碼的具體作用:

cv::cvtColor(image, hsv_image, cv::COLOR_BGR2HSV);
  • image:代表原始的BGR格式圖片。
  • hsv_image:這也是一個 cv::Mat 變數,用於存儲轉換後的HSV格式影像。
  • cv::COLOR_BGR2HSV:這是轉換色彩空間的Flag,它告訴 cv::cvtColor 函數將影像從BGR格式轉換為HSV格式。

2) 使用演算法實踐轉換色彩空間

  1. 將BGR值的每個通道(藍色、綠色、紅色)從0到255正規化為0到1的浮點數,以便進行後續的計算。
  2. 計算BGR通道的最大值和最小值。
  3. 計算色相(H)、飽和度(S)、明度(V)的值。
cv::Mat bgr2hsv(cv::Mat original) {
	cv::Mat hsv_img=cv::Mat::zeros(cv::Size(original.cols,original.rows),CV_8UC3);
	for (int y = 0;y < hsv_img.rows;y++) {
		for (int x = 0;x < hsv_img.cols;x++) {
			cv::Vec3b vec = original.at<cv::Vec3b>(y, x);
			float b = vec[0]/255.0f;
			float g = vec[1]/255.0f;
			float r = vec[2]/255.0f;
			float max = std::max(std::max(b, g),r);
			float min = std::min(std::min(b, g),r);

			float h;
			if (max == min)
				h = 0;
			else if (max == r && g >= b)
				h = 60.0f * (g - b) / (max - min);
			else if (max == r && g < b)
				h = 60.0f * (g - b) / (max - min) + 360.0f;
			else if (max == g)
				h = 60.0f * (b - r) / (max - min) + 120.0f;
			else if (max == b)
				h = 60.0f * (r-g) / (max - min) + 240.0f;
			h-= 360. * std::floor(h* (1. / 360.));

			float s;
			if (max == 0)
				s = 0.0f;
			else
				s = 1.0f - (float)min / max;
			
			float v = max;

			hsv_img.at<cv::Vec3b>(y,x)[0]=(uint8_t) 255.0f*(h/360.0f);
			hsv_img.at<cv::Vec3b>(y,x)[1]=(uint8_t) 255.0f*s;
			hsv_img.at<cv::Vec3b>(y,x)[2]=(uint8_t) 255.0f*v;
		}
	}
	return hsv_img;
}

2. 完整程式碼

#include <iostream>
#include <opencv2/opencv.hpp>
#include "opencv2/core/utils/logger.hpp"
#define USE_OPENCV 1
using namespace std;

cv::Mat bgr2hsv(cv::Mat original);
cv::Mat hsv_image;
cv::Mat image;
int h_range;
int s_range;
int v_range;
void onClick(int event, int x, int y, int flags, void* param)
{
    if (event & cv::EVENT_LBUTTONDOWN)
    {
		cv::Vec3b vec = hsv_image.at<cv::Vec3b>(y, x);
		uint8_t h = vec[0];
		uint8_t s = vec[1];
		uint8_t v = vec[2];
		printf("f(%d,%d) = [%.2f* %.2f%% %.2f%%]\n", x,y,360.0 * (vec[0] / 255.0),100*vec[1]/255.0,100*vec[2]/255.0);
		cv::Mat mask,dst;
		
		uint8_t h_norm = 255.0 * (h_range / 100.0);
		uint8_t s_norm = 255.0 * (s_range / 100.0);
		uint8_t v_norm = 255.0 * (v_range / 100.0);

		cv::inRange(hsv_image,
			cv::Scalar(max(h-h_norm,0),max(s-s_norm,0),max(v-v_norm,0)),
			cv::Scalar(min(h+h_norm,255),min(s+s_norm,255),min(v+v_norm,255)),
			mask);
		cv::imshow("Mask",mask);
		cv::bitwise_and(image,image, dst, mask);
		cv::imshow("Output",dst);
    }
}

int main()
{
    cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
	image = cv::imread("C:\\Users\\vince\\Downloads\\hand.jpg",cv::IMREAD_COLOR);

#if USE_OPENCV
	cv::cvtColor(image, hsv_image, cv::COLOR_BGR2HSV);
#else
	hsv_image = bgr2hsv(image);
#endif // USE_OPENCV

	cv::namedWindow("Original",cv::WindowFlags::WINDOW_NORMAL);
	cv::resizeWindow("Original", cv::Size(512.0 * ((float)image.cols / image.rows), 512));

	cv::namedWindow("Output",cv::WindowFlags::WINDOW_NORMAL);
	cv::resizeWindow("Output", cv::Size(512.0 * ((float)image.cols / image.rows), 512));

	cv::namedWindow("Mask",cv::WindowFlags::WINDOW_NORMAL);
	cv::resizeWindow("Mask", cv::Size(512.0 * ((float)image.cols / image.rows), 512));

	cv::imshow("Original",image);
	cv::setMouseCallback("Original", onClick);
	cv::createTrackbar("H Range", "Original", &h_range, 100,NULL);
	cv::createTrackbar("S Range", "Original", &s_range, 100,NULL);
	cv::createTrackbar("V Range", "Original", &v_range, 100,NULL);

	cv::setTrackbarPos("H Range", "Original",5);
	cv::setTrackbarPos("S Range", "Original", 49);
	cv::setTrackbarPos("V Range", "Original", 100);
	cv::waitKey(0);
	return 0;
}

cv::Mat bgr2hsv(cv::Mat original) {
	cv::Mat hsv_img=cv::Mat::zeros(cv::Size(original.cols,original.rows),CV_8UC3);
	for (int y = 0;y < hsv_img.rows;y++) {
		for (int x = 0;x < hsv_img.cols;x++) {
			cv::Vec3b vec = original.at<cv::Vec3b>(y, x);
			float b = vec[0]/255.0f;
			float g = vec[1]/255.0f;
			float r = vec[2]/255.0f;
			float max = std::max(std::max(b, g),r);
			float min = std::min(std::min(b, g),r);

			float h;
			if (max == min)
				h = 0;
			else if (max == r && g >= b)
				h = 60.0f * (g - b) / (max - min);
			else if (max == r && g < b)
				h = 60.0f * (g - b) / (max - min) + 360.0f;
			else if (max == g)
				h = 60.0f * (b - r) / (max - min) + 120.0f;
			else if (max == b)
				h = 60.0f * (r-g) / (max - min) + 240.0f;
			h-= 360. * std::floor(h* (1. / 360.));

			float s;
			if (max == 0)
				s = 0.0f;
			else
				s = 1.0f - (float)min / max;
			
			float v = max;

			hsv_img.at<cv::Vec3b>(y,x)[0]=(uint8_t) 255.0f*(h/360.0f);
			hsv_img.at<cv::Vec3b>(y,x)[1]=(uint8_t) 255.0f*s;
			hsv_img.at<cv::Vec3b>(y,x)[2]=(uint8_t) 255.0f*v;
		}
	}
	return hsv_img;
}

3. 測試圖

這是一張裁切過後的手掌圖,我們將會使用這張圖片取出的膚色並提取出手掌的區域。你可以調整H、S、V三個通道的顏色範圍,透過左鍵點擊圖片取得圖片座標上的顏色,用此顏色作為目標顏色進行顏色辨識。完整原圖可以到下方連結下載。

https://ithelp.ithome.com.tw/upload/images/20230925/20161732sG5bnjlSQr.jpg

Photo by Kevin Malik: https://www.pexels.com/photo/a-close-up-shot-of-a-person-s-palms-9017565/

4. 測試結果

1) 遮罩圖

這張圖片是透過顏色辨識以後得出的遮罩圖,這張遮罩圖會拿下去跟原圖做遮罩運算。

https://ithelp.ithome.com.tw/upload/images/20230925/201617321rm4ACS7mZ.png

2) 遮罩結果

可以看到,顏色辨識將和皮膚色不相關的背景去除,得到最終的結果。

https://ithelp.ithome.com.tw/upload/images/20230925/201617325ipkpMEEHk.png


上一篇
【Day17】​使用OpenCV進行影像縮放、拼貼、剪裁
下一篇
【Day19】使用OpenCV進行形態學運算(Morphology)
系列文
圖解C++影像處理與OpenCV應用:從基礎到高階,深入學習超硬核技術!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言