OpenCV_距离变换的图像分割和Watershed算法详解
在学习watershed算法的时候,书写代码总会出现一些错误:
上述代码运行报错,显示OpenCV(4.10.0) Error: Assertion failed (src.type() == CV_8UC3 && dst.type() == CV_32SC1) in cv::watershed
查找资料:目前已解决
这个错误明确指出是在 watershed 函数中输入的参数类型不匹配。根据错误信息,要求 src (即 image )的类型是 CV_8UC3 , dst (即 markers )的类型是 CV_32SC1 。
您需要确保 image 是 8 位无符号的 3 通道彩色图像,并且 markers 是 32 位有符号的单通道矩阵。
请检查以下几点:
1. image 的初始化和类型是否正确。
2. markers 的创建和后续操作是否保持了 CV_32SC1 类型。
如果问题仍然存在,请提供更多关于 image 的初始化和加载的代码部分,以便更准确地帮助您找到问题所在。
分水岭算法
分水岭算法的基本原理为:将任意的灰度图像视为地形图表面,其中灰度值高的部分表示山峰和丘陵,而灰度值低的地方表示山谷。用不同颜色的水(标签)填充每个独立的山谷(局部最小值);随着水平面的上升,来自不同山谷(具有不同颜色)的水将开始合并。为了避免出现这种情况,需要在水汇合的位置建造水坝;持续填充水和建造水坝,直到所有的山峰和丘陵都在水下。整个过程中建造的水坝将作为图像分割的依据。
使用分水岭算法执行图像分割操作时通常包含下列步骤:
(1) 加载源图像并检查是否加载没有任何问题,然后显示
(2) 将原图转换为灰度图像,然后显示
(3) 应用形态学变换中的开运算和膨胀操作,去除图像噪声,获取图像边缘信息,确定图像背景
(4) 显示矩阵marks(只为显示,可省略)
(5) 执行分水岭算法分割图像
(6) 对每一个区域进行颜色填充
(7) 跟原始图像融合
//生成随机颜色函数
Vec3b randomColor(int value);
Vec3b randomColor(int value) {
value = value % 255;
RNG rng;
int aa = rng.uniform(0, value);
int bb = rng.uniform(0, value);
int cc = rng.uniform(0, value);
return Vec3b(aa, bb, cc);
}
void QuickDemo::thirtyFive(Mat& image) {
//灰度化,滤波,Canny边缘检测
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
GaussianBlur(gray, gray, Size(5, 5), 2);
Canny(gray, gray, 80, 150);
//imshow("gray", gray);
//查找轮廓
vector<vector<Point>>contours;
vector<Vec4i>hierarchy;
findContours(gray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
Mat contoursImage = Mat::zeros(image.size(), CV_8UC1);//轮廓
Mat marks(image.size(), CV_32S); //Opencv分水岭第二个矩阵参数
marks = Scalar::all(0);
//int index = 0;
//int compCount = 0;
for (size_t i = 0; i < contours.size(); i++)
{
//对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
drawContours(marks, contours, i, Scalar::all(i + 1), 1, 8, hierarchy);
drawContours(contoursImage, contours, i, Scalar(255), 1, 8, hierarchy);
}
//我们来看一下传入的矩阵marks里是什么东西
//Mat marksShows;
//convertScaleAbs(marks,marksShows);
//imshow("marksShows", marksShows);
//imshow("contoursImage", contoursImage);
watershed(image, marks);
//我们再来看一下分水岭算法之后的矩阵marks里是什么东西
Mat afterwatershed;
convertScaleAbs(marks, afterwatershed);
//imshow("afterwatershed", afterwatershed);
//对每一个区域进行颜色填充
Mat perspectiveImage = Mat::zeros(image.size(), CV_8UC3);
for (size_t i = 0; i < marks.rows; i++)
{
for (size_t j = 0; j < marks.cols; j++)
{
int index = marks.at<int>(i, j);
if (index == -1) {
perspectiveImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
}
else {
perspectiveImage.at<Vec3b>(i, j) = randomColor(index);
}
}
}
imshow("perspectiveImage", perspectiveImage);
//分割并填充颜色的结果跟原始图像融合
Mat wshed;
addWeighted(image, 0.4, perspectiveImage, 0.6, 0, wshed);
imshow("wshed", wshed);
}
说明/结果
1.加载源图像并检查是否加载没有任何问题,然后显示:
2.将原图转换为灰度图像,然后显示:
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
imshow("gray", gray);
3.应用形态学变换中的开运算和膨胀操作,去除图像噪声,获取图像边缘信息,确定图像背景
GaussianBlur(gray, gray, Size(5, 5), 2);
Canny(gray, gray, 80, 150);
imshow("gray", gray);
//查找轮廓
vector<vector<Point>>contours;
vector<Vec4i>hierarchy;
findContours(gray, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
Mat contoursImage = Mat::zeros(image.size(), CV_8UC1);//轮廓
Mat marks(image.size(), CV_32S); //Opencv分水岭第二个矩阵参数
marks = Scalar::all(0);
//int index = 0;
//int compCount = 0;
for (size_t i = 0; i < contours.size(); i++)
{
//对marks进行标记,对不同区域的轮廓进行编号,相当于设置注水点,有多少轮廓,就有多少注水点
drawContours(marks, contours, i, Scalar::all(i + 1), 1, 8, hierarchy);
drawContours(contoursImage, contours, i, Scalar(255), 1, 8, hierarchy);
}
imshow("contoursImage", contoursImage);
4.显示矩阵marks(只为显示,可省略)
//我们来看一下传入的矩阵marks里是什么东西
Mat marksShows;
convertScaleAbs(marks,marksShows);
imshow("marksShows", marksShows);
5.执行分水岭算法分割图像
watershed(image, marks);
//我们再来看一下分水岭算法之后的矩阵marks里是什么东西
Mat afterwatershed;
convertScaleAbs(marks, afterwatershed);
imshow("afterwatershed", afterwatershed);
6.对每一个区域进行颜色填充
//生成随机颜色函数
Vec3b randomColor(int value);
Vec3b randomColor(int value) {
value = value % 255;
RNG rng;
int aa = rng.uniform(0, value);
int bb = rng.uniform(0, value);
int cc = rng.uniform(0, value);
return Vec3b(aa, bb, cc);
}
//对每一个区域进行颜色填充
Mat perspectiveImage = Mat::zeros(image.size(), CV_8UC3);
for (size_t i = 0; i < marks.rows; i++)
{
for (size_t j = 0; j < marks.cols; j++)
{
int index = marks.at<int>(i, j);
if (index == -1) {
perspectiveImage.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
}
else {
perspectiveImage.at<Vec3b>(i, j) = randomColor(index);
}
}
}
imshow("perspectiveImage", perspectiveImage);
7.跟原始图像融合
Mat wshed;
addWeighted(image, 0.4, perspectiveImage, 0.6, 0, wshed);
imshow("wshed", wshed);
距离变换实现分水岭算法
实现步骤:
1.输入图像
2.灰度化
3.二值化
4.执行距离变换
5.归一化
6.二值化
7.生成marker:通过findContours+drawContours来创建一个marker
8.将7生成的marker放入分水岭函数:watershed
9.给marker着色
10.输出着色后的图像
此算法关键点在于生成marker。生成marker之后其实已经完成了算法,后面的着色只是为了让输出更加好看。
void QuickDemo::thirtyFour(Mat& image) {
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
Mat binary;
threshold(gray, binary, 175, 255, THRESH_BINARY);
//imshow("binary image ",bw);
Mat dist;
distanceTransform(binary, dist, DIST_L2, 3);
normalize(dist, dist, 0, 1, NORM_MINMAX);
threshold(dist, dist, 0.1, 1, THRESH_BINARY);
normalize(dist, dist, 0, 255, NORM_MINMAX);
dist.convertTo(dist, CV_8UC1);
imshow("distanceTransform", dist);
vector<vector<Point>>contours;
findContours(dist, contours, RETR_CCOMP, CHAIN_APPROX_SIMPLE, Point());
vector<Vec4i>hierarchy;
Mat markers(dist.size(), CV_8U); //Opencv分水岭第二个矩阵参数
markers = Scalar::all(0);
for (size_t i = 0; i < contours.size(); i++)
{
drawContours(markers, contours, i, Scalar::all(i + 1), -1, 8, hierarchy, INT_MAX);
}
circle(markers, Point(3, 3), 3, Scalar(255), -1);
//imshow("markers", markers*20);
image.convertTo(image, CV_8UC3);
markers.convertTo(markers, CV_32SC1);
watershed(image, markers);
markers.convertTo(markers, CV_8UC1);
imshow("markers", markers*50);
RNG rng(12345);
vector<Vec3b>colors;
for (size_t i = 0; i < contours.size(); i++)
{
int r = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int b = rng.uniform(0, 255);
colors.push_back(Vec3b(r, g, b));
}
//给marker着色
Mat finalResult = Mat::zeros(dist.size(), CV_8UC3);//三通道彩色图像
int index = 0;
for (int row = 0; row < markers.rows; row++) {
for (int col = 0; col < markers.cols; col++) {
index = markers.at<uchar>(row, col);
if (index > 0 && index <= contours.size()) {
finalResult.at<Vec3b>(row, col) = colors[index - 1];
}
else {
finalResult.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
}
}
}
imshow("finalResult", finalResult);
}
说明/结果
1. 输入图像并显示
2.获取灰度图像并显示
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
imshow("gray image ", gray);
3.获取二值图像并显示
Mat binary;
threshold(gray, binary, 175, 255, THRESH_BINARY);
imshow("binary image ", binary);
4.执行距离变换归一化,并显示
Mat dist;
distanceTransform(binary, dist, DIST_L2, 3);
normalize(dist, dist, 0, 1, NORM_MINMAX);
imshow("dist image ", dist);
5.进行二值变换归一化,并显示
threshold(dist, dist, 0.1, 1, THRESH_BINARY);
normalize(dist, dist, 0, 255, NORM_MINMAX);
dist.convertTo(dist, CV_8UC1);
imshow("distanceTransform", dist);
6.生成marker:通过findContours+drawContours来创建一个marker
vector<vector<Point>>contours;
findContours(dist, contours, RETR_CCOMP, CHAIN_APPROX_SIMPLE, Point());
vector<Vec4i>hierarchy;
Mat markers(dist.size(), CV_8U); //Opencv分水岭第二个矩阵参数
markers = Scalar::all(0);
for (size_t i = 0; i < contours.size(); i++)
{
drawContours(markers, contours, i, Scalar::all(i + 1), -1, 8, hierarchy, INT_MAX);
}
circle(markers, Point(3, 3), 3, Scalar(255), -1);
imshow("markers", markers*20);
7.将7生成的marker放入分水岭函数:watershed
image.convertTo(image, CV_8UC3);
markers.convertTo(markers, CV_32SC1);
watershed(image, markers);
markers.convertTo(markers, CV_8UC1);
imshow("markers", markers*50);
8.给marker着色,并输出着色后的图像
RNG rng(12345);
vector<Vec3b>colors;
for (size_t i = 0; i < contours.size(); i++)
{
int r = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int b = rng.uniform(0, 255);
colors.push_back(Vec3b(r, g, b));
}
//给marker着色
Mat finalResult = Mat::zeros(dist.size(), CV_8UC3);//三通道彩色图像
int index = 0;
for (int row = 0; row < markers.rows; row++) {
for (int col = 0; col < markers.cols; col++) {
index = markers.at<uchar>(row, col);
if (index > 0 && index <= contours.size()) {
finalResult.at<Vec3b>(row, col) = colors[index - 1];
}
else {
finalResult.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
}
}
}
imshow("finalResult", finalResult);
9.跟原始图像融合
Mat wdst;
addWeighted(finalResult,0.6,image,0.4,0, wdst);
imshow("wdst", wdst);
threshold函数
threshold(
InputArray src,
OutputArray dst,
double thresh,
double maxval,
type
);
//参数1:输入的灰度图像
//参数2:输出图像
//参数3:进行阈值操作时阈值的大小
//参数4:设定的最大灰度值(该参数运用在二进制与反二进制阈值操作中)
//参数5:阈值的类型。从下面提到的5种中选择出的结果
THRESH_BINARY=0: 二进制阈值
THRESH_BINARY_INV=1: 反二进制阈值
THRESH_TRUNC=2: 截断阈值
THRESH_TOZERO=3: 0阈值
THRESH_TOZERO_INV=4: 反0阈值
THRESH_OTSU=8 自适应閾值
(1)正向二值化,THRESH_BINARY
正向二值化,如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为maxval;否则,将该点的像素值设置为0;
(2)反向二值化,THRESH_BINARY_INV
反向二值化,如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为0;否则,将该点的像素值设置为maxval
(3)THRESH_TRUNC
如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为threshold;否则,将该点的像素值不变
(4)THRESH_TOZERO
如果当前的像素值大于设置的阈值(thresh),则将该点的像素值不变;否则,将该点的像素值设置为0
THRESH_TOZERO_INV
如果当前的像素值大于设置的阈值(thresh),则将该点的像素值设置为0;否则,将该点的像素值不变
adaptiveThreshold函数
void adaptiveThreshold(
InputArray src,
OutputArray dst,
double maxValue,
int adaptiveMethod,
int thresholdType,
int blockSize,
double C
);
第一个参数,InputArray src,原图,即输入图像,是一个8位单通道的图像;
第二个参数,OutputArray dst,目标图像,与原图像具有同样的尺寸与类型;
第三个参数,double maxValue,分配给满足条件的像素的非零值;
第四个参数,int adaptiveMethod,自适应阈值的方法,通常有以下几种方法;
ADAPTIVE_THRESH_MEAN_C,阈值T(x,y)是(x,y)减去C的Blocksize×Blocksize
邻域的平均值。
ADAPTIVE_THRESH_GAUSSIAN_C ,阈值T(x,y)是(x,y)减去C的Blocksize×Blocksize
邻域的加权和(与高斯相关),默认sigma(标准差)用于指定的Blocksize;具体的情况可以参见getGaussianKernel函数;
第五个参数,int thresholdType,阈值的类型必须是以下两种类型,
THRESH_BINARY,正向二值化
THRESH_BINARY_INV ,反向二值化
第六个参数,int blockSize,计算blocksize x blocksize大小的领域内的阈值,必须为奇数,
例如,3,5,7等等,一般二值化使用21,31,41;
第七个参数,double C,从平均数或加权平均数减去常量。通常,它是正的,但也可能是零或负数。
二值化时使用的7。
补充
函数cvAdaptiveThreshold的确可以将灰度图像二值化,但它的主要功能应该是边缘提取,
关键是里面的block_size参数,该参数是决定局部阈值的block的大小
1)当block很小时,如block_size=3 or 5 or 7时,“自适应”的程度很高,
即容易出现block里面的像素值都差不多,这样便无法二值化,而只能在边缘等梯度大的地方实现二值化
,结果显得它是边缘提取函数;
2)当把block_size设为比较大的值时,如block_size=21 or 31 or 41时,cvAdaptiveThreshold便是
二值化函数了;
3)src与dst 这两个都要是单通道的图像。
findContours函数
void findContours (
InputOutputArray image, // 输入图像
OutputArrayOfArrays contours, // 检测到的轮廓
OutputArray hierarchy, // 可选的输出向量
int mode,
// 轮廓获取模式 (RETR_EXTERNAL, RETR_LIST, RETR_CCOMP,RETR_TREE, RETR_FLOODFILL)
int method,
// 轮廓近似算法 (CHAIN_APPROX_NONE, CHAIN_APPROX_SIMPLE, CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOS)
Point offset = Point() // 轮廓偏移量
)
hierarchy 为可选的参数,如果不选择该参数,则可得到 findContours 函数的第二种形式
void findContours (
InputOutputArray image,
OutputArrayOfArrays contours,
int mode,
int method,
Point offset = Point()
)
drawContours() 函数如下:
void drawContours (
InputOutputArray image, // 目标图像
InputArrayOfArrays contours, // 所有的输入轮廓
int contourIdx, //
const Scalar & color, // 轮廓颜色
int thickness = 1, // 轮廓线厚度
int lineType = LINE_8, //
InputArray hierarchy = noArray(), //
int maxLevel = INT_MAX, //
Point offset = Point() //
)
watershed函数
watershed(src,markers);
src:原图像
markers:目标markers,生成markers是通过findContours边沿查找+drawContours来实现的。
ps:这一步非常重要,有了marker就可以使用分水岭算法了。
distanceTransform函数
distanceTransform()距离变换的定义是计算一个图像中非零像素点到最近的零像素点的
距离,也就是到零像素点的最短距离。即距离变换的定义是计算一个图像中非零像素点到
最近的零像素点的距离,也就是到零像素点的最短距离。
通常处理的是一个二值化的图,所以求距离可以归一化,距离(像素距离)单位为1。
void distanceTransform(
InputArray src,
OutputArray dst,
int distanceType,
int maskSize,
int dstType=CV_32F )
void distanceTransform(
InputArray src,
OutputArray dst,
OutputArray labels,
int distanceType,
int maskSize,
int labelType=DIST_LABEL_CCOMP )
src:源矩阵
dst:目标矩阵
distanceType:距离类型。可以的类型是CV_DIST_L1、CV_DIST_L2、CV_DIST_C,具体各
类型的意义,请查阅相关算法文档。
maskSize:距离变换运算时的掩码大小。值可以是3、5或CV_DIST_MASK_PRECISE
(5或CV_DIST_MASK_PRECISE只能用在第一个原型中)。
当distanceType=CV_DIST_L1 或 CV_DIST_C时,maskSize只能为3。
dstType:输出图像(矩阵)的类型,可以是CV_8U 或 CV_32F。CV_8U只能用在第一个原型中,
而且distanceType只能为CV_DIST_L1。
labels:输出二维阵列标签。
labelType:标签数组类型。可选值为DIST_LABEL_CCOMP和DIST_LABEL_PIXEL