1--分水岭算法的原理
详细原理讲解可参考:博客1和视频1;
原理简述:分水岭算法的基本思想是把图像视为拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而两个集水盆之间的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明:在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸入水中,随着浸入水的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝,即形成分水岭。
2--OpenCV API
void cv::watershed(cv::InputArray image, cv::InputOutputArray markers)
// cv::InputArray image:待分割的源图像;
// cv::InputOutputArray markers:标记图像;
函数 cv::watershed() 实现的分水岭算法是基于标记的分割算法。在把源图像传给函数之前,需要大致勾画标记出图像期望分割的区域。
3--代码实例
3-1--OpenCV处理流程:
① 背景转换:白色背景转换为黑色背景;
② 图像锐化:利用拉普拉斯算子模糊处理图像,将源图像减去模糊图像;
③ 距离变换:对锐化后的图像进行距离变换,基于cv::distanceTransform() 计算源图像的每个像素到最近的零像素的距离;
④ 生成标记:通过二值化、腐蚀、轮廓检测等生成图像标记;
⑤ 分水岭变换:基于cv::watershed() 使用源图像和标记图像实现分水岭变换;
⑥ 填充:对分割得到的对象进行颜色填充;
前4步可以理解为:将源图像期望分割的区域进行标记出来。
3-2--完整代码
# include <opencv2/opencv.hpp>
# include <cstdio>
# include <iostream>
# include <math.h>
int main(int argc, char** argv){
cv::Mat src;
src = cv::imread("C:/Users/Liujinfu/Desktop/opencv_bilibili/test0103.jpg");
if(src.empty()){
printf("Could not load image ...");
return -1;
}
cv::imshow("src", src);
// 将白色背景转换为黑色背景
for (int row = 0; row < src.rows; row++){
for (int col = 0; col < src.cols; col++){
// 当前位置为白色
if(src.at<cv::Vec3b>(row, col) == cv::Vec3b(255, 255, 255)){
src.at<cv::Vec3b>(row, col)[0] = 0;
src.at<cv::Vec3b>(row, col)[1] = 0;
src.at<cv::Vec3b>(row, col)[2] = 0;
}
}
}
cv::imshow("change background", src);
// 利用拉普拉斯算子锐化
cv::Mat sharp_img, temp1, temp2;
cv::Mat kernel = (cv::Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1); // 定义拉普拉斯算子
cv::filter2D(src, temp1, CV_32F, kernel, cv::Point(-1, -1), 0, cv::BORDER_DEFAULT);
src.convertTo(temp2, CV_32F);
sharp_img = temp2 - temp1;
sharp_img.convertTo(sharp_img, CV_8UC3);
cv::imshow("sharp image", sharp_img);
// 二值化
cv::Mat binary_img;
cv::cvtColor(sharp_img, binary_img, cv::COLOR_BGR2GRAY);
cv::threshold(binary_img, binary_img, 40, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
cv::imshow("binary image", binary_img);
// 距离变换
cv::Mat distImg;
cv::distanceTransform(binary_img, distImg, cv::DIST_L1, 3, 5);
cv::normalize(distImg, distImg, 0, 1, cv::NORM_MINMAX);
cv::imshow("distance image", distImg);
// 再次二值化,腐蚀
cv::threshold(distImg, distImg, 0.4, 1, cv::THRESH_BINARY);
cv::Mat k1 = cv::Mat::zeros(13, 13, CV_8UC1);
cv::erode(distImg, distImg, k1, cv::Point(-1, -1));
cv::imshow("dist binary image", distImg);
// 生成标记
cv::Mat dist_8u;
distImg.convertTo(dist_8u, CV_8U);
std::vector<std::vector<cv::Point>> contours;
cv::findContours(dist_8u, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE, cv::Point(0, 0));
cv::Mat markers = cv::Mat::zeros(src.size(), CV_32SC1);
for(size_t i = 0; i < contours.size(); i++){
cv::drawContours(markers, contours, static_cast<int>(i), cv::Scalar::all(static_cast<int>(i) + 1), -1);
}
cv::circle(markers, cv::Point(5, 5), 3, cv::Scalar(255, 255, 255), -1);
//cv::imshow("markers image", markers*1000);
// 分水岭变换
cv::watershed(src, markers);
cv::Mat mark = cv::Mat::zeros(markers.size(), CV_8UC1);
markers.convertTo(mark, CV_8UC1);
cv::bitwise_not(mark, mark, cv::Mat());
cv::imshow("watershed image", mark);
// 随机产生颜色
std::vector<cv::Vec3b> colors;
for(size_t i = 0; i < contours.size(); i++){
int r = cv::theRNG().uniform(0, 255);
int g = cv::theRNG().uniform(0, 255);
int b = cv::theRNG().uniform(0, 255);
colors.push_back(cv::Vec3b((uchar)b, (uchar)g, (uchar)r));
}
// 填充颜色
cv::Mat dst = cv::Mat::zeros(markers.size(), CV_8UC3);
for (int row = 0; row < markers.rows; row++){
for (int col = 0; col < markers.cols; col++){
int index = markers.at<int>(row, col);
if (index > 0 && index <= static_cast<int>(contours.size())){
dst.at<cv::Vec3b>(row, col) = colors[index - 1];
}
else{
dst.at<cv::Vec3b>(row, col) = cv::Vec3b(0, 0, 0);
}
}
}
cv::imshow("Final Result", dst);
cv::waitKey(0);
return 0;
}
补充说明:如果要展示标记图像,需要将markers转换为CV_8U,否则会报错。