OpenCV之影象處理(三十四) 基於距離變換與分水嶺的影象分割
阿新 • • 發佈:2019-02-06
影象分割(Image Segmentation)是影象處理最重要的處理手段之一 影象分割的目標是將影象中畫素根據一定的規則分為若干(N)個cluster集合,每個集合包含一類畫素。 根據演算法分為監督學習方法和無監督學習方法,影象分割的演算法多數都是無監督學習方法 - KMeans 距離變換常見演算法有兩種 - 不斷膨脹/ 腐蝕得到 - 基於倒角距離 分水嶺變換常見的演算法 - 基於浸泡理論實現,假設顏色資料為一個個山頭,在山底不停加水,直到各大山頭之間形成了明顯的分水線 distanceTransform ( // 距離變換 InputArray src, // 同下 OutputArray dst, // 同下 int distanceType, // 同下 int maskSize, // 同下 int dstType=CV_32F // 表示輸出影象的深度,輸出影象的通道數與輸入圖形一致 ) distanceTransform ( // 距離變換 InputArray src, // 輸入的影象,一般為二值影象 OutputArray dst, // 輸出8位或者32位的浮點數,單一通道,大小與輸入影象一致 OutputArray labels, // 輸出 2D 的標籤(離散Voronoi(維諾)圖),型別為 CV_32SC1 ,相同距離的算做同一個 label ,算出總共由多少個 labels int distanceType, // 所用的求解距離的型別 CV_DIST_L1 distance = |x1-x2| + |y1-y2| CV_DIST_L2 distance = sqrt((x1-x2)^2 + (y1-y2)^2) 歐幾里得距離 CV_DIST_C distance = max(|x1-x2|, |y1-y2|) int maskSize, // 最新的支援5x5,推薦3x3 int labelType=DIST_LABEL_CCOMP // Type of the label array to build, see cv::DistanceTransformLabelTypes ) watershed ( // 分水嶺變換 InputArray image, InputOutputArray markers ) 處理流程: 1. 將白色背景變成黑色-目的是為後面的變換做準備 2. 使用filter2D與拉普拉斯運算元實現影象對比度提高,sharp 3. 轉為二值影象通過threshold 4. 距離變換 5. 對距離變換結果進行歸一化到[0~1]之間 6. 使用閾值,再次二值化,得到標記(山頭) 7. 腐蝕得到每個Peak - erode 8. 發現輪廓 – findContours 9. 繪製輪廓- drawContours 10. 分水嶺變換 watershed 11. 對每個分割區域著色輸出結果
程式碼
#include "../common/common.hpp" void main(int argc, char** argv) { Mat src = imread(getCVImagesPath("images/cards.png"), IMREAD_COLOR); imshow("src34", src); for (int row = 0; row < src.rows; row++) { for (int col = 0; col < src.cols; col++) { if (src.at<Vec3b>(row, col) == Vec3b(255, 255, 255)) // 白色變為黑色,改變背景色 { src.at<Vec3b>(row, col)[0] = 0; src.at<Vec3b>(row, col)[1] = 0; src.at<Vec3b>(row, col)[2] = 0; } } } imshow("src back", src); // 銳化 sharpen Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);// 類似於拉普拉斯運算元 Mat imgLaplance; Mat sharpenImg = src; // 拷貝建構函式 printf("%d,%d,%d,%d\n", src.depth(), CV_32F, src.type(), CV_8UC3);// 0,5,16,16 // 這裡計算的顏色資料有可能是負值,所以深度傳 CV_32F, 不要傳 -1,原圖的深度是 CV_8U,不能儲存負值 filter2D(src, imgLaplance, CV_32F, kernel, Point(-1, -1), 0, BORDER_DEFAULT); // 1 depth=5, type=21, channels=3 即 depth=CV_32F type=CV_32FC3 printf("1 depth=%d, type=%d, channels=%d\n", imgLaplance.depth(), imgLaplance.type(), imgLaplance.channels()); imshow("laplance34", imgLaplance); src.convertTo(sharpenImg, CV_32F); // mat.type 由 CV_8UC3 轉換為 CV_32FC3 ,為了下面的減法計算 Mat resultImg = sharpenImg - imgLaplance; // mat.type 由 CV_32FC3 轉換為 CV_8UC3, 如果不轉換的話,影象感覺像失真了,同時 做閾值二值化的時候會報錯 resultImg.convertTo(resultImg, CV_8UC3); imgLaplance.convertTo(imgLaplance, CV_8UC3); // 2 depth = 0, type = 16, channels = 3 即 depth=CV_8U type=CV_8UC3 printf("2 depth=%d, type=%d, channels=%d\n", imgLaplance.depth(), imgLaplance.type(), imgLaplance.channels()); imshow("sharpen image", resultImg); // 轉換為灰度圖,並閾值二值化 Mat binaryImg; //cvtColor(src, resultImg, CV_BGR2GRAY); // 如果以這種方式,並且腐蝕的Mat的size為13*13,發現輪廓的size為14 //Mat k1 = Mat::ones(13, 13, CV_8UC1); // 不過相比於這種方式,把contours[][].size<=2過濾掉,影象分割會更好些 cvtColor(resultImg, resultImg, CV_BGR2GRAY); imshow("resultImg gray", resultImg); Mat k1 = Mat::ones(3, 3, CV_8UC1); // 做腐蝕或膨脹的Mat的元素的值為1最適合? 取哪個值都不影響影象分割的結果 threshold(resultImg, binaryImg, 40, 255, THRESH_BINARY | THRESH_OTSU);//閾值二值化,通過THRESH_OTSU產生閾值 imshow("binary image", binaryImg); // 黑白圖 // 距離變換 Mat distImg; // = binaryImg; // 解開上句註釋,然後不做距離變換,也能得出一種影象分割的結果,誤差也不大,contours.size=17 // 距離變換生成的輸出影象與原圖差距不大,還是隻是這裡是特例? 如果不做距離變換,後面的再次二值化也沒必要 // 因為這裡的距離變換,讓原先的二值圖,輸出的不再是二值 // 對於各個物件內部的畫素點會根據其離邊緣的距離不同生成顏色值,距離越遠(物件的中心點)顏色值越大 // 這是最重要的一步,為了後面的再次二值化能夠準確尋找到山頭(也就是各物件的中心區域) distanceTransform(binaryImg, distImg, DIST_L1, 3, CV_32F); // CV_32F表示輸出影象的深度,通道數與輸入圖形一致 imshow("distanceTransform34", distImg); // 與 binaryImg 影象感官上沒差別 normalize(distImg, distImg, 0, 1, NORM_MINMAX); // 歸一化,為了下面的再次二值化,顯現影象的輪廓 imshow("distance result", distImg);// 由於距離變化的原因,這裡影象的顏色資料,不是二值了 // 將歸一化後的mat再次二值化,(即顏色值達到0.4的地方,表示輪廓的邊界,為發現輪廓做準備) threshold(distImg, distImg, 0.4, 1, THRESH_BINARY); Mat cop1, cop2; distImg.copyTo(cop1); distImg.copyTo(cop2); // 腐蝕的size達到9,發現輪廓的數目就只有13, 9之前的輪廓數目與原圖的撲克數一致,為15 erode(distImg, distImg, k1, Point(-1, -1)); // 腐蝕一些白點,k1元素的值為0的話,相比與1,腐蝕的部分會少一些 imshow("distance binary erode image", distImg); // 二值圖 // 發現輪廓 Mat dist_8u; // distImg depth=5, type=5 即 CV_32F 與 CV_32FC1 printf("distImg depth=%d, type=%d\n", distImg.depth(), distImg.type()); distImg.convertTo(dist_8u, CV_8UC1); // 將 CV_32FC1 轉換到 CV_8UC1 因為findContours的輸入影象是8-bit imshow("dist_8u * 100", dist_8u * 100); // 元素值放大100倍,以便肉眼觀看 vector<vector<Point>> contours; findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0)); printf("contours.size=%d\n", contours.size()); // contours.size=15 // 繪製輪廓,建立標記 RNG rng(12345); Mat show_contours; src.copyTo(show_contours); // 因為 dist_8u 是單通道的,所以這裡也是單通道,如果使用 CV_8UC1 ,watershed 函式會報錯 Mat markers = Mat::zeros(src.size(), CV_32SC1); for (size_t i = 0; i < contours.size(); i++) { if (contours[i].size() <= 2) continue; // 過濾排除點數不夠的輪廓,最終的影象分割效果更好了 // 因為顏色傳的是 Scalar::all(i + 1) 所以 各撲克牌間灰度還是有一定差距的,但是不明顯 // 這裡傳 Scalar::all(i + 1), -1) 最主要的是用顏色給各輪廓做一個下標 drawContours(markers, contours, i, Scalar::all(i + 1), -1); // thickness傳 -1 表示填充輪廓 printf("contours[%d].size=%d\n", i, contours[i].size()); if (i == 1) // 腐蝕的Mat尺寸為3*3時,下標1的輪廓只有兩個點,在上面已排除 { printf("contours[1][0].x=%d, contours[1][0].y=%d, contours[1][1].x=%d,contours[1][1].y=%d\n", contours[1][0].x, contours[1][0].y, contours[1][1].x, contours[1][1].y); circle(show_contours, contours[1][0], 5, Scalar(0, 0, 255), -1); circle(show_contours, contours[1][1], 5, Scalar(0, 0, 0), -1); } Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)); drawContours(show_contours, contours, i, color, -1); // 繪製輪廓 } // 建立標記,標記的位置如果在要分割的影象塊上會影響分割的結果,如果不建立,分水嶺變換會無效 circle(markers, Point(5, 5), 3, Scalar(255, 255, 255), -1); imshow("markers * 1000", markers * 1000); // 元素值放大1000倍,以便肉眼觀看 imshow("show_contours", show_contours); // 分水嶺變換,將繪製的輪廓區域的顏色資料蔓延到各輪廓所在的分水嶺,這樣,影象分割已完成,後續不同著色顯示即可 watershed(src, markers); // markers depth=4, type=4 即 CV_32S 與 CV_32SC1 printf("markers depth=%d, type=%d\n", markers.depth(), markers.type()); imshow("watershed image", markers * 1000); Mat mark = Mat::zeros(markers.size(), CV_8UC1); // 為了做顏色反差,所以將 CV_32SC1 轉到 CV_8UC1 markers.convertTo(mark, CV_8UC1); bitwise_not(mark, mark, Mat()); // 顏色反差 imshow("bitwise_not watershed image", mark); // 各撲克牌間灰度還是有一定差距的,但是不明顯 // 為每個輪廓生成隨機顏色 vector<Vec3b> colors; for (size_t i = 0; i < contours.size(); i++) { int r = theRNG().uniform(0, 255); int g = theRNG().uniform(0, 255); int b = theRNG().uniform(0, 255); colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r)); } // fill with color and display final result Mat dst = Mat::zeros(markers.size(), CV_8UC3); for (int row = 0; row < markers.rows; row++) { for (int col = 0; col < markers.cols; col++) { int index = markers.at<int>(row, col); // 對應上面傳的 Scalar::all(i + 1), -1) if (index > 0 && index <= static_cast<int>(contours.size())) { // 給各輪廓上不同色 dst.at<Vec3b>(row, col) = colors[index - 1]; // 因為上面傳的是 Scalar::all(i + 1), -1) 所以要減1 } else { dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0); // 輪廓之外全部黑色 } } } imshow("Final Result", dst); waitKey(0); }
效果圖