一、遍歷圖像實現色彩掩碼
本節我們實現這樣一個算法,我們指定某種顏色和一個閾值,根據輸入圖片生成一張掩碼,標記符合的像素(和指定顏色的差異在閾值容忍內)。
源代碼如下,我們使用一個class完成這個目標,其指定了兩種構建函數,並通過逐像素掃描的形式生成掩碼(process成員函數)。另外,本class做了仿函數處理(operator成員函數),類似於python中的__call__方法,可以直接調用實例像函數一樣進行處理。注意迭代器的使用,需要++it而非
class ColorDetector { private: int maxDist; // 允許的最小差距 cv::Vec3b target; // 目標顏色 cv::Mat result; // 結果Mask圖像 public: // 空構造函數 ColorDetector() :maxDist(100), target(0, 0, 0) {}; ColorDetector(uchar blue, uchar green, uchar red, int maxDist) : maxDist(maxDist) { setTargetColor(blue, green, red); }; // 設置顏色差距閾值 void setColorDistanceThreshold(int distance) { if (distance < 0) distance = 0; maxDist = distance; }; // 獲取顏色差距閾值 int getColorDistanceThreshold() { return maxDist; }; // 設置待檢測顏色 void setTargetColor(uchar blue, uchar green, uchar red) { target = cv::Vec3b(blue, green, red); }; void setTargetColor(cv::Vec3b color) { target = color; }; // 計算與目標顏色的差距 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::Mat process(const cv::Mat &image); // operator()使類像函數一樣工作 cv::Mat operator()(const cv::Mat &image) { return process(image); }; }; cv::Mat ColorDetector::process(const cv::Mat &image) { // 為Mask結果申請空間 result.create(image.size(), CV_8U); 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; }; }; return result; };
兩種調用方法都列舉了出來:
void code_3() { // 創建色彩檢測器對象 ColorDetector cdetect; cdetect.setTargetColor(10, 50, 10); // 讀取圖片 cv::Mat image = cv::imread("test.jpg"); // 處理圖片 cv::Mat result = cdetect.process(image); cv::imshow("色彩檢測", result); // 仿函數 ColorDetector colordetector(230, 190, 130, 150); result = colordetector(image); cv::imshow("仿函數色彩檢測", result); };
輸出圖像展示:
二、threshold函數
使用如下成員函數替換上面的同名成員函數,
cv::Mat ColorDetector::process(const cv::Mat &image) { cv::Mat output; // output存儲每個像素點(3通道)殘差絕對值 cv::absdiff(image, cv::Scalar(target), output); std::vector<cv::Mat> channels; cv::split(output, channels); // output存儲每個位置3通道殘差和 output = channels[0] + channels[1] + channels[2]; std::cout << output.channels() << std::endl; // 判斷每個位置像素和偏差不大於閾值即為所尋點,生成掩碼 cv::threshold( output, // 輸入 output, // 輸出 maxDist, // 閾值,需要小與255 255, // 標記值(符合條件點) cv::THRESH_BINARY_INV // 不大於閾值點標記為標記值 ); return output; };
使用OpenCV的掩碼生成函數threshold可以優化速度,不過由於需要一些中間過程,會消耗額外的內存,輸出也可能(萬一OpenCV工程師們在源碼里添加了奇技淫巧呢)略有差異,
三、Mat(包含Scalar)數值運算API的優勢
cv::Vec3b和cv::Scalar
我們簡單的提一句cv::Vec3b和cv::Scalar的區別,兩者都可以表示3通道像素的基本點,不過Vec更傾向對於原始的數據的格式化view,即和Mat耦合度不高,僅僅是個3元素數組;而Scalar更抽象傾向於表示一個像素,可以和Mat直接廣播運算。
Mat的數值運算API
對比兩種方式生成的殘差矩陣,可以看到第一幅圖中偏黑的部分在第二幅圖上對應位置是特別亮的部分,聯想到我們矩陣的類型是uchar,即無符號整形,存在上溢情況,可以推斷API計算(即Mat的數值運算,包含上面程序中使用的加法運算符重載)出來的殘差針對0~255做了截斷,有效的防止了上溢:
我們將自己實現顏色差值計算的函數進行修改,添加上截斷部分,
// 計算兩個顏色之間的距離 int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const { return cv::saturate_cast<uchar>( abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2])); };
再查看輸出的殘差圖像,發現和API計算結果已經一致(如下圖)。
四、基於HSV色彩空間分割皮膚
原理很簡單的一個例子,使用HSV色彩空間的兩個通道設定閾值篩選符合的像素即可,要點:
HSV空間中的色調、飽和度兩項指標可以用於分割皮膚
cv::cvtColor函數用於色彩空間轉換
色調空間呈現環狀(0~360),所以當max>min時,取兩者之間的,當max<=min時,取小於max的和大於min的並集
色調空間在8位表示時使用0~180代替0~360
cv::inRange(img, minpixel, maxpixel, mask) 函數和threshold函數類似,生成掩碼
Mat操作熟練地使用位運算可以提升程序效率,並且簡化邏輯設計
函數源碼見下面
void detectHScolor( const cv::Mat& image, // 輸入圖片 double minHue, double maxHue, // 色調區間 double minSat, double maxSat, // 飽和度區間 cv::Mat& mask // 輸出掩碼 ) { cv::Mat hsv; cv::cvtColor(image, hsv, CV_BGR2HSV); std::vector<cv::Mat> channels; cv::split(hsv, channels); // 色調掩碼,色調是環形的 // 記錄小於maxHue cv::Mat mask0; cv::threshold( channels[0], mask0, maxHue, 255, cv::THRESH_BINARY_INV ); // 記錄大於minHue cv::Mat mask1; cv::threshold( channels[0], mask1, maxHue, 255, cv::THRESH_BINARY ); cv::Mat hueMask; if (minHue < maxHue) hueMask = mask0 & mask1; else hueMask = mask0 | mask1; // 飽和度掩碼 cv::Mat satMask; cv::inRange(channels[1], minSat, maxSat, satMask); mask = hueMask & satMask; }
調用代碼:
cv::Mat skin = cv::imread("skin.jfif"); cv::Mat mask; detectHScolor(skin, 160, 10, 25, 166, mask); cv::Mat detected(skin.size(), CV_8UC3, cv::Scalar(0, 0, 0)); skin.copyTo(detected, mask); cv::imshow("皮膚檢測", detected);
效果如下,很一般(本來是找了張人臉做實驗的,不過出來的圖容易引起不適,雖改之 ),而且由於濾鏡的關系地面呈暖色調,沒有成功的剔除掉。