實戰(6)——策略設計模式
0. 前言
良好的計算機視覺程序始于良好的編程實踐,構建無錯誤的應用程序只是一個開始。我們真正想要的是一個能夠隨著新需求的出現而輕松適應和發展的應用程序。本節將介紹如何充分利用一些面向對象的編程原則以構建高質量的軟件程序,我們將學習一些重要的設計模式,幫助我們使用易于測試、維護和可重用的組件構建應用程序。
設計模式是軟件工程中的一個常見概念,設計模式是針對軟件設計中經常出現的通用問題的可靠的、可重用的解決方案。當前有許多設計模式在軟件設計中被引入,我們應該對現有設計模式有所了解。
1. 策略設計模式顏色識別 1.1 顏色比較
假設我們想要構建一個簡單的算法來識別圖像中具有給定顏色的所有像素。為了達到目的,算法需要接受圖像和顏色作為輸入,并返回二值圖像,其中在輸入圖像中與指定顏色相同的像素位置值為 1,否則為 0,例如,輸入圖像中位置 (1,1) 處的像素值與指定顏色相同,則在二值圖像的 (1,1) 位置處像素值為 1。同時,函數也可以接受顏色容差作為參數。
1.2 策略設計模式
為了實現顏色比較,本節將使用策略設計模式,這種面向對象的設計模式將算法封裝在類中。這比用另一種算法替換給定算法或將幾種算法鏈接在一起以構建更復雜的過程更容易。此外,這種模式通過隱藏盡可能多的復雜性來簡化算法的部署。
一旦使用策略設計模式將算法封裝在一個類中,就可以通過創建該類的實例來部署它。通常,實例在程序初始化時創建。在構造的時候,類實例會用它們的默認值初始化算法的不同參數,也可以使用合適的方法讀取和設置算法的參數值。對于具有 GUI 的應用程序,可以使用不同的小部件(文本、滑塊等)來顯示和修改這些參數。
1.3 實現顏色比較
接下來,我們將介紹 類的結構。在此之前,我們編寫一個簡單的 main 函數來運行上述顏色檢測算法。
(1) 首先,在 main 函數中,我們必須為類 創建一個實例(對于類的具體代碼,我們將在下一節中介紹):
// 創建圖像處理器對象
ColorDetector cdetect;
(2) 讀取圖像進行處理:
// 讀取輸入圖像
cv::Mat image = cv::imread("1.png");
(3) 用 empty() 函數檢查我們是否正確加載了圖像,如果圖像為空,則退出應用程序:
if (image.empty()) return 0;
(4) 使用 的新實例設置目標顏色(該函數是在類中定義的):
cdetect.setTargetColor(230, 190, 130);
(5) 創建一個窗口顯示圖像處理結果。因此,我們必須使用 實例的 函數:
// 處理圖像并顯示結果
cv::namedWindow("Result");
cv::Mat result = cdetect.process(image);
cv::imshow("Result", result);
6. 最后,在退出之前等待用戶按鍵操作:
cv::waitKey();
return 0;
運行此程序,可以得到以下輸出:
在上圖中,白色像素表示圖像中與給定顏色相同的像素,黑色表示圖像中與給定顏色不同的像素。我們封裝在 類中的算法比較簡單(僅由一個掃描循環和一個容差參數組成)。當要實現的算法更復雜、步驟較多且包含多個參數時,策略設計模式也會變得更加有用。
1.3.1 完整代碼
完整代碼 (.cpp) 如下所示:
#include
#include
#include "colordetector.h"
#include
int main() {
// 創建圖像處理器對象
ColorDetector cdetect;
// 讀取輸入圖像
cv::Mat image = cv::imread("1.png");
if (image.empty()) return 0;
cv::namedWindow("Original Image");
cv::imshow("Original Image", image);
// 設置輸入參數
cdetect.setTargetColor(230, 190, 130);
// 處理圖像并顯示結果
cv::namedWindow("Result");
cv::Mat result = cdetect.process(image);
cv::imshow("Result", result);
// 或者使用函子
ColorDetector colordetector(230, 190, 130, 45, true);
cv::namedWindow("Result (functor)");
result = colordetector(image);
cv::imshow("Result (functor)", result);

// 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 // 像素與種子位置顏色進行比較
);
cv::namedWindow("Flood fill result");
result = colordetector(image);
cv::imshow("Flood fill result", image);
// 創建圖像,演示顏色空間屬性
cv::Mat colors(100, 300, CV_8UC3, cv::Scalar(100, 200, 150));
cv::Mat range = colors.colRange(0, 100);
range = range + cv::Scalar(10, 10, 10);
range = colors.colRange(200, 300);
range = range + cv::Scalar(-10, -10, -10);
cv::namedWindow("3 colors");
cv::imshow("3 colors", colors);
cv::Mat labImage(100, 300, CV_8UC3, cv::Scalar(100, 200, 150));
cv::cvtColor(labImage, labImage, cv::COLOR_BGR2Lab);
range = colors.colRange(0, 100);
range = range + cv::Scalar(10, 10, 10);
range = colors.colRange(200, 300);
range = range + cv::Scalar(-10, -10, -10);
cv::cvtColor(labImage, labImage, cv::COLOR_Lab2BGR);
cv::namedWindow("3 colors (Lab)");
cv::imshow("3 colors (Lab)", colors);
cv::Mat grayLevels(100, 256, CV_8UC3);
for (int i=0; i<256; i++) {
grayLevels.col(i) = cv::Scalar(i, i, i);
}
range = grayLevels.rowRange(50, 100);
cv::Mat channels[3];
cv::split(range, channels);
channels[1] = 128;
channels[2] = 128;
cv::merge(channels, 3, range);
cv::cvtColor(range, range, cv::COLOR_Lab2BGR);
cv::namedWindow("Luminance vs Brightness");
cv::imshow("Luminance vs Brightness", grayLevels);
cv::waitKey();
return 0;
}
1.4 類
顏色比較算法的核心過程很容易實現,通過一個簡單的掃描循環,遍歷每個像素,將其顏色與給定目標顏色進行比較:
// 迭代器
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>();
if (useLab) {
it = converted.begin<cv::Vec3b>();
itend = converted.end<cv::Vec3b>();
}
for (; it != itend; ++it, ++itout) {
if (getDistanceToTargetColor(*it) < maxDist) {
*itout = 255;
} else {
*itout = 0;

}
}
cv::Mat 變量 image 指輸入圖像,而 是指二值輸出圖像。因此,第一步需要設置所需的迭代器,使掃描循環易于實現。在每次迭代時評估當前像素顏色和給定目標顏色之間的距離,以檢查它們之間的距離是否在 定義的容差參數范圍內。如果在容差范圍內,則將輸出圖像像素值設為 255 (白色);否則,將其值設為 0 (黑色)。我們可以使用 olor 方法計算當前像素顏色與給定目標顏色之間的距離, 中也有許多不同的方法來計算距離。例如,可以計算包含 RGB 顏色值的向量之間的歐幾里得距離,或對 RGB 值之間差值的絕對值求和(這也稱為城市街區距離)。在現代架構中,浮點歐幾里得距離的計算速度比城市街區距離更快。為了獲得更高的靈活性,我們根據 方法編寫 olor 方法:
// 計算與目標顏色的距離
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 值的三個無符號字符。 變量是指給定的目標顏色,它在我們定義的類算法中定義為類 (class) 變量。對于類方法 ,其根據提供的輸入圖像,掃描圖像完成后返回結果:
cv::Mat ColorDetector::process(const cv::Mat& image) {
result.create(image.size(), CV_8U);
// 循環處理過程
...
return result;
}
每次調用此方法時,需要檢查包含結果二值圖的輸出圖像是否需要重新分配內存以匹配輸入圖像的大小,這就是我們使用 cv::Mat 的 方法的原因。需要注意的是,只有在指定的大小和深度(圖像深度是指存儲每個像素所用的位數)與當前圖像結構不對應時,此方法才會進行重新分配。
定義了核心處理方法之后,我們需要繼續添加一些額外的類方法來部署算法。由于我們已經確定了算法需要哪些輸入和輸出數據,因此,我們首先定義保存這些數據的類屬性:
class ColorDetector {
private:
// 容差
int maxDist;
// 目標顏色
cv::Vec3b target;
// 結果二值圖像
cv::Mat result;
為了創建封裝算法的類的實例(命名為 ),我們需要定義一個構造函數,策略設計模式的目標之一是使算法部署盡可能簡單??梢远x的最簡單的構造函數是空構造函數,它將創建一個有效的類算法實例。然后,我們令構造函數將所有輸入參數初始化為其默認值,在此算法中,我們將通常是可接受的容差參數設為 100;此外,我們還需要設置默認的目標顏色。用于確保我們總是從可預測和有效的輸入值開始測試算法:
// 空構造函數
// 默認參數初始化
ColorDetector() : maxDist(100), target(0, 0, 0) {}
創建類算法實例的后,我們可以使用有效圖像調用 方法并獲得有效輸出圖像處理與計算機視覺算法及應用(第2版),這是策略模式的另一個目標,即確保算法始終以有效參數運行。顯然,使用這個類時,我們會需要使用自定義參數,這可以通過提供相應的 和 方法實現。以顏色容差參數為例:
// 設置顏色閾值距離
void setColorDistanceThreshold(int distance) {
if (distance < 0) distance = 0;
maxDist = distance;
}
// 獲取顏色閾值距離
int getColorDistanceThreshold() const {
return maxDist;
}
需要注意的是,我們首先需要檢查輸入的有效性,這是為了確保我們的算法永遠不會在無效狀態下運行。我們也可以用類似的方式設置給定目標顏色參數:
// 設置顏色閾值距離
void setColorDistanceThreshold(int distance) {
if (distance < 0) distance = 0;
maxDist = distance;
}
// 獲取顏色閾值距離
int getColorDistanceThreshold() const {
return maxDist;
}
在以上代碼中,我們提供了 方法的兩個定義。在定義的第一個版本中,三個顏色分量被指定為三個參數,而在第二個版本中,使用 cv::Vec3b 保存顏色值,目標是便于類算法的使用,以方便在使用時選擇最適合的 。
本節介紹了如何使用策略設計模式將算法封裝在類中,本節我們使用顏色比較算法識別足夠接近指定目標顏色的圖像像素。此外,策略設計模式的實現可以通過使用函數對象進行補充。
1.3.1 完整代碼
完整代碼 (.h) 如下所示:
#if !defined COLORDETECT
#define COLORDETECT
#include
#include
class ColorDetector {
private:
// 容差
int maxDist;
// 目標顏色
cv::Vec3b target;
// 轉換色彩后的圖像
cv::Mat converted;
bool useLab;
// 結果二進制圖像
cv::Mat result;
public:
// 空構造函數
// 默認參數初始化
ColorDetector() : maxDist(100), target(0, 0, 0), useLab(false) {}
// Lab顏色空間的額外構造函數
ColorDetector(bool useLab) : maxDist(100), target(0, 0, 0), useLab(true) {}
// 完整構造函數
ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100, bool useLab=false) : maxDist(maxDist), useLab(useLab) {
// 目標顏色
setTargetColor(blue, green, red);
}
// 計算與目標顏色的距離
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]);
// 或者
// return static_cast(cv::norm(cv::Vec3i(color1[0]-color2[0],color1[1]-color2[1],color1[2]-color2[2])));
// 或者
// cv::Vec3b dist;
// cv::absdiff(color1, color2, dist);
// return cv::sum(dist)[0];
}
// 處理圖像,返回單通道二進制圖像
cv::Mat process(const cv::Mat& image);
cv::Mat operator()(const cv::Mat& image) {
cv::Mat input;
if (useLab) {
cv::cvtColor(image, input, cv::COLOR_BGR2Lab);
} else {
input = image;
}
cv::Mat output;
cv::absdiff(input, cv::Scalar(target), output);
// 分割圖像通道
std::vector<cv::Mat> images;
cv::split(output, images);
// 對三個通道進行加法運算
output = images[0] + images[1] + images[2];
// 應用閾值
cv::threshold(output, // 輸入圖像
output, // 輸出圖像
maxDist, // 閾值

255, // 最大值
cv::THRESH_BINARY_INV // 閾值運算類型
);
return output;
}
// 設置顏色閾值距離
void setColorDistanceThreshold(int distance) {
if (distance < 0) distance = 0;
maxDist = distance;
}
// 獲取顏色閾值距離
int getColorDistanceThreshold() const {
return maxDist;
}
// 設置BGR顏色空間中給定的待檢測顏色
void setTargetColor(uchar blue, uchar green, uchar red) {
target = cv::Vec3b(blue, green, red);
if (useLab) {
cv::Mat tmp(1, 1, CV_8UC3);
tmp.at<cv::Vec3b>(0, 0) = cv::Vec3b(blue, green, red);
// 將目標顏色轉換到 Lab 色彩空間
cv::cvtColor(tmp, tmp, cv::COLOR_BGR2Lab);
target = tmp.at<cv::Vec3b>(0, 0);
}
}
// 設置待檢測顏色
void setTargetColor(cv::Vec3b color) {
target = color;
}
// 獲取待檢測顏色
cv::Vec3b getTargetColor() const {
return target;
}
};
cv::Mat ColorDetector::process(const cv::Mat& image) {
result.create(image.size(), CV_8U);
// 轉換色彩空間
if (useLab) cv::cvtColor(image, converted, cv::COLOR_BGR2Lab);
// 迭代器
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>();
if (useLab) {
it = converted.begin<cv::Vec3b>();
itend = converted.end<cv::Vec3b>();
}
for (; it != itend; ++it, ++itout) {
if (getDistanceToTargetColor(*it) < maxDist) {
*itout = 255;
} else {
*itout = 0;
}
}
return result;
}
#endif
1.4 計算兩個顏色向量之間的距離
為了計算兩個顏色向量之間的距離,我們可以使用以下公式:
return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);

