搜索

查看: 3125|回复: 11

[Python] OpenCV实战记录之基于分水岭算法的图像分割

[复制链接]
发表于 2023-5-4 17:24:01 | 显示全部楼层 |阅读模式
Editor 2023-5-4 17:24:01 3125 11 看全部
目录
  • 0. 前言
  • 1. 分水岭算法
  • 2. 分水岭算法直观理解
  • 3. 完整代码
  • 总结
    0. 前言
    分水岭变换是一种流行的图像处理算法,用于快速将图像分割成同质区域。分水岭变换主要基于以下思想:当图像被视为拓扑浮雕时,均质区域对应于相对平坦且由陡峭的边缘界定的盆地。算法的原始版本倾向于过度分割图像,从而产生多个小区域,因此 OpenCV 中实现了该算法的改进版本,通过使用一组预定义的标记来指导图像分割区域的定义。

    1. 分水岭算法
    分水岭分割可以通过使用 cv::watershed 函数实现,函数的输入是一个 32 位有符号整数标记图像,其中每个非零像素表示一个标签。
    通过标记图像中已知属于给定区域的一些像素,利用初始标记,分水岭算法可以确定其他像素所属的区域。
    (1) 首先,将标记图像读取为灰度图像,然后将其转换为整数类型:
    class WatershedSegmentater {
        private:
            cv::Mat markers;
        public:
            void setMarkers(const cv::Mat& markerImage) {
                // 转换数据类型
                markerImage.convertTo(markers, CV_32S);
            }
            cv::Mat process(const cv::Mat& image) {
                // 应用分水岭算法
                cv::watershed(image, markers);
                return markers;
            }
    有多种获取标记的方式,例如,使用预处理步骤识别出属于感兴趣对象的某些像素,然后利用分水岭算法根据初始标记分割完整的对象。在本节中,我们将使用二值图像来识别相应原始图像中的动物。因此,从二值图像中,我们需要识别属于前景(动物)的像素和属于背景(主要是雪地)的像素,我们用标签 255 标记前景像素,用标签 128 标记背景像素,其他像素则标记为 0。
    (2) 初始二值图像包含过多属于图像各个部分的白色像素,为了只保留属于重要对象的像素,我们首先需要腐蚀该图像:
    // 消除噪音
    cv::Mat fg;
    cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 4);
    结果如下图所示:

    2023022210412545.png

    2023022210412545.png


    (3) 图中仍然存在一些属于背景(雪地)的像素,我们通过对原始二值图像进行膨胀来选择几个属于背景的像素:
    // 标记图像像素
    cv::Mat bg;
    cv::dilate(binary, bg, cv::Mat(), cv::Point(-1, -1), 4);
    cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
    结果如下图所示,黑色像素对应于背景像素:

    2023022210412646.png

    2023022210412646.png


    (4) 将这些图像组合起来形成标记图像:
    cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
    markers = fg+bg;
    我们使用重载的 + 运算符来组合图像,得到用作分水岭算法的输入:

    2023022210412647.png

    2023022210412647.png


    (5) 在这个输入图像中,白色区域属于前景对象,灰色区域是背景的一部分,黑色区域则属于未知标签,得到分割结果如下:
    // 创建分水岭分割对象
    WatershedSegmentater segmenter;
    segmenter.setMarkers(markers);
    segmenter.process(image);
    更新标记图像,以便为黑色区域中的像素重新分配标签,而属于边界的像素的值为 -1。结果标签图像如下:

    2023022210412648.png

    2023022210412648.png


    图像中对象边缘的可视化结果如下图所示:

    2023022210412649.png

    2023022210412649.png


    2. 分水岭算法直观理解
    我们使用拓扑图进行类比,为了创建分水岭分割,我们从级别 0 开始注水,随着水位逐渐增加,就形成了集水盆地。这些盆地的大小也会逐渐增加,两个不同盆地的水最终会汇合,发生这种情况时,会创建一个分水岭,以将两个盆地分开。一旦水位达到最高水位,这些水域和分水岭就形成了分水岭分割。
    在注水过程中最初会产生许多小盆地,当这些盆地进行合并时,会创建许多分水岭线,从而导致图像被过度分割。为了克服这个问题,已经提出了多种改进算法,在 OpenCV 调用 cv::watershed 函数时,注水过程从一组预定义的标记像素开始,根据分配给初始标记的值对盆地进行标记,当具有相同标签的两个盆地合并时,不会创建分水岭,从而防止过度分割,更新输入标记图像以获得最终的分水岭分割。用户可以输入带有任意数量的标签和未知标签的标记图像,标记图像的像素类型为为 32 位有符号整数,以便能够定义超过 255 个标签。cv::watershed 函数还允许返回与分水岭关联的像素(使用特殊值 -1 进行标记)。
    为了便于显示结果,我们引入两种特殊的方法。第一个方法 getSegmentation() 通过阈值返回标签图像,分水岭值为 0:
    // 返回结果
    cv::Mat getSegmentation() {
        cv::Mat tmp;
        markers.convertTo(tmp, CV_8U);
        return tmp;
    }
    第二种方法 getWatersheds() 返回的图像中,分水岭线使用值 0 进行标记,图像的其余部分像素值为 255,可以使用 cv::convertTo 方法实现:
    // 返回分水岭
    cv::Mat getWatersheds() {
        cv::Mat tmp;
        markers.convertTo(tmp,CV_8U,255,255);
        return tmp;
    }
    在转换之前应用线性变换,可以将像素值 -1 转换为 0 ( − 1 × 255 + 255 = 0 -1\times 255+255=0 −1×255+255=0)。由于将有符号整数转换为无符号字符时需应用饱和操作,大于 255 的像素值将转换为 255。
    我们也可以通过许多不同的方式获得标记图像。例如,可以令用户以交互方式在图像中标记属于对象和背景的像素区域;或者,如果我们需要识别位于图像中心的物体,可以输入一个中心区域标有特定标签的图像,且图像背景标记带有另一个标签,可以按以下方式创建标记图像:
    // 标记背景像素
    cv::Mat imageMask(image.size(), CV_8U, cv::Scalar(0));
    cv::rectangle(imageMask,
                cv::Point(5, 5),
                cv::Point(image.cols-5, image.rows-5),
                cv::Scalar(255),
                3);
    // 标记前景像素
    cv::rectangle(imageMask,
                cv::Point(image.cols/2-10, image.rows/2-10),
                cv::Point(image.cols/2+10, image.rows/2+10),
                cv::Scalar(1),
                10);
    如果我们将此标记图像叠加在测试图像上,可以得到以下图像:

    2023222105005335.jpg

    2023222105005335.jpg


    生成的分水岭图像如下图所示:

    2023022210412651.png

    2023022210412651.png


    3. 完整代码
    头文件 (watershedSegmentation.h) 完整代码如下:
    #if !defined WATERSHS
    #define WATERSHS
    #include
    #include
    class WatershedSegmentater {
        private:
            cv::Mat markers;
        public:
            void setMarkers(const cv::Mat& markerImage) {
                // 转换数据类型
                markerImage.convertTo(markers, CV_32S);
            }
            cv::Mat process(const cv::Mat& image) {
                // 应用分水岭算法
                cv::watershed(image, markers);
                return markers;
            }
            // 返回结果
            cv::Mat getSegmentation() {
                cv::Mat tmp;
                markers.convertTo(tmp, CV_8U);
                return tmp;
            }
            // 返回分水岭
            cv::Mat getWatersheds() {
                cv::Mat tmp;
                markers.convertTo(tmp,CV_8U,255,255);
                return tmp;
            }
    };
    #endif
    主文件 (segment.cpp) 完整代码如下所示:
    #include
    #include
    #include
    #include
    #include "watershedSegmentation.h"
    int main() {
        // 读取输入图像
        cv::Mat image = cv::imread("1.png");
        if (!image.data) return 0;
        cv::namedWindow("Original Image");
        cv::imshow("Original Image",image);
        // 读取二值图像
        cv::Mat binary;
        binary = cv::imread("binary.png", 0);
        cv::namedWindow("Binary Image");
        cv::imshow("Binary Image", binary);
        // 消除噪音
        cv::Mat fg;
        cv::erode(binary, fg, cv::Mat(), cv::Point(-1, -1), 4);
        cv::namedWindow("Foreground Image");
        cv::imshow("Foreground Image", fg);
        // 标记图像像素
        cv::Mat bg;
        cv::dilate(binary, bg, cv::Mat(), cv::Point(-1, -1), 4);
        cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV);
        cv::namedWindow("Background Image");
        cv::imshow("Background Image", bg);
        cv::Mat markers(binary.size(), CV_8U, cv::Scalar(0));
        markers = fg+bg;
        cv::namedWindow("Markers");
        cv::imshow("Markers", markers);
        // 创建分水岭分割对象
        WatershedSegmentater segmenter;
        segmenter.setMarkers(markers);
        segmenter.process(image);
        cv::namedWindow("Segmentation");
        cv::imshow("Segmentation", segmenter.getSegmentation());
        cv::namedWindow("Watersheds");
        cv::imshow("Watersheds", segmenter.getWatersheds());
        // 打开另一张图像
        image = cv::imread("3.png");
        // 标记背景像素
        cv::Mat imageMask(image.size(), CV_8U, cv::Scalar(0));
        cv::rectangle(imageMask,
                    cv::Point(5, 5),
                    cv::Point(image.cols-5, image.rows-5),
                    cv::Scalar(255),
                    3);
        // 标记前景像素
        cv::rectangle(imageMask,
                    cv::Point(image.cols/2-10, image.rows/2-10),
                    cv::Point(image.cols/2+10, image.rows/2+10),
                    cv::Scalar(1),
                    10);
        segmenter.setMarkers(imageMask);
        segmenter.process(image);
        cv::rectangle(image,
                    cv::Point(5, 5),
                    cv::Point(image.cols-5, image.rows-5),
                    cv::Scalar(255, 255, 255),
                    3);
        cv::rectangle(image,
                    cv::Point(image.cols/2-10, image.rows/2-10),
                    cv::Point(image.cols/2+10, image.rows/2+10),
                    cv::Scalar(1, 1, 1),
                    10);
        cv::namedWindow("Image with marker");
        cv::imshow("Image with marker", image);
        cv::namedWindow("Watershed");
        cv::imshow("Watershed", segmenter.getWatersheds());
        cv::waitKey();
        return 0;
    }

    总结
    到此这篇关于OpenCV实战记录之基于分水岭算法的图像分割的文章就介绍到这了,更多相关OpenCV分水岭算法的图像分割内容请搜索知鸟论坛以前的文章或继续浏览下面的相关文章希望大家以后多多支持知鸟论坛
  • 回复

    使用道具 举报

    发表于 2023-6-29 06:38:28 | 显示全部楼层
    哈哈SE7 2023-6-29 06:38:28 看全部
    楼主太厉害了!楼主,I*老*虎*U!我觉得知鸟论坛真是个好地方!
    回复

    使用道具 举报

    发表于 2023-6-29 14:10:00 | 显示全部楼层
    计划你大爷计j 2023-6-29 14:10:00 看全部
    我看不错噢 谢谢楼主!知鸟论坛越来越好!
    回复

    使用道具 举报

    发表于 2023-6-29 17:35:26 | 显示全部楼层
    六翼天使494 2023-6-29 17:35:26 看全部
    我看不错噢 谢谢楼主!知鸟论坛越来越好!
    回复

    使用道具 举报

    发表于 2023-6-29 18:45:16 | 显示全部楼层
    丁侦球 2023-6-29 18:45:16 看全部
    这个帖子不回对不起自己!我想我是一天也不能离开知鸟论坛
    回复

    使用道具 举报

    发表于 2023-6-29 23:17:13 | 显示全部楼层
    戏做顿 2023-6-29 23:17:13 看全部
    我看不错噢 谢谢楼主!知鸟论坛越来越好!
    回复

    使用道具 举报

    发表于 2023-6-30 03:18:05 | 显示全部楼层
    462710480 2023-6-30 03:18:05 看全部
    楼主发贴辛苦了,谢谢楼主分享!我觉得知鸟论坛是注册对了!
    回复

    使用道具 举报

    发表于 2023-6-30 13:34:26 | 显示全部楼层
    无人岛屿颈 2023-6-30 13:34:26 看全部
    其实我一直觉得楼主的品味不错!呵呵!知鸟论坛太棒了!
    回复

    使用道具 举报

    发表于 2023-6-30 23:19:58 | 显示全部楼层
    麻辣鸡翅 2023-6-30 23:19:58 看全部
    这个帖子不回对不起自己!我想我是一天也不能离开知鸟论坛
    回复

    使用道具 举报

    发表于 2023-7-3 12:27:09 | 显示全部楼层
    永远就三年疗 2023-7-3 12:27:09 看全部
    楼主,大恩不言谢了!知鸟论坛是最棒的!
    回复

    使用道具 举报

    • 您可能感兴趣
    点击右侧快捷回复 【请勿灌水】
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则 返回列表

    RSS订阅| SiteMap| 小黑屋| 知鸟论坛
    联系邮箱E-mail:zniao@foxmail.com
    快速回复 返回顶部 返回列表