前言

图像分割作为图像识别的基础,在图像处理中占有重要地位,通常需要在进行图像分割算法前找到轮廓或分割线,因此传统的分割算法主要集中在边缘检测、阈值处理等。

分水岭算法

封闭性是分水岭算法的一个重要特征。其他图像分割方法,如阈值,边缘检测等都不会考虑像素在空间关系上的相似性和封闭性这一概念,彼此像素间互相独立,没有统一性。分水岭算法较其他分割方法更具有思想性,更符合人眼对图像的印象。

opencv 双阈值分割 opencv的分割算法设计_分水岭算法

在上面的水岭算法示意图中局部极小值、积水盆地,分水岭线以及水坝的概念可以描述为:

  1.区域极小值:导数为0的点,局部范围内的最小值点;

  2.集水盆(汇水盆地):当“水”落到汇水盆地时,“水”会自然而然地流到汇水盆地中的区域极小值点处。每一个汇水盆地中有且仅有一个区域极小值点;

  3.分水岭:当“水”处于分水岭的位置时,会等概率地流向多个与它相邻的汇水盆地中;

  4.水坝:人为修建的分水岭,防止相邻汇水盆地之间的“水”互相交汇影响。

OpenCV中的watershed可以实现分水岭的功能,函数原型如下:

void watershed( InputArray image, InputOutputArray markers );
注意:第一个参数 image,必须是一个8bit 3通道彩色图像矩阵序列,可以将单通道图像进行扩充,比如OpenCV函数内置CV_GRAY2BGR函数可以将单通道图像扩展成三通道图像。

它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现,这个是执行分水岭之前的要求。算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。

每一个线条代表了一个种子,线条的不同灰度值其实代表了对不同注水种子的编号,有多少不同灰度值的线条,就有多少个种子,图像最后分割后就有多少个区域。

  传入watershed中的makers图像如下图所示:


opencv 双阈值分割 opencv的分割算法设计_OpenCV_02

 findcountours后寻找的轮廓如下图所示:

opencv 双阈值分割 opencv的分割算法设计_OpenCV_03

  从上面两幅图中我们从图像底部往上,线条的灰度值是越来越高的,并且merkers图像底部部分线条的灰度值由于太低,已经观察不到了,但相互连接在一起的线条灰度值是一样的。

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

using namespace cv;
using namespace std;

Vec3b RandomColor(int value);  //生成随机颜色函数

int main( int argc, char* argv[] )
{
    Mat image=imread(argv[1]);    //载入RGB彩色图像
    imshow("Source Image",image);

    //灰度化,滤波,Canny边缘检测
    Mat imageGray;
    cvtColor(image,imageGray,CV_RGB2GRAY);//灰度转换
    GaussianBlur(imageGray,imageGray,Size(5,5),2);   //高斯滤波
    imshow("Gray Image",imageGray);
    Canny(imageGray,imageGray,80,150);
    imshow("Canny Image",imageGray);

    //查找轮廓
    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(imageGray,contours,hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE,Point());
    Mat imageContours=Mat::zeros(image.size(),CV_8UC1);  //轮廓
    Mat marks(image.size(),CV_32S);   //Opencv分水岭第二个矩阵参数
    marks=Scalar::all(0);
    int index = 0;
    int compCount = 0;
    for( ; index >= 0; index = hierarchy[index][0], compCount++ )
    {
        //对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
        drawContours(marks, contours, index, Scalar::all(compCount+1), 1, 8, hierarchy);
        drawContours(imageContours,contours,index,Scalar(255),1,8,hierarchy);
    }

    //我们来看一下传入的矩阵marks里是什么东西
    Mat marksShows;
    convertScaleAbs(marks,marksShows);
    imshow("marksShow",marksShows);
    imshow("轮廓",imageContours);
    watershed(image,marks);

    //我们再来看一下分水岭算法之后的矩阵marks里是什么东西
    Mat afterWatershed;
    convertScaleAbs(marks,afterWatershed);
    imshow("After Watershed",afterWatershed);

    //对每一个区域进行颜色填充
    Mat PerspectiveImage=Mat::zeros(image.size(),CV_8UC3);
    for(int i=0;i<marks.rows;i++)
    {
        for(int j=0;j<marks.cols;j++)
        {
            int index=marks.at<int>(i,j);
            if(marks.at<int>(i,j)==-1)
            {
                PerspectiveImage.at<Vec3b>(i,j)=Vec3b(255,255,255);
            }
            else
            {
                PerspectiveImage.at<Vec3b>(i,j) =RandomColor(index);
            }
        }
    }
    imshow("After ColorFill",PerspectiveImage);

    //分割并填充颜色的结果跟原始图像融合
    Mat wshed;
    addWeighted(image,0.4,PerspectiveImage,0.6,0,wshed);
    imshow("AddWeighted Image",wshed);

    waitKey();
}

Vec3b RandomColor(int value)    <span style="line-height: 20.8px; font-family: sans-serif;">//生成随机颜色函数</span>
{
    value=value%255;  //生成0~255的随机数
    RNG rng;
    int aa=rng.uniform(0,value);
    int bb=rng.uniform(0,value);
    int cc=rng.uniform(0,value);
    return Vec3b(aa,bb,cc);
}

按照分水岭算法对下图进行分割:

opencv 双阈值分割 opencv的分割算法设计_距离变换_04

  对上图进行分水岭分割后发现结果并不好:

opencv 双阈值分割 opencv的分割算法设计_分水岭算法_05


  原因是很多目标物体距离太近,findcountours后导致无法分离,分水岭的注水点太少,因此应该先将目标物体分离,可以考虑腐蚀等形态学操作,这里采用距离变换来细化目标物体的大小,从而分离目标,增加分水岭注水点。

距离变换/distanceTransform函数

每一个非零点距离离自己最近的零点的距离,distanceTransform的第二个Mat矩阵参数dst保存了每一个点与最近的零点的距离信息,图像上越亮的点,代表了离零点的距离越远。可以根据距离变换的这个性质,经过简单的运算,用于细化字符的轮廓。

Mat imageThin(imageGray.size(),CV_32FC1); //定义保存距离变换结果的Mat矩阵
distanceTransform(imageGray,imageThin,CV_DIST_L2,3);  //距离变换
Mat distShow;
distShow=Mat::zeros(imageGray.size(),CV_8UC1); //定义细化后的字符轮廓
for(int i=0;i<imageThin.rows;i++)
{
    for(int j=0;j<imageThin.cols;j++)
    {
        if(imageThin.at<float>(i,j)>maxValue)
        {
            maxValue=imageThin.at<float>(i,j);  //获取距离变换的极大值
        }
    }
}
for(int i=0;i<imageThin.rows;i++)
{
    for(int j=0;j<imageThin.cols;j++)
    {
        if(imageThin.at<float>(i,j)>maxValue/1.9)
        {
            distShow.at<uchar>(i,j)=255;   //符合距离大于最大值一定比例条件的点设为255
        }
    }
}

 对毛毡柱的灰度图进行距离变换,结果如下图:

opencv 双阈值分割 opencv的分割算法设计_距离变换_06

对距离变换之后的图像进行findcontours,距离变换,结果如下图:

opencv 双阈值分割 opencv的分割算法设计_分水岭算法_07

  结果显示,基于标记的分水岭比传统分水岭的分割效果要好很多。