C++OpenCV系统学习(16)——图像分割与抠图(3)分水岭算法

        在图像分割与抠图(1)和(2)中说到的图像分割的方式都是机器学习的方法,分水岭算法是基于图像图像形态学和结构的方法来进行图像的分割。基于机器学习的方式本质是通过概率统计与建模,通过数学的方式对图像进行分割与分类。分水岭算法是通过图像本身的特征对图像进行分割与分类。

1.分水岭算法概述

1.1.分水岭算法的理解

        分水岭(Watershed)是基于地理形态的分析的图像分割算法,模仿地理结构(比如山川、沟壑,盆地)来实现对不同物体的分类。分水岭算法中会用到一个重要的概念——测地线距离

        图像的灰度空间很像地球表面的整个地理结构,每个像素的灰度值代表高度。其中的灰度值较大的像素连成的线可以看做山脊,也就是分水岭。其中的水就是用于二值化的gray threshold level,二值化阈值可以理解为水平面,比水平面低的区域会被淹没,刚开始用水填充每个孤立的山谷(局部最小值)。

        当水平面上升到一定高度时,水就会溢出当前山谷,可以通过在分水岭上修大坝,从而避免两个山谷的水汇集,这样图像就被分成2个像素集,一个是被水淹没的山谷像素集,一个是分水岭线像素集。最终这些大坝形成的线就对整个图像进行了分区,实现对图像的分割。

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

 1.2.分水岭算法的过程

分水岭算法的整个过程:

  1. 把梯度图像中的所有像素按照灰度值进行分类,并设定一个测地距离阈值。
  2. 找到灰度值最小的像素点(默认标记为灰度值最低点),让threshold从最小值开始增长,这些点为起始点。
  3. 水平面在增长的过程中,会碰到周围的邻域像素,测量这些像素到起始点(灰度值最低点)的测地距离,如果小于设定阈值,则将这些像素淹没,否则在这些像素上设置大坝,这样就对这些邻域像素进行了分类。
  4. 随着水平面越来越高,会设置更多更高的大坝,直到灰度值的最大值,所有区域都在分水岭线上相遇,这些大坝就对整个图像像素的进行了分区。

        用上面的算法对图像进行分水岭运算,由于噪声点或其它因素的干扰,可能会得到密密麻麻的小区域,即图像被分得太细(over-segmented,过度分割),这因为图像中有非常多的局部极小值点,每个点都会自成一个小区域。

其中的解决方法:

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

 其中标记的每个点就相当于分水岭中的注水点,从这些点开始注水使得水平面上升,但是如上图所示,图像中需要分割的区域太多了,手动标记太麻烦,我们可是使用距离转换的方法进行标记,OpenCV中就是使用的这种方法。

 1.3.分水岭算法应用

  • 分割粘连对象。实现形态学操作与对象计数
  • 图像分割

1.4.基于距离的分水岭分割流程

              

 2.案例实战

2.1粘连对象分离与计数

void MyApi::Watershed_image_segmentation(Mat& image)
{
	Mat gray, binary, shiffted;
	pyrMeanShiftFiltering(image, shiffted, 21, 51);
	imshow("shiftd", shiffted);

	cvtColor(shiffted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("binary", binary);
}

        如果用二值化后的图,由于还有很多的小点,再利用分水岭算法可能会导致过度分割。因此我们可以先使用均值偏移再进行二值化就可以消除出毛点。

 通过均值漂移后再进行二值化毛点得以处理。

 再对二值化后的图像进行距离变换:

void MyApi::Watershed_image_segmentation(Mat& image)
{
	Mat gray, binary, shiffted;
	//均值漂移:消除毛点
	pyrMeanShiftFiltering(image, shiffted, 21, 51);
	cvtColor(shiffted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("binary", binary);

	//distance transform
	Mat dist;
	distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
	normalize(dist,dist,0,1,NORM_MINMAX);//由于距离变换后值变得非常小,需要归一化到0-1
	imshow("distance transform result", dist);

}

 左图是二值化的, 右图是距离变换后的,在图像中最亮的部分就是山头最高的部分(像素值最大的部分)

 距离变换之后寻找种子:

void MyApi::Watershed_image_segmentation(Mat& image)
{
	Mat gray, binary, shiffted;
	//均值漂移:消除毛点
	pyrMeanShiftFiltering(image, shiffted, 21, 51);
	cvtColor(shiffted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("binary", binary);

	//distance transform
	Mat dist;
	distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
	normalize(dist,dist,0,1,NORM_MINMAX);//由于距离变换后值变得非常小,需要归一化到0-1
	imshow("distance transform result", dist);

	//x寻找种子:距离变换只有再进行二值化,主要是为了找局部最大
	threshold(dist, dist, 0.3, 1, THRESH_BINARY);//由于是经过归一化后的数据,所以阈值我们设置在0.4
	imshow("distance binary", dist);
}

 可以看到一共有九个种子。

 分水岭操作以后结果如下:

void MyApi::Watershed_image_segmentation(Mat& image)
{
	Mat gray, binary, shiffted;
	//均值漂移:消除毛点
	pyrMeanShiftFiltering(image, shiffted, 21, 51);
	cvtColor(shiffted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	//imshow("binary", binary);

	//distance transform
	Mat dist;
	distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
	normalize(dist,dist,0,1,NORM_MINMAX);//由于距离变换后值变得非常小,需要归一化到0-1
	//imshow("distance transform result", dist);

	//寻找种子:距离变换只有再进行二值化,主要是为了找局部最大
	threshold(dist, dist, 0.2, 1, THRESH_BINARY);//由于是经过归一化后的数据,所以阈值我们设置在0.4
	//imshow("distance binary", dist);

	//生成Masker
	Mat dist_m;
	dist.convertTo(dist_m, CV_8U);//转换了8位单通道
	//找轮廓
	vector<vector<Point>>contours;
	findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	//create markers
	Mat markers = Mat::zeros(image.size(), CV_32SC1);
	for (size_t t = 0; t < contours.size(); t++)
	{
		drawContours(markers, contours, static_cast<int>(t), Scalar::all(static_cast<int>(t) + 1), -1);
	}
	circle(markers, Point(5, 5), 3, Scalar(255), -1);
	//imshow("marker", markers*10000);

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

但是我们可以通过原图看出,如下图用红色标记的,两个三个硬币之间是有缝隙的,但是经过分水岭操作以后缝隙没有了,所以我们需要对他先做一个形态学操作,去除干扰再进行分水岭操作。

 经过形态学操作以后再进行分水岭操作结果如下:

void MyApi::Watershed_image_segmentation(Mat& image)
{
	Mat gray, binary, shiffted;
	//均值漂移:消除毛点
	pyrMeanShiftFiltering(image, shiffted, 21, 51);
	cvtColor(shiffted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	//imshow("binary", binary);

	//distance transform
	Mat dist;
	distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
	normalize(dist,dist,0,1,NORM_MINMAX);//由于距离变换后值变得非常小,需要归一化到0-1
	//imshow("distance transform result", dist);

	//寻找种子:距离变换只有再进行二值化,主要是为了找局部最大
	threshold(dist, dist, 0.2, 1, THRESH_BINARY);//由于是经过归一化后的数据,所以阈值我们设置在0.4
	//imshow("distance binary", dist);

	//生成Masker
	Mat dist_m;
	dist.convertTo(dist_m, CV_8U);//转换了8位单通道
	//找轮廓
	vector<vector<Point>>contours;
	findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	//create markers
	Mat markers = Mat::zeros(image.size(), CV_32SC1);
	for (size_t t = 0; t < contours.size(); t++)
	{
		drawContours(markers, contours, static_cast<int>(t), Scalar::all(static_cast<int>(t) + 1), -1);
	}
	circle(markers, Point(5, 5), 3, Scalar(255), -1);
	//imshow("marker", markers*10000);

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

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

 这样的效果就很好了。

然后生成随机颜色对他进行颜色。

全部代码如下:

void MyApi::Watershed_image_segmentation(Mat& image)
{
	Mat gray, binary, shiffted;
	//均值漂移:消除毛点
	pyrMeanShiftFiltering(image, shiffted, 21, 51);
	cvtColor(shiffted, gray, COLOR_BGR2GRAY);
	threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
	//imshow("binary", binary);

	//distance transform
	Mat dist;
	distanceTransform(binary, dist, DistanceTypes::DIST_L2, 3, CV_32F);
	normalize(dist,dist,0,1,NORM_MINMAX);//由于距离变换后值变得非常小,需要归一化到0-1
	//imshow("distance transform result", dist);

	//寻找种子:距离变换只有再进行二值化,主要是为了找局部最大
	threshold(dist, dist, 0.2, 1, THRESH_BINARY);//由于是经过归一化后的数据,所以阈值我们设置在0.4
	//imshow("distance binary", dist);

	//生成Masker
	Mat dist_m;
	dist.convertTo(dist_m, CV_8U);//转换了8位单通道
	//找轮廓
	vector<vector<Point>>contours;
	findContours(dist_m, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
	//create markers
	Mat markers = Mat::zeros(image.size(), CV_32SC1);
	for (size_t t = 0; t < contours.size(); t++)
	{
		drawContours(markers, contours, static_cast<int>(t), Scalar::all(static_cast<int>(t) + 1), -1);
	}
	circle(markers, Point(5, 5), 3, Scalar(255), -1);
	//imshow("marker", markers*10000);

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

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

	//generate random color
	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++)
		{
			if (index > 0 && index <= contours.size())
			{
				dst.at<Vec3b>(row, col) = colors[index - 1];
			}
			else
			{
				dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
			}
		}
	}
	imshow("Finall result", dst);
	printf("number of object:%d", contours.size());
}

2.2图像分割

#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
//执行分水岭算法函数
Mat watershedCluster(Mat &srcImg, int &numSegments);
//结果显示函数
void DisplaySegments(Mat &markersImg, int numSegments);
void test()
{
    Mat srcImg;
    srcImg = imread("toux.jpg");
    if (srcImg.empty())
    {
        cout << "could not load image...\n" << endl;
    }

    namedWindow("Original image", CV_WINDOW_AUTOSIZE);
    imshow("Original image", srcImg);

    int numSegments;
    Mat markers = watershedCluster(srcImg, numSegments);
    DisplaySegments(markers, numSegments);
}

Mat watershedCluster(Mat &srcImg, int &numSegments)
{
    //二值化
    Mat grayImg, binaryImg;
    cvtColor(srcImg, grayImg, COLOR_BGR2GRAY);
    threshold(grayImg, binaryImg, 0, 255, THRESH_BINARY | THRESH_OTSU);

    //形态学和距离变换
    Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    morphologyEx(binaryImg, binaryImg, MORPH_OPEN, kernel, Point(-1, -1));

    Mat distImg;
    distanceTransform(binaryImg, distImg, DistanceTypes::DIST_L2, 3, CV_32F);
    normalize(distImg, distImg, 0.0, 1.0, NORM_MINMAX);

    //开始生成标记
    threshold(distImg, distImg, 0.1, 1.0, THRESH_BINARY);
    normalize(distImg, distImg, 0, 255, NORM_MINMAX);
    distImg.convertTo(distImg, CV_8UC1);  //CV_32F 转成 CV_8UC1

    //标记开始
    vector<vector<Point>>contours;
    vector<Vec4i>hireachy;
    findContours(distImg, contours, hireachy, RETR_CCOMP, CHAIN_APPROX_SIMPLE);
    if (contours.empty())
    {
        return Mat();
    }

    Mat markersImg(distImg.size(), CV_32S);
    markersImg = Scalar::all(0);
    for (int i = 0; i < contours.size(); i++)
    {
        drawContours(markersImg, contours, i, Scalar(i + 1), -1, 8, hireachy, INT_MAX);
    }
    circle(markersImg, Point(5, 5) ,3, Scalar(255), -1);

    //分水岭变换
    watershed(srcImg, markersImg);
    numSegments = contours.size();
    return markersImg;
}

void DisplaySegments(Mat &markersImg, int numSegments)
{
    //生成随机颜色
    vector<Vec3b>colors;
    for (int i = 0; i < numSegments; 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 dstImg = Mat::zeros(markersImg.size(), CV_8UC3);
    int index = 0;
    for (int i = 0; i < markersImg.rows; i++)
    {
        for (int j = 0; j < markersImg.cols; j++)
        {
            index = markersImg.at<int>(i, j);
            if (index > 0 && index <= numSegments)
            {
                dstImg.at<Vec3b>(i, j) = colors[index - 1];
            }
            else
            {
                dstImg.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
            }
        }
    }
    cout << "number of objects:" << numSegments << endl;
    namedWindow("Final Result", CV_WINDOW_AUTOSIZE);
    imshow("Final Result", dstImg);
}
int main()
{
    test();
    waitKey(0);
    return 0;
}

  • 12
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
分水岭算法(watershed algorithm)是一种用于图像分割算法,可以自动将图像分割成不同的区域。OpenCV提供了分水岭算法的实现,可以通过调用cv2.watershed()函数来进行图像分割。 使用分水岭算法进行图像分割的基本步骤如下: 1. 读取图像并将其转换为灰度图像。 2. 对灰度图像进行二值化处理,得到前景(foreground)和背景(background)。 3. 对图像进行距离变换,得到每个像素到最近的背景像素的距离。 4. 对距离变换的结果进行阈值处理,得到一张标记(markers)图像。 5. 对标记图像进行分水岭算法处理,得到分割结果。 下面是一个简单的示例代码,演示了如何使用分水岭算法对图像进行分割: ```python import cv2 import numpy as np # 读取图像并转换为灰度图像 img = cv2.imread('image.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 对灰度图像进行二值化处理 ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) # 进行距离变换 dist_transform = cv2.distanceTransform(thresh, cv2.DIST_L2, 5) ret, markers = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0) # 对标记图像进行分水岭算法处理 markers = cv2.watershed(img, markers) img[markers == -1] = [0,255,0] # 显示分割结果 cv2.imshow('Segmented Image', img) cv2.waitKey(0) cv2.destroyAllWindows() ``` 在上面的代码中,我们首先读取一张名为"image.jpg"的图像,并将其转换为灰度图像。然后利用cv2.threshold()函数对灰度图像进行二值化处理,得到前景和背景。接下来,我们使用cv2.distanceTransform()函数进行距离变换,得到每个像素到最近的背景像素的距离。然后对距离变换的结果进行阈值处理,得到一张标记图像。最后,我们利用cv2.watershed()函数对标记图像进行分水岭算法处理,得到分割结果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI炮灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值