open cv基于距离变换与分水岭的图像分割

什么是图像分割(Image Segmentation)

图像分割是图像处理最重要的处理手段之一

图像分割的目标是将图像中像素根据一定的规则分为若干(N)个cluster(集群)集合
每个集合包含一类像素,根据算法分为监督学习和无监督学习方法,图像分割的
算法多数都是无监督学习方法—Kmeans

应用领域:如果应用到单幅图像上则是主要为图像窄化 骨架提取 粘连物体的分离等
如果应用到原图跟模板图则是应用于图像匹配

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

OpenCV中的watershed函数实现的分水岭算法是基于“标记”的分割算法,用于解决传统的分水岭算法过度分割的问题


距离变换与分水岭介绍

距离变换常见算法有两种
不断膨胀/腐蚀得到
基于倒角距离

分水岭常见的算法
基于浸泡理论实现


距离变换APIcv::distanceTransform
distanceTransform
(
InputArray src,//输入图像
OutputArray dst,//输出8位或者32位的浮点数,单一通道,大小与输入图像一致
OutputArray labels,//离散维诺图输出 (相同距离的分为同一个labels)
int distanceType,//distanceType=DIST_L1/DIST_L2 (距离变换的类型 曼哈顿和欧几里得)
int maskSize, //maskSize=3
3,也支持5
5,推荐3
3
int labelType=DIST_LABEL_CCOMP
)***


分水岭APIcv::watershed
watershed(
InputArray image,//输入图像
InputOutputArray markers//既做为输入也做为输出,其为具有一个个小山头的图像
)


处理流程
1.将白色背景变成黑色目的是为后面的变换做准备
2.使用ilter2D与拉普拉斯算子实现图像对比度提高, sharp
3.转为二值图像通过threshold
4.距离变换
5.对距离变换结果进行归一化到0~1之间
6.使用阈值,再次二值化, 得到标记(山头高低和让连在一起的山头分开)
7.腐蚀得到每个Peak(山峰)-erode(侵蚀)
8.发现轮廓- findContours
9.绘制轮廓- drawContours
10.分水岭变换watershed
11.对每个分割区域着色输出结果

#include 
#include 
#include 
#include 

using namespace cv;
using namespace std;

Mat src, dst;
int Osize = 0;
void DTwatershed(int, void*);

int main()
{

	src = imread("D:/实验台/机器视觉/测试图片/图像分割2.jpg");
	if (src.empty())//如果src这个数据库属性为空
	{
		cout << "无法打开" << endl;
		return -1;
	}
	//imshow("原图", src);

    //namedWindow("去背景的原图", CV_WINDOW_AUTOSIZE);
	//createTrackbar("各值调节", "去背景的原图", &Osize, 1, DTwatershed);
	DTwatershed(0, 0);
	waitKey(0);
	return 0;
}

//去背景实验
/*void DTwatershed(int, void*)
{
	Mat test;
	src.copyTo(test);
	imshow("test", test);

	//去背景
	//将白色背景变成黑色,目的是为后面的变换做准备(要确保输入的图片背景不能失真)
	for (int row = 0; row < src.rows; row++)
	{
		for (int col = 0; col < src.cols; col++)
		{
		 // if (src.at(row, col)[0] >= 250 && src.at(row, col)[1] >= 250 && src.at(row, col)[2] >= 250)
			if (src.at(row, col) == Vec3b(255, 255, 255))
			{
				src.at(row, col)[0] = 0;
				src.at(row, col)[1] = 0;
				src.at(row, col)[2] = 0;
			}
		}
	}
	imshow("去背景的原图", src);
}*/


