OpenCV入门(九)——图像分割技术之分水岭分割

content

图像分割是利用图像的灰度、颜色、纹理和形状等特征,对图像中具有独特属性的特定区域进行划分,进而实现感兴趣对象的提取。根据分割的原因,可以分为连续分割和非连续分割。连续分割是指将具有相同灰度或相同特征的像素分成不同的区域。常见的分割方法包括区域生成、阈值分割和聚类分割。不连续分割是利用像素值的变异特性来呈现不同的边界区域来实现图像分割。常用的分割方法包括点线检测、边缘和能量等。

0x01 分水岭分割

(1) 流域分割

流域分割是一种基于自然启发式算法的分割方法,通过地形波动模拟水流现象,然后进行研究总结。 **特征,将每个符合特征的区域划分形成边界,形成**分水岭**。分水岭分割图像被认为是地形起伏,其中梯度幅度被解释为高度相关的信息。对于分水岭算法中的图像像素,一般需要注意以下三个特征点:

(1)局部最值点:这类点反映的是图像中的局部最小值点或局部最大值点。
(2)交汇边缘点:这类的产生是由于灰度不均匀变化,对应于分水岭中不同地域形成的交接点。
(3)连接区域点:通过局部最小值的像素点向外慢慢扩展,集水区间内的像素点会承担连接作用,同时水域流向会通过连接区域点留到局部最小值的点。

分水岭算法是一种很好的分割相互接触物体图像的方法,但是在边缘分割的准确性上存在一定的问题。例如,在实际应用过程中,由于噪声过大或其他信息的干扰,局部极值点过多,难以实现图像分割。对于图像的一些小区域的背景和前景分割,这将意味着交叉边缘点会更少,容易造成过度分离。常见的解决方案是将视觉小区域部分合并,作为独立的部分进行分割。操作。

(2)实现流域分割

在计算机视觉中,我们经常关注的目标特征是**颜色**和**灰度**。其实还有很多其他的方法,比如形状描述符、颜色特征、距离特征等。具有独特纹理的物体可以在某些场景下使用好的纹理描述符。对于单个对象在不同颜色区域的相同扩展,我们可以使用颜色特征来衡量对象不同部分的相似度。

  • 使用图像距离变换实现分水岭分割算法:
  • 对源图像进行灰度化,使用OTSU进行二值化操作。
  • 对二值化图像进行形态学开运算。
  • 利用distanceTransform完成图像距离变换操作。
  • 归一化距离变换的统计图像并计算相应的连接域分割。

代码显示如下:

#include <iostream>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include "opencv2/core/core.hpp"
#include "opencv2/core/utility.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <opencv2/imgproc/imgproc_c.h>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace std;

cv::Mat displaySegResult(cv::Mat& segments, int numOfSegments) //, cv::Mat& image
{
	cv::Mat wshed(segments.size(), CV_8UC3); // 创建对于颜色分量 
	vector<Vec3b> colorTab;

	for (int i = 0; i < numOfSegments; i++)
	{
		int b = theRNG().uniform(0, 255);
		int g = theRNG().uniform(0, 255);
		int r = theRNG().uniform(0, 255);
		colorTab.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
	} //应用不同颜色对每个部分 

	for (int i = 0; i < segments.rows; i++)
	{
		for (int j = 0; j < segments.cols; j++)
		{
			int index = segments.at<int>(i, j);
			if (index == -1)
				wshed.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
			else if (index <= 0 || index > numOfSegments)
				wshed.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
			else
				wshed.at<Vec3b>(i, j) = colorTab[index - 1];
		}
	}

	//if (image.dims > 0)
		//wshed = wshed * 0.5 + image * 0.5;
	return wshed;
}

Mat watershedSegment(Mat& srcImage, int& noOfSegments)
{
	Mat grayMat;
	Mat otsuMat;
	cvtColor(srcImage, grayMat, CV_BGR2GRAY);
	imshow("grayMat", grayMat);
	// 阈值操作
	threshold(grayMat, otsuMat, 0, 255,
		CV_THRESH_BINARY_INV + CV_THRESH_OTSU);
	imshow("otsuMat", otsuMat);
	// 形态学开操作
	morphologyEx(otsuMat, otsuMat, MORPH_OPEN,
		Mat::ones(9, 9, CV_8SC1), Point(4, 4), 2);
	imshow("Mor-openMat", otsuMat);
	// 距离变换
	Mat disTranMat(otsuMat.rows, otsuMat.cols, CV_32FC1);
	distanceTransform(otsuMat, disTranMat, CV_DIST_L2, 3);
	// 归一化
	normalize(disTranMat, disTranMat, 0.0, 1, NORM_MINMAX);
	imshow("DisTranMat", disTranMat);
	// 阈值化分割图像
	threshold(disTranMat, disTranMat, 0.1, 1, CV_THRESH_BINARY);
	//归一化统计图像到0-255
	normalize(disTranMat, disTranMat, 0.0, 255.0, NORM_MINMAX);
	disTranMat.convertTo(disTranMat, CV_8UC1);
	imshow("TDisTranMat", disTranMat);
	//计算标记的分割块
	int i, j, compCount = 0;
	vector<vector<Point> > contours;
	vector<Vec4i> hierarchy;
	findContours(disTranMat, contours, hierarchy,
		CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
	if (contours.empty())
		return Mat();
	Mat markers(disTranMat.size(), CV_32S);
	markers = Scalar::all(0);
	int idx = 0;
	// 绘制区域块
	for (; idx >= 0; idx = hierarchy[idx][0], compCount++)
		drawContours(markers, contours, idx,
			Scalar::all(compCount + 1), -1, 8,
			hierarchy, INT_MAX);
	if (compCount == 0)
		return Mat();
	//计算算法的时间复杂度
	double t = (double)getTickCount();
	watershed(srcImage, markers);
	t = (double)getTickCount() - t;
	printf("execution time = %gms\n", t * 1000. /
		getTickFrequency());
	Mat wshed = displaySegResult(markers,compCount);
	imshow("watershed transform", wshed);
	noOfSegments = compCount;
	return markers;
}



int main()
{
	Mat srcImage = imread("./image/beauty2.png");
	imshow("src", srcImage);
	int resultall = 0;
	Mat result = watershedSegment(srcImage, resultall);

	waitKey(0);

}

以这张图片为例:

第一步是灰度操作,第一步后的图像如下:

第二步是阈值化操作,第二步的图像如下:

第三步是形态开运算,其图像为:

这一步的作用是填充操作,为了更好的分割。

第三步,距离变换:

距离变换是针对二值化图像的一种变换,是计算标识空间点(对目标点)距离的过程,它最终把二值化图像变换为灰度图像(其中每个栅格的灰度值等于它到最近目标点的距离)。在二维空间中,一幅二值化图像可以认为仅仅包含目标和背景两种像素,目标的像素值为1,背景的像素值为0;距离变换的结果不是另一幅二值化图像,而是一幅灰度级图像,即距离图像,图像中的每个像素的灰度值为该像素与距离其最近的背景像素间的距离。

现有的距离变换算法主要采用两类距离测度:非欧式距离和欧式距离。前者常用的有城市街区、棋盘、倒角等距离,算法采用串行扫描实现距离变换,在扫描过程中传递最短距离信息。这些算法简单快速,易于实现,但是得到的仅仅是欧式距离变换(EDT)的一种近似值,在很多应用中不能满足精度要求。

void distanceTransform( InputArray src, 	//8通道二值化图像
						OutputArray dst,	//输出距离图像
                        int distanceType, 	//距离类型
                        int maskSize, 		//距离变换掩膜大小
                        int dstType=CV_32F);

距离类型:**CV_DIST_L1 /CV_DIST_L2  /CV_DIST_C**

maskSize:是 3 或 5,对 CV_DIST_L1 或 CV_DIST_C 的情况,参数值被强制设定为 3, 因为 3×3 mask(由两个数(水平/垂直位量,对角线位移量)组成) 给出 5×5 mask(由三个数组成(水平/垂直位移量,对角位移和 国际象棋里的马步(马走日))) 一样的结果,而且速度还更快。

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

然后对变换后的距离进行归一化:

normalize函数:该函数归一化输入数组使得它的范数或者数值范围在一定的范围内。简单来说,就是把0-255的数值,到0-1范围内,方便后面的处理。

最终显示的图像如下:

第四步,继续对分割图像进行阈值化,使用普通二值化,再次归一化:

它是距离图像可以更好地表达。

第五步,开始计算标记段,即找到连通域,最后用不同的颜色进行区分,效果如下:

使用源图可能更直观:

看到大家对分水岭算法有了一些大致的了解,我们继续细说:

在上面我们也看到了分水岭处理的效果,那么它的实现的具体依据在哪:分水岭算法中会用到一个很重要的概念——测地线距离(Geodesic Distance)。

测地线距离就是地球表面两点之间的最短路径(可执行路径)的距离,在图论中,Geodesic Distance就是图中两节点之间最短路径的距离,这与平时在几何空间通常用到的Euclidean Distance(欧氏距离),即两点之间最短距离有所区别。

可以看看下图,两个黑点Euclidean Distance是用虚线所表示的距离长度d15,而Geodesic Distance作为实际路径的最短距离,其距离应为沿途实线段距离之和的最小值,即d12 + d23 + d34 + d45。

 

三维曲面空间中两点之间的测地距离是两点之间沿三维曲面曲面的最短路径。

然后回过头来看看这个算法:

图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。连接灰度值较大的像素点的线可视为分水岭。其中的水是阈值二值化后得到的值。我们将其设置为水平面。如果该区域低于水平面,它将被淹没,然后每个孤立的山谷(即局部最小值)将被水填满。那么当水位上升到一定高度时,水会溢出当前的山谷,那么我们可以在分水岭上建一个水坝,避免两个山谷的积水,然后这样的图像被分成两个像素集,然后一个是潜谷像素集,另一个是分水岭像素集。最后,这些堤坝的线条对整个图像进行分割,实现了图像的分割。

在该算法中,空间上相邻且灰度值相近的像素被划分为一个区域。

那么分水岭算法的全过程:

  • 根据灰度值对梯度图像中的所有像素进行分类并设置阈值。 (其实就是我们二值化得到的阈值),在上面做了两次分割。
  • 找到灰度值最小的像素点(默认标记为灰度值最低点),让threashold从小值开始增长,这些点为起始点。(其实就是每次调用threashold函数的时候,往上抬高一些,把水位再往上涨)
  • 在水平面的生长过程中,会遇到周围的邻域像素,并测量这些像素到起点(灰度值最低点)的测地线距离。在像素上设置大坝意味着绘制一个白点,用于对这些邻域值进行分类。
  • 随着水位越来越高,会设置越来越多的大坝,直到灰度值达到最大值,所有区域都在分水岭线上划分,即划分成功。

使用上述算法对图像进行分水岭运算,由于噪声点或其他因素的干扰,可能会得到密集的小区域,即图像被分割得太细(过度分割),因为有很多局部图像中的极点。小值点,每个点会自己形成一个小区域。

在解决方法中:

1. 对图像进行高斯平滑操作,抹除很多小的最小值,这些小分区就可以合并。
2. 不从最小值开始增长,可以将相对较高的灰度值的像素作为起始点,那就是我们需要手动设定一个阈值,从标记处开始进行淹没,很多小区域都会被合并为一个区域,这被称为基于图像标记(mark)的分水岭算法。

0x02 分水岭分割合并

上述方法使用色度直方图提取图像的每个区域,并测量其bhatcharya距离,用于距离相似度测量。 Bhatcharya 距离衡量两个分离或连续区域的部分的概率分布的相似性。当测量距离较小时,两个区域将合并为一个部分。当图像的过度分离很明显时,可以使用这种合并。重复。分水岭分割合并首先计算分割部分的像素属性,利用直方图信息统计相关特征,统计每个分割部分的直方图比较相似度,然后判断分割的两部分是否需要合并根据**相似性**。一个地区。

看一下代码:

void segMerge(Mat& image, Mat& segments, int& numSeg)
{
	// 对一个分割部分进行像素统计
	vector<Mat> samples;
	// 统计数据更新
	int newNumSeg = numSeg;
	// 初始化分割部分
	for (int i = 0; i <= numSeg; i++)
	{
		Mat sampleImage;
		samples.push_back(sampleImage);
	}
	// 统计每一个部分
	for (int i = 0; i < segments.rows; i++)
	{
		for (int j = 0; j < segments.cols; j++)
		{
			// 检查每个像素的归属
			int index = segments.at<int>(i, j);
			if (index >= 0 && index < numSeg)
				samples[index].push_back(image(Rect(j, i, 1, 1)));
		}
	}
	// 创建直方图
	vector<MatND> hist_bases;
	Mat hsv_base;
	// 直方图参数设置
	int h_bins = 35;
	int s_bins = 30;
	int histSize[] = { h_bins, s_bins };
	// hue 变换范围 0 to 256, saturation 变换范围0 to 180
	float h_ranges[] = { 0, 256 };
	float s_ranges[] = { 0, 180 };
	const float* ranges[] = { h_ranges, s_ranges };
	// 使用第0与1通道
	int channels[] = { 0, 1 };
	// 直方图生成
	MatND hist_base;
	for (int c = 1; c < numSeg; c++)
	{
		if (samples[c].dims > 0) {
			// 将区域部分转换成hsv
			cvtColor(samples[c], hsv_base, CV_BGR2HSV);
			// 直方图统计
			calcHist(&hsv_base, 1, channels, Mat(),
				hist_base, 2, histSize, ranges, true, false);
			// 直方图归一化
			normalize(hist_base, hist_base, 0, 1,
				NORM_MINMAX, -1, Mat());
			// 添加到统计集
			hist_bases.push_back(hist_base);
		}
		else
		{
			hist_bases.push_back(MatND());
		}
		hist_base.release();
	}
	double similarity = 0;
	vector<bool> mearged;
	for (int k = 0; k < hist_bases.size(); k++)
	{
		mearged.push_back(false);
	}
	// 统计每一个部分的直方图相似
	for (int c = 0; c < hist_bases.size(); c++)
	{
		for (int q = c + 1; q < hist_bases.size(); q++)
		{
			if (!mearged[q])
			{
				// 判断直方图的维度
				if (hist_bases[c].dims > 0 && hist_bases[q].dims > 0)
				{
					// 直方图对比
					similarity = compareHist(hist_bases[c],
						hist_bases[q], CV_COMP_BHATTACHARYYA);
					if (similarity > 0.8)
					{
						mearged[q] = true;
						if (q != c)
						{
							// 区域部分减少
							newNumSeg--;
							for (int i = 0; i < segments.rows; i++)
							{
								for (int j = 0; j < segments.cols; j++)
								{
									int index = segments.at<int>(i, j);
									// 合并
									if (index == q)
									{
										segments.at<int>(i, j) = c;
									}
								}
							}
						}
					}
				}
			}
		}
	}
	numSeg = newNumSeg;
}

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2022年5月8日
下一篇 2022年5月8日

相关推荐