文章目录

  • 一、原理
  • 二、程序实现
  • 三、结果展示
  • 四、API说明


一、原理

当洪水淹没所有的山头的时候,只露出山顶,这些山顶相当于marker。当洪水退去的时候,水位慢慢的下降,下降到刚好将山头都分开的山谷,这个时候就是刚好将所有山头分开的山谷。这就是分水岭分割方法。

  • 基于浸泡理论的分水岭分割方法
  • 基于连通图的方法
  • 基于距离变换的方法
二、程序实现

基本步骤是:输入图像 -> 灰度 -> 二值图像 -> 距离变换 -> 寻找种子 -> 生成marker -> 分水岭变换 -> 输出图像

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
using namespace ml;

int main()
{
	Mat src = imread("D:/source/images/coins.jpg");
	if (src.empty())
	{
		printf("read image error\n");
		system("pause");
		return -1;
	}
	imshow("src", src);

	Mat gray, binary, shiffted;
	// 保留边缘的平滑滤波
	pyrMeanShiftFiltering(src, shiffted, 21, 51);
	imshow("shiffted", shiffted);

	// 二值化
	cvtColor(shiffted, binary, COLOR_BGR2GRAY);
	threshold(binary, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("binary", binary);

	// 距离变换
	Mat dist;
	distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
	// 归一化
	normalize(dist, dist, 0, 1, NORM_MINMAX);
	imshow("dist", dist);

	// binary 得到山头,寻找种子
	threshold(dist, dist, 0.4, 1, THRESH_BINARY);
	imshow("dist", dist);

	// markers标记山头
	Mat dist_m;
	dist.convertTo(dist_m, CV_8U);
	// 寻找轮廓
	vector<vector<Point>> contours;
	findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	// create markers
	Mat markers = Mat::zeros(src.size(), CV_32SC1);
	for (size_t i = 0; i < contours.size(); i++)
	{
		drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 2), -1); // 填充
	}
	// imshow("markers", markers * 10000);

	// 形态学腐蚀操作 - 彩色图像,目的是去掉干扰,让结果更好
	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
	morphologyEx(src, src, MORPH_ERODE, k);

	// 完成分水岭变换
	watershed(src, markers);
	imshow("watershed", markers*10000);

	Mat mark = Mat::zeros(markers.size(), CV_8UC1);
	markers.convertTo(mark, CV_8UC1);
	bitwise_not(mark, mark, Mat());
	
	imshow("完成分水岭变换", mark);

	// 生成随机颜色
	vector<Vec3b> colors;
	for (size_t i = 0; i < contours.size(); 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);
	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);

			if (index > 0 && index <= contours.size())
				dst.at<Vec3b>(row, col) = colors[index - 1];
			else
				dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
		}
	}

	printf("number of objects: %d\n", contours.size());
	imshow("Final Result", dst);

	waitKey(0);
	return 0;
}
三、结果展示

opencv 提取 连通域 opencv连通域分割_迭代


opencv 提取 连通域 opencv连通域分割_opencv_02

四、API说明

distanceTransform()

功能:用来计算原图像中距离变换图像;

void distanceTransform( InputArray src,  
                    OutputArray dst,
                    OutputArray labels,
                    int distanceType,
                    int maskSize,
                    int labelType=DIST_LABEL_CCOMP );

函数说明:
用于计算图像中每一个非零点像素与其最近的零点像素之间的距离,输出的是保存每一个非零点与最近零点的距离信息;图像上越亮的点,代表了离零点的距离越远。
参数:
(1)src 是单通道的8bit的二值图像(只有0或1)。
(2)dst 表示的是计算距离的输出图像,可以使单通道32bit浮点数据。
(3)labels 表示可选输出2维数组;
(4)distanceType 表示的是选取距离的类型,可以设置为CV_DIST_L1,CV_DIST_L2,CV_DIST_C等,具体如下:

DIST_L1       = 1,   //!< distance = |x1-x2| + |y1-y2| 
  DIST_L2       = 2,   //!< the simple euclidean distance 
  DIST_C        = 3,   //!< distance = max(|x1-x2|,|y1-y2|) 
  DIST_L12      = 4,   //!< L1-L2 metric: distance =2(sqrt(1+x*x/2) - 1)) 
  DIST_FAIR     = 5,   //!< distance = c^2(|x|/c-log(1+|x|/c)),c = 1.3998 
  DIST_WELSCH = 6,  //!< distance = c^2/2(1-exp(-(x/c)^2)), c= 2.9846 
  DIST_HUBER  = 7   //!< distance = |x|<c ? x^2/2 :c(|x|-c/2), c=1.345

(6)maskSize 表示的是距离变换的掩膜模板,可以设置为3,5或CV_DIST_MASK_PRECISE,对 CV_DIST_L1 或CV_DIST_C 的情况,参数值被强制设定为 3, 因为3×3 mask 给出5×5 mask 一样的结果,而且速度还更快。
(7)labelType 表示的是输出二维数组的类型;

watershed()

void watershed( InputArray image, InputOutputArray markers )

1)InputArray类型的src,输入图像,填Mat类的对象即可,且需为8位三通道的彩色图像;
2)InputOutputArray类型的markers,函数调用后的元算结果存在这里,输入/输出32位单通道图像的标记结果。即这个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型;

pyrMeanShiftFiltering()
一种边缘保留的滤波方法。
我们先借助Mean Shift算法的分割特性将灰度值相近的元素进行聚类,然后,在此基础上应用阈值分割算法,达到将图像与背景分离的目的。 简单来说,基于Mean Shift的图像分割过程就是首先利用Mean Shift算法对图像中的像素进行聚类,即把收敛到同一点的起始点归为一类,然后把这一类的标号赋给这些起始点,同时把包含像素点太少的类去掉。然后,采用阈值化分割的方法对图像进行二值化处理 基于Mean Shift的图像分割算法将图像中灰度值相近的像素点聚类为一个灰度级,因此,经过Mean Shift算法分割后的图像中的灰度级较该算处理有所减少。

该函数严格意义上并不是分割,而是在色彩层面的平滑滤波,中和与色彩分布相近的颜色,平滑掉细节色彩,侵蚀掉面积较小的颜色区域。函数的输出是一个“色调分离”的新图像,意味着去除了精细纹理、颜色梯度大部分变得平坦。如果最终需要图像分割,就可以cv::Canny()结合cv::findContours()进一步细分此类图像。

可以利用均值偏移算法的这个特性,实现彩色图像分割,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) 
);

参数说明:输入输出必须为8位三通道彩色图像,高宽相同;搜索窗口半径sr和颜色窗口搜索半径sp,对于640*480图像,sr=2, sp=40,max_level=2或3时,效果最好。

(1)第一个参数src,输入图像,8位,三通道的彩色图像,并不要求必须是RGB格式,HSV、YUV等Opencv中的彩色图像格式均可;
(2)第二个参数dst,输出图像,跟输入src有同样的大小和数据格式;
(3)第三个参数sp,定义的漂移物理空间半径大小;
(4)第四个参数sr,定义的漂移色彩空间半径大小;
(5)第五个参数maxLevel,定义金字塔的最大层数;
(6)第六个参数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,遍历完所有点位后,整个均值偏移色彩滤波完成,这里忽略对金字塔的讨论。 (效果好,但计算耗时)