【图像处理】OpenCV系列二十五 --- 距离变换(dista
上一节我们学习了漫水填充算法(floodFill),相信大家学习之后,对漫水填充算法已经有了基本的认识,本节呢,我们对学习距离变换算法distanceTransform!
一、函数详解
1、函数原型
void distanceTransform(InputArray src,
OutputArray dst,
OutputArray labels,
int distanceType,
int maskSize,
int labelType = DIST_LABEL_CCOMP);
2、函数功能
用于计算图像中每一个非零点距离离自己最近的零点的近似或精确距离;输出的是保存每一个非零点与最近零点的距离信息;图像上越亮的点,代表了离零点的距离越远;对于零图像像素,距离显然为零;
当 maskSize=DIST_MASK_PRECISE 并且 distanceType=DIST_L2 时,该函数运行OpenCV参考文献[58]中描述的算法,该算法与TBB库并行运算;
在其他情况下,该函数运行OpenCV参考文献[20]中描述的算法;这意味着对于一个像素,函数会找到最接近的零像素的最短路径,该路径由基本移位组成:水平移动、垂直移动、对角线移动或骑士移动(最近的路径可用于5×5掩码);总距离是以这些基本距离之和计算的;
由于距离函数应该是对称的,所有水平移动和垂直移动必须具有相同的成本(表示为a),所有对角线移动必须具有相同的成本(表示为b),而所有骑士的移动必须具有相同的成本(表示为c);
对于 DIST_C 和 DIST_L1 类型,距离是精确计算的,而对于 Dist_L2 (欧几里得距离),距离只能用一个相对误差来计算(5×5掩码给出了更精确的结果);
对于a、b和c,OpenCV使用原始论文中建议的值:
(1) DIST_L1:a = 1,b = 2
(2) DIST_L2:
掩码为3 x 3,则 a=0.955,b=1.3693;
掩码为5 x 5,则 a=1,b=1.4,c=2.1969;
(3) DIST_C,则 a = 1,b = 1
通常,对于快速、粗略的距离估计Dist_L2,使用3×3掩码;为了获得更精确的距离估计DIST_L2,需要使用5×5掩码或精确算法;
Note:精确算法和近似算法都与像素数成线性关系;
该函数的此变体不仅计算每个像素(x,y)的最小距离,而且还标识由零像素组成的最近连接分量( labelType = DIST_LABEL_CCOMP )或最近的零像素( labelType=DIST_LABEL_PIXEL 像素);要素/像素的索引存储在标签(x,y)中;当时 labelType = DIST_LABEL_CCOMP ,函数会自动在输入图像中找到零像素的连接组件,并用不同的标签标记它们;当 labelType=DIST_LABEL_PIXEL 时,函数扫描输入图像并用不同的标签标记所有零像素。
在这种模式下,复杂度仍然是线性的;也就是说,该函数提供了一种非常快速的方法来计算二值图像的Voronoi图;目前,第二个变体只能使用近似的距离变换算法,即 maskSize=DIST_MASK_PRECISE 还不支持。
3、参数详解
-
第一个参数,InputArray src,8位单通道(二值化)输入图片;
-
第二个参数,OutputArray dst,输出结果中包含计算的距离,这是一个8位或者32位浮点类型单通道的Mat类型数组,大小与输入图片相同;
-
第三个参数,OutputArray labels,输出二维标签数组(离散Voronoi图)。类型为 CV_32SC1,尺寸与src相同;
-
第四个参数,int distanceType,计算距离的类型;
distanceType
(1) DIST_USER,用户自定义距离;
(2) DIST_L1,distance = |x1-x2| + |y1-y2|;
(3) DIST_L2 ,简单欧氏距离;
(4) DIST_C,distance = max(|x1-x2|,|y1-y2|);
(5) DIST_L12, distance = 2(sqrt(1+x*x/2) - 1));
(6) DIST_FAIR ,distance = c^2(|x|/c-log(1+|x|/c)), c = 1.3998;
(7) DIST_WELSCH, distance = c2/2(1-exp(-(x/c)2)), c = 2.9846;
(8) DIST_HUBER ,distance = |x|<c ? x^2/2 : c(|x|-c/2), c=1.345
- 第五个参数,int maskSize,距离变换掩码矩阵的大小;当前函数的变体不支持 DIST_MASK_PRECISE,对于 Dist_L1 或 Dist_C 距离类型,由于3×3掩模的结果与5×5或任何更大尺寸掩模的结果是相同的,所以参数被强制为3;
距离转换掩码的大小
(1) DIST_MASK_3,mask=3;
(2) DIST_MASK_5,mask=5;
(3) DIST_MASK_PRECISE;
- 第六个参数,int labelType = DIST_LABEL_CCOMP,要生成的标签数组的类型;
距离转换算法的标记
(1) DIST_LABEL_CCOMP,输入图像src中零像素的每个连接组件(以及最接近连接组件的所有非零像素)都将被指定为相同的标签;
(2) DIST_LABEL_PIXEL,每个零像素(以及离它最近的所有非零像素)都有自己的标签;
二、函数原型2
1、函数原型
void distanceTransform(InputArray src,
OutputArray dst,
int distanceType,
int maskSize,
int dstType = CV_32F);
2、参数详解
-
第一个参数,InputArray src,8位单通道(二值化)输入图片;
-
第二个参数,OutputArray dst,输出结果中包含计算的距离,这是一个8位或者32位浮点类型单通道的Mat类型数组,大小与输入图片相同;
-
第三个参数,int distanceType,计算距离的类型;
-
第四个参数,int maskSize,距离变换掩码矩阵的大小;当前函数的变体不支持 DIST_MASK_PRECISE,对于 Dist_L1 或 Dist_C 距离类型,由于3×3掩模的结果与5×5或任何更大尺寸掩模的结果是相同的,所以参数被强制为3;
-
第五个参数,int dstType = CV_32F,输出图像的类型,它可以是CV_8U 或者 CV_32F;类型CV_8U只能用于函数的第一个变体并且distanceType = DIST_L1;
3、实验实例
距离变换,在图像的质心绘制圆
#include <core/core.hpp>
#include <imgproc/imgproc.hpp>
#include <highgui/highgui.hpp>
using namespace cv;
int main(int argc,char *argv[])
{
//定义距离变换矩阵中的最大值
float maxValue=0;
Point Pt(0,0);
// 载入图像
Mat image=imread(argv[1]);
Mat imageGray;
// 转换为灰度图像
cvtColor(image,imageGray,COLOR_BGR2GRAY);
// 取反
imageGray=~imageGray;
// 滤波
GaussianBlur(imageGray,imageGray,Size(5,5),2);
// 二值化
threshold(imageGray,imageGray,20,200,CV_THRESH_BINARY);
// 定义保存距离变换结果的Mat矩阵
Mat imageThin(imageGray.size(),CV_32FC1);
// 距离变换
distanceTransform(imageGray,imageThin,CV_DIST_L2,3);
Mat distShow;
// 定义最终显示的图像
distShow=Mat::zeros(imageGray.size(),CV_8UC1);
for(int i=0;i<imageThin.rows;i++)
{
for(int j=0;j<imageThin.cols;j++)
{
distShow.at<uchar>(i,j)=imageThin.at<float>(i,j);
if(imageThin.at<float>(i,j)>maxValue)
{
// 获取距离变换的极大值
maxValue=imageThin.at<float>(i,j);
// 坐标
Pt=Point(j,i);
}
}
}
//为了显示清晰,做了0~255归一化
normalize(distShow,distShow,0,255,CV_MINMAX);
// 防错处理
if(pt.x != 0 || pt.y != 0)
{
circle(image,Pt,maxValue,Scalar(0,0,255),3);
circle(image,Pt,3,Scalar(0,255,0),3);
}
imshow("Source Image",image);
imshow("Thin Image",distShow);
waitKey();
return 0;
}
实验结果
原始图像 归一化之后的效果图 质心绘制圆三、综合实例
1、实验案例
#include <opencv2/core/utility.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <stdio.h>
using namespace std;
using namespace cv;
// 掩码的大小默认为5
int maskSize0 = DIST_MASK_5;
int voronoiType = -1;
// 边缘阈值100
int edgeThresh = 100;
// 距离类型DIST_L1
int distType0 = DIST_L1;
// The output and temporary images
Mat gray;
// 滑动条调整阈值
static void onTrackbar( int, void* )
{
// 颜色表
static const Scalar colors[] =
{
Scalar(0,0,0),
Scalar(255,0,0),
Scalar(255,128,0),
Scalar(255,255,0),
Scalar(0,255,0),
Scalar(0,128,255),
Scalar(0,255,255),
Scalar(0,0,255),
Scalar(255,0,255)
};
// 掩码的大小
int maskSize = voronoiType >= 0 ? DIST_MASK_5 : maskSize0;
// 距离转换的类型
int distType = voronoiType >= 0 ? DIST_L2 : distType0;
// 边缘图像
Mat edge = gray >= edgeThresh, dist, labels, dist8u;
// 使用距离转换算法
if( voronoiType < 0 )
distanceTransform( edge, dist, distType, maskSize );
else
distanceTransform( edge, dist, labels, distType, maskSize, voronoiType );
if( voronoiType < 0 )
{
// begin "painting" the distance transform result
// 开始绘制距离转换算法的结果
dist *= 5000;
pow(dist, 0.5, dist);
Mat dist32s, dist8u1, dist8u2;
// 转换图像的类型
dist.convertTo(dist32s, CV_32S, 1, 0.5);
dist32s &= Scalar::all(255);
// 转换图像的类型
dist32s.convertTo(dist8u1, CV_8U, 1, 0);
dist32s *= -1;
dist32s += Scalar::all(255);
// 转换图像的类型
dist32s.convertTo(dist8u2, CV_8U);
Mat planes[] = {dist8u1, dist8u2, dist8u2};
// 合并图像
merge(planes, 3, dist8u);
}
else
{
// 创建标签图像
dist8u.create(labels.size(), CV_8UC3);
for( int i = 0; i < labels.rows; i++ )
{
const int* ll = (const int*)labels.ptr(i);
const float* dd = (const float*)dist.ptr(i);
uchar* d = (uchar*)dist8u.ptr(i);
for( int j = 0; j < labels.cols; j++ )
{
int idx = ll[j] == 0 || dd[j] == 0 ? 0 : (ll[j]-1)%8 + 1;
float scale = 1.f/(1 + dd[j]*dd[j]*0.0004f);
int b = cvRound(colors[idx][0]*scale);
int g = cvRound(colors[idx][1]*scale);
int r = cvRound(colors[idx][2]*scale);
// 为创建的图像的每一个像素位置赋值
d[j*3] = (uchar)b;
d[j*3+1] = (uchar)g;
d[j*3+2] = (uchar)r;
}
}
}
// 显示图像
imshow("Distance Map", dist8u );
}
static void help()
{
printf("\n使用边缘图像之间的距离变换函数\n"
"用法:\n"
"\nHot keys: \n"
"\tESC - 退出程序\n"
"\tC - 使用C/Inf metric\n"
"\tL1 - 使用L1 metric\n"
"\tL2 - 使用L2 metric\n"
"\t3 - 使用3x3的掩码\\n"
"\t5 - 使用5x5的掩码\n"
"\t0 - 使用精确的距离转换\n"
"\tv - 切换到Voronoi图模式\n"
"\tp - 切换到基于像素的Voronoi图模式\n"
"\tSPACE - 循环使用所有的模式\n\n");
}
int main( int argc, const char** argv )
{
// 辅助信息
help();
// 载入图像,灰度图像
gray = imread("lena.png", 0);
// 判断图像是否存在
if(gray.empty())
{
printf("image error !\n");
help();
return -1;
}
// 创建窗体
namedWindow("Distance Map", 1);
// 创建滑动条控制图像的阈值
createTrackbar("Brightness Threshold", "Distance Map", &edgeThresh, 255, onTrackbar, 0);
// 死循环,保证程序不退出
for(;;)
{
// 调用滑动条更新界面
onTrackbar(0, 0);
// 等待键盘输入
char c = (char)waitKey(0);
// ESC键退出
if( c == 27 )
break;
// 初始进来为无掩码的距离转换函数
// 先初始化voronoiType为-1
if( c == 'c' || c == 'C' || c == '1' || c == '2' ||
c == '3' || c == '5' || c == '0' )
voronoiType = -1;
if( c == 'c' || c == 'C' )
distType0 = DIST_C;
else if( c == '1' )
distType0 = DIST_L1;
else if( c == '2' )
distType0 = DIST_L2;
else if( c == '3' )
maskSize0 = DIST_MASK_3;
else if( c == '5' )
maskSize0 = DIST_MASK_5;
else if( c == '0' )
maskSize0 = DIST_MASK_PRECISE;
else if( c == 'v' )
voronoiType = 0;
else if( c == 'p' )
voronoiType = 1;
// 切换有掩码还是没有掩码的距离转换函数
else if( c == ' ' )
{
if( voronoiType == 0 )
voronoiType = 1;
// 存在掩码的距离转换函数
else if( voronoiType == 1 )
{
voronoiType = -1;
maskSize0 = DIST_MASK_3;
distType0 = DIST_C;
}
else if( distType0 == DIST_C )
distType0 = DIST_L1;
else if( distType0 == DIST_L1 )
distType0 = DIST_L2;
else if( maskSize0 == DIST_MASK_3 )
maskSize0 = DIST_MASK_5;
else if( maskSize0 == DIST_MASK_5 )
maskSize0 = DIST_MASK_PRECISE;
else if( maskSize0 == DIST_MASK_PRECISE )
voronoiType = 0;
}
}
return 0;
}
2、实验结果
我是奕双,现在已经毕业将近两年了,从大学开始学编程,期间学习了C语言编程,C++语言编程,Win32编程,MFC编程,毕业之后进入一家图像处理相关领域的公司,掌握了用OpenCV对图像进行处理,如果大家对相关领域感兴趣的话,可以关注我,我这边会为大家进行解答哦!如果大家需要相关学习资料的话,可以私聊我哦!