分水岭算法的思想是把图像看作是一个拓扑地貌,同类区域就相当于陡峭边缘内相对平摊的盆地。当从高度为0开始逐步用“水”淹没图像时,会形成好多个聚水的盆地,随着盆地的面积逐渐增大,两个盆地的水最终会汇合到一起,这时就需要创建一个分水岭把这两个盆地分割开。当水位达到最大高度时,创建的盆地和分水岭就组成了分水岭分割图。
二、实现过程 1、数据准备本实验需要一张原始图像,一张原始图像对应的二值图像,注意:这两张照片的尺寸必须一致,不然会报错。如果没有原始图像的二值图像,可以用以下代码转换。(假设原始图像是RGB图像,转换过程是:RGB—>灰度图—>二值图)
cv::Mat image3; cv::cvtColor(image, image3, CV_BGR2GRAY); cv::imwrite("dog_gray.jpg", image3);//RGB转换灰度图像
void Thresholded(cv::Mat image) { cv::Mat thresholded; // 定义输出的二值图像 cv::threshold(image, thresholded, 70, // 阈值 255, // 对超过阈值的像素赋值 cv::THRESH_BINARY); // 阈值化类型 cv::bitwise_not(thresholded, thresholded);//对图像做反向处理,白色作为前景物体,黑色作为背景 cv::imshow("thresholded", thresholded); cv::imwrite("image_2.jpg", thresholded);//输出二值化后的图像,对其进行后续处理 }
原图
原图对应的二值图
2、形态学运算:腐蚀和膨胀腐蚀是把当前像素替换成所定义像素集合中的最小像素值;膨胀是腐蚀的反运算,把当前像素值替换成所定义像素集合中的最大像素值。由于输入的二值图像只包含黑色(值为0)和白色(值为255)像素,因此每个像素都会被替换成白色和黑色像素。
要形象地理解这两种运算的作用,可考虑背景(黑色)和前景(白色)的物体。腐蚀时,如果结构元素放到某个像素位置时碰到了背景(即交集中有一个像素是黑色的),那么这个像素就变为背景;膨胀时,如果结构元素放到某个背景像素位置时碰到了前景物体,那么这个像素就被标为白色。
(1)腐蚀图像
对图像做深度腐蚀运算,只保留明显属于前景的像素,参数cv::Point(-1,-1)表示原点是矩阵的中心点,也可以定义在结构元素上的其他位置。//消除噪声和细小物体 cv::Mat fg; //前景图 cv::erode(image2, fg, cv::Mat(), cv::Point(-1, -1), 4);//腐蚀图像4次 cv::imshow("Foreground Image", fg);
(2)膨胀图像
对图像做膨胀运算,来选择一些背景像素,得到的黑色像素对应背景像素,在膨胀后要立即通过阈值化运算把他们赋值为128。
// 标识不含物体的图像像素 cv::Mat bg; cv::dilate(image2, bg, cv::Mat(), cv::Point(-1, -1), 4);//膨胀图像4次 cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV); cv::imshow("Background Image", bg);
(3)合并图像
合并这两幅图像,得到标记图像
// 创建标记图像 cv::Mat markers(image2.size(), CV_8U, cv::Scalar(0)); markers = fg + bg;//合并图像,得到标记图像 cv::imshow("markers", markers);
在这个合并的图像中,白色区域属于前景物体,灰色区域属于背景,黑色区域属于未知标签。
3、分水岭算法分水岭算法就是将合并的图像中,前景和背景区分开,并对黑色区域的像素做出标记(属于前景还是背景)。我创建了一个关于分水岭函数的类WatershedSegmenter。
class WatershedSegmenter { private: cv::Mat markers; public: void setMarkers(const cv::Mat& markerImage) { //转换成整数型图像 markerImage.convertTo(markers, CV_32S); } cv::Mat process(const cv::Mat &image) { //应用分水岭函数 //输入对象是一个标记图像,图像的像素值为32位有符号整数,每个非零像素代表一个标签 cv::watershed(image, markers); return markers; } }
//创建分水岭分割类的对象 WatershedSegmenter segmenter; //设置标记图像,然后执行分割过程 segmenter.setMarkers(markers); segmenter.process(image);4、结果展示
在结果输出时,会修改标记图像,每个值为0的像素会被赋予一个输入标签,而边缘处的像素赋值为-1。
返回标签组成的图像(包含值为0的分水岭)。
// 以图像的形式返回结果 cv::Mat getSegmentation() { cv::Mat tmp; // 所有标签值大于 255 的区段都赋值为 255 markers.convertTo(tmp, CV_8U); return tmp; }
标签图像
返回一幅图像,图像中分水岭线条赋值为0,其他部分赋值为255。
// 以图像的形式返回分水岭 cv::Mat getWatersheds() { cv::Mat tmp; // 在变换前,把每个像素 p 转换为 255p+255 markers.convertTo(tmp, CV_8U, 255, 255); return tmp; }
边缘图像
三、原理补充在调用cv::watershed函数时,执行了这样的过程,在水淹过程的开始阶段会创建很多细小的独立盆地。当所有盆地汇合时,就会创建很多分水岭线条,导致图像被过度分割。要解决这个问题,就要对这个算法进行修改,使水淹过程从一组预先定义好的标记像素开始。每个用标记创建的盆地,都按照初始标记的值加上标签。如果两个标签相同的盆地汇合,就不创建分水岭,以避免过度分割。
四、其他实验结果五、完整代码
#include#include //图像数据结构的核心 #include //所有图形接口函数 #include #include #include #include using namespace std; class WatershedSegmenter { private: cv::Mat markers; public: void setMarkers(const cv::Mat& markerImage) { //转换成整数型图像 markerImage.convertTo(markers, CV_32S); } cv::Mat process(const cv::Mat &image) { //应用分水岭函数 //输入对象是一个标记图像,图像的像素值为32位有符号整数,每个非零像素代表一个标签 cv::watershed(image, markers); return markers; } // 以图像的形式返回结果 cv::Mat getSegmentation() { cv::Mat tmp; // 所有标签值大于 255 的区段都赋值为 255 markers.convertTo(tmp, CV_8U); return tmp; } // 以图像的形式返回分水岭 cv::Mat getWatersheds() { cv::Mat tmp; // 在变换前,把每个像素 p 转换为 255p+255 markers.convertTo(tmp, CV_8U, 255, 255); return tmp; } }; void Thresholded(cv::Mat image) { cv::Mat thresholded; // 定义输出的二值图像 cv::threshold(image, thresholded, 70, // 阈值 255, // 对超过阈值的像素赋值 cv::THRESH_BINARY); // 阈值化类型 cv::bitwise_not(thresholded, thresholded);//对图像做反向处理,白色作为前景物体,黑色作为背景 cv::imshow("thresholded", thresholded); cv::imwrite("dog_2.jpg", thresholded);//输出二值化后的图像,对其进行后续处理 } int main() { cv::Mat image = cv::imread("group.jpg"); if (!image.data) return 0; //RGB转换灰度图 //Thresholded(image);//对图像做二值化处理 cv::Mat image2 = cv::imread("binary.bmp",0); //读二值图像 //消除噪声和细小物体 cv::Mat fg; //前景图 cv::erode(image2, fg, cv::Mat(), cv::Point(-1, -1), 4);//腐蚀图像4次 cv::imshow("Foreground Image", fg); // 标识不含物体的图像像素 cv::Mat bg; cv::dilate(image2, bg, cv::Mat(), cv::Point(-1, -1), 4);//膨胀图像4次 cv::threshold(bg, bg, 1, 128, cv::THRESH_BINARY_INV); cv::imshow("Background Image", bg); // 创建标记图像 cv::Mat markers(image2.size(), CV_8U, cv::Scalar(0)); markers = fg + bg;//合并图像,得到标记图像 cv::imshow("markers", markers); //创建分水岭分割类的对象 WatershedSegmenter segmenter; //设置标记图像,然后执行分割过程 segmenter.setMarkers(markers); segmenter.process(image); cv::imshow("Segmentation", segmenter.getSegmentation()); cv::imshow("Watersheds", segmenter.getWatersheds()); cv::waitKey(0); return 0; }
本篇文章是我学习opencv做的笔记,可能存在许多不足,欢迎大家批评指正!有问题可以随时和我交流。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)