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);
    }

效果图

这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

猜你喜欢

转载自blog.csdn.net/huanghuangjin/article/details/81194552