但是, 中也包含了用于計算向量歐幾里得范數的函數。因此,我們可以按如下方式計算距離:
return static_cast<int>(cv::norm<int,3>(cv::Vec3i(color1[0]-color2[0],color1[1]-color2[1],color1[2]-color2[2])));
根據以上方式使用 方法,可以得到獲得相似的結果。在以上代碼中,我們使用 cv::Vec3i (三向量整數數組),因為減法的結果是整數值。
由于 矩陣和向量數據結構包括基本算術運算符。因此,我們也可以使用以下方法計算距離:
return static_cast<int>(cv::norm<uchar,3>(color-target)); // wrong!
以上語法初看之下很容易誤以為是正確的,但并非如此。這是因為 中運算符包含對 的調用以確保結果保持在輸入類型的域內(以上代碼中為 uchar)。因此,在給定目標顏色大于當前圖像顏色值的情況下,結果為 0 而非預期的負值。正確的計算方法如下:
cv::Vec3b dist;
cv::absdiff(color1, color2, dist);
return cv::sum(dist)[0];
但是,使用兩個函數來計算兩個三元向量數組之間的距離的效率較低。
2. 使用 函數
在上一節中,我們將使用帶有迭代器的循環來執行顏色比較計算。我們也可以通過調用一系列 函數來得到相同的結果,使用 函數重寫以上顏色檢測方法:
cv::ColorDetector::process(const cv::Mat& image) {
cv::Mat output;
// 計算與目標顏色的絕對差值
cv::absdiff(image, cv::Scalar(target), output);
// 分割圖像通道
std::vector<cv::Mat> images;
cv::split(output, images);
// 對通道進行加法運算
output = images[0] + images[1] + images[2];
// 應用閾值
cv::threshold(output, output, maxDist, 255, cv::THRESH_BINARY_INV);
return output;
}
此方法使用 () 函數計算圖像像素之間的絕對差,以上代碼計算圖像與標量值 cv::() 之間的距離,除了標量值外,我們也可以提供另一個圖像作為 函數的第二個參數,在這種情況下,將逐像素計算像素差;因此,兩個圖像必須具有相同的尺寸大小。使用 split 函數提取圖像的各個通道,以便將各通道像素值相加,當計算結果結果大于 255 時,由于應用飽和運算 cv::圖像處理與計算機視覺算法及應用(第2版),結果將被限制為 255。最后,使用 () 函數創建二值圖像,該函數通常用于將所有像素與閾值(第三個參數,必須小于 256)進行比較,在常規閾值模式 (cv::) 下,將所有大于閾值的像素設為定義的最大值(第四個參數),而其他值設為 0;在逆模式 (cv::) 下,將所有低于或等于閾值的像素設為定義的最大值;而 cv:: 和 cv:: 模式令大于或小于閾值的像素保持不變。
使用 函數可以快速構建復雜的應用程序并減少出錯的可能性,并且通常更高效。但是,當使用多個中間步驟時,可能會消耗較多內存。
3. 函子或函數對象
使用 C++ 運算符重載,可以創建一個類,其實例的行為類似于函數。為此,我們可以重載 () 方法,這樣對類處理方法的調用就像函數調用一樣簡單。生成的類實例稱為函數對象或函子 (),通常,函子包含一個完整的構造函數,這樣它就可以在創建后立即使用。例如,我們可以將以下構造函數添加到 類中:
// 完整構造函數
ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100, bool useLab=false) : maxDist(maxDist), useLab(useLab) {
// 目標顏色
setTargetColor(blue, green, red);
}
顯然,我們仍然可以使用之前定義的 和 。函子方法可以定義如下:
cv::Mat operator()(const cv::Mat& image) {
// 色彩檢測代碼
...
}
要使用此函子方法檢測給定顏色,只需使用以下代碼:
ColorDetector colordetector(230, 190, 130, 100);
cv::Mat result= colordetector(image); // 調用函子
如上所示,對顏色檢測方法的調用與函數調用一樣。事實上, 變量可以像函數一樣使用。
4. 算法的基類
提供了許多執行各種計算機視覺任務的算法。為了方便它們的使用,這些算法中的大多數屬于通用基類 cv:: 的子類,這實現了策略設計模式所規定的一些概念。所有這些算法都是動態創建的,使用專門的靜態方法來確保算法始終在有效狀態下創建(即未指定參數時應具有有效的默認值)。例如,子類 cv::ORB 是一個興趣點算子(本節我們將其用作算法的說明性示例,關于此算子的詳細用法將在之后的學習中詳細介紹)。cv::ORB 算法的實例創建如下:
cv::Ptr<cv::ORB> ptrORB = cv::ORB::create();
創建完成后,就可以使用該算法。例如,通用方法 read 和 write 可用于加載或存儲算法的狀態。這些算法也有專門的方法(例如,在 ORB 中,可以使用 和 方法來觸發其主要計算單元);算法也有專門的 方法指定它們的內部參數。我們可以將指針聲明為 cv::Ptr,但在這種情況下,我們無法使用其專用方法。
小結
設計模式是軟件工程中的一個常見概念,設計模式是針對軟件設計中經常出現的通用問題的可靠的、可重用的解決方案。良好的計算機視覺程序始于良好的編程實踐,我們需要應用程序能夠隨著新需求的出現而輕松適應和擴展。本節中,我們介紹了如何充分利用一些面向對象的編程原則以構建高質量的軟件程序,我們學習了一些重要的設計模式,幫助我們使用易于測試、維護和可重用的組件構建應用程序。
系列鏈接
實戰(1)——與圖像處理基礎
實戰(2)——核心數據結構
實戰(3)——圖像感興趣區域
實戰(4)——像素操作
實戰(5)——圖像運算詳解