meanShfit均值漂移算法是一种通用的聚类算法,它的基本原理是:对于给定的一定数量样本,任选其中一个样本,以该样本为中心点划定一个圆形区域,求取该圆形区域内样本的质心,即密度最大处的点,再以该点为中心继续执行上述迭代过程,直至最终收敛。
可以利用均值偏移算法的这个特性,实现彩色图像分割,Opencv中对应的函数是pyrMeanShiftFiltering。这个函数严格来说并不是图像的分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域,所以在Opencv中它的后缀是滤波“Filter”,而不是分割“segment”。先列一下这个函数,再说一下它“分割”彩色图像的实现过程。
//第一个参数src,输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等Opencv中的彩色图像格式均可;
//第二个参数dst,输出图像,跟输入src有同样的大小和数据格式;
//第三个参数sp,定义的漂移物理空间半径大小;
//第四个参数sr,定义的漂移色彩空间半径大小;
//第五个参数maxLevel,定义金字塔的最大层数;
//第六个参数termcrit,定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合;
void pyrMeanShiftFiltering( InputArray src, OutputArray dst,
double sp, double sr, int maxLevel=1,
TermCriteria termcrit=TermCriteria(
TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );
pyrMeanShiftFiltering函数的执行过程是这样的:
以输入图像上src上任一点P0为圆心,建立物理空间上半径为sp,色彩空间上半径为sr的球形空间,物理空间上坐标2个—x、y,色彩空间上坐标3个—R、G、B(或HSV),构成一个5维的空间球体。
其中物理空间的范围x和y是图像的长和宽,色彩空间的范围R、G、B分别是0~255。
在1中构建的球形空间中,求得所有点相对于中心点的色彩向量之和后,移动迭代空间的中心点到该向量的终点,并再次计算该球形空间中所有点的向量之和,如此迭代,直到在最后一个空间球体中所求得的向量和的终点就是该空间球体的中心点Pn,迭代结束。
更新输出图像dst上对应的初始原点P0的色彩值为本轮迭代的终点Pn的色彩值,如此完成一个点的色彩均值漂移。
对输入图像src上其他点,依次执行步骤1,、2、3,遍历完所有点位后,整个均值偏移色彩滤波完成,这里忽略对金字塔的讨论。
在这个过程中,关键参数是sp和sr的设置,二者设置的值越大,对图像色彩的平滑效果越明显,同时函数耗时也越多。
范例代码:
int main()
{
//读入图像
cv::Mat src = imread("1.jpg",IMREAD_GRAYSCALE);
cv::Mat src_RGB = imread("1.jpg");
int dep = src.channels();
if (!src.data)
{
return 0;
}
cv::imshow("src", src);
cv::Mat bin_Src = cv::Mat::zeros(src.size(), CV_8UC1);
cv::threshold(src, bin_Src, 50, 255, cv::THRESH_BINARY);
cv::imshow("原图-->二值化", bin_Src);
cv::Mat filledContourImg = cv::Mat::zeros(src_RGB.size(), src_RGB.type());
cv::Mat src_roi_mask = cv::Mat::zeros(src.size(), CV_8UC1);
//检测所有的轮廓,包括外围轮廓,去除面积过小的轮廓,findContour要求输入单通道图像
vector<cv::Vec4i> vHierarchy;
vector<vector<cv::Point>> vContours;
vHierarchy.clear();
findContours(bin_Src, vContours, vHierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE, cv::Point(0, 0));
for (int i = 0; i < vContours.size(); i++)
{
float tmpContouArea = fabs(contourArea(vContours[i], true));
if (tmpContouArea > 1000*1.0)
{
drawContours(filledContourImg, vContours, i, cv::Scalar(0, 0, 255),1 /*cv::FILLED*/);
drawContours(src_roi_mask, vContours, i, cv::Scalar(255, 255, 255), cv::FILLED);
}
}
cv::imshow("筛选出的轮廓", filledContourImg);
//因为pyrMeanShiftFiltering要求输入三通道
//利用mask抠图
cv::Mat roiSrc;
src_RGB.copyTo(roiSrc, src_roi_mask);
cv::imshow("用于均值漂移的原图ROI部分", roiSrc);
cv::Mat res = cv::Mat::zeros(src.size(), src.type());
double sp = 20;
double sr = 20;
int maxLevel = 2;
/********************************
src 输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等OpenCV中的彩色图像格式均可;
dst 输出图像,跟输入src有同样的大小和数据格式;
sp 定义的漂移物理空间半径大小;
sr 定义的漂移色彩空间半径大小;
maxLevel 定义金字塔的最大层数;
termcrit 定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合;
*/
cv::pyrMeanShiftFiltering(roiSrc,
res,
sp,
sr,
maxLevel);
cv::imshow("after meanshift dst", res);
RNG rng = theRNG();
Mat mask(res.rows + 2, res.cols + 2, CV_8UC1, Scalar::all(0)); //掩模,初始全0
int clusterCnt = 0;
Scalar newVal(0, 0, 0);
Scalar last_newVal(0, 0, 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,表示已经经过填充,不再处理
{
if (last_newVal != Scalar(rng(256), rng(256), rng(256)))
{
newVal = Scalar(rng(256), rng(256), rng(256));
last_newVal = newVal;
clusterCnt++;
}
/* CV_EXPORTS_W int floodFill(InputOutputArray image, InputOutputArray mask,
Point seedPoint, Scalar newVal, CV_OUT Rect* rect = 0,
Scalar loDiff = Scalar(), Scalar upDiff = Scalar(),
int flags = 4);*/
floodFill(res, mask, Point(x, y), newVal, 0, Scalar::all(10), Scalar::all(10)); //执行漫水填充
}
}
}
/*
InputOutputArray img, const String& text, Point org,
int fontFace, double fontScale, Scalar color,
int thickness = 1, int lineType = LINE_8,
bool bottomLeftOrigin = false
*/
std::string text = std::string("Total ")+ std::to_string(clusterCnt - 1) + std::string(" clusters");
Scalar tmpVal(0, 0, 255);
cv::putText(res, text, Point2i(res.cols/8, res.rows / 2), 2, 2, tmpVal, 2);
imshow("meanShift图像分割", res);
waitKey();
getchar();
return 0;
}