图像处理之基于标记的分水岭算法(C++)
文章目录
- 图像处理之基于标记的分水岭算法(C++)
- 前言
- 一、基于标记点的分水岭算法应用
- 1.实现步骤:
- 2.代码实现
- 总结
前言
传统分水岭算法存在过分割的不足,OpenCV提供了一种改进的分水岭算法,使用一系列预定义标记来引导图像分割的定义方式。使用OpenCV的分水岭算法cv::wathershed,需要输入一个标记图像,图像的像素值为32位有符号正数(CV_32S类型),每个非零像素代表一个标签。**它的原理是对图像中部分像素做标记,表明它的所属区域是已知的。分水岭算法可以根据这个初始标签确定其他像素所属的区域。**传统的基于梯度的分水岭算法和改进后基于标记的分水岭算法示意图如下图所示。
从上图可以看出,传统基于梯度的分水岭算法由于局部最小值过多造成分割后的分水岭较多。而基于标记的分水岭算法,水淹过程从预先定义好的标记图像(像素)开始,较好的克服了过度分割的不足。本质上讲,基于标记点的改进算法是利用先验知识来帮助分割的一种方法。因此,改进算法的关键在于如何获得准确的标记图像,即如何将前景物体与背景准确的标记出来。
一、基于标记点的分水岭算法应用
1.实现步骤:
- 封装分水岭算法
- 获取标记图像(在标记图中前景设置为255,背景像素设置为128,未知像素设置为0)
- 将原图和标记图输入分水岭算法
- 显示结果
2.代码实现
#include <iostream>
#include <opencv.hpp>class WatershedSegmenter {private:cv::Mat markers; // 标记图public:/** @param const cv::Mat& markerImage 传入的标记图* @brief 将传入的标记图转换成CV_32S的标记图*/void setMarkers(const cv::Mat& markerImage) {// Convert to image of intsmarkerImage.convertTo(markers, CV_32S);}/** @param const cv::Mat& image 原图* @brief 对原图和标记图执行基于标记的分水岭分割*/cv::Mat process(const cv::Mat& image) {// Apply watershedcv::watershed(image, markers);return markers;}// Return result in the form of an image/** @brief 得到分割结果的标签*/cv::Mat getSegmentation() {cv::Mat tmp;// all segment with label higher than 255// will be assigned value 255markers.convertTo(tmp, CV_8U);return tmp;}// Return watershed in the form of an image以图像的形式返回分水岭cv::Mat getWatersheds() {cv::Mat tmp;//在变换前,把每个像素p转换为255p+255(在conertTo中实现)markers.convertTo(tmp, CV_8U, 255, 255);return tmp;}
};int main()
{// 读取图片std::string filepath = "F://work_study//algorithm_demo//watershedSeg.jpg";cv::Mat src = cv::imread(filepath);if (src.empty()){return -1;}imshow("src", src);// 高斯滤波平滑图像cv::Mat gaussImg;// 彩色图转换为灰度图cv::cvtColor(src, gaussImg, cv::COLOR_BGR2GRAY);cv::GaussianBlur(gaussImg, gaussImg,cv::Size(7,7),1.0);imshow("gaussImg", gaussImg);// 形态学梯度cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));cv::morphologyEx(gaussImg, gaussImg, cv::MORPH_GRADIENT,kernel);imshow("morphologyEx", gaussImg);// OTSU大津法阈值cv::threshold(gaussImg, gaussImg, 0, 255, cv::THRESH_OTSU | cv::THRESH_BINARY);imshow("threshold", gaussImg);// 形态学操作,生成确定的背景区域cv::Mat kernel_bg = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(7, 7)); 开运算(先腐蚀后膨胀)消除噪声点//cv::morphologyEx(gaussImg, gaussImg, cv::MORPH_OPEN, kernel1,cv::Point(-1,-1),2);// 生成确定的背景区域cv::Mat backgroundImg;cv::dilate(gaussImg, backgroundImg, kernel_bg);imshow("backgroundImg", backgroundImg);// 距离变换,生成确定的前景区域cv::Mat distanceImg,foregroundImg;cv::distanceTransform(gaussImg, distanceImg, cv::DIST_L1,5);double min, max;cv::minMaxLoc(distanceImg, &min, &max);cv::threshold(distanceImg, distanceImg, 0.1 * max, 255, cv::THRESH_BINARY);cv::normalize(distanceImg, foregroundImg, 0, 255, cv::NORM_MINMAX, CV_8U);imshow("foregroundImg", foregroundImg);// 去除连通域中的背景部分cv::Mat unknownImg;cv::subtract(backgroundImg, foregroundImg, unknownImg); //待定区域,减去前景与背景的重合区域imshow("unknownImg", unknownImg);cv::Mat makers(gaussImg.size(),CV_8U,cv::Scalar(128));for (int i = 0; i < makers.rows; i++){for (int j = 0; j < makers.cols; j++){if (foregroundImg.at<uchar>(i, j) == 255){makers.at<uchar>(i, j) = 255;}else if (unknownImg.at<uchar>(i, j) == 255){makers.at<uchar>(i, j) = 0;}}}imshow("makers", makers);// 分水岭算法分割图像WatershedSegmenter seg; // 实例化一个分水岭分割对象seg.setMarkers(makers); // 设置算法的标记图像,使得水淹过程从这组预定义好的标记像素开始seg.process(src); // 传入待分割的原图(要求为CV_8UC3)cv::Mat resultImg;resultImg = seg.getSegmentation(); // 将修改后的标记图makers转换为可显示的8位灰度图并返回分割结果(白色为前景,灰色为背景,0为边缘)cv::threshold(resultImg, resultImg, 250, 1, cv::THRESH_BINARY);cv::cvtColor(resultImg, resultImg, cv::COLOR_GRAY2BGR);resultImg = src.mul(resultImg);cv::cvtColor(resultImg, resultImg, cv::COLOR_BGR2GRAY);std::cout << resultImg.type() << std::endl;for(int i=0;i<resultImg.rows;i++)for (int j = 0; j < resultImg.cols; j++){if (resultImg.at<uchar>(i, j) == 0){resultImg.at<uchar>(i, j) = 255;}}imshow("resultImg", resultImg);cv::waitKey(0);return 0;}
总结
本文主要介绍了一种基于标记的分水岭算法的应用,关键在于如何巧妙地设计“标记图”,欢迎讨论交流。