1. 程式人生 > >OpenCV之影象處理(三十四) 基於距離變換與分水嶺的影象分割

OpenCV之影象處理(三十四) 基於距離變換與分水嶺的影象分割

影象分割(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);
    }

效果圖

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述