精华内容
下载资源
问答
  • 应用领域:如果应用到单幅图像上则主要为图像窄 骨架提取 粘连物体的分离等 如果应用到原图跟模板图则应用于图像匹配 封闭性分水岭算法的一个重要特征 其他图像分割方法,如阈值,边缘检测等都不会考虑像素...

    图像分割的概念(Image Segmentation)

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

    2.根据灰度、颜色、纹理和形状等特征,把图像分成若干个特定的、具有独特性质的区域,这些特征在同一区域内呈现出相似性,而在不同区域间呈现出明显的差异性,并提出感兴趣目标的技术和过程。 它是由图像处理到图像分析的关键步骤。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像素赋予相同的编号

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


    图像分割应用(Image Segmentation)

    如果应用到单幅图像上则是主要为 图像窄化 骨架提取 粘连物体的分离

    如果应用到原图跟模板图则是应用于 图像匹配


    图像分割-分水岭算法

    分水岭的概念

    分水岭概念是以对图像进行三维可视化处理为基础的:其中两个是坐标,另一个是灰度级。基于“地形学”的这种解释,我们考虑三类点:

    a点.属于 局部性最小值的点 ,也可能存在一个最小值面,该平面内的都是最小值点

    b点.当一滴水放在某点的位置上的时候,水 一定会下落到一个单一的最小值点

    c点.当水处在某个点的位置上时,水会 等概率地流向不止一个这样的最小值点
    在这里插入图片描述

    对一个特定的区域最小值:
    满足条件(b)的点的集合称为这个最小值的“汇水盆地”或“分水岭”
    满足条件©的点的集合组成地形表面的峰线,称做“分割线”或“分水线”


    分水岭的分割方法

    分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,目前较著名且使用较多的有2种算法:(1) 自下而上的模拟泛洪的算法 (2) 自上而下的模拟降水的算法


    泛洪算法:

    我们把图像看作是测地学上的拓扑地貌,图像中 每一点像素的灰度值表示该点的海拔高度 ,模拟泛洪算法的基本思想是:假设在每个区域最小值的位置上打一个洞并且让水以均匀的上升速率从洞中涌出,从低到高淹没整个地形。当处在不同的汇聚盆地中的水将要聚合在一起时,修建的大坝将阻止聚合。水将达到在水线上只能见到各个水坝的顶部这样一个程度。这些大坝的边界对应于分水岭的分割线。所以,它们是由分水岭算法提取出来的(连续的)边界线
    在这里插入图片描述
    右一图片 显示了一个简单的灰度级图像,其中“山峰”的高度与输入图像的灰度级值成比例,为了阻止上升的水从这些结构的边缘溢出,我们想像将整幅地形图的周围用比最高山峰还高的大坝包围起来。最高山峰的值是由输入图像灰度级具有的最大值决定的

    水淹没的第一个阶段,这里水用浅灰色表示,覆盖了对应于图中深色背景的区域
    在这里插入图片描述
    水分别在第一和第二汇水盆地中上升。由于水持续上升,最终水将从一个汇水盆地中溢出到另一个之中
    在这里插入图片描述
    上图中水从左边的盆地溢出到右边的盆地,并且两者之间有一个短“坝”(由单像素构成)阻止这一水位的水聚合在一起。随着水位不断上升,如右图所显示的那样。这幅图中在两个汇水盆地之间显示了一条更长的坝,另一条水坝在右上角。这条水坝阻止了盆地中的水和对应于背景的水的聚合

    这个过程不断延续直到到达水位的最大值(对应于图像中灰度级的最大值)。水坝最后剩下的部分对应于分水线,这条线就是要得到的分割结果。
    在这里插入图片描述
    分水线在图中显示为叠加到原图上的一个像素宽的深色路径。注意一条重要的性质就是分水线组成一条连通的路径,由此给出了区域之间的连续的边界(知道了边界就能根据边界分割粘连的图像)


    分水岭算法详解:

    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


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

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


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

    距离变换

    距离变换常见算法有两种
    1.不断膨胀/腐蚀得到
    2.基于倒角距离
    距离变化用于获取每个图像的中心区域 配合后面的二值化和腐蚀操作之后就可以使粘连的目标进行分离

    分水岭
    基于浸泡(泛洪)理论实现的分水岭算法


    距离变换API cv::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
    )
    

    分水岭API cv::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 <opencv2/opencv.hpp>
    #include <opencv2/highgui/highgui_c.h>
    #include <iostream>
    #include <math.h>
    
    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<Vec3b>(row, col)[0] >= 250 && src.at<Vec3b>(row, col)[1] >= 250 && src.at<Vec3b>(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;
    			}
    		}
    	}
    	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<Vec3b>(row, col)[0] >= 250 && src.at<Vec3b>(row, col)[1] >= 250 && src.at<Vec3b>(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<int>(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
    
    

    效果

    原图
    在这里插入图片描述

    基于距离变换与分水岭的图像分割后的图像(各个颜色不一的图像块都互相独立)此时以实现了图像的分割处理
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述


    基于距离变换与分水岭的图像分割的应用场景
    在这里插入图片描述

    展开全文
  • 定义一个操作中算法骨架,而将这些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。好抽象的概念啊,文绉绉的东西就是不讨人喜欢,下面我用一个生活中常见的例子来...

    什么是模板方法模式?

    定义一个操作中算法的骨架,而将这些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

    好抽象的概念啊,文绉绉的东西就是不讨人喜欢,下面我用一个生活中常见的例子来举例说明吧

    c7e352db9004071e5acd9fb8918e7b33.png

    上图是个饮料机,大家都很熟悉吧,各大商场随处可见的一个东西,这里举个例子,比如我们想喝某种饮料,只要按下对应的饮料类别,饮料就自动出来了。

    这里我们可以抽象化的想象下饮料在机器里的制作过程(这里只是简单举例,别钻牛角尖哈)

    大致我们可以分成4个步骤

    ①烧水  ②冲泡饮料  ③把饮料倒入杯中  ④加入调料

    例如:

    咖啡:烧开热水-->加入咖啡粉冲泡-->把饮料倒入杯中-->加入少许糖

    奶茶:烧开热水-->加入奶茶粉冲泡-->把饮料加入杯中-->加入椰果/珍珠

    不难发现,饮料制作过程中的步骤中的①烧水、③把饮料倒入杯中是重复工作,制泡哪种饮料都一样,那么也就是重复工作,我们可以把它设定为通用性操作。

    我们只需要去关心步骤②和步骤④即可

    由于制泡饮料的步骤就是这4步,所以我们可以把它抽象成一个"制作饮料模板"出来,下面就以上面这个例子,我用代码来说明

    DrinkTemplate.java(模板类)

    这是一个制作饮料的模板类,也就是制作所有饮料的基类

    我们可以把这4个步骤封装到一个模板方法里,并实现里面的通用步骤

    由于避免继承它的子类去修改整体制作架构,所以这个方法用了final修饰符来修饰,好比著名的"好莱坞原则":Don't call us, we'll call you 子类需要听从父类的安排

    由于步骤②和步骤④需要根据具体制泡的饮料来确定,所以需要延迟到子类去实现,这里采用了protected修饰符以便子类可以复写,其他方法就可以直接写"死"掉,用private修饰符修饰,这样的使得代码工作人员能够更加关注自身的工作,而不必去考虑一些其他因素。

    1 packagecom.lcw.template.test;2

    3 public abstract classDrinkTemplate {4

    5 /**抽象基类6 *7 * 制作饮料方法模板8 * 4个步骤 1、烧水 2、冲泡饮料 3、把饮料倒入杯中 4、加调料9 * 由于步骤1、3是通用的步骤,适合于制作任何饮料,所以可以把它写死10 * 2和4步骤,针对不同的饮料有不同的选择,所以可以把它延迟到子类去复写实现(注意访问修饰符)11 */

    12 public final voiddrinkTempLate(){13 boilWater();//烧水

    14 brew();//冲泡饮料

    15 pourInCup();//把饮料倒入杯中

    16 addCondiments();//加调料

    17 }18

    19 protected abstract void addCondiments();//加调料,由于饮料所加调料各不相同,所以可以延迟到子类实现

    20

    21 private voidpourInCup() {22 System.out.println("把饮料倒入杯中...");23 }24

    25 protected abstract void brew();//冲泡饮料 ,由于饮料所用的材料各不相同,所以可以延迟到子类实现

    26

    27 private voidboilWater() {28 System.out.println("烧水步骤进行中...");29 }30 }

    MakeCoffee.java(冲泡咖啡类)

    这个没啥好说的,就是继承了抽象基类,并复写了它的抽象方法

    1 packagecom.lcw.template.test;2 /**

    3 *4 *@authorBalla_兔子5 * 冲泡咖啡6 *7 */

    8 public class MakeCoffee extendsDrinkTemplate {9

    10 @Override11 protected voidaddCondiments() {12 System.out.println("加糖...");13 }14

    15 @Override16 protected voidbrew() {17 System.out.println("加入咖啡粉冲泡...");18 }19

    20 }

    MakeMilkTea.java(冲泡奶茶类)

    1 packagecom.lcw.template.test;2 /**

    3 * 冲泡奶茶4 *@authorBalla_兔子5 *6 */

    7 public class MakeMilkTea extendsDrinkTemplate {8

    9 @Override10 protected voidaddCondiments() {11 System.out.println("加椰果...");12 }13

    14 @Override15 protected voidbrew() {16 System.out.println("加入奶茶粉冲泡...");17 }18

    19 }

    Test.java(测试类)

    1 packagecom.lcw.template.test;2

    3 public classTest {4

    5 /**

    6 *@authorBalla_兔子7 */

    8 public static voidmain(String[] args) {9 DrinkTemplate coffee=newMakeCoffee();10 coffee.drinkTempLate();11 System.out.println("*******************************");12 DrinkTemplate milkTea=newMakeMilkTea();13 milkTea.drinkTempLate();14 }15

    16 }

    看下运行效果:

    613161d96c95508e390f2548ed8760f5.png

    哈哈,这样的实现类写起来是不是很清晰明了啊,只需要去复写我们需要关心的方法即可,大大提高了代码的复用性。

    但这里有个问题就暴露出来了,冲泡咖啡的实现固然没错,但总有些人喝咖啡是不加糖的,这是该怎么办呢?

    这里就引入了一个"钩子"hook概念

    b97f05df5f68ebc5182156dc61909179.png

    我们可以在某个具体实现方法前后分别加入钩子,就好比是前置方法或者后置方法,就像日志技术一样,在每完成一个业务动作前都需要记录日志

    而这个前置方法,我们可以利用一个布尔来做判断,并给它一个默认,来看看具体实现方法

    DrinkTemplate.java(模板类)

    1 packagecom.lcw.template.test;2

    3 public abstract classDrinkTemplate {4

    5 /**抽象基类6 *7 * 制作饮料方法模板8 * 4个步骤 1、烧水 2、冲泡饮料 3、把饮料倒入杯中 4、加调料9 * 由于步骤1、3是通用的步骤,适合于制作任何饮料,所以可以把它写死10 * 2和4步骤,针对不同的饮料有不同的选择,所以可以把它延迟到子类去复写实现(注意访问修饰符)11 */

    12 public final voiddrinkTempLate(){13 boilWater();//烧水

    14 brew();//冲泡饮料

    15 pourInCup();//把饮料倒入杯中

    16 if(condition()==true){//若条件允许,则加入调料,默认允许

    17 addCondiments();//加调料

    18 }19 }20

    21 protected booleancondition() {22 return true;23 }24

    25 protected abstract void addCondiments();//加调料,由于饮料所加调料各不相同,所以可以延迟到子类实现

    26

    27 private voidpourInCup() {28 System.out.println("把饮料倒入杯中...");29 }30

    31 protected abstract void brew();//冲泡饮料 ,由于饮料所用的材料各不相同,所以可以延迟到子类实现

    32

    33 private voidboilWater() {34 System.out.println("烧水步骤进行中...");35 }36 }

    Test.java(测试类)

    1 packagecom.lcw.template.test;2

    3 public classTest {4

    5 /**

    6 *@authorBalla_兔子7 */

    8 public static voidmain(String[] args) {9 DrinkTemplate coffee=newMakeCoffee();10 coffee.drinkTempLate();11 System.out.println("咖啡制作完毕!");12 System.out.println("*******************************");13 DrinkTemplate milkTea=newMakeMilkTea();14 milkTea.drinkTempLate();15 System.out.println("奶茶制作完毕!");16 }17

    18 }

    看下这次的效果,哈哈,无糖咖啡出炉~

    c1e0936911e1ea2238b1e76875f6fd74.png

    总结下:

    先说说模板方法模式的优点:

    1、封装性好  2、复用性好、  3、屏蔽细节  4、便于维护

    至于缺点呢,就是继承问题,在JAVA里只能继承一个父类。

    流程: 分析场景-->步骤抽取-->重构代码-->重要、复杂的算法,核心算法设计为模版

    注意点:

    模版方法需要声明成 public final

    private方法是基本逻辑

    protect abstract 方法是可扩展方法

    钩子使得模板方法更加灵活

    展开全文
  • 是什么: 顾名思义,就是通过模板拓印的方式, 定义模板采用抽象类来定义,公共的结构逻辑需要在抽象类中完成,只将非公共的部分逻辑抽象成抽象方法,留待子类充实实现。 AbstractClass:实现一个模板方法,定义...

    是什么:
    顾名思义,就是通过模板拓印的方式, 定义模板采用抽象类来定义,公共的结构化逻辑需要在抽象类中完成,只将非公共的部分逻辑抽象成抽象方法,留待子类充实实现。
    在这里插入图片描述
    AbstractClass:实现一个模板方法,定义了算法的骨架,具体子类将重定义PrimitiveOperation以实现一个算法的步骤。AbstractClass其实就是一个抽象模板,定义并实现了一个模板方法。
    ConcreteClasses:ConcreteClass实现父类所定义的一个或多个抽象方法。每一个AbstractClass都可以有任意多个ConcreteClass与之对应,而每一个ConcreteClass都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。

    网上有朋友总结的例子挺好的,在此借用一下。
    具体代码实现例子:
    AbstractClass(抽象模板):

    TestPaper类

    public class TestPaper {
    
        public void testQuestion1() {
            System.out.println("杨过得到,后来给了郭靖," +
                    "炼成倚天剑、屠龙刀的玄铁可能是[ ]\n" +
                    "a.球墨铸铁 b.马口铁 c.高速合金钢 d.碳素纤维");
            System.out.println("答案: " + answer1());
        }
    
        public void testQuestion2() {
            System.out.println("杨过、程英、陆无双铲除了情花,造成[ ]\n" +
                    "a.使这种植物不再伤人 b.使一种珍惜物种灭绝\n" +
                    "c.破坏了那个生物圈的生态平衡 d.造成该地区荒漠化");
            System.out.println("答案: " + answer2());
        }
    
        public void testQuestion3() {
            System.out.println("蓝凤凰致使华山师徒、桃谷六仙呕吐不止," +
                    "如果你是大夫,会给他们开什么药[ ]\n" +
                    "a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.大量生牛奶");
            System.out.println("答案: " + answer3());
        }
    
        public String answer1() {
            return null;
        }
    
        public String answer2() {
    
    
            return null;
        }
    
        public String answer3() {
            return null;
        }
    }
    

    ConcreteClass(实现父类所定义的一个或多个抽象方法)
    TestPaperA类:

    /**
     * 学生甲抄的试卷
     */
    public class TestPaperA extends TestPaper {
    
        @Override
        public String answer1() {
            return "d";
        }
        @Override
        public String answer2() {
            return "c";
        }
        @Override
        public String answer3() {
            return "c";
        }
    }
    

    TestPaperB(ConcreteClass):

    /**
     * 学生乙抄的试卷
     */
    public class TestPaperB extends TestPaper {
    
        @Override
        public String answer1() {
            return "a";
        }
        @Override
        public String answer2() {
            return "a";
        }
        @Override
        public String answer3() {
            return "a";
        }
    }
    

    客户端:

    public class TestPaperClient {
    
        public static void main(String[] args) {
            System.out.println("学生甲抄的试卷:");
            TestPaperA studentA = new TestPaperA();
            studentA.testQuestion1();
            studentA.testQuestion2();
            studentA.testQuestion3();
            
            System.out.println();
    
            System.out.println("学生乙抄的试卷:");
            TestPaperB studentB = new TestPaperB();
            studentB.testQuestion1();
            studentB.testQuestion2();
            studentB.testQuestion3();
        }
    }
    

    运行结果:

    学生甲做的试卷:
    杨过得到,后来给了郭靖,炼成倚天剑、屠龙刀的玄铁可能是[ ]
    a.球墨铸铁 b.马口铁 c.高速合金钢 d.碳素纤维
    答案: d
    杨过、程英、陆无双铲除了情花,造成[ ]
    a.使这种植物不再伤人 b.使一种珍惜物种灭绝
    c.破坏了那个生物圈的生态平衡 d.造成该地区荒漠化
    答案: c
    蓝凤凰致使华山师徒、桃谷六仙呕吐不止,如果你是大夫,会给他们开什么药[ ]
    a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.大量生牛奶
    答案: c
    
    
    学生乙做的试卷:
    杨过得到,后来给了郭靖,炼成倚天剑、屠龙刀的玄铁可能是[ ]
    a.球墨铸铁 b.马口铁 c.高速合金钢 d.碳素纤维
    答案: a
    杨过、程英、陆无双铲除了情花,造成[ ]
    a.使这种植物不再伤人 b.使一种珍惜物种灭绝
    c.破坏了那个生物圈的生态平衡 d.造成该地区荒漠化
    答案: a
    蓝凤凰致使华山师徒、桃谷六仙呕吐不止,如果你是大夫,会给他们开什么药[ ]
    a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.大量生牛奶
    答案: a
    

    这是以前在印象笔记中总结的,原文链接没有记录,敬请谅解。

    展开全文
  • 设计模式对设计原则的具体。用江湖话说就是武林秘籍,总结出来的一些固定套路,可以帮助有根基的程序员迅速打通任督二脉,从此做什么都特别快。常用的模式及其场景如下 C++常见设计模式——模板模式 模板模式:...

    设计模式是对设计原则的具体化。用江湖话说就是武林秘籍,总结出来的一些固定套路,可以帮助有根基的程序员迅速打通任督二脉,从此做什么都特别快。常用的模式及其场景如下

    C++常见设计模式——模板模式

    模板模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

    当多个类有相同的方法,并且逻辑相同,只是细节上有差异时,可以考虑使用模板模式。具体的实现上可以将相同的核心算法设计为模板方法,具体的实现细节有子类实现。

    缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

    以生产电脑为例,电脑生产的过程都是一样的,只是一些装配的器件可能不同而已。

    应用场景如下:

    对于一些功能,在不同的对象身上展示不同的作用,但是功能的框架是一样的。

    /*
    * 关键代码:在抽象类实现通用接口,细节变化在子类实现。
    */#include <iostream>using namespace std;class Computer
    {
    public:
        void product()
        {
            installCpu();
            installRam();
            installGraphicsCard();
        }protected:
        virtual void installCpu() = 0;
        virtual void installRam() = 0;
        virtual void installGraphicsCard() = 0;};class ComputerA : public Computer
    {
    protected:
        void installCpu() override
        {
            cout << "ComputerA install Inter Core i5" << endl;
        }void installRam() override
        {
            cout << "ComputerA install 2G Ram" << endl;
        }void installGraphicsCard() override
        {
            cout << "ComputerA install Gtx940 GraphicsCard" << endl;
        }
    };class ComputerB : public Computer
    {
    protected:
        void installCpu() override
        {
            cout << "ComputerB install Inter Core i7" << endl;
        }void installRam() override
        {
            cout << "ComputerB install 4G Ram" << endl;
        }void installGraphicsCard() override
        {
            cout << "ComputerB install Gtx960 GraphicsCard" << endl;
        }
    };int main()
    {
        ComputerB* c1 = new ComputerB();
        c1->product();delete c1;
        c1 = nullptr;return 0;
    }
    
    展开全文
  • 是什么不是什么? 这个引擎不会是一个可以用在生产环境的项目,但会是一个了解正则引擎背后工作原理的项目。 当前 目前支持的语义基本语义:。 ? * +()|字符集:[]非打印字符:\ d \ D \ s \ S \ w \ W支持DFA和...
  • 是什么不是什么? 这个引擎不会是一个可以用在生产环境的项目,但会是一个了解正则引擎背后工作原理的项目。 现状 目前支持的语义 基本语义: . ? * + () | 字符集合: [] 非打印字符: \d \D \s \S \w \W 支持DFA和...
  • 软件工程教程

    2012-07-06 23:10:29
    问:开发这个软件目标是什么? 答: 提高用户对音乐的学习和娱乐 参与创作音乐 项目背景--钢琴练奏师 问:为什么传统音乐程序不好? 答: 传统音乐程序功能单一,容易令人感到枯燥无味,没有吸引力; 传统音乐...
  • 要让电脑知道该在哪一点下子,就要根据盘面的形势,为每一可能落子的点计算其重要程度,也就是当这子落下后会形成什么棋型(如:“冲四”、“活三”等),然后通览全盘选出最重要的一点,这便最基本的算法。...

空空如也

空空如也

1 2
收藏数 37
精华内容 14
关键字:

骨架化算法是什么