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

概述

什么是图像分割

  • 图像分割(image segmentation)是图像处理最重要的处理手段之一
  • 图像分割的目标是将图像中像素根据一定的规则分为若干(N)个cluser集合,每个集合包含一类像素
  • 根据算法分为监督学习方法和无监督学习方法,图像分割的算法多数都是无监督的学习方法-KMeans
  • 距离变换常见的额算法有两种
    • 不断膨胀/腐蚀得到
    • 基于倒角距离
  • 分水岭变换常见方法
    • 基于浸泡理论实现

相关函数API

distanceTransform函数API

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

函数功能
用于计算图像中每一个非零点像素与其最近的零点像素之间的距离,输出的是保存每一个非零点与最近零点的距离信息;图像上越亮的点,代表了离零点的距离越远。
参数介绍

  • src是单通道的8bit的二值图像(只有01
  • dst表示的是计算距离的输出图像,可以使单通道32bit浮点数据
  • distanceType表示的是选取距离的类型,可以设置为CV_DIST_L1,CV_DIST_L2,CV_DIST_C等,具体如下:
DIST_USER User defined distance
DIST_L1=1 distance = |x1-x2| + |y1-y2
DIST_L2 the simple euclidean distance
DIST_C distance = max(|x1-x2|,|y1-y2|)
DIST_L12 L1-L2 metric: distance =2(sqrt(1+x*x/2) - 1))
DIST_FAIR distance = c^2(|x|/c-log(1+|x|/c)),c = 1.3998
DIST_WELSCH distance = c2/2(1-exp(-(x/c)2)), c= 2.9846
DIST_HUBER distance = |x|
  • maskSize表示的是距离变换的掩膜模板,可以设置为35CV_DIST_MASK_PRECISECV_DIST_L1CV_DIST_C 的情况,参数值被强制设定为 3, 因为3×3 mask 给出5×5 mask 一样的结果,而且速度还更快。
DIST_MASK_3 mask=3
DIST_MASK_5 mask=5
DIST_MASK-PRECISE
  • labels表示可选输出2维数组;
  • labelType表示的是输出二维数组的类型,8位或者32位浮点数,单一通道,大小与输入图像一致

watershed 分水岭函数API
void watershed( InputArray image, InputOutputArray markers );

参数介绍

  • 第一个参数 image,必须是一个8bit3通道彩色图像矩阵序列。
  • 第二个参数 markersOpencv官方文档的说明如下:

Before passing the image to the function, you have to roughly outline the desired regions in the image markers with positive (>0) indices. So, every region is represented as one or more connected components with the pixel values 1, 2, 3, and so on. Such markers can be retrieved from a binary mask using findContours() and drawContours(). The markers are “seeds” of the future image regions. All the other pixels in markers , whose relation to the outlined regions is not known and should be defined by the algorithm, should be set to 0’s. In the function output, each pixel in markers is set to a value of the “seed” components or to -1 at boundaries between the regions.

就不一句一句翻译了,大意说的是在执行分水岭函数watershed之前,必须对第二个参数markers进行处理,它应该包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过OpencvfindContours方法实现,这个是执行分水岭之前的要求。
接下来执行分水岭会发生什么呢?算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。
简单概括一下就是说第二个入参markers必须包含了种子点信息。Opencv官方例程中使用鼠标划线标记,其实就是在定义种子,只不过需要手动操作,而使用findContours可以自动标记种子点。而分水岭方法完成之后并不会直接生成分割后的图像,还需要进一步的显示处理,如此看来,只有两个参数的watershed其实并不简单。

代码演示处理流程

  • 将白色背景变为黑色背景-目的是为后面的变换做准备
  • 使用filter2D与拉普拉斯算子实现图像对比度提高,**sharp
  • 转换为二值图像**threshold****
  • 距离变换
  • 对距离变换结果进行归一化处理[0~1]之间
  • 使用阈值,再次二值化,得到标记
  • 腐蚀得到每个Peak-erode
  • 发现轮廓-findContours
  • 会之轮廓-drawContours
  • 分水岭变换watershed
  • 对每个分割区着色输出结果

代码演示

#include 
#include 
#include 

#define PIC_PATH "C:\\pic\\"
#define PIC_NAME "4.jpg"

using namespace cv;
using namespace std;

int main(void)
{
	Mat src;
	string pic = string(PIC_PATH) + string(PIC_NAME);
	cout << "原始图片为:" << pic << endl;

	src = imread(pic);
	if (src.empty()) {
		cout << "图片不存在" << endl;
		return -1;
	}
	namedWindow("原始图片", WINDOW_AUTOSIZE);
	imshow("原始图片", src);

	//将图片背景转换为黑色
	for(size_t row=0;row<src.rows;row++)
		for (size_t col = 0; col < src.cols; col++)
		{
			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;
			}
		}
	namedWindow("背景转换图", WINDOW_AUTOSIZE);
	imshow("背景转换图", src);
	
	//拉普拉斯变换  图片进行锐化
	Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1); //定义拉普拉斯算子
	Mat imgLapLance;
	Mat sharp ;
	
	//由于拉普拉斯变化可能会产生负数 图片类型为32f
	filter2D(src, imgLapLance, CV_32F, kernel, Point(-1, -1), 0,BORDER_DEFAULT);
	src.convertTo(sharp, CV_32F);
	Mat imgResult =  sharp - imgLapLance;          //增强锐化效果  
	imgResult.convertTo(imgResult, CV_8UC3);       //图片转化为3通道rgb格式

	namedWindow("锐化图", WINDOW_AUTOSIZE);
	imshow("锐化图", imgResult);


	//二值距离变换
	Mat binImage;
	cvtColor(imgResult, binImage, COLOR_BGR2GRAY);    //图片转化为灰度图
	threshold(binImage, binImage, 40, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);   //图片进行阈值处理变为二值图
	imshow("二值图像", binImage);


	//距离检测
	Mat distImage;
	distanceTransform(binImage, distImage, DIST_L1, 3, 5);      
	normalize(distImage, distImage, 0, 1, NORM_MINMAX);       //距离结果进行归一化处理
	threshold(distImage, distImage, 0.3, 1, THRESH_BINARY);   //对结果再次归一化处理
	
	Mat k1 = Mat::zeros(3, 3, CV_8UC1);
	erode(binImage, binImage, k1);         //二值腐蚀  将粘连的部分分开
	imshow("距离变换", distImage);

	Mat dist_8u;
	distImage.convertTo(dist_8u, CV_8U);     //图片转化为单通道图
	vector<vector<Point>> contours;
	
	findContours(dist_8u, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point(0, 0));   //寻找轮廓

	Mat marks = Mat::zeros(src.size(), CV_32SC1);     //定义mark图
	for (size_t i = 0; i < contours.size(); i++)
	{
		drawContours(marks, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i)+1),-1);    //绘制轮廓
	}
	circle(marks, Point(5, 5), 3, Scalar(255, 255, 255), -1);   //画个白圈标记一下
	imshow("marks", marks*1000);   //灰度值较小  看不出来  *1000更明显
	
	//分水岭变换
	watershed(src, marks);     //分水岭处理
	Mat mark = Mat::zeros(marks.size(), CV_8UC1);
	marks.convertTo(mark, CV_8UC1);
	bitwise_not(mark, mark, Mat());   //像素取反
	imshow("mark", 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(marks.size(), CV_8UC3);
	for (size_t row = 0; row < dst.rows; row++)
		for (size_t col = 0; col < dst.cols; col++)
		{
			int index = marks.at<int>(col, row);
			if (index > 0 && index<=static_cast<int>(contours.size()))
			{
				dst.at<Vec3b>(col, row) = colors[index-1];
			}
			else
			{
				dst.at<Vec3b>(col, row) = Vec3b(0, 0, 0);
			}
		}
	imshow("dst", dst);

	waitKey(0);
	destroyAllWindows();
}

程序运行结果

opencv之基于距离变换与分水岭的图像分割_第1张图片

你可能感兴趣的:(opencv,opencv,计算机视觉)