天天看點

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

前言

前期回顧: ​​OpenCV計算機視覺程式設計篇二《操作像素》​​ 上面這篇裡面寫了操作像素相關。

本章包括以下内容:

  • 用政策設計模式比較顔色;
  • 用 GrabCut 算法分割圖像;
  • 轉換顔色表示法;
  • 用色調、飽和度和亮度表示顔色。

3.1 簡介

人類視覺系統的一個重要特征就是能感覺顔色。人眼的視網膜中有一種被稱作視錐細胞的特 殊感光細胞,專門負責感覺各種顔色。視錐細胞分為三種,分别負責不同波長的光線,人腦就是 通過這些細胞産生的信号來識别各種顔色的。大多數動物卻隻有視杆細胞,它對光線的敏感度更 高,但是覆寫了整個可見光的光譜,無法區分不同的顔色。人眼中的視杆細胞主要分布在視網膜 的邊緣,而視錐細胞分布在視網膜的中心。

在數位攝影中,則是用加色法三原色(紅、綠、藍)來建構各種顔色,将它們組合起來可以 産生各種顔色,且色域很寬。實際上,選用這三種顔色也模仿了人類的顔色識别系統——人眼中 不同的視錐細胞分别負責紅色、綠色和藍色附近的光譜。本章将分析像素的顔色,并介紹如何用 顔色資訊分割圖像。此外,在處理彩色圖像時,還可以使用其他的顔色表示法。

3.2 用政策設計模式比較顔色

假設我們要建構一個簡單的算法,用來識别圖像中具有某種顔色的所有像素。這個算法必須 輸入一幅圖像和一個顔色,并且傳回一個二值圖像,顯示具有指定顔色的像素。在運作算法前, 還要指定一個參數,即能接受的顔色的公差。

本節将采用政策設計模式來實作這一目标,它是一種面向對象的設計模式,用很巧妙的方法 将算法封裝進類。采用這種模式後,可以很輕松地替換算法,或者組合多個算法以實作更複雜的功能。而且這種模式能夠盡可能地将算法的複雜性隐藏在一個直覺的程式設計接口後面,更有利于算 法的部署。

3.2.1 如何實作

一旦用政策設計模式把算法封裝進類,就可以通過建立類的執行個體來部署算法,執行個體通常是在 程式初始化的時候建立的。在運作構造函數時,類的執行個體會用預設值初始化算法的各種參數,使 其立即進入可用狀态。我們還可以用适當的方法來讀寫算法的參數值。在 GUI 應用程式中,可 以用多種部件(文本框、滑動條等)顯示和修改參數,使用者操作起來很容易。

下一節将展示一個政策類的結構,這裡先看一個部署和使用它的例子。寫一個簡單的主函數, 調用顔色檢測算法:

int main()
{
 // 1.建立圖像處理器對象
 ColorDetector cdetect;
 // 2.讀取輸入的圖像
 cv::Mat image= cv::imread("boldt.jpg");
 if (image.empty()) return 0;
 // 3.設定輸入參數
 cdetect.setTargetColor(230,190,130); // 這裡表示藍天
 // 4.處理圖像并顯示結果
 cv::namedWindow("result");
 cv::Mat result = cdetect.process(image);
 cv::imshow("result",result);
 cv::waitKey();
 return 0;
}      

運作這個程式,檢測第 2 章用過的彩色城堡圖中的藍天,輸出結果如下所示。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

這裡的白色像素表示檢測到指定的顔色,黑色表示沒有檢測到。

很明顯,封裝進這個類的算法相對簡單(下面會看到它隻是組合了一個掃描循環和一個公差 參數)。當算法的實作過程變得更加複雜、步驟繁多并且包含多個參數時,政策設計模式才會真 正展現出強大的威力。

3.2.2 實作原理

這個算法的核心過程非常簡單,隻是對每個像素進行循環掃描,把它的顔色和目标顔色做比 較。利用 2.3 節所學,可以這樣寫這個循環:

// 取得疊代器
cv::Mat_<cv::Vec3b>::const_iterator it= image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::const_iterator itend= image.end<cv::Vec3b>();
cv::Mat_<uchar>::iterator itout= result.begin<uchar>();
// 對于每個像素
for ( ; it!= itend; ++it, ++itout) {
 // 比較與目标顔色的差距
 if (getDistanceToTargetColor(*it)<=maxDist) {
 *itout= 255;
 } else {
 *itout= 0;
 }
}      

cv::Mat 類型的變量 image 表示輸入圖像,result 表示輸出的二值圖像。是以要先建立 疊代器,這樣掃描循環就很容易實作了。注意,輸入圖像疊代器定義為常量,它們的元素無法修 改。在每個疊代步驟中計算目前像素的顔色與目标顔色的差距,檢查它是否在公差(maxDist) 範圍之内。如果是,就在輸出圖像中指派 255(白色),否則就指派 0(黑色)。這裡用 getDistance ToTargetColor 方法來計算與目标顔色的差距。

也有其他可以計算這個差距的方法,例如計算包含 RGB 顔色值的三個向量之間的歐幾裡得 距離。為了簡化計算過程,我們把 RGB 值差距的絕對值(也稱為城區距離)進行累加。注意, 在現代體系結構中,浮點數的歐幾裡得距離的計算速度可能比簡單的城區距離更快(還可以采用 平方歐氏距離,以避免耗時的平方根運算),在做設計時也要考慮到這點。另外,為了增加靈活 性,我們依據 getColorDistance 方法來編寫 getDistanceToTargetColor 方法:

// 計算與目标顔色的差距
int getDistanceToTargetColor(const cv::Vec3b& color) const {
 return getColorDistance(color, target);
}
// 計算兩個顔色之間的城區距離
int getColorDistance(const cv::Vec3b& color1,
const cv::Vec3b& color2) const { 
return abs(color1[0]-color2[0])+
 abs(color1[1]-color2[1])+
 abs(color1[2]-color2[2]);
}      

我們用 cv::Vec3d 存儲三個無符号字元型,即顔色的 RGB 值。變量 target 表示指定的目 标顔色,是算法類的成員變量。現在來定義處理方法。使用者提供一個輸入圖像,圖像掃描完成後 即傳回結果:

cv::Mat ColorDetector::process(const cv::Mat &image) {
 // 必要時重新配置設定二值映像
 // 與輸入圖像的尺寸相同,不過是單通道
 result.create(image.size(),CV_8U);
 // 在這裡放前面的處理循環
 return result;
}      

在調用這個方法時,一定要檢查輸出圖像(包含二值映像)是否需要重新配置設定,以比對輸入 圖像的尺寸。是以我們使用了 cv::Mat 的 create 方法。注意,隻有在指定的尺寸或深度與當 前圖像結構不比對時,它才會進行重新配置設定。

我們已經定義了核心的處理方法,下面就看一下為了部署該算法,還需要添加哪些額外方法。 前面已經明确了算法需要的輸入和輸出資料,是以要定義類的屬性來存儲這些資料:

class ColorDetector {
 private:
 // 允許的最小差距
 int maxDist;
 // 目标顔色
 cv::Vec3b target;
 // 存儲二值映像結果的圖像
 cv::Mat result;      

要為封裝了算法的類(已命名為 ColorDetector)建立執行個體,就需要定義一個構造函數。 使用政策設計模式的原因之一,就是讓算法的部署盡可能簡單。最簡單的構造函數當然是空函數, 它會建立一個算法類的執行個體,并處于有效狀态。然後在構造函數中初始化全部輸入參數,設定為 預設值(或采用通常會帶來好結果的值)。這裡認為通常能接受的公差參數是 100。我們還需要 設定預設的目标顔色,這裡選用黑色(選用黑色沒有什麼特别的原因),總的原則是要確定輸入 值可預測并且有效。

// 空構造函數
// 在此初始化預設參數
ColorDetector() : maxDist(100), target(0,0,0) {}      

也可以不使用空的構造函數,而是采用複雜的構造函數,要求使用者輸入目标顔色和顔色距離:

// 另一種構造函數,使用目标顔色和顔色距離作為參數
ColorDetector(uchar blue, uchar green, uchar red, int mxDist);      

建立該算法類的使用者此時可以立即調用處理方法并傳入一個有效的圖像,然後得到一個有效 的輸出。這是政策設計模式的另一個目的,即隻要保證參數正确,算法就能正常運作。使用者顯然 希望使用個性化設定,我們可以用相應的設定方法和擷取方法來實作這個功能。首先要實作 color 公差參數的定制:

// 設定顔色差距的門檻值
// 門檻值必須是正數,否則就設為 0
void setColorDistanceThreshold(int distance) {
 if (distance<0)
 distance=0;
 maxDist= distance;
 }
 // 取得顔色差距的門檻值
 int getColorDistanceThreshold() const {
 return maxDist;
 }      

注意,我們首先檢查了輸入的合法性。再次強調,這是為了確定算法運作的有效性。可以用 類似的方法設定目标顔色:

// 設定需要檢測的顔色
void setTargetColor(uchar blue,
 uchar green,
 uchar red) {
 // 次序為 BGR
 target = cv::Vec3b(blue, green, red);
}
// 設定需要檢測的顔色
void setTargetColor(cv::Vec3b color) {
 target= color;
}
// 取得需要檢測的顔色
cv::Vec3b getTargetColor() const {
 return target;
}      

這次我們提供了 setTargetColor 方法的兩種定義,第一個版本用三個參數表示三個顔色元件,第二個版本用 cv::Vec3b 儲存顔色值。再次強調,這麼做是為了讓算法類更便于使用, 使使用者隻需要選擇最合适的設定函數。

3.2.3 擴充閱讀

例子中的算法可識别出圖像中與指定目标顔色足夠接近的像素。過程中已經完成了計算步驟。有趣的是,OpenCV 中有一個具有類似功能的函數,可以從圖像中提取出與特定顔色相關聯的部件。另外,我們也可以用函數對象來補充政策設計模式。OpenCV 中定義了一個基類 cv::Algorithm,實作政策設計模式的概念。

