色彩模型是一種數學和視覺模型,用於描述和表示顏色的方式。這些模型基於不同的原理和特性,可以幫助我們理解、分類、比較和操控顏色。不同的色彩模型將顏色表示為不同的數值或屬性,並在不同的應用中有不同的用途。
從事電腦繪圖或印刷會非常注意繪圖軟體內的畫布是使用何種色彩模型,因為他們非常注重印刷出來的色差。如果你使用RGB的圖片輸出到印表機做印刷,因為彩色印表機使用的是CMYK這四個基底顏色進行噴墨,即使可以透過特定的數學公式將RGB轉換為CMYK,有可能會出現色差,最好的方式還是使用CMYK的圖片進行輸出。
常見的色彩空間有:
在之前的文章中,我們介紹了一張彩色圖片是由三原色紅(Red)、綠(Green)、藍(Blue)組成,廣泛用於顯示器、感光元件。RGB 色彩空間可以表示為一個三維立方體。這個立方體的每個座標點分別代表了紅、綠和藍的通道的不同亮度值。紅色、綠色和藍色通道的值分別對應於 X、Y 和 Z 軸的坐標位置。
HSV 是一種用來描述和表示顏色的色彩模型,它的名稱代表著色相(Hue)、飽和度(Saturation)、明度(Value)。HSV 是一個非常直觀和常用的色彩模型,通常用於影像處理、圖形設計、電腦繪圖和藝術創作等領域。HSV 色彩空間可以表示為一個圓錐或圓柱體。色相(H)環繞著中心軸,飽和度(S)從中心向外延伸,明度(V)則從底部向頂部延伸。
由 SharkD - 自己的作品 Source-code available at the POV-Ray Object Collection., CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=3375025
由 Jacob Rus - 自己的作品, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=9445469
顏色辨識用於識別影像中特定顏色的區域。以辨識膚色為例,由於光源通常不太可能平均分布在皮膚上,每個在皮膚上的顏色值不會完全一樣,因此需要定義一個特定顏色範圍。這個顏色範圍區間可以規定在特定顏色範圍內的任何顏色都會被視為皮膚色彩,即使顏色稍微變化。這樣可以增加顏色辨識的穩定性,使其在不同光線環境下正確識別膚色的機率也提升。
顏色辨識可以使用HSV(色相、飽和度、明度)色彩模型。這種模型將顏色的屬性分解為色相(描述顏色類型)、飽和度(描述顏色的鮮艷度)和明度(描述顏色的明暗度)。通過在HSV色彩模型中設置顏色範圍,我們可以更好地應對不同光線和皮膚色調變化,以實現準確的膚色辨識。
在影像處理中,RGB 色彩模型雖然能夠透露出顏色,但它無法提供有關色相、飽和度和明度等其他關鍵資訊。使用 RGB 色彩模型進行影像處理時會面臨一個棘手的問題。RGB 值會受到照明環境的影響,這意味著只要光源的顏色或強度發生變化,RGB三個通道的值都有可能改變,造成顏色誤判。
這就是為什麼在進行顏色辨識時,特別是在光源不穩定的環境中,HSV 色彩模型變得非常有用。HSV 模型將顏色描述為色相、飽和度和明度三個獨立的參數,當光源的強度改變時對色相的影響較小,因此我們可以單獨辨識色相進行顏色辨識。
RGB可以透過計算轉換成HSV,為了方便計算,需要先將r、g、b這三個值做正規化,讓值域分布在0~1之間。
以下是各個參數的值域定義:
接下來取出r、g、b這三個值中的最大值以及最小值,將前者命名為max,後者命名為min。
透過下列的公式即可計算出h、s、v三個通道值:
這個程式碼提供了兩種運算方式,一個是使用OpenCV內建的函式,另一個是使用蜂巢迴圈實現的演算法,兩者效果一樣。你可以透過設定USE_OPENCV
成1
或0
來決定你要使用前者還是後者的實現方式。
#define USE_OPENCV 1
在 setMouseCallback
中設置了滑鼠點擊事件處理函數,以便用戶可以點擊原始影像來獲取像素的HSV值。onClick
函數是一個滑鼠點擊事件處理函數。當在原始影像視窗上點擊滑鼠左鍵時,這個函數會被觸發。它會從HSV影像中獲取點擊位置的像素值,然後計算並顯示該點的HSV值。
cv::setMouseCallback("Original", onClick);
通過 createTrackbar
函數,建立了三個滑條,用於調整HSV範圍的值。當滑條改變的時候,滑條的值會被分別分配到h_range、s_range、v_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
,用於處理在原始影像上點擊滑鼠左鍵的事件。
(x, y)
的HSV值,這些值存儲在 h
(色相)、s
(飽和度)和 v
(明度)變數中。h_range
、s_range
和 v_range
)計算了HSV範圍,這些值將在後續的遮罩操作中使用。cv::inRange
函數基於這些HSV範圍計算遮罩 mask
,這個遮罩將標記出位於指定HSV範圍內的像素。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);
}
}
使用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格式。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;
}
#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;
}
這是一張裁切過後的手掌圖,我們將會使用這張圖片取出的膚色並提取出手掌的區域。你可以調整H、S、V三個通道的顏色範圍,透過左鍵點擊圖片取得圖片座標上的顏色,用此顏色作為目標顏色進行顏色辨識。完整原圖可以到下方連結下載。
Photo by Kevin Malik: https://www.pexels.com/photo/a-close-up-shot-of-a-person-s-palms-9017565/
這張圖片是透過顏色辨識以後得出的遮罩圖,這張遮罩圖會拿下去跟原圖做遮罩運算。
可以看到,顏色辨識將和皮膚色不相關的背景去除,得到最終的結果。