- 操作系统:ubuntu22.04
- OpenCV版本:OpenCV4.9
- IDE:Visual Studio Code
- 编程语言:C++11
描述
我们将学会使用基于标记的分水岭算法来进行图像分割。我们将看到:watershed()函数的用法。
任何灰度图像都可以被视为一个地形表面,其中高强度对应着山峰和丘陵,而低强度则对应着山谷。你可以想象,从每个孤立的山谷(局部最小值)开始,用不同颜色的水(标记)来填充。随着水位上升,依据附近的山峰(梯度),来自不同山谷的水,显然带有不同的颜色,将会开始融合。为了避免这种情况发生,你必须在水开始汇合的地方建立起屏障。你持续进行填充水和构建屏障的工作,直到所有的山峰都被水覆盖。此时,你所建立的这些屏障就构成了分割的结果。这就是分水岭算法背后的理念。你可以在CMM网页上关于分水岭的页面,通过观看一些动画来更直观地理解这个概念。
但是,这种方法会因为图像中的噪声或其他不规则性而导致过度分割的结果。因此,OpenCV实现了一种基于标记的分水岭算法,其中你指明了哪些山谷点应该被合并,哪些不应该。这是一种交互式的图像分割方式。我们所做的就是给已知的对象赋予不同的标记。将我们确信属于前景或对象的区域标记为一种颜色(或强度),将我们确信属于背景或非对象的区域标记为另一种颜色,最后,对于那些我们不确定的区域,我们将其标记为0。这就是我们的标记。接着,应用分水岭算法。随后,我们的标记将被更新为我们给予的标签,而对象的边界将拥有一个值为-1的特殊标记。
代码
假设我们有一张硬币的图像,其中硬币彼此接触。即使你对图像进行了阈值处理,硬币的边缘仍然会粘连在一起,原图如下:
我们开始着手于对硬币数量进行一个大致的估算。为此,我们可以使用大津的二值化方法(Otsu’s binarization)。
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <cstdio>
#include <iostream>
#include <opencv2/core/utility.hpp>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
Mat img = imread( "/media/dingxin/data/study/OpenCV/sources/images/water_coins.jpg", 1 ), imgGray;
if ( img.empty() )
{
cout << "Couldn't open image " << std::endl;
return 0;
}
cvtColor( img, imgGray, COLOR_BGR2GRAY );
// 二值化图像
cv::Mat binary;
cv::threshold( imgGray, binary, 150, 255, cv::THRESH_BINARY_INV+cv::THRESH_OTSU );
cv::imshow( "Original Image", img );
cv::imshow( "Gray Image", imgGray );
cv::imshow( "binary Image", binary );
cv::waitKey( 0 );
return 0;
}
运行结果:
现在我们需要去除图像中的任何细小的白色噪声。为此,我们可以使用形态学开运算。为了消除物体上的任何微小孔洞,我们可以使用形态学闭运算。因此,我们现在可以确信,靠近物体中心的区域是前景,而远离物体的区域则是背景。唯一不确定的区域是硬币的边界区域。
所以我们需要提取那些我们确信是硬币的区域。腐蚀操作可以移除边界像素。因此,剩下的区域,我们可以确信那就是硬币。这在物体彼此不接触的情况下是可行的。但由于它们相互接触,另一个好的选择是找到距离变换并应用一个适当的阈值。接下来我们需要找出那些我们确信不是硬币的区域。为此,我们对结果进行膨胀处理。膨胀操作会使物体边界扩展到背景。这样一来,我们就可以确保结果中处于背景中的任何区域确实是背景,因为边界区域已经被去除了。请参见下图。
剩余的区域是我们无法确定是硬币还是背景的部分。这些不确定区域通常位于硬币边界处,也就是前景与背景相遇的地方(甚至可能是两个不同硬币相遇的区域)。我们称这部分区域为边界区域。边界区域可以通过从确定的背景区域(sure_bg)中减去确定的前景区域(sure_fg)得到。
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <cstdio>
#include <iostream>
#include <opencv2/core/utility.hpp>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
Mat img = imread( "/media/dingxin/data/study/OpenCV/sources/images/water_coins.jpg", 1 ), imgGray;
if ( img.empty() )
{
cout << "Couldn't open image " << std::endl;
return 0;
}
cvtColor( img, imgGray, COLOR_BGR2GRAY );
// 二值化图像
cv::Mat binary;
cv::threshold( imgGray, binary, 150, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU );
// noise removal
cv::Mat kernel = cv::Mat::ones( 3, 3, CV_8UC1 ) * 255;
// 执行开运算
cv::Mat opening;
cv::morphologyEx( binary, opening, cv::MORPH_OPEN, kernel, cv::Point( -1, -1 ), 2 ); // 迭代次数为2
cv::Mat sure_bg;
// 执行膨胀操作
cv::dilate(opening, sure_bg, kernel, cv::Point(-1,-1), 3); // 迭代次数为3
cv::Mat dist_transform;
// 执行距离变换
cv::distanceTransform(opening, dist_transform, cv::DIST_L2, 3);
cv::Mat sure_fg;
double maxVal;
// 查找矩阵中的最大值
cv::minMaxLoc(dist_transform, nullptr, &maxVal);
// 设置阈值
double thresholdValue = 0.7 * maxVal;
cv::threshold(dist_transform, sure_fg, thresholdValue, 255, cv::THRESH_BINARY);
// Finding unknown region
sure_fg.convertTo(sure_fg, CV_8U);
cv::Mat unknown;
// 执行矩阵相减操作
cv::subtract(sure_bg, sure_fg, unknown);
// cv::imshow( "原始图", img );
// cv::imshow( "灰度图", imgGray );
// cv::imshow( "二值化后的图", binary );
cv::imshow( "sure_fg", sure_fg );
cv::imshow( "dist_transform", dist_transform );
cv::waitKey( 0 );
return 0;
}
在阈值处理后的图像中,如下图,我们可以看到一些硬币区域,我们确信这些区域属于硬币,并且它们现在是分离的。在某些情况下,你可能只对前景分割感兴趣,而不关心相互接触的物体是否分离。在这种情况下,你不需要使用距离变换,仅仅使用腐蚀操作就足够了。腐蚀操作其实只是另一种提取确定前景区域的方法,仅此而已。
现在我们已经确定了哪些区域属于硬币,哪些属于背景。因此,我们可以创建一个标记(marker)图像,它与原始图像具有相同的尺寸,但数据类型为int32。在这个标记图像中,我们将确定的区域(无论是前景还是背景)标记为不同的正整数,而不确定的区域则保持为零。
在OpenCV中,我们可以使用cv::connectedComponentsWithStats函数来实现这一目的。该函数会将图像的背景标记为0,其他对象则从1开始分配不同的整数标签。然而,正如你所提到的,如果背景被标记为0,那么在Watershed算法中,它将被视为未知区域。为了避免这种情况,我们应该将未知区域,即由unknown定义的区域,标记为0,而将背景标记为一个不同的整数。
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <cstdio>
#include <iostream>
#include <opencv2/core/utility.hpp>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
Mat img = imread( "/media/dingxin/data/study/OpenCV/sources/images/water_coins.jpg", 1 ), imgGray;
if ( img.empty() )
{
cout << "Couldn't open image " << std::endl;
return 0;
}
cvtColor( img, imgGray, COLOR_BGR2GRAY );
// 二值化图像
cv::Mat binary;
cv::threshold( imgGray, binary, 150, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU );
// noise removal
cv::Mat kernel = cv::Mat::ones( 3, 3, CV_8UC1 ) * 255;
// 执行开运算
cv::Mat opening;
cv::morphologyEx( binary, opening, cv::MORPH_OPEN, kernel, cv::Point( -1, -1 ), 2 ); // 迭代次数为2
cv::Mat sure_bg;
// 执行膨胀操作
cv::dilate( opening, sure_bg, kernel, cv::Point( -1, -1 ), 3 ); // 迭代次数为3
cv::Mat dist_transform;
// 执行距离变换
cv::distanceTransform( opening, dist_transform, cv::DIST_L2, 3 );
cv::Mat sure_fg;
double maxVal;
// 查找矩阵中的最大值
cv::minMaxLoc( dist_transform, nullptr, &maxVal );
// 设置阈值
double thresholdValue = 0.7 * maxVal;
cv::threshold( dist_transform, sure_fg, thresholdValue, 255, cv::THRESH_BINARY );
// Finding unknown region
sure_fg.convertTo( sure_fg, CV_8U );
cv::Mat unknown;
// 执行矩阵相减操作
cv::subtract( sure_bg, sure_fg, unknown );
// Marker labelling
cv::Mat markers; // 将会存储标记结果
// 执行连通组件标记
int num_labels = cv::connectedComponents( sure_fg, markers );
cv::Mat ones = cv::Mat::ones( markers.size(), markers.type() );
// 将 markers 矩阵的所有元素值增加1
cv::add( markers, ones, markers );
// 创建一个与 markers 大小相同的掩码矩阵,其中 unknown 矩阵中值为255的位置为 true,其余位置为 false
cv::Mat mask = unknown == 255;
// 将 markers 矩阵中对应于 mask 矩阵中 true 的位置的元素设置为0
markers.setTo( 0, mask );
// 创建一个与原图像大小相同的输出图像
cv::Mat colorImage;
// 将灰度图像转换为具有Jet色彩映射的彩色图像
cv::applyColorMap(mask, colorImage, cv::COLORMAP_JET);
// Add one to all labels so that sure background is not 0, but 1
// cv::imshow( "原始图", img );
// cv::imshow( "灰度图", imgGray );
// cv::imshow( "二值化后的图", binary );
cv::imshow( "sure_fg", sure_fg );
cv::imshow( "dist_transform", dist_transform );
cv::imshow( "mask", colorImage );
cv::waitKey( 0 );
return 0;
}
在应用了JET色彩映射的结果中,红色区域代表了未知区域,这是在硬币分割过程中尚未确定为硬币或背景的部分。确定的硬币区域则被赋予了不同的色彩值。而确定为背景的区域则以较浅的蓝色显示,与未知区域的红色色形成对比。
现在我们的标记图像已经准备好了,下一步就是应用Watershed算法。一旦应用了Watershed算法,标记图像将会被修改。在硬币和背景之间的边界区域将会被标记为-1,这是OpenCV中Watershed算法的一个特性,它用-1来表示分割出的边界区域。
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <cstdio>
#include <iostream>
#include <opencv2/core/utility.hpp>
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
Mat img = imread( "/media/dingxin/data/study/OpenCV/sources/images/water_coins.jpg", 1 ), imgGray;
if ( img.empty() )
{
cout << "Couldn't open image " << std::endl;
return 0;
}
cvtColor( img, imgGray, COLOR_BGR2GRAY );
// 二值化图像
cv::Mat binary;
cv::threshold( imgGray, binary, 150, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU );
// noise removal
cv::Mat kernel = cv::Mat::ones( 3, 3, CV_8UC1 ) * 255;
// 执行开运算
cv::Mat opening;
cv::morphologyEx( binary, opening, cv::MORPH_OPEN, kernel, cv::Point( -1, -1 ), 2 ); // 迭代次数为2
cv::Mat sure_bg;
// 执行膨胀操作
cv::dilate( opening, sure_bg, kernel, cv::Point( -1, -1 ), 3 ); // 迭代次数为3
cv::Mat dist_transform;
// 执行距离变换
cv::distanceTransform( opening, dist_transform, cv::DIST_L2, 3 );
cv::Mat sure_fg;
double maxVal;
// 查找矩阵中的最大值
cv::minMaxLoc( dist_transform, nullptr, &maxVal );
// 设置阈值
double thresholdValue = 0.7 * maxVal;
cv::threshold( dist_transform, sure_fg, thresholdValue, 255, cv::THRESH_BINARY );
// Finding unknown region
sure_fg.convertTo( sure_fg, CV_8U );
cv::Mat unknown;
// 执行矩阵相减操作
cv::subtract( sure_bg, sure_fg, unknown );
// Marker labelling
cv::Mat markers; // 将会存储标记结果
// 执行连通组件标记
int num_labels = cv::connectedComponents( sure_fg, markers );
cv::Mat ones = cv::Mat::ones( markers.size(), markers.type() );
// 将 markers 矩阵的所有元素值增加1
cv::add( markers, ones, markers );
// 创建一个与 markers 大小相同的掩码矩阵,其中 unknown 矩阵中值为255的位置为 true,其余位置为 false
cv::Mat mask = unknown == 255;
// 将 markers 矩阵中对应于 mask 矩阵中 true 的位置的元素设置为0
markers.setTo( 0, mask );
// 创建一个与原图像大小相同的输出图像
cv::Mat colorImage;
// 将灰度图像转换为具有Jet色彩映射的彩色图像
cv::applyColorMap(mask, colorImage, cv::COLORMAP_JET);
cv::imshow( "原始图", img );
cv::watershed(img, markers);
mask = markers == -1;
img.setTo(cv::Scalar(255, 0, 0), mask);
cv::imshow( "watershed", img );
cv::waitKey( 0 );
return 0;
}