  1. 計算兩個顔色向量間的距離

要計算兩個顔色向量間的距離,可使用這個簡單的公式:

return abs(color[0]-target[0])+
 abs(color[1]-target[1])+
 abs(color[2]-target[2]);      

然而,OpenCV 中也有計算向量的歐幾裡得範數的函數,是以也可以這樣計算距離:

return static_cast<int>(
 cv::norm<int,3>(cv::Vec3i(color[0]-target[0],
 color[1]-target[1],
 color[2]-target[2])));      

改用這種方式定義 getDistance 方法後,得到的結果與原來的非常接近。這裡之是以使用 cv::Vec3i(三個向量的整型數組),是因為減法運算得到的是整數值。

還有一點非常有趣,回顧一下第 2 章的内容,我們會發現 OpenCV 中矩陣和向量等資料結構 定義了基本的算術運算符。是以,有人會想這樣計算距離:

return static_cast<int>( cv::norm<uchar,3>(color-target));// 錯誤!      

這種做法看上去好像是對的,但實際上是錯誤的,因為為了確定結果在輸入資料類型的範圍 之内(這裡是 uchar),這些運算符通常都調用了 saturate_cast(詳情請參見 2.6 節)。是以 在 target 的值比 color 大的時候,結果就會是 0 而不是負數。正确的做法應該是:

cv::Vec3b dist;
cv::absdiff(color,target,dist);
return cv::sum(dist)[0];      

不過在計算三個數組間距離時調用這兩個函數的效率并不高。

  1. 使用 OpenCV 函數

本節采用了在循環中使用疊代器的方法來進行計算。還有一種做法是調用 OpenCV 的系列函 數,也能得到一樣的結果。是以,檢測顔色的方法還可以這樣寫:

cv::Mat ColorDetector::process(const cv::Mat &image) {
 cv::Mat output;
 // 計算與目标顔色的距離的絕對值
  cv::absdiff(image,cv::Scalar(target),output);
 // 把通道分割進 3 幅圖像
 std::vector<cv::Mat> images;
 cv::split(output,images);
 // 3 個通道相加(這裡可能出現飽和的情況)
 output= images[0]+images[1]+images[2];
 // 應用門檻值
 cv::threshold(output, // 相同的輸入/輸出圖像
 output,
 maxDist, // 門檻值(必須<256)
 255, // 最大值
 cv::THRESH_BINARY_INV); // 門檻值化模式
 return output;
}      

該方法使用了 absdiff 函數計算圖像的像素與标量值之間差距的絕對值。該函數的第二個 參數也可以不用标量值,而是改用另一幅圖像,這樣就可以逐個像素地計算差距。是以兩幅圖像 的尺寸必須相同。然後,用 split 函數提取出存放差距的圖像的單個通道(詳情請參見 2.7.4 節) 以便求和。注意,累加值有可能超過 255,但因為飽和度對值範圍有要求,是以最終結果不會超 過 255。這樣做的結果,就是這裡的 maxDist 參數也必須小于 256。如果你覺得這樣不合理, 可以進行修改。

最後一步是用 cv::threshold 函數建立一個二值圖像。這個函數通常用于将所有像素與某 個門檻值(第三個參數)進行比較,并且在正常門檻值化模式(cv::THRESH_BINARY)下,将所有 大于指定門檻值的像素指派為預定的最大值(第四個參數),将其他像素指派為 0。這裡使用相反 的模式(cv::THRESH_BINARY_INV)把小于或等于門檻值的像素指派為預定的最大值。此外還 有 cv::THRESH_TOZERO 和 cv::THRESH_TOZERO_INV 模式,它們使大于或小于門檻值的像素保持 不變。

一般來說,最好直接使用 OpenCV 函數。它可以快速建立複雜程式,減少潛在的錯誤,而且 程式的運作效率通常也比較高(得益于 OpenCV 項目參與者做的優化工作)。不過這樣會執行很 多的中間步驟,消耗更多記憶體。

  1. floodFill 函數

ColorDetector 類可以在一幅圖像中找出與指定顔色接近的像素,它的判斷方法是對像素 進行逐個檢查。cv::floodFill 函數的做法與之類似,但有一個很大的差別,那就是它在判斷 一個像素時,還要檢查附近像素的狀态,這是為了識别某種顔色的相關區域。使用者隻需指定一個 起始位置和允許的誤差,就可以找出顔色接近的連續區域。

首先根據亞像素确定搜尋的顔色,并檢查它旁邊的像素,判斷它們是否為顔色接近的像素; 然後,繼續檢查它們旁邊的像素,并持續操作。這樣就可以從圖像中提取出特定顔色的區域。例如要從圖中提取出藍天,可以執行以下語句:

cv::floodFill(image, // 輸入/輸出圖像
 cv::Point(100, 50), // 起始點
 cv::Scalar(255, 255, 255), // 填充顔色
 (cv::Rect*)0, // 填充區域的邊界矩形
 cv::Scalar(35, 35, 35), // 偏差的最小/最大門檻值
 cv::Scalar(35, 35, 35), // 正差門檻值,兩個門檻值通常相等
 cv::FLOODFILL_FIXED_RANGE); // 與起始點像素比較      

圖像中亞像素(100, 50)所處的位置是天空。函數會檢查所有的相鄰像素,顔色接近的像素會 被重繪成第三個參數指定的新顔色。為了判斷顔色是否接近,需要分别定義比參考色更高或更低 的值作為門檻值。這裡使用固定範圍模式,即所有像素都與亞像素的顔色進行對比,預設模式是将 每個像素與和它鄰近的像素進行對比。得到的結果如下圖所示。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

這種算法重繪了一個獨立的連續區域(這裡是把天空畫成白色)。即使其他地方有顔色接近 的像素(例如水面),除非它們與天空相連,否則也不會被識别出來。

