|
(四)用分水岭算法实现图像分割
分水岭变换,是一种流行的图像处理算法,用于快速将图像分割成多个同质区域。若把图像看成是一个拓扑地貌,同类区域相当于陡峭边缘内相对平坦的盆地;算法通过逐步增高水位,将其分割成多个部分。因原始版本算法会过度分割图像,产生很多小的区域,因此OpenCV中提出改进版本,使用一系列预定义标记来引导图像分割的方式。
4.1 实现
分水岭分割法,需要调用cv::watershed函数。输入对象是一个标记图像,图像的像素值为32位有符号整数,每个非零像素代表一个标签。其原理是对图像中部分图像做标记,表明它们所属区域是已知的。
分水岭算法,可根据这个初始化标签确定其他像素所属的区域。本节先建立一个标记图像作为灰度图像,后将其转换成整型图像。把该步骤封装进WatershedSegmenter类,包含指定标记图像和计算分水岭的方法:
class WatershedSegmenter
{
Private:
cv::Mat markers;
Public:
Void setMarkers(const cv::Mat& markerImage)
{
//转换成整数型图像
markerImage.converTo(markers,CV_32S);
}
cv::Mat process(const cv::Mat &image)
{
//应用分水岭
cv::watershed(image,markers);
return markers;
}
}
不同应用程序获得标记的方式各不相同。如,可在预处理过程中识别出一些属于某个感兴趣物体的像素,然后根据初始检测结果,使用分水岭算法划分出整个物体的边缘。如:从二值图像中识别出属于前景和属于背景的像素,前景像素标记为255,背景像素标记为128(数字随意选择,任何不等于255的数字都可以),其他像素的标签是未知的,标记为 0。
因二值图像包含属于图像不同部分的白色像素,因此对图像做深度腐蚀运算,只保留明显属于前景物体的像素:
//消除噪声和细小物体
cv::Mat fg;
cv::erode(binary,fg,cv::Mat(),cv::Point(-1,-1),4);
对原二值图像做一次大幅的膨胀运算,选中一些背景像素:
//标识不含物体的图像像素
cv::Mat bg;
cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),4);
cv::threshold(bg,bg,1,128,cv::THRESH_BINARY_INV);
得到的黑色像素对应背景像素。因此膨胀后,要立即通过阈值化运算把它们赋值为128得到图像。
合并前景和背景两幅图像,得到标记图像,代码:
//创建标记图像
cv::Mat markers(binary,size(),CV_8U,cv::Scalar(0));
markers=fg+bg;
合并后的图像,被作为分水岭算法的输入。白色区域属于前景,灰色区域属于背景,黑色区域带有未知标签。分水岭算法的作用,即明确划分前景和背景,并对黑色区域的像素做出标记(属于前景还是背景)。
分割图像的方法:
//创建分水岭分割类的对象
WatershedSegmenter segmenter;
//设置标记图像,然后执行分割过程
segmenter.setMarkers(markers);
segmenter.process(image);
上面的代码会修改标记图像,每个值为0的像素都会被赋予一个输入标签,而边缘处的像素被赋值为-1,得到标签图像和边缘图像。
4.2 原理
用分水岭算法分割图像的原理,是从高度0开始逐步用洪水淹没图像。需要从一组预先定义好的标记像素开始,每个用标记创建的盆地,都按照初始标记的值加上标签,如果两个标签相同的盆地汇合,就不创建分水岭,以免过度分割。调用cv::watershed函数即为执行该过程。
函数输入的标记图像会被修改,用以生成最终的分水岭分割图。输入的标记图像可以含有任意数值的标签,未知标签的像素值为0。标记图像的类型选用32位有符号整数,以便定义超过255个的标签。另,可把分水岭的对应像素设为特殊值-1。
为方便显示结果,采用两种特殊方法。
第一种:返回由标签组成的图像(包含值为0的分水岭)。该方法通过阈值化易实现,代码如示:
//以图像的形式返回结果
cv::Mat getSegmentation()
{
cv::Mat tmp;
//所有标签值大于255的区段都赋值为255
markers.convertTo(tmp,CV_8U);
return tmp;
}
第二种:返回一幅图像,图像中分水岭线条赋值为0,其他部分赋值为255。用cv::convertTo方法来获得结果,代码如下:
//以图像的形式返回分水岭
cv::Mat getWatersheds()
{
cv::Mat tmp;
//在变化前,把每个像素p转换为255p+255
markers.convertTo(tmp,CV_8U,255,255);
return tmp;
}
在变换前性转换,使值为-1的像素变为0(因为-1*255+255=0)。
值大于255的像素赋值为255。这是因为将有符号整数转化为无符号字符型时,应用了饱和度运算。
图像饱和度的调整方法有很多,最简单的就是判断每个像素的R、G、B值是否大于或小于128,大于加上调整值,小于则减去调整值;也可以将像素RGB转换为HSV或者HSL,然后调整其S部分,从而达到线性调整图像饱和度的目的。
4.3 拓展
标记图像方法有:第一种,用户可以交互地在场景中的物体和背景上绘制区域,以标注物体;第二种,当需要标识的物体位于图像中间时,可以简单地在输入图像中的中心位置标记特定标签,在图像的边缘位置(假设背景在边缘位置)标记上另一个标签。在创建标记图像时,可以在标记图像上绘制加粗的矩形:
//标识背景像素
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);
如果把这个标记图像叠加到实验图像上,得到其结果和分水岭的图像。
|