基于距离变换与分水岭的图像分割
1、 图像分割(Image Segmentation)
图像分割(Image Segmentation)是图像处理最重要的处理手段之一。图像分割的目标是将图像中像素根据一定的规则分为若干(N)个cluster集合,每个集合包含一类像素。根据算法分为监督学习方法和无监督学习方法,图像分割的算法多数都是无监督学习方法-Kmeans。
2、 距离变换和分水岭
(1) 距离变换
距离变换的定义 :计算图像中像素点到最近零像素点的距离,也就是零像素点的最短距离。
距离变换的方法:首先对图像进行二值化处理,然后给每个像素赋值为离它最近的背景像素点与其距离(Manhattan距离or欧氏距离),得到distance metric(距离矩阵),那么离边界越远的点越亮。
距离变换常见算法有两种
1)不断膨胀/腐蚀得到
2)基于倒角距离
距离变换常用应用:
• 细化轮廓;
• 寻找质心;
(2) 分水岭变换
传统的分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,如下图所示。
图像中每一像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆地,而集水盆地的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸人水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝如下图所示,即形成分水岭。
因为传统分水岭算法存在过分割的不足,OpenCV提供了一种改进的分水岭算法,使用一系列预定义标记来引导图像分割的定义方式。使用OpenCV的分水岭算法cv::wathershed,需要输入一个标记图像,图像的像素值为32位有符号正数(CV_32S类型),每个非零像素代表一个标签。它的原理是对图像中部分像素做标记,表明它的所属区域是已知的。分水岭算法可以根据这个初始标签确定其他像素所属的区域。
分水岭变换常见的算法:
基于浸泡理论实现。假设图像每个位置的像素值为不同的地貌势必会形成山峰和山谷,在山底不停加水,直到各大山头之间形成了明显的分水线——分水岭算法的基本思想。
3、 相关API
(1)距离变换
distanceTransform( InputArray src, OutputArray dst,int distanceType, int maskSize, int dstType=CV_32F);
• src – 8-bit, 单通道(二值化)输入图片。
• dst – 输出结果中包含计算的距离,这是一个32-bit float 单通道的Mat类型数组,大小与输入图片相同。
• distanceType – 计算距离的类型那个,可以是 CV_DIST_L1、CV_DIST_L2 、CV_DIST_C。
• maskSize – 距离变换掩码矩阵的大小,可以是
(2)分水岭变换
watershed( InputArray image, InputOutputArray markers );
• image – 8-bit, 三通道彩色输入图片。
• markers–图片调用函数后的结果放在这里
4、 处理流程
处理流程分为11步,如下图所示。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace std;
using namespace cv;
int main(int argc, char** argv) {
char input_win[] = "input image";
char watershed_win[] = "watershed segmentation demo";
Mat src = imread("D:/vcprojects/images/cards.png");
// Mat src = imread("D:/kuaidi.jpg");
if (src.empty()) {
printf("could not load image...\n");
return -1;
}
namedWindow(input_win, CV_WINDOW_AUTOSIZE);
imshow(input_win, src);
// 1.将白色背景变为黑色change background
for (int row = 0; row < src.rows; row++) {
for (int col = 0; col < src.cols; col++) {
if (src.at<Vec3b>(row, col) == Vec3b(255, 255, 255)) {
src.at<Vec3b>(row, col)[0] = 0;
src.at<Vec3b>(row, col)[1] = 0;
src.at<Vec3b>(row, col)[2] = 0;
}
}
}
namedWindow("black background", CV_WINDOW_AUTOSIZE);
imshow("black background", src);
//2.锐化sharpen
Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);
Mat imgLaplance;
Mat sharpenImg = src;
filter2D(src, imgLaplance, CV_32F, kernel, Point(-1, -1), 0, BORDER_DEFAULT);
src.convertTo(sharpenImg, CV_32F);
Mat resultImg = sharpenImg - imgLaplance;
resultImg.convertTo(resultImg, CV_8UC3);
imgLaplance.convertTo(imgLaplance, CV_8UC3);
imshow("sharpen image", resultImg);
// src = resultImg; // copy back
//3.转换为二值图像convert to binary
Mat binaryImg;
cvtColor(src, resultImg, CV_BGR2GRAY);
threshold(resultImg, binaryImg, 40, 255, THRESH_BINARY | THRESH_OTSU);
imshow("binary image", binaryImg);
//4.距离变换distance transform
Mat distImg;
distanceTransform(binaryImg, distImg, DIST_L1, 3, 5);//距离变换:计算源图像的每个像素到最近的零像素的距离
//5.对距离变换结果进行归一化(0-1之间)
normalize(distImg, distImg, 0, 1, NORM_MINMAX);
imshow("distance result", distImg);
//6.再次二值化
threshold(distImg, distImg, .4, 1, THRESH_BINARY);
//7.二值腐蚀
Mat k1 = Mat::ones(13, 13, CV_8UC1);
erode(distImg, distImg, k1, Point(-1, -1));
imshow("distance binary image", distImg);
//8.发现轮廓
Mat dist_8u;
distImg.convertTo(dist_8u, CV_8U);
vector<vector<Point>> contours;
findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));
//9.绘制轮廓
Mat markers = Mat::zeros(src.size(), CV_32SC1);
for (size_t i = 0; i < contours.size(); i++) {
drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 1), -1);
}
circle(markers, Point(5, 5), 3, Scalar(255, 255, 255), -1);
imshow("my markers", markers*1000);//markers灰度级很低,所以乘以1000
//10.分水岭变换
watershed(src, markers);//分水岭:用分水岭算法执行基于标记的图像分割
Mat mark = Mat::zeros(markers.size(), CV_8UC1);
markers.convertTo(mark, CV_8UC1);
bitwise_not(mark, mark, Mat());
imshow("watershed image", mark);
// 对每个分割区域着色
vector<Vec3b> colors;
for (size_t i = 0; i < contours.size(); i++) {
int r = theRNG().uniform(0, 255);
int g = theRNG().uniform(0, 255);
int b = theRNG().uniform(0, 255);
colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
}
// 显示最终结果
Mat dst = 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<Vec3b>(row, col) = colors[index - 1];
}
else {
dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
}
}
}
imshow("Final Result", dst);
waitKey(0);
return 0;
}