  1. 仿函數或函數對象

利用 C++的操作符重載功能,我們可以讓類的執行個體表現得像函數。它的原理是重載 operator()方法,讓調用類的處理方法就像調用純粹的函數一樣。這種類的執行個體被稱為函數對 象或者仿函數(functor)。一個仿函數通常包含一個完整的構造函數,是以能夠在建立後立即使 用。例如,可以在 ColorDetector 類中添加完整的構造函數:

// 完整的構造函數
ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100):
 maxDist(maxDist) { 
  // 目标顔色
 setTargetColor(blue, green, red);
}      

很顯然,前面定義的擷取方法和設定方法仍然可以使用。可以這樣定義仿函數方法:

cv::Mat operator()(const cv::Mat &image) {
 // 這裡放檢測顔色的代碼
}      

若想用仿函數方法檢測指定的顔色,隻需要用這樣的代碼片段:

ColorDetector colordetector(230,190,130, // 顔色
 100); // 門檻值
cv::Mat result= colordetector(image); // 調用仿函數      

可以看到,這裡對顔色檢測方法的調用類似于對某個函數的調用。

  1. OpenCV 的算法基類

為實作計算機視覺的各項功能,OpenCV 提供了很多算法。為友善使用,大多數算法都被封 裝成了通用基類 cv::Algorithm 的子類。這展現了政策設計模式的一些概念。首先,所有算法 都在專門的靜态方法中動态地建立,以確定建立的算法總是有效的(即每個缺少的參數都有有效 的預設值)。來看一個例子,即它的其中一個子類 cv::ORB(用于興趣點運算,詳情請參見 8.5 節)。這裡隻把它作為一個算法示例。

用下面的方法建立一個算法執行個體:

cv::Ptr<cv::ORB> ptrORB = cv::ORB::create(); // 預設狀态      

算法一旦建立完畢,就可以開始使用,例如通用方法 read 和 write 可用于裝載或存儲算 法的狀态值。算法也有一些專用方法(例如 ORB 的方法 detect 和 compute 用于觸發它的主體 計算單元),也有專門用來設定内部參數的設定方法。需要注意的是,你可以把指針類型定為 cv::Ptr,但那樣就無法使用它的專用方法了。

3.3 用 GrabCut 算法分割圖像

上一節介紹了如何利用顔色資訊,根據場景中的特定元素分割圖像。物體通常有自己特有的 顔色,通過識别顔色接近的區域,通常可以提取出這些顔色。OpenCV 提供了一種常用的圖像分割算法,即 GrabCut 算法。GrabCut 算法比較複雜,計算量也很大,但結果通常很精确。如果要 從靜态圖像中提取前景物體(例如從圖像中剪切一個物體,并粘貼到另一幅圖像),最好采用 GrabCut 算法。

3.3.1 如何實作

cv::grabCut 函數的用法非常簡單,隻需要輸入一幅圖像,并對一些像素做上“屬于背景” 或“屬于前景”的标記即可。根據這個局部标記,算法将計算出整幅圖像的前景/背景分割線。

一種指定輸入圖像局部前景/背景标簽的方法是定義一個包含前景物體的矩形:

// 定義一個帶邊框的矩形
// 矩形外部的像素會被标記為背景
cv::Rect rectangle(5,70,260,120);      

這段代碼定義了圖像中的一個區域。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

矩形之外的像素都會被标記為背景。調用 cv::grabCut 時,除了需要輸入圖像和分割後的 圖像,還需要定義兩個矩陣,用于存放算法建構的模型,代碼如下所示:

cv::Mat result; // 分割結果(四種可能的值)
cv::Mat bgModel,fgModel; // 模型(内部使用)
// GrabCut 分割算法
cv::grabCut(image, // 輸入圖像
 result, // 分割結果
 rectangle, // 包含前景的矩形
 bgModel,fgModel, // 模型
 5, // 疊代次數
 cv::GC_INIT_WITH_RECT); // 使用矩形      

注意,我們在函數的中用 cv::GC_INIT_WITH_RECT 标志作為最後一個參數,表示将使用帶邊框的矩形模型(3.3.2 節會讨論其他模式)。輸入/輸出的分割圖像可以是以下四個值之一。

  • cv::GC_BGD:這個值表示明确屬于背景的像素(例如本例中矩形之外的像素)。
  • cv::GC_FGD:這個值表示明确屬于前景的像素(本例中沒有這種像素)。
  • cv::GC_PR_BGD:這個值表示可能屬于背景的像素。
  • cv::GC_PR_FGD:這個值表示可能屬于前景的像素(即本例中矩形之内像素的初始值)。

通過提取值為 cv::GC_PR_FGD 的像素,可得到包含分割資訊的二值圖像,實作代碼為:

// 取得标記為“可能屬于前景”的像素
cv::compare(result,cv::GC_PR_FGD,result,cv::CMP_EQ);
// 生成輸出圖像
cv::Mat foreground(image.size(),CV_8UC3,cv::Scalar(255,255,255));
image.copyTo(foreground, result); // 不複制背景像素      

要提取全部前景像素,即值為 cv::GC_PR_FGD 或 cv::GC_FGD 的像素,可以檢查第一位 的值,代碼如下所示:

// 用“按位與”運算檢查第一位
result= result&1; // 如果是前景像素,結果為 1      

這可能是因為這幾個常量被定義的值為1 和3,而另外兩個(cv::GC_BGD 和cv::GC_PR_BGD) 被定義為 0 和 2。本例因為分割圖像不含 cv::GC_FGD 像素(隻輸入了 cv::GC_BGD 像素),所 以得到的結果是一樣的。

得到的圖像如下所示。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

3.3.2 實作原理

在前面的例子中,隻需要指定一個包含前景物體(城堡)的矩形,GrabCut 算法就能提取出 它。此外,還可以把輸入圖像中的幾個特定像素指派為 cv::GC_BGD 和 cv::GC_FGD,以掩碼 圖像的形式提供這些值,作為 cv::grabCut 函數的第二個參數。同時要把輸入模式标志指定為 GC_INIT_WITH_MASK。獲得這些輸入标簽的方法有很多種,例如可以提示使用者在圖像中互動式 地标記一些元素。當然,将這兩種輸入模式結合使用也未嘗不可。

利用輸入資訊,GrabCut 算法通過以下步驟進行背景/前景分割。首先,把所有未标記的像素 臨時标為前景(cv::GC_PR_FGD)。基于目前的分類情況,算法把像素劃分為多個顔色相似的組 (即 K 個背景組和 K 個前景組)。下一步是通過引入前景和背景像素之間的邊緣,确定背景/前景 的分割,這将通過一個優化過程來實作。在此過程中,将試圖連接配接具有相似标記的像素,并且避 免邊緣出現在強度相對均勻的區域。使用 Graph Cuts 算法可以高效地解決這個優化問題,它尋找 最優解決方案的方法是:把問題表示成一幅連通的圖形,然後在圖形上進行切割,以形成最優的 形态。分割完成後,像素會有新的标記。然後重複這個分組過程,找到新的最優分割方案,如此 反複。是以,GrabCut 算法是一個逐漸改進分割結果的疊代過程。根據場景的複雜程度,找到最 佳方案所需的疊代次數各不相同(如果情況簡單,疊代一次就足夠了)。

這解釋了函數中用來表示疊代次數的參數。結合代碼看,原意應該是:先把參數傳遞給函數, 函數傳回時會修改參數的值。是以,如果希望通過執行額外的疊代過程來改進分割結果,可以在 調用函數時重複使用上次運作的模型。

3.4 轉換顔色表示法

RGB 色彩空間的基礎是對加色法三原色(紅、綠、藍)的應用。本章最開始就說過,選用 這三種顔色作為三原色,是因為将它們組合後可以産生色域很寬的各種顔色,與人類視覺系統對 應。這通常是數字成像中預設的色彩空間,因為這就是用紅綠藍三種濾波器生成彩色圖像的方式。 紅綠藍三個通道還要做歸一化處理,當三種顔色強度相同時就會取得灰階,即從黑色(0, 0, 0)到白 色(255, 255, 255)。

但利用 RGB 色彩空間計算顔色之間的差距并不是衡量兩個顔色相似度的最好方式。實際上,RGB 并不是感覺均勻的色彩空間。也就是說,兩種具有一定差距的顔色可能看起來非常接近, 而另外兩種具有同樣差距的顔色看起來卻差别很大。

為解決這個問題,引入了一些具有感覺均勻特性的顔色表示法。CIE Lab*就是一種這樣的 顔色模型。把圖像轉換到這種表示法後,我們就可以真正地使用圖像像素與目标顔色之間的歐幾 裡得距離,來度量顔色之間的視覺相似度。本節将介紹如何轉換顔色表示法,以便使用其他色彩空間。

3.4.1 如何實作

使用 OpenCV 的函數 cv::cvtColor 可以輕松轉換圖像的色彩空間。回顧一下 3.2 節提到的 ColorDetector 類。在 process 方法中先把輸入圖像轉換成 CIE Lab*色彩空間:

cv::Mat ColorDetector::process(const cv::Mat &image) {
 // 必要時重新配置設定二值圖像
 // 與輸入圖像的尺寸相同,但用單通道
 result.create(image.rows,image.cols,CV_8U);
 // 轉換成 Lab 色彩空間
 cv::cvtColor(image, converted, CV_BGR2Lab);
 // 取得轉換圖像的疊代器
 cv::Mat_<cv::Vec3b>::iterator it= converted.begin<cv::Vec3b>();
 cv::Mat_<cv::Vec3b>::iterator itend= converted.end<cv::Vec3b>();
 // 取得輸出圖像的疊代器
 cv::Mat_<uchar>::iterator itout= result.begin<uchar>();
 // 針對每個像素
 for ( ; it!= itend; ++it, ++itout) {      

轉換後的變量包含顔色轉換後的圖像,被定義為類 ColorDetector 的一個屬性:

class ColorDetector {
 private:
 // 顔色轉換後的圖像
 cv::Mat converted;      

輸入的目标顔色也需要進行轉換——通過建立一個隻有單個像素的臨時圖像,可以實作這種 轉換。注意,需要讓函數保持與前面幾節一樣的簽名,即使用者提供的目标顔色仍然是 RGB 格式:

// 設定需要檢測的顔色
void setTargetColor(unsigned char red, unsigned char green,
 unsigned char blue) {
 // 臨時的單像素圖像
 cv::Mat tmp(1,1,CV_8UC3);
 tmp.at<cv::Vec3b>(0,0)= cv::Vec3b(blue, green, red); 
  // 将目标顔色轉換成 Lab 色彩空間
 cv::cvtColor(tmp, tmp, CV_BGR2Lab);
 target= tmp.at<cv::Vec3b>(0,0);
}      

如果在上一節的程式中使用這個修改過的類,它就會在檢測符合目标顔色的像素時,使用 CIE Lab*顔色模型。

3.4.2 實作原理

在将圖像從一個色彩空間轉換到另一個色彩空間時,會在每個輸入像素上做一個線性或非線 性的轉換,以得到輸出像素。輸出圖像的像素類型與輸入圖像是一緻的。即使你經常使用 8 位像 素,也可以用浮點數圖像(通常假定像素值的範圍是 0~1.0)或整數圖像(像素值範圍通常是 0~65 535)進行顔色轉換。但是,實際的像素值範圍取決于指定的色彩空間和目标圖像的類型。 比如說 CIE Lab*色彩空間中的 L 通道表示每個像素的亮度,範圍是 0~100;在使用 8 位圖像時, 它的範圍就會調整為 0~255。a 通道和 b 通道表示色度元件,這些通道包含了像素的顔色資訊, 與亮度無關。它們的值的範圍是127~127;對于 8 位圖像,為了适應 0~255 的區間,每個值會加 上 128。但是要注意,進行 8 位顔色轉換時會産生舍入誤差,是以轉換過程并不是完全可逆的。

大多數常用的色彩空間都是可以轉換的。你隻需要在 OpenCV 函數中指定正确的色彩空間轉 換代碼(CIE Lab*的代碼為 CV_BGR2Lab),其中就有 YCrCb,它是在 JPEG 壓縮中使用的色 彩空間。把色彩空間從 BGR 轉換成 YCrCb 的代碼為 CV_BGR2YCrCb。注意,所有涉及三原色(紅、 綠、藍)的轉換過程都可以用 RGB 和 BGR 的次序。

CIE Luv是另一種感覺均勻的色彩空間。若想從 BGR 轉換成 CIE Luv,可使用代碼 CV_BGR2Luv。Lab和 Luv對亮度通道使用同樣的轉換公式,但對色度通道則使用不同的表 示法。另外,為了實作視覺感覺上的均勻,這兩種色彩空間都扭曲了 RGB 的顔色範圍,是以這 些轉換過程都是非線性的(是以計算量巨大)。

此外還有 CIE XYZ 色彩空間(用代碼 CV_BGR2XYZ 表示)。它是一種标準色彩空間,用與設 備無關的方式表示任何可見顔色。在 Lab和 Luv色彩空間的計算中,用 XYZ 色彩空間作 為一種中間表示法。RGB 與 XYZ 之間的轉換是線性的。還有一點非常有趣,就是 Y 通道對應着 圖像的灰階版本。

HSV 和 HLS 這兩種色彩空間很有意思,它們把顔色分解成加值的色調和飽和度元件或亮度 元件。人們用這種方式來描述的顔色會更加自然。下一節将介紹這種色彩空間。

你可以把彩色圖像轉換成灰階圖像,輸出是一個單通道圖像:

cv::cvtColor(color, gray, CV_BGR2Gray);      

也可以進行反向的轉換,但是那樣得到的彩色圖像的三個通道是相同的,都是灰階圖像中對 應的值。

3.4.3 參閱

  • 4.6 節将使用 HSV 色彩空間來尋找圖像中的目标。
  • 關于色彩空間理論的參考資料有很多,其中有一套完整的資料:The Structure and Properties of Color Spaces and the Representation of Color Images(E. Dubois 著,Morgan & Claypool, 2009 年出版)。

3.5 用色調、飽和度和亮度表示顔色

本章處理了圖像的顔色,使用了不同的色彩空間,并且設法識别出圖像中具有均勻顔色的區 域。RGB 是一種被廣泛接受的色彩空間。雖然它被視為一種在電子成像系統中采集和顯示顔色 的有效方法,但它其實并不直覺,也并不符合人類對于顔色的感覺方式——我們更習慣用色彩、 亮度或彩度(即表示該顔色是鮮豔的還是柔和的)來描述顔色。為了能讓使用者用更直覺的屬性描 述顔色,我們引入了基于色調、飽和度和亮度的色彩空間。本節将把色調、飽和度和亮度作為描 述顔色的方法,并對這些概念加以探讨。

3.5.1 如何實作

上一節講過,可用 cv::cvtColor 函數把 BGR 圖像轉換成另一種色彩空間。這裡使用轉換 代碼 CV_BGR2HSV:

// 轉換成 HSV 色彩空間
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);      

我們可以用代碼 CV_HSV2BGR 把圖像轉換回 BGR 色彩空間。通過把圖像的通道分割到三個 獨立的圖像中,我們可以直覺地看到每一種 HSV 元件,方法如下所示:

// 把 3 個通道分割進 3 幅圖像中
std::vector<cv::Mat> channels;
cv::split(hsv,channels);
// channels[0]是色調
// channels[1]是飽和度
// channels[2]是亮度      

注意第三個通道表示顔色值,即顔色亮度的近似值。因為處理的是 8 位圖像,是以 OpenCV 會把通道值的範圍重新調節為 0~255(色調除外,它的範圍被調節為 0~180,下節會解釋原因)。 這個方法非常實用,因為我們可以把這幾個通道作為灰階圖像進行顯示。

城堡圖的亮度通道顯示如下

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

該圖像的飽和度通道顯示如下。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

最後是該圖像的色調通道。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

下一節會對這幾幅圖像進行解釋。

3.5.2 實作原理

之是以要引入色調/飽和度/亮度的色彩空間概念,是因為人們喜歡憑直覺分辨各種顔色,而 它與這種方式吻合。實際上,人類更喜歡用色彩、彩度、亮度等直覺的屬性來描述顔色,而大多 數直覺色彩空間正是基于這三個屬性。色調(hue)表示主色,我們使用的顔色名稱(例如綠色、 黃色和紅色)就對應了不同的色調值;飽和度(saturation)表示顔色的鮮豔程度,柔和的顔色飽 和度較低,而彩虹的顔色飽和度就很高;最後,亮度(brightness)是一個主觀的屬性,表示某種 顔色的光亮程度。其他直覺色彩空間使用顔色明度(value)或顔色亮度(lightness)的概念描述 有關顔色的強度。

利用這些顔色概念,能盡可能地模拟人類對顔色的直覺感覺。是以,它們沒有标準的定義。 根據文獻資料,色調、飽和度和亮度都有多種不同的定義和計算公式。OpenCV 建議的兩種直覺 色彩空間的實作是 HSV 和 HLS 色彩空間,它們的轉換公式略有不同,但是結果非常相似。

亮度成分可能是最容易解釋的。在 OpenCV 對 HSV 的實作中,它被定義為三個 BGR 成分中 的最大值,以非常簡化的方式實作了亮度的概念。為了讓定義更符合人類視覺系統,應該使用均 勻感覺的色彩空間 Lab和 Luv的 L 通道。舉個例子,L 通道已經考慮到了,在強度相同的 情況下,人們會覺得綠色比藍色等顔色的亮度更高。

OpenCV 用一個公式來計算飽和度,該公式基于 BGR 元件的最小值和最大值:

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

其原理是:灰階顔色包含的 R、G、B 的成分是相等的,相當于一種極不飽和的顔色,是以 它的飽和度是 0(飽和度是一個 0~1.0 的值)。對于 8 位圖像,飽和度被調節成一個 0~255 的值, 并且作為灰階圖像顯示的時候,較亮區域對應的顔色具有較高的飽和度。

舉個例子,在前面的飽和度圖檔中,水的藍色比天空的柔和淺藍色的飽和度高,這和我們的 推斷是一緻的。根據定義,各種灰色陰影的飽和度都是 0(因為它們的三種 BGR 元件是相等的)。 從城堡的屋頂能看到這種現象,因為屋頂是由深灰色石頭砌成的。最後,你還會在飽和度圖像中 看到一些白色的斑點,它們對應着原始圖像中非常暗的區域。這是由飽和度的定義引起的——飽 和度隻計算 BGR 中最大值和最小值的相對差距,是以像 (1, 0, 0) 這樣的組合就會得到飽和度 1.0, 盡管這個顔色看起來是黑的。是以,在黑色區域中計算得到的飽和度是不可靠的,沒有參考價值。

顔色的色調通常用 0~360 的角度來表示,其中紅色是 0 度。對于 8 位圖像,OpenCV 把角度 除以 2,以适合單位元組的存儲範圍。是以,每個色調值對應指定顔色的色彩,與亮度和飽和度無 關。例如天空和水的色調是一樣的,都約為 200 度(強度 100),對應色度為藍色;背景樹林的 色調約為 90 度,對應色度為綠色。有一點要特别注意,如果顔色的飽和度很低,它計算出來的 色調就不可靠。

HSB 色彩空間通常用一個圓錐體來表示,圓錐體内部的每個點代表一種特定的顔色,角度位 置表示顔色的色調,到中軸線的距離表示飽和度,高度表示亮度。圓錐體的頂點表示黑色,它的 色調和飽和度是沒有意義的。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

我們還可以人為生成一幅圖像,用來說明各種色調/飽和度組合。

cv::Mat hs(128, 360, CV_8UC3);
for (int h = 0; h < 360; h++) {
 for (int s = 0; s < 128; s++) {
 hs.at<cv::Vec3b>(s, h)[0] = h/2; // 所有色調角度
 // 飽和度從高到低
 hs.at<cv::Vec3b>(s, h)[1] = 255-s*2;
 hs.at<cv::Vec3b>(s, h)[2] = 255; // 常數
 }
}      

下圖從左到右表示不同的色調(0~180),從上到下表示不同的飽和度。圖像頂端為飽和度最 高的顔色,底部為飽和度最低的顔色。圖中所有顔色的亮度都為 255。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

使用 HSV 的值可以生成一些非常有趣的效果。一些用照片編輯軟體生成的色彩特效就是用 這個色彩空間實作的。你可以修改一幅圖像,把它的所有像素都設定為一個固定的亮度,但不改 變色調和飽和度。可以這樣實作:

// 轉換成 HSV 色彩空間
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);
// 将 3 個通道分割到 3 幅圖像中
std::vector<cv::Mat> channels;
cv::split(hsv,channels);
// 所有像素的顔色亮度通道将變成 255
channels[2]= 255;
// 重新合并通道
cv::merge(channels,hsv);
// 轉換回 BGR
cv::Mat newImage;
cv::cvtColor(hsv,newImage,CV_HSV2BGR);      

得到的結果如下圖所示,看起來像是一幅繪畫作品。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

3.5.3 拓展閱讀

在搜尋特定顔色的物體時,HSV 色彩空間也是非常實用的。

顔色用于檢測:膚色檢測

在對特定物體做初步檢測時,顔色資訊非常有用。例如輔助駕駛程式中的路标檢測功能,就 要憑借标準路标的顔色快速識别可能是路标的資訊。另一個例子是膚色檢測,檢測到的皮膚區域 可作為圖像中有人存在的标志。手勢識别就經常使用膚色檢測确定手的位置。

通常來說,為了用顔色來檢測目标,首先需要收集一個存儲有大量圖像樣本的資料庫,每個 樣本包含從不同觀察條件下捕捉到的目标,作為定義分類器的參數。你還需要選擇一種用于分類 的顔色表示法。膚色檢測領域的大量研究已經表明,來自不同人種的人群的皮膚顔色,可以在色 調飽和度色彩空間中很好地歸類。是以,在後面的圖像中,我們将隻使用色調和飽和度值來識别膚色。

OpenCV計算機視覺程式設計篇三《處理圖像的顔色》

我們定義了一個基于數值區間(最小和最大色調、最小和最大飽和度)的函數,把圖像中的 像素分為皮膚和非皮膚兩類:

void detectHScolor(const cv::Mat& image, // 輸入圖像
 double minHue, double maxHue, // 色調區間
 double minSat, double maxSat, // 飽和度區間
 cv::Mat& mask) { // 輸出掩碼
 // 轉換到 HSV 空間
 cv::Mat hsv;
 cv::cvtColor(image, hsv, CV_BGR2HSV);
 // 将 3 個通道分割到 3 幅圖像
 std::vector<cv::Mat> channels;
 cv::split(hsv, channels);
 // channels[0]是色調
 // channels[1]是飽和度
 // channels[2]是亮度
  // 色調掩碼
 cv::Mat mask1; // 小于 maxHue
 cv::threshold(channels[0], mask1, maxHue, 255,
 cv::THRESH_BINARY_INV);
 cv::Mat mask2; // 大于 minHue
 cv::threshold(channels[0], mask2, minHue, 255, cv::THRESH_BINARY);
 cv::Mat hueMask; // 色調掩碼
 if (minHue < maxHue)
 hueMask = mask1 & mask2;
 else // 如果區間穿越 0 度中軸線
 hueMask = mask1 | mask2;
 // 飽和度掩碼
 // 從 minSat 到 maxSat
 cv::Mat satMask; // 飽和度掩碼
 cv::inRange(channels[1], minSat, maxSat, satMask);
 // 組合掩碼
 mask = hueMask & satMask;
}      

如果在處理時有了大量的皮膚(以及非皮膚)樣本,我們就可以使用機率方法估算在皮膚樣 本中和非皮膚樣本中發現指定顔色的可能性。此處,我們依據經驗定義了一個合理的色調飽和 度區間,用于這裡的測試圖像(記住,8 位版本的色調在 0~180,飽和度在 0~255):

// 檢測膚色
cv::Mat mask;
detectHScolor(image, 160, 10, // 色調為 320 度~20 度
 25, 166, // 飽和度為~0.1~0.65
 mask);
// 顯示使用掩碼後的圖像
cv::Mat detected(image.size(), CV_8UC3, cv::Scalar(0, 0, 0));
image.copyTo(detected, mask);      

得到下面的檢測圖像。

繼續閱讀