void DTwatershed(int, void*)
{
	//去背景(为后期的距离变换做准备)
	//将白色背景变成黑色,目的是为后面的变换做准备(要确保输入的图片背景不能失真)
	for (int row = 0; row < src.rows; row++)
	{
		for (int col = 0; col < src.cols; col++)
		{
			//当图像在传输过程中发生图像畸变导致图像应为白的色域混入杂色使用以下判断语句
	        // if (src.at(row, col)[0] >= 250 && src.at(row, col)[1] >= 250 && src.at(row, col)[2] >= 250)
			
			//当图像白色区域分布均匀 边缘没有杂色的判断语句
			if (src.at<Vec3b>(row, col) == Vec3b(255, 255, 255))
			{
				src.at<Vec3b>(row, col)[0] = 0;
				src.at<Vec3b>(row, col)[1] = 0;
				src.at<Vec3b>(row, col)[2] = 0;
			}
		}
	}
	//图像赋值操作 可直接把原图的矩阵赋值给目标矩阵 
	Mat sb = src;
	//imshow("去背景的原图", sb);

	/*
	Mat srcGray;
	cvtColor(src, srcGray, CV_BGR2GRAY);
	imshow("src灰度处理 实验用", srcGray);*/

	//使用拉普拉斯算子与filter2D实现图像对比度提高并提取边缘-sharp
	Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);//边缘检测卷积模板
	Mat Lapimg;
	Mat sharpimg = src;//把src的矩阵信息复制给sharpimg
	filter2D(src, Lapimg, CV_32F, kernel, Point(-1, -1), 0, BORDER_DEFAULT);
	//以32F浮点型深度进行 拉普拉斯算子滤波 CV32F是因为输出的深度要大于输入的深度()
	//CV_32F是 float -像素是在0-1.0之间的任意值(直接显示CV32F类型的图像为二值图像 0*255或1*255),这对于一些数据集的计算很有用
	//但是它必须通过将每个像素乘以255来转换成8位(CV8UC1)来正常显示 CV_32F图像虽然看起来是黑白两色 但是并没有将图像二值化 图像的RGB像素值和通道数是不变的
	sharpimg.convertTo(sharpimg, CV_32F);
//	imshow("复制原图矩阵的sharpimg", sharpimg);
	//imshow("边缘检测结果 Lapimg  CV_32F 类型", Lapimg);
	Mat resultimg = sharpimg-Lapimg;//将转换为CV_32F的去背景原图像素值减去检测出的图像边缘像素值 导致原图边缘部分的像素值减少达到了显示边缘的效果
	//imshow("锐化效果  resultimg 32F类型", resultimg);

	resultimg.convertTo(resultimg, CV_8UC3);
	Lapimg.convertTo(Lapimg, CV_8UC3);
//	imshow("像素减操作后 边缘检测结果 Lapimg CV8UC3 类型", Lapimg);
	//imshow("锐化效果 resultimg CV8UC3类型", resultimg);
	

	src = resultimg;
	//通过threshold二值化后进行距离变换(距离变换需要输入图像为二值化图像)
	Mat binimg;
	cvtColor(src, resultimg, CV_BGR2GRAY);
	threshold(resultimg, binimg, 40, 255, THRESH_BINARY|THRESH_OTSU);
	//imshow("二值图像", binimg);
	
	//距离变化API详解https://blog.csdn.net/liubing8609/article/details/78483667
	//进行距离变换(求每个像素点到轮廓边缘像素的最短距离 目的是提取每个图像的中心区域为粘连图像分割做准备)
	Mat distimg;
	distanceTransform(binimg, distimg, DIST_L1, 3, 5);
	//距离变换
	//src-输入图像(8位单通道(二值化)图片)
	//dst-输出距离图像矩阵(8位整型或32位浮点型单通道图像 保存了每一个点与最近的零点的距离信息,图像上越亮的点,代表了离零点的距离越远)
	//distanceType-距离变化类型(取值有DIST   _USER ,_L1,_L2,_C,_L12,_FAIR,_WELSCH,_HUBER 8种类型 )
	//maskSize-距离变换-掩膜大小(取值有 DIST_MASK_3,_5,对 CV_DIST_L1 或 CV_DIST_C 的情况,参数值被强制设定为 3, 因为 3×3 mask 给出 5×5 mask 一样的结果,而且速度还更快)
	//dstType:输出图像(矩阵)的类型,可以是CV_8U 或 CV_32F,CV_8U只能用在第一个原型中,而且distanceType只能为CV_DIST_L1(此原型中只有一个值为5)

	//以下两个参数不需要使用
	//labels:输出二维阵列标签(这是啥意思?抱歉,我也不知道,等以后知道了再来补充)
	//labelType:标签数组类型。可选值为DIST_LABEL_CCOMP和DIST_LABEL_PIXEL,具体各是什么含义,我现在也不清楚,等以后清楚了再来补充。
	//我们通常使用的是这个函数的第一个原型,所以对于参数“labels”和“labelType”,我们可以暂时不管

	//注意:CV_DIST_C、CV_DIST_L1、CV_DIST_L2(maskSize=5)的计算结果是精确的,CV_DIST_L2(maskSize=3)是一个快速计算方法
	//使用完distanceTransform得到的resultImgtemp矩阵里面的数据都是浮点型(输出矩阵类型为CV_32F 如果不进行归一化处理 会得到的跟二值化图像一样的图)

	//由于输出的距离信息矩阵为CV_32F类型(32-bit floating-point 像素点值乘以255,即把[0,1] 映射到 [0,255])
	//所以对距离变换结果进行归一化到0-1之间进行距离变换图像的显示 (否则直接显示和二值化图像一致的图像)
	normalize(distimg, distimg, 0, 1, NORM_MINMAX);
	//imshow("距离变换的结果", distimg);
	
	//使用阈值,再次二值化,得到标记,它的目的是用来区分单独的扑克牌
	threshold(distimg, distimg, 0.4, 1, THRESH_BINARY);//过滤灰度值少于0.4的像素并输出二值化图像
	//imshow("距离变换后二值化的结果", distimg);

	//使用二值化后的图片腐蚀连在一起的二值化图块(为粘连物体的分离做第一步准备)
	Mat k1 = Mat::ones(13, 13, CV_8UC1);//建立腐蚀结构体(这个黑色结构体会在图像白色边缘区域进行腐蚀)
	//imshow("腐蚀结构体k1", k1);
	//腐蚀得到每个扑克牌的腐蚀图像,它的目的就是使经过二值化仍然连在一起的地方分开
	erode(distimg, distimg, k1, Point(-1, -1));
	imshow("二值化后腐蚀的效果", distimg);
	
	//预定义注水点标记来引导图像分割(分水岭算法对噪声等影响非常的敏感 由于噪声等干扰会过度分割 指定mark区域 可以得到很好的分段效果) 
	Mat dist8u;
	distimg.convertTo(dist8u, CV_8U);//不带通道数的类型,如:CV_32S,CV_8U等,这些类型就是默认通道数为1 例如,CV_8U就等同于CV_8UC1,CV_32S就等同于CV_32SC1
	vector<vector<Point>>contours;
	findContours(dist8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	//轮廓发现


	Mat markers = Mat::zeros(src.size(), CV_32SC1);//CV_32SC1(32位有符号整型单通道矩阵 在此版本不能直接显示)	//原因https://blog.csdn.net/qq_36534731/article/details/97036038
	//markers-分水岭算法cv::wathershed,需要输入一个标记图像,图像的像素值为32位有符号正数(CV_32S类型)
	
	for (size_t i = 0; i < contours.size(); i++)
	{
		drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 1), -1);
		//轮廓绘制(目标图片矩阵,输入轮廓点集合,轮廓序号,轮廓颜色,轮廓线宽)
		//markers-为了适配分水岭函数的输入矩阵类型 建立为与原图大小一致 深度类型为CV_32SC1的图像
		//static_cast(i)-强制转换轮廓序列号为int类型 (此案例转换不转换都没有影响)
		//Scalar::all-Scalar数型中所有元素(3个)设置为i+1
		//-1 -绘制轮廓线型为-1 将会填充封闭轮廓  
	}
	circle(markers, Point(5, 5),5, Scalar(255, 255, 255), -1);
	markers.convertTo(markers, CV_8UC1);//将不能直接显示的CV_32SC1类型的图形markers更改为8位单通道的灰度图片才能正确显示
	imshow("编号处理markers", markers*20);//再显示完标记山峰图像后 还需要把CV_8UC1变回CV_32SC1类型 便于分水岭处理
    //标记原理:每个非零像素代表一个标签 它的原理是对图像中部分像素做标记,表明它的所属区域是已知的 分水岭算法可以根据这个初始标签确定其他像素所属的区域
	
	/*
	Mat mar = markers * 20;
	Mat mix;
	//Lapimg.convertTo(Lapimg,CV_8UC1);
	cvtColor(Lapimg, Lapimg, CV_BGR2GRAY);
	addWeighted(mar, 0.8, Lapimg, 0.2, 0.0, mix);//权重混合
	imshow("分水岭在原图上的标记", src);
	*/


	//分水岭变换—watershed
	markers.convertTo(markers, CV_32SC1);//把CV_8UC1变回CV_32SC1类型
	imshow("分水岭变换之前的src",src);
	watershed(src, markers);
	//分水岭变换watershed(srcImage_, maskWaterShed);
	//srcImage_是没做任何修改的原图(需要处理的原图)必须是一个8bit 3通道彩色图像矩阵序列

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

	//于是这个传入的轮廓种子markers经过分水岭处理后变为区域划定值(每个像素都有不同的区域划定值)
	//各个分割区域内的像素值置为大于0的值 (0,1,2,3...顺序不详) 
	//而区域与区域间的像素值被置为-1
	//属于分水岭(边缘)的像素值置为255
	
	//cout << markers << endl;//显示分水岭区域划定值
	Mat mark = Mat::zeros(markers.size(), CV_8UC1);

	markers.convertTo(mark, CV_8UC1);
	
	bitwise_not(mark, mark, Mat());//像素反显操作(0变为255 255变为0)
	imshow("分水岭", mark);

	//对每个分割区域着色输出结果
	vector<Vec3b>colors;//定义一个三维向量数组存放每个分割区域的随机着色参数(有三个int类型参数RGB 读取方式为color[int类型的序号])
	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));
		//.push_back-向vector动态数组colors从尾部 压入Vec3b类型的元素
		cout<<"第"<<i+1<<"个轮廓的随机颜色为"<<colors[i]<< endl;
	}


	Mat finimg = Mat::zeros(markers.size(), CV_8UC3);
	for (int row = 0; row < markers.rows; row++)
	{	
		for (int col = 0; col < markers.cols; col++)
		{
			int index = markers.at<int>(row, col);//分水岭处理过后 每个像素都有一个区域值 在分水岭内的为大于0的整型数 区域与区域之间的像素为-1
			//判断是不是区域与区域之间的分界,如果是分界中(-1),则使用黑色显示
			if (index > 0 && index <= static_cast<int>(contours.size()))//区域值大于0的为被分割区域(要加上上限条件)
			{
				finimg.at<Vec3b>(row, col) = colors[index - 1];//index-1的目的是 分割区域的值从1开始 而随机颜色序号的取值从0开始 
			}
			else
			{
				finimg.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
			}
		}
	}
	imshow("基于距离变换与分水岭的图像分割",finimg);
}

关于分水岭算法的详细解释
https://blog.csdn.net/Lemon_jay/article/details/89355937?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-6.edu_weight&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-6.edu_weight
https://blog.csdn.net/iracer/article/details/49225823?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159350484619724845048440%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=159350484619724845048440&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-9-49225823.first_rank_ecpm_v3_pc_rank_v3&utm_term=%E5%88%86%E6%B0%B4%E5%B2%AD
https://www.cnblogs.com/mikewolf2002/p/3304118.html
https://blog.csdn.net/jumencibaliang92/article/details/81514766
https://blog.csdn.net/zhangSMILE123456/article/details/47271955?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight
https://www.cnblogs.com/ssyfj/p/9278815.html
https://blog.csdn.net/qq_33414271/article/details/78664123

图像的通道和深度以及图像的像素点操作完全解析
https://blog.csdn.net/u013355826/article/details/64905921?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight

OpenCV 创建图像时,CV_8UC1,CV_32FC3,CV_32S,CV_32F等参数的含义
https://blog.csdn.net/Young__Fan/article/details/81868666

C++ vector::push_back 用法剖析
https://blog.csdn.net/u010545732/article/details/24385701?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.edu_weight&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.edu_weight

OpenCV学习三十四:watershed 分水岭算法
https://blog.csdn.net/kakiebu/article/details/82965629


//关于距离变换的原理distanceTransform
简介
图像的距离变换被定义为一幅新的图像,该图像的每个输出像素被设成与输入像素中0像
素最近的距离。显然,典型
的距离变换的输入应为某些边缘图像。在多数应用中,距离变换的输入是例如Canny边
缘检测的检测图像的转换输
出(即边缘的值是0,非边缘的是非0) 也就是求每个像素点到轮廓边缘像素的最短距离

Opencv中distanceTransform方法用于计算图像中每一个非零点距离离自己最近的零点
的距离,distanceTransform的第二个Mat矩阵参数dst保存了每一个点与最近的零点的
距离信息,图像上越亮的点,代表了离零点的距离越远

可以根据距离变换的这个性质,经过简单的运算,用于细化字符的轮廓和查找物体质心(中心)

//关于距离变化原理及其参数的详解
https://blog.csdn.net/qq_30490125/article/details/53049180

distanceTransformAPI详解
https://blog.csdn.net/kakiebu/article/details/82967085

你可能感兴趣的:(温职,THINK,TWICE,party)