meanShfit均值漂移算法是一种通用的聚类算法,它的基本原理是:对于给定的一定数量样本,任选其中一个样本,以该样本为中心点划定一个圆形区域,求取该圆形区域内样本的质心,即密度最大处的点,再以该点为中心继续执行上述迭代过程,直至最终收敛。
可以利用均值偏移算法的这个特性,实现彩色图像分割,Opencv中对应的函数是pyrMeanShiftFiltering。这个函数严格来说并不是图像的分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域,所以在Opencv中它的后缀是滤波“Filter”,而不是分割“segment”。先列一下这个函数,再说一下它“分割”彩色图像的实现过程。
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,定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合;
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的设置,二者设置的值越大,对图像色彩的平滑效果越明显,同时函数耗时也越多。以下对一幅图像执行pyrMeanShiftFiltering操作,来看一下效果:
原始图像:
物理空间半径sp=10,色彩空间半径sr=10时色彩滤波效果:
物理空间半径sp=50,色彩空间半径sr=50时色彩滤波效果:
对比可以发现,半径为10时,图像色彩细节大部分存在,半径为50时,山体和草地的色彩细节基本都已经丢失。
到这里,meanShift均值偏移算法对彩色图像的平滑操作就完成了,为了达到分割的目的,需要借助另外一个漫水填充函数的进一步处理来实现,那就是floodFill:
int floodFill( InputOutputArray image, InputOutputArray mask,
Point seedPoint, Scalar newVal, CV_OUT Rect* rect=0,
Scalar loDiff=Scalar(), Scalar upDiff=Scalar(),
int flags=4 );
第一个参数image,输入三通道8bit彩色图像,同时作为输出。
第二个参数mask,是掩模图像,它的大小是输入图像的长宽左右各加1个像素,mask一方面作为输入的掩模图像,另一方面也会在填充的过程中不断被更新。floodFill漫水填充的过程并不会填充mask上灰度值不为0的像素点,所以可以使用一个图像边缘检测的输出作为mask,这样填充就不会填充或越过边缘轮廓。mask在填充的过程中被更新的过程是这样的:每当一个原始图上一个点位(x,y)被填充之后,该点位置对应的mask上的点(x+1,y+1)的灰度值随机被设置为1(原本该点的灰度值为0),代表该点已经被填充处理过。
第三个参数seedPoint,是漫水填充的起始种子点。
第四个参数newVal,被充填的色彩值。
第五个参数rect,可选的参数,用于设置floodFill函数将要重绘区域的最小矩形区域;
第六个参数loDiff和第七个参数upDiff,用于定义跟种子点相比色彩的下限值和上限值,介于种子点减去loDiff和种子点加上upDiff的值会被填充为跟种子点同样的颜色。
第八个参数,定义漫水填充的模式,用于连通性、扩展方向等的定义。
这里边好玩的参数是掩模mask,一个像素值全为0的mask在运算过程中,随着填充过程,mask上像素不断被置为1,代表当前位置被充填过了,仍以上图为例,看一下mask的变化:
这个是初始输入的值全为0的mask:
这个是漫水填充执行完后的mask:
OMG!怎么都是两个黑乎乎的图像,并且也没啥变化啊!难道是Opencv欺骗了我们?确实是有人欺骗了我们,这个人不是Opencv而是上帝~~,执行漫水填充后,mask上的值从原本全是0变成了全是1,置1就代表了该点被漫水填充执行过,之所以我们看不出来,是因为人眼对灰阶为1的亮度和灰阶为0的亮度完全区分不出来。
为了显示mask内像素值的变化过程,我们对mask执行0~255的归一化。这个是左上角第一个像素执行完漫水填充后归一化的mask显示:
左上角是天空的开始的图像上半部分是蓝色天空的颜色,占据了很大部分空间,都被填充成一个颜色,对应的在mask上这些区域的灰度值被从全黑的0置为了1。
中间过程1,这时候两朵云所在的区域也被填充:
中间过程2,这时候已经接近填充完成,除了底部,其余区域都被置为了1:
Opencv中自带例程文件meanshift_segmentation.cpp,位置在\opencv\sources\samples\cpp中,其中有一行代码是有关mask掩模的:
if( mask.at<uchar>(y+1, x+1) == 0 ) //非0处即为1,表示已经经过填充,不再处理
{
Scalar newVal( rng(256), rng(256), rng(256) );
floodFill( res, mask, Point(x,y), newVal, 0, Scalar::all(5), Scalar::all(5) ); //执行漫水填充
}
这里在填充过程中对mask的值做了判断,不为0表示已经填充过,不再重复填充,这就是对mask的巧妙运用,可以特高运算效率。当然仅仅是基于效率的考虑,如果不加这个判定条件,重复填充,最终的结果是一样的。
以下Opencv代码是对原有自带例程的简化,代码量较少,核心操作只有两个函数,但需要对这两个操作的逻辑阐述清楚,这就是以上啰嗦了这么多所试图做的事情。
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
int main(int argc, char** argv)
{
Mat img = imread( argv[1] ); //读入图像,RGB三通道
imshow("原图像",img);
Mat res; //分割后图像
int spatialRad = 50; //空间窗口大小
int colorRad = 50; //色彩窗口大小
int maxPyrLevel = 2; //金字塔层数
pyrMeanShiftFiltering( img, res, spatialRad, colorRad, maxPyrLevel); //色彩聚类平滑滤波
imshow("res",res);
RNG rng = theRNG();
Mat mask( res.rows+2, res.cols+2, CV_8UC1, Scalar::all(0) ); //掩模
for( int y = 0; y < res.rows; y++ )
{
for( int x = 0; x < res.cols; x++ )
{
if( mask.at<uchar>(y+1, x+1) == 0 ) //非0处即为1,表示已经经过填充,不再处理
{
Scalar newVal( rng(256), rng(256), rng(256) );
floodFill( res, mask, Point(x,y), newVal, 0, Scalar::all(5), Scalar::all(5) ); //执行漫水填充
}
}
}
imshow("meanShift图像分割", res );
waitKey();
return 0;
}
经pyrMeanShiftFiltering处理过色彩平滑滤波后的结果:
进一步进过floodFill漫水填充后的最终分割效果: