opencv图像分割之分水岭分割

分水岭分割

watershed图像自动分割的实现步骤:

  1. 图像灰度化、滤波、Canny边缘检测
  2. 查找轮廓,并且把轮廓信息按照不同的编号绘制到watershed的第二个入参merkers上,相当于标记注水点。
  3. watershed分水岭运算(实质是将markers的轮廓线的信息,转化为按轮廓区分的块状信息)
  4. 绘制分割出来的区域,视觉控还可以使用随机颜色填充,或者跟原始图像融合以下,以得到更好的显示效果。
    在这里插入图片描述

相关api

函数api

void watershed( InputArray image, InputOutputArray markers );  

参数介绍

  • image:必须是一个8bit,3通道彩色图像矩阵序列
  • markers:在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。
    接下来执行分水岭会发生什么呢?算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。
    简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。

均值漂移函数api

void pyrMeanShiftFiltering( InputArray src, 
                            OutputArray dst,
                            double sp, 
                            double sr, 
                            int maxLevel=1,
                            TermCriteria termcrit=TermCriteria(
                            TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );

参数介绍

  • src,输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等Opencv中的彩色图像格式均可;
  • dst,输出图像,跟输入src有同样的大小和数据格式;
  • sp,定义的漂移物理空间半径大小;
  • sr,定义的漂移色彩空间半径大小;
  • maxLevel,定义金字塔的最大层数;
  • termcrit,定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合;

函数介绍

meanShfit均值漂移算法是一种通用的聚类算法,它的基本原理是:对于给定的一定数量样本,任选其中一个样本,以该样本为中心点划定一个圆形区域,求取该圆形区域内样本的质心,即密度最大处的点,再以该点为中心继续执行上述迭代过程,直至最终收敛。
可以利用均值偏移算法的这个特性,实现彩色图像分割,Opencv中对应的函数是pyrMeanShiftFiltering。这个函数严格来说并不是图像的分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域,所以在Opencv中它的后缀是滤波“Filter”,而不是分割“segment”。先列一下这个函数,再说一下它“分割”彩色图像的实现过程。
pyrMeanShiftFiltering函数的执行过程是这样的:

  1. 迭代空间构建:
    以输入图像上src上任一点P0为圆心,建立物理空间上半径为sp,色彩空间上半径为sr的球形空间,物理空间上坐标2个—x、y,色彩空间上坐标3个—R、G、B(或HSV),构成一个5维的空间球体。其中物理空间的范围x和y是图像的长和宽,色彩空间的范围R、G、B分别是0~255。

  2. 求取迭代空间的向量并移动迭代空间球体后重新计算向量,直至收敛:
    在1中构建的球形空间中,求得所有点相对于中心点的色彩向量之和后,移动迭代空间的中心点到该向量的终点,并再次计算该球形空间中所有点的向量之和,如此迭代,直到在最后一个空间球体中所求得的向量和的终点就是该空间球体的中心点Pn,迭代结束。

  3. 更新输出图像dst上对应的初始原点P0的色彩值为本轮迭代的终点Pn的色彩值,如此完成一个点的色彩均值漂移。

  4. 对输入图像src上其他点,依次执行步骤1,、2、3,遍历完所有点位后,整个均值偏移色彩滤波完成,这里忽略对金字塔的讨论。

在这个过程中,关键参数是sp和sr的设置,二者设置的值越大,对图像色彩的平滑效果越明显,同时函数耗时也越多


代码

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace  std;
using namespace cv;
using namespace cv::ml;

#define PIC_PATH "/work/opencv_pic/"
#define PIC_NAME "man_face.jpg"

int main(void)
{
     Mat src;

    //获取完整的图片路径及名称
    string pic = string(PIC_PATH)+string(PIC_NAME);

    //打印图片路径
    cout << "pic path is :"<<pic<<endl;

    //读取图片
    src = imread(pic);

    //判断图片是否存在
    if(src.empty())
    {
        cout<<"pic is not exist!!!!"<<endl;
        return -1;
    }

    //显示图片
    namedWindow("src pic",WINDOW_AUTOSIZE);
    imshow("src pic",src);

    Mat gray_src,binaryimg;

    //转化为灰度图
    cvtColor(src,gray_src,COLOR_BGR2GRAY);


    //二值化
    threshold(gray_src,binaryimg,0,255,THRESH_BINARY | THRESH_OTSU);
    imshow("binary",binaryimg);

    //形态学操作  消除噪点 防止小的尖峰影响分水岭检测
    Mat kernel = getStructuringElement(MORPH_RECT,Size(3,3));
    morphologyEx(binaryimg,binaryimg,MORPH_OPEN,kernel);

    //距离变换  寻找水坝基础
    Mat dist;
    distanceTransform(binaryimg,dist,DIST_L2,3,CV_32F);
    normalize(dist,dist,0,1,NORM_MINMAX);
    imshow("dist",dist);

    //绘制轮廓
    threshold(dist,dist,0.1,1,THRESH_BINARY);  //水坝定位
    imshow("distimg",dist);

    //准备marks roi的轮廓信息 也就是分水岭的水坝
    dist.convertTo(dist,CV_8UC1);
    vector<vector<Point>> contours;
    vector<Vec4i> hireachy;
    findContours(dist,contours,hireachy,RETR_TREE,CHAIN_APPROX_SIMPLE);
    if(contours.empty())
        return -1;

    //创建maker
    Mat markers = Mat::zeros(src.size(),CV_32S);
    markers = Scalar::all(0);
    for(size_t i=0;i<contours.size();i++)
    {
        drawContours(markers,contours,i,Scalar(i+1),-1,8,hireachy,INTER_MAX); //绘制轮廓 每条轮廓颜色一定一定要区分开
    }

    circle(markers,Point(5,5),3,Scalar(255),-1);  //mark做一个小标记

    int index = 0;
    //打印轮廓数据 有值的均为轮廓线
    for(int row=0;row<markers.rows;row++)
        for(int col=0;col<markers.cols;col++)
        {
            index = markers.at<int>(row,col);
            cout << index <<",";
        }
    //进行分水岭变换
    watershed(src,markers);
    int  num_segments = contours.size();   //获取分割区块数

    vector<Vec3b> colors;   //准备着色候选
    for(int i=0;i<num_segments;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 dst = Mat::zeros(markers.size(),CV_8UC3);

    for(int row=0;row<markers.rows;row++)
        for(int col=0;col<markers.cols;col++)
        {
            index = markers.at<int>(row,col);
            //打印区域数据  我们可以看到markers的数据已经变为块状数据 值为-1的为块状的边界线
            cout << index <<",";
            if(index>0 && index <=num_segments)
            {
                dst.at<Vec3b>(row,col) = colors[index-1];
            }else
            {
                dst.at<Vec3b>(row,col) = Vec3b(0,0,0);
            }
        }
    imshow("分水岭分割演示",dst);

    waitKey(0);
    destroyAllWindows();
    return 0;
}

效果

在这里插入图片描述


分水岭运算之前 的markers数据

可以看到数据是每条不同颜色的轮廓线

在这里插入图片描述


分水岭运算之后的markers数据

数据是以-1为边界的,块状索引数据,按这些数据可以对各个块进行着色处理
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值