圖像分割實戰-分水嶺分割方法和GrabCut 算法

1. 分水嶺分割方法

它是依賴於形態學的,圖像的灰度等級不一樣,如果圖像的灰度等級一樣的情況下怎麼人為的把它造成不一樣?可以通過距離變換實現,這樣它們的灰度值就有了階梯狀的變換。風水嶺算法常見的有三種方法:(1)基於浸泡理論的分水嶺分割方法;(2)基於連通圖方法;(3)基於距離變換的方法。OpenCV 中是基於距離變換的分割方法,就相當於我們的小山頭(認為造成的)。

基本的步驟:


圖像分割實戰-分水嶺分割方法和GrabCut 算法

例子1 粘連對象分離和計數。

例子代碼:

<code>#include<opencv2>
#include<iostream>
using namespace std;
using namespace cv;
void test(){
    Mat srcImg;
    srcImg = imread("pill_002.png");
    if (srcImg.empty())
    {
        cout <    }

    namedWindow("Original image", CV_WINDOW_AUTOSIZE);
    imshow("Original image", srcImg);

    Mat grayImg, binaryImg, shiftedImg;

    //做濾波,使圖像更加平滑,保留邊緣,類似於雙邊濾波
    pyrMeanShiftFiltering(srcImg, shiftedImg, 21, 51);  
    namedWindow("shifted", CV_WINDOW_AUTOSIZE);
    imshow("shifted", shiftedImg);

    cvtColor(shiftedImg, grayImg, COLOR_BGR2GRAY);  //轉為灰度圖像
    //二值化
    threshold(grayImg, binaryImg, 0, 255, THRESH_BINARY | THRESH_OTSU);  
    namedWindow("binary", CV_WINDOW_AUTOSIZE);
    imshow("binary", binaryImg);

    //距離變換
    Mat distImg;
    distanceTransform(binaryImg, distImg, DistanceTypes::DIST_L2, 3, CV_32F);
    //歸一化,因為距離變換後得出來的值都比較小。
    normalize(distImg, distImg, 0, 1, NORM_MINMAX);  
    namedWindow("distance", CV_WINDOW_AUTOSIZE);
    imshow("distance", distImg);

    //這個二值化的作用是尋找局部最大。
    threshold(distImg, distImg, 0.4, 1, THRESH_BINARY);
    namedWindow("distance_binary", CV_WINDOW_AUTOSIZE);
    imshow("distance_binary", distImg);

    //生成 marker
    Mat distMaskImg;
    // distImg 得到的是 0- 1之間的數,轉化成8位單通道的。
    distImg.convertTo(distMaskImg, CV_8U);  
    vector<vector>>contours;
    //找到 marker 的輪廓
    findContours(distMaskImg, contours, RETR_EXTERNAL,
                 CHAIN_APPROX_SIMPLE, Point(0, 0));
    //create marker 填充 marker
    Mat  markersImg = Mat::zeros(srcImg.size(), CV_32SC1);
    for (int i = 0; i     {
        drawContours(markersImg, contours, static_cast(i),
                     Scalar::all(static_cast(i)+1), -1); 
    }
    circle(markersImg, Point(5, 5), 3, Scalar(255), -1);

    //形態學操作 - 彩色圖像,目的是去掉干擾,讓結果更好。
    Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    morphologyEx(srcImg, srcImg, MORPH_ERODE, kernel);

    //完成分水嶺變換
    watershed(srcImg, markersImg);
    Mat mark = Mat::zeros(markersImg.size(), CV_8UC1);
    markersImg.convertTo(mark, CV_8UC1);
    bitwise_not(mark, mark, Mat());

    namedWindow("watershed", CV_WINDOW_AUTOSIZE);
    imshow("watershed", mark);

    //下面的步驟可以不做,最好做出來讓結果顯示更美觀。
    //生成隨機顏色
    vector<vec3b>colors;
    for (int i = 0; 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));
    }

    //顏色填充和最終顯示
    Mat dstImg = Mat::zeros(markersImg.size(), CV_8UC3);
    int index = 0;
    for (int i = 0; i     {
        for (int j = 0; j         {
            index = markersImg.at(i, j);
            if (index > 0 && index <= contours.size())
            {
                dstImg.at<vec3b>(i, j) = colors[index - 1];
            }
            else
            {
                dstImg.at<vec3b>(i, j) = Vec3b(0, 0, 0);
            }
        }
    }
    cout <    namedWindow("Final Result", CV_WINDOW_AUTOSIZE);
    imshow("Final Result", dstImg);
}
int main(){
    test();
    waitKey(0);
    return 0;
}/<vec3b>/<vec3b>
/<vec3b>
/<vector>/<iostream>/<opencv2>/<code>


圖像分割實戰-分水嶺分割方法和GrabCut 算法

總結:有時候會導致碎片化,過度分割,因為二值化中如果有很多小的黑點或碎片,在分割的時候導致很多 mask ,即小山頭太多了,這個時候我們要考慮怎麼去合併它,可以通過聯通區域的直方圖,或者像素值均值相似程度等。

例子2:圖像分割

<code>#include<opencv2>
#include<iostream>
using namespace std;
using namespace cv;
//執行分水嶺算法函數
Mat watershedCluster(Mat &srcImg, int &numSegments);
//結果顯示函數
void DisplaySegments(Mat &markersImg, int numSegments);
void test(){
    Mat srcImg;
    srcImg = imread("toux.jpg");
    if (srcImg.empty())
    {
        cout <    }

    namedWindow("Original image", CV_WINDOW_AUTOSIZE);
    imshow("Original image", srcImg);

    int numSegments;
    Mat markers = watershedCluster(srcImg, numSegments);
    DisplaySegments(markers, numSegments);
}

Mat watershedCluster(Mat &srcImg, int &numSegments){
    //二值化
    Mat grayImg, binaryImg;
    cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
    threshold(grayImg, binaryImg, 0, 255, THRESH_BINARY | THRESH_OTSU);

    //形態學和距離變換
    Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));

    morphologyEx(binaryImg, binaryImg, MORPH_OPEN, kernel, Point(-1, -1));

    Mat distImg;
    distanceTransform(binaryImg, distImg, DistanceTypes::DIST_L2, 3, CV_32F);
    normalize(distImg, distImg, 0.0, 1.0, NORM_MINMAX);

    //開始生成標記
    threshold(distImg, distImg, 0.1, 1.0, THRESH_BINARY);
    normalize(distImg, distImg, 0, 255, NORM_MINMAX);
    distImg.convertTo(distImg, CV_8UC1);  //CV_32F 轉成 CV_8UC1

    //標記開始
    vector<vector>>contours;
    vector<vec4i>hireachy;
    findContours(distImg, contours, hireachy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
    if (contours.empty())
    {
        return Mat();
    }

    Mat markersImg(distImg.size(), CV_32S);
    markersImg = Scalar::all(0);
    for (int i = 0; i     {
        drawContours(markersImg, contours, i, Scalar(i + 1), -1, 8, hireachy, INT_MAX);
    }
    circle(markersImg, Point(5, 5) ,3, Scalar(255), -1);

    //分水嶺變換
    watershed(srcImg, markersImg);
    numSegments = contours.size();
    return markersImg;
}

void DisplaySegments(Mat &markersImg, int numSegments){
    //生成隨機顏色
    vector<vec3b>colors;
    for (int i = 0; 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));
    }

    //顏色填充和最終顯示
    Mat dstImg = Mat::zeros(markersImg.size(), CV_8UC3);
    int index = 0;
    for (int i = 0; i     {

        for (int j = 0; j         {
            index = markersImg.at(i, j);
            if (index > 0 && index <= numSegments)
            {
                dstImg.at<vec3b>(i, j) = colors[index - 1];
            }
            else
            {
                dstImg.at<vec3b>(i, j) = Vec3b(255, 255, 255);
            }
        }
    }
    cout <    namedWindow("Final Result", CV_WINDOW_AUTOSIZE);
    imshow("Final Result", dstImg);
}
int main(){
    test();
    waitKey(0);
    return 0;
}/<vec3b>/<vec3b>
/<vec3b>/<vec4i>/<vector>/<iostream>/<opencv2>/<code>

效果圖:


圖像分割實戰-分水嶺分割方法和GrabCut 算法

2. GrabCut 算法分割圖像

GrabCut 算法的原理前面有介紹過,這裡就不在介紹了,具體可以看下文章末尾往期推薦中閱讀。下面例子實現圖像中對象的摳圖。

基本步驟:


圖像分割實戰-分水嶺分割方法和GrabCut 算法

例子代碼:

<code>#include<opencv2>
#include<iostream>
using namespace std;
using namespace cv;
int numRun = 0; //算法迭代次數
bool init = false;
Rect rect;
Mat srcImg, MaskImg, bgModel, fgModel;

//鼠標回調函數
void onMouse(int event, int x, int y, int flags, void* param);
void showImg();  //顯示畫的圖片
void setRoiMask();  //選擇 ROI 的函數
void runGrabCut();  //執行算法函數
static void ShowHelpText();  //提示用戶操作函數

void test(){
    srcImg = imread("toux.jpg");
    if (srcImg.empty())
    {
        cout <    }

    namedWindow("Original image", CV_WINDOW_AUTOSIZE);
    imshow("Original image", srcImg);

    //初始化 mask,單通道 8 位
    MaskImg.create(srcImg.size(), CV_8UC1);  
    //在不知道它是前景還是背景的情況下,把它全部設為背景。
    MaskImg.setTo(Scalar::all(GC_BGD));  //結果不是 0 就是 1  GC_BGD為0

    setMouseCallback("Original image", onMouse, 0);

    while (true)
    {
        char c = (char)waitKey(0);
        if (c == 'n')  // 按下 n 建開始執行算法
        {

            runGrabCut();
            numRun++;
            showImg();
            cout <        }
        if (c == 27)
        {
            break;
        }
    }
}

void onMouse(int event, int x, int y, int flags, void* param){
    switch (event)
    {
    case EVENT_LBUTTONDOWN:
        rect.x = x;
        rect.y = y;
        rect.width = 1;
        rect.height = 1;
        break;

    case EVENT_MOUSEMOVE:
        if (flags& EVENT_FLAG_LBUTTON)
        {
            rect = Rect(Point(rect.x, rect.y), Point(x, y));
            showImg();
        }
        break;
    case EVENT_LBUTTONUP:
        if (rect.width > 1 && rect.height > 1)
        {
            showImg();
        }
        break;

    default:
        break;
    }
}

void showImg(){
    Mat result, binMask;
    binMask.create(MaskImg.size(), CV_8UC1);
    binMask = MaskImg & 1;
    if (init)
    {
        srcImg.copyTo(result,binMask);
    }
    else
    {

        srcImg.copyTo(result);
    }
    rectangle(result, rect, Scalar(0, 0, 255), 2, 8);

    namedWindow("Original image", CV_WINDOW_AUTOSIZE);
    imshow("Original image", result);
}

void setRoiMask(){
    //GC_BGD = 0   明確屬於背景的像素
    //GC_FGD = 1   明確屬於前景的像素
    //GC_PR_BGD = 2  可能屬於背景的像素
    //GC_PR_FGD = 3  可能屬於前景的像素

    MaskImg.setTo(GC_BGD);  
    //為了避免選擇越界
    rect.x = max(0, rect.x);
    rect.y = max(0, rect.y);
    rect.width = min(rect.width, srcImg.cols - rect.x);
    rect.height = min(rect.height, srcImg.rows - rect.y);

    //把我們選取的那一塊設為前景
    MaskImg(rect).setTo(Scalar(GC_PR_FGD));
}

void runGrabCut(){
    if (rect.width     {
        return;
    }
    if (init)
    {
        grabCut(srcImg, MaskImg, rect, bgModel, fgModel, 1);
    }

    else

    {
        grabCut(srcImg, MaskImg, rect, bgModel, fgModel, 1, GC_INIT_WITH_RECT);
        init = true;
    }
}

static void ShowHelpText(){
    cout <    cout <    cout <}

int main(){
    ShowHelpText();
    test();
    waitKey(0);
    return 0;
}/<iostream>/<opencv2>/<code>

效果圖:


圖像分割實戰-分水嶺分割方法和GrabCut 算法


圖像分割實戰-分水嶺分割方法和GrabCut 算法


分享到:


相關文章: