7.1 基 于OpenCV 的 边 缘 检 测
本节中,我们将一起学习OpenCV 中边缘检测的各种算子和滤波器——Canny 算子、Sobel 算 子 、Laplacian 算子以及Scharr 滤波器。
7.1.1 边缘检测的一般步骤
在具体介绍之前,先来一起看看边缘检测的一般步骤。
1.【第一步】滤波
边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声 很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤 波方法主要有高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核,然 后基于高斯核函数对图像灰度矩阵的每一点进行加权求和。
2.【第二步】增强
增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰 度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度 幅值来确定。
3.【第三步】检测
经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中, 这些点并不是要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际 工程中,常用的方法是通过阈值化方法来检测。
另外,需要注意,下文中讲到的Laplacian 算 子 、sobel 算子和Scharr 算子都 是带方向的,所以,示例中我们分别写了X 方向、Y 方向和最终合成的的效果图。
7.1.2 canny算 子
1.canny 算子简介
Canny 边缘检测算子是John F.Canny于1986年开发出来的一个多级边缘检 测算法。更为重要的是,Canny 创立了边缘检测计算理论 (Computational theory ofedge detection),解释了这项技术是如何工作的。Canny 边缘检测算法以Canny 的名字命名,被很多人推崇为当今最优的边缘检测的算法。
其 中 ,Canny 的目标是找到一个最优的边缘检测算法,让我们看一下最优边 缘检测的三个主要评价标准。
- 低错误率:标识出尽可能多的实际边缘,同时尽可能地减少噪声产生的误 报。
- 高定位性:标识出的边缘要与图像中的实际边缘尽可能接近。
- 最小响应:图像中的边缘只能标识一次,并且可能存在的图像噪声不应标 识为边缘。
为了满足这些要求,Canny 使用了变分法,这是一种寻找满足特定功能的函 数的方法。最优检测用4个指数函数项的和表示,但是它非常近似于高斯函数的 一阶导数。
2.Canny 边缘检测的步骤
(1)【第一步】消除噪声
一般情况下,使用高斯平滑滤波器卷积降噪。以下显示了一个size=5 的高
斯内核示例:
2)【第二步】计算梯度幅值和方向
此处,按照Sobel 滤波器的步骤来操作。
①运用一对卷积阵列(分别作用于x 和 y 方向)
此处,按照Sobel 滤波器的步骤来操作。
①运用一对卷积阵列(分别作用于x 和 y 方向)
②使用下列公式计算梯度幅值和方向
而梯度方向一般取这4个可能的角度之一——0度,45度,90度,135度。
(3)【第三步】非极大值抑制
这一步排除非边缘像素,仅仅保留了一些细线条(候选边缘)。
(4)【第四步】滞后阈值
这是最后一步,Canny 使用了滞后阈值,滞后阈值需要两个阈值(高阈值和 低阈值):
①若某一像素位置的幅值超过高阈值,该像素被保留为边缘像素。
②若某一像素位置的幅值小于低阈值,该像素被排除。
③若某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于高 阈值的像素时被保留。
对于Canny 函数的使用,推荐的高低阈值比在2:1到3:1之间。
3.Canny 边缘检测:Canny() 函数
Canny 函数利用Canny 算子来进行图像的边缘检测操作。
void Canny(InputArray image, OutputArray edges, double threshold1,double threshold2, int apertureSize = 3, bool L2gradient = false)
- 第 一 个参数,InputArray类 型 的image, 输入图像,即源图像,填Mat 类 的 对象即可,且需为单通道8位图像。
- 第二个参数,OutputArray 类 型 的edges, 输出的边缘图,需要和源图片有 一样的尺寸和类型。
- 第三个参数,double类 型 的threshold1,第一个滞后性阈值。
- 第四个参数,double 类 型 的threshold2,第二个滞后性阈值。
- 第五个参数,int类型的apertureSize,表示应用Sobel算子的孔径大小,其 有默认值3。
- 第六个参数,bool 类 型 的L2gradient,一个计算图像梯度幅值的标识,有默 认值false。
需要注意的是,这个函数阈值1和阈值2两者中较小的值用于边缘连接,而 较大的值用来控制强边缘的初始段,推荐的高低阈值比在2:1到3:1之间。
void Test39() {Mat src = imread("image.jpg");Mat src1 = src.clone();imshow("src", src);Mat dst, edge, gray;dst.create(src1.size(), src1.type());cvtColor(src1, gray, COLOR_BGR2GRAY); //转为灰度图blur(gray, edge, Size(3, 3));//使用3*3内核来降噪Canny(edge, edge, 3, 9, 3); //Canny算子dst = Scalar::all(0);src1.copyTo(dst, edge);//edge作为掩码imshow("Canny", dst);waitKey(0);
}
7.1.3 sobel算 子
1.sobel 算子的基本概念
Sobel 算子是一个主要用于边缘检测的离散微分算子 (discrete differentiation operator)。它结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。 在图像的任何一点使用此算子,都将会产生对应的梯度矢量或是其法矢量。
2.sobel 算子的计算过程
我们假设被作用图像为I 然后进行如下操作。
(1)分别在x 和 y 两个方向求导。
①水平变化:将I 与一个奇数大小的内核Gx进行卷积。比如,当内核大小
为 3 时 ,Gx的计算结果为:
②垂直变化:将:I 与一个奇数大小的内核进行卷积。比如,当内核大小为 3时,计算结果为:
(2)在图像的每一点,结合以上两个结果求出近似梯度:
G = G x 2 + G y 2 G=\sqrt {G_x^2+G_y^2} G=Gx2+Gy2
另外有时,也可用下面更简单的公式代替:
G = ∣ G x ∣ + ∣ G y ∣ G=|G_x|+|G_y| G=∣Gx∣+∣Gy∣
3 . 使 用Sobel 算子:Sobel() 函数
Sobel函数使用扩展的Sobel算子,来计算一阶、二阶、三阶或混合图像差分。
void Sobel(InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize = 3, double scale = 1, double delta = 0, int borderType = BORDER_DEFAULT);
(1)第 一个参数,InputArray 类型的src, 为输入图像,填Mat 类型即可。
(2)第二个参数,OutputArray 类型的dst, 即目标图像,函数的输出参数, 需要和源图片有一样的尺寸和类型。
(3)第三个参数,int 类型的ddepth, 输出图像的深度,支持如下src.depth( 和ddepth 的组合:
- 若src.depth()=CV_8U,取 ddepth =-1/CV_16S/CV_32F/CV_64F
- 若src.depth()=CV_16U/CV_16S, 取 ddepth =-1/CV_32F/CV_64F
- 若src.depth(=CV_32F, 取 ddepth =-1/CV_32F/CV_64F
- 若src.depth()=CV_64F,取 ddepth=-1/CV_64F
(4)第四个参数,int 类 型dx,x 方向上的差分阶数。
(5)第五个参数,int 类 型dy,y 方向上的差分阶数。
(6)第六个参数,int 类 型ksize, 有默认值3,表示Sobel 核的大小;必须取1、
3、5或7。
(7)第七个参数,double 类型的scale,计算导数值时可选的缩放因子,默认 值是1,表示默认情况下是没有应用缩放的。可以在文档中查阅getDerivKernels 的相关介绍,来得到这个参数的更多信息。
(8)第八个参数,double类型的delta,表示在结果存入目标图(第二个参数 dst) 之前可选的delta 值,有默认值0。
(9)第九个参数,int 类型的 borderType, 边界模式,默认值为 BORDER_ DEFAULT。 这个参数可以在官方文档中borderInterpolate处得到更详细的信息。
一般情况下,都是用ksize ×ksize内核来计算导数的。然而,有一种特殊情 况——当ksize 为1时,往往会使用3x1 或 者 1x3 的内核。且这种情况下,并没 有进行高斯平滑操作。
一些补充说明如下。
① 当 内 核 大 小 为 3 时 ,Sobel 内核可能产生比较明显的误差(毕竟,Sobel 算子只是求取了导数的近似值而已)。为解决这一问题,OpenCV 提 供 了Scharr 函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快, 但结果却更加精确,其内核是这样的:
② 因 为Sobel 算子结合了高斯平滑和分化(differentiation), 因此结果会具有 更多的抗噪性。大多数情况下,我们使用sobel 函数时,取 [ x o r d e r = 1 , y o r d e r = 0 , k s i z e = 3 ] [xorder=1,yorder=0,ksize=3] [xorder=1,yorder=0,ksize=3] 来计算图像X 方向的导数, [ x o r d e r = 0 , y o r d e r = 1 , k s i z e = 3 ] [xorder=0,yorder=1,ksize=3] [xorder=0,yorder=1,ksize=3] 来
计算图像y 方向的导数。
计算图像X 方向的导数,取 [ x o r d e r = 1 , y o r d e r = 0 , k s i z e = 3 [xorder=1,yorder=0,ksize=3 [xorder=1,yorder=0,ksize=3] 情况对应的内核:
而计算图像Y 方向的导数,取 [ x o r d e r = 0 , y o r d e r = 1 , k s i z e = 3 ] [xorder=0,yorder=1,ksize=3] [xorder=0,yorder=1,ksize=3] 对应的内
核:
void Test40() {Mat grad_x, grad_y;Mat abs_grad_x, abs_grad_y, dst;Mat src = imread("image.jpg");imshow("src", src);//求x方向的梯度Sobel(src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT);convertScaleAbs(grad_x, abs_grad_x);imshow("abs_grad_x", abs_grad_x);//求y方向的梯度Sobel(src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT);convertScaleAbs(grad_y, abs_grad_y);imshow("abs_grad_y", abs_grad_y);//合并梯度addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst);imshow("dst", dst);waitKey(0);
}
-
x方向
-
y方向
-
合并图
7.1.4 Laplacian 算子
1.Laplacian 算子简介
Laplacian 算子是n 维欧几里德空间中的一个二阶微分算子,定义为梯度grad 的散度div 。因此如果f是二阶可微的实函数,则f 的拉普拉斯算子定义如下。
(1)f 的拉普拉斯算子也是笛卡儿坐标系xi 中的所有非混合二阶偏导数求和。
(2)作为一个二阶微分算子,拉普拉斯算子把C 函数映射到C 函数。对于k ≥2,表达式(1)(或(2))定义了一个算子 △ : C ( R ) → C ( R ) △:C(R)→C(R) △:C(R)→C(R) 或更一般地,对于任何开集Ω,定义了一个算子△:C(Ω)→C(Ω)。
根据图像处理的原理可知,二阶导数可以用来进行检测边缘。因为图像是 “二维”,需要在两个方向进行求导。使用 Laplacian 算子将会使求导过程变得 简单。
Laplacian算子的定义:
需要说明的是,由于Laplacian 使用了图像梯度,它内部的代码其实是调用了 Sobel 算子的。
让一幅图像减去它的Laplacian算子可以增强对比度。
2.计算拉普拉斯变换: Laplacian() 函数
Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。
void Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize = 1, double scale = 1, double delta = 0,intborderType = BORDER_DEFAULT);
- 第一个参数,InputArray类型的image, 输入图像,即源图像,填Mat 类的 对象即可,且需为单通道8位图像。
- 第二个参数,OutputArray 类型的edges, 输出的边缘图,需要和源图片有 一样的尺寸和通道数。
- 第三个参数,int类型的ddept, 目标图像的深度。
- 第四个参数,int类型的ksize,用于计算二阶导数的滤波器的孔径尺寸,大 小必须为正奇数,且有默认值1。
- 第五个参数,double类型的scale,计算拉普拉斯值的时候可选的比例因子, 有默认值1。
- 第六个参数,double类型的delta,表示在结果存入目标图(第二个参数dst) 之前可选的delta 值,有默认值0。
- 第七个参数,int 类型的 borderType,边界模式,默认值为 BORDER_
DEFAULT。这个参数可以在官方文档中borderInterpolate() 处得到更详细的 信息。
Laplacian()函数其实主要是利用sobel算子的运算。它通过加上sobel算子运 算出的图像x 方向和y 方向上的导数,来得到载入图像的拉普拉斯变换结果。
其 中 ,sobel 算 子(ksize>1) 如下:
而当ksize=1 时 ,Laplacian() 函数采用以下3x3 的孔径:
void Test41() {Mat src, src_gray, dst, abs_dst;src = imread("image.jpg");imshow("src", src);GaussianBlur(src, src, Size(3, 3), 0, 0, BORDER_DEFAULT); //使用高斯滤波降噪cvtColor(src, src_gray, COLOR_RGB2GRAY);Laplacian(src_gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT); //使用拉普拉斯算子convertScaleAbs(dst, abs_dst); //计算绝对值,把结果转为8位imshow("abs_dst", abs_dst);waitKey(0);
}
7.1.5 scharr滤 波 器
我们一般直接称 scharr 为滤波器,而不是算子。上文已经讲到,它在OpenCV 中主要是配合Sobel 算子的运算而存在的。下面让我们直接来看看其函数讲解。
1.计算图像差分:Scharr() 函数
使用Scharr 滤波器运算符计算x 或 y 方向的图像差分。其实它的参数变量和 Sobel基本上是一样的,除了没有ksize 核的大小。
void Scharr(InputArray src, // 源图OutputArray dst, // 目标图int ddepth, // 图像深度int dx, // x方向上的差分阶数 int dy,//y方向上的差分阶数 double scale=1,//缩放因子 double delta=0,//delta值intborderType = BORDER_DEFAULT) // 边界模式
(1)第一个参数,InputArray 类型的src, 为输入图像,填Mat 类型即可。
(2)第二个参数,OutputArray 类型的dst, 即目标图像,函数的输出参数, 需要和源图片有一样的尺寸和类型。
(3)第三个参数,int 类型的ddepth, 输出图像的深度,支持如下 src.depthO 和ddepth 的组合:
· 若src.depthO=CV_8U, 取 ddepth=1/CV_ 16S/CV_32F/CV_64F · 若sre.depthO=CV_ 16U/CV_ 16S, 取 ddepth=1/CV_32F/CV_64F
- 若src.depth()=CV_32F,取 ddepth=-1/CV_32F/CV_64F
- 若src.depth(=CV_64F, 取 ddepth=-1/CV_64F
(4)第四个参数,int 类型dx,x 方向上的差分阶数。
(5)第五个参数,int类 型dy,y 方向上的差分阶数。
(6)第六个参数,double类型的scale,计算导数值时可选的缩放因子,默认 值是1,表示默认情况下是没有应用缩放的。我们可以在文档中查阅 getDerivKernels的相关介绍,来得到这个参数的更多信息。
(7)第七个参数,double 类型的delta, 表示在结果存入目标图(第二个参数 dst)之前可选的delta值,有默认值0。
(8)第八个参数,int 类型的border Type,边界模式,默认值为BORDER_ DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。
不难理解,如下两者是等价的,即:
Scharr(src,dst,ddepth,dx,dy,scale,delta,borderType);
Sobel(src,dst,ddepth,dx,dy,cV_SCHARR,scale,delta,borderType);
-
x方向
-
y方向
-
合并后的图
7.1.6 综 合 示 例 : 边 缘 检 测
本节依然是配给大家一个详细注释的配套示例程序,把这节中介绍的知识点 以代码为载体,更形象地展现出来。
这个示例程序中,分别演示了canny 边缘检测、 sobel 边缘检测、scharr 滤波 器的使用,经过详细注释的的代码如下。
-
Sobel
-
Scharr
-
Canny
7.2 霍夫变换
本节中,我们将一起探讨 OpenCV 中霍夫变换相关的知识点,并了解了 OpenCV 中实现霍夫线变换的HoughLines 、HoughLinesP 函数的使用方法,以及 实现霍夫圆变换的HoughCircles 函数的使用方法。
在图像处理和计算机视觉领域中,如何从当前的图像中提取所需要的特征信 息是图像识别的关键所在。在许多应用场合中需要快速准确地检测出直线或者圆。 其中一种非常有效的解决问题的方法是霍夫(Hough) 变换,其为图像处理中从 图像中识别几何形状的基本方法之一,应用很广泛,也有很多改进算法。最基本 的霍夫变换是从黑白图像中检测直线(线段)。本节就将介绍OpenCV 中霍夫变换 的使用方法和相关知识。
7.2.1 霍夫变换概述
霍夫变换(Hough Transform) 是图像处理中的一种特征提取技术,该过程在 一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合 作为霍夫变换结果。霍夫变换于1962年由PaulHough 首次提出,最初的Hough 变换是设计用来检测直线和曲线的。起初的方法要求知道物体边界线的解析方程, 但不需要有关区域位置的先验知识。这种方法的一个突出优点是分割结果的 Robustness, 即对数据的不完全或噪声不是非常敏感。然而,要获得描述边界的解析表达常常是不可能的。后于1972年由Richard Duda&Peter Hart推广使用,经 典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别, 多为圆和椭圆。霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同 形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意 形状的问题转化为统计峰值问题。
霍夫变换在OpenCV 中分为霍夫线变换和霍夫圆变换两种,下面将分别进行 介 绍 。
7.2.2 OpenCV中的霍夫线变换
我们知道,霍夫线变换是一种用来寻找直线的方法.在使用霍夫线变换之前, 首先要对图像进行边缘检测的处理,即霍夫线变换的直接输入只能是边缘二值图 像。
OpenCV 支持三种不同的霍夫线变换,它们分别是:标准霍夫变换(Standard Hough Transform,SHT)、多尺度霍夫变换(Multi-Scale Hough Transform,MSHT) 和累计概率霍夫变换(Progressive Probabilistic Hough Transform,PPHT)。
其中,多尺度霍夫变换 (MSHT) 为经典霍夫变换 (SHT) 在多尺度下的一 个变种。而累计概率霍夫变换(PPHT) 算法是标准霍夫变换(SHT) 算法的一个 改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减 少计算量,缩短计算时间。之所以称PPHT 为“概率”的,是因为并不将累加器 平面内的所有可能的点累加,而只是累加其中的一部分,该想法是如果峰值如果 足够高,只用一小部分时间去寻找它就够了。按照猜想,可以实质性地减少计算 时 间 。
在OpenCV 中,可以用HoughLines 函数来调用标准霍夫变换(SHT) 和多尺 度霍夫变换 (MSHT)。
而 HoughLinesP 函数用于调用累计概率霍夫变换PPHT。 累计概率霍夫变换 执行效率很高,所有相比于HoughLines 函数,我们更倾向于使用HoughLinesP 函 数。
总结一下,OpenCV 中的霍夫线变换有如下三种:
- 标准霍夫变换(StandardHough Transform,SHT),由 HoughLines 函数调用。
- 多尺度霍夫变换(Multi-ScaleHough Transform,MSHT),由 HoughLines 函数调用。
- 累计概率霍夫变换(ProgressiveProbabilistic Hough Transform,PPHT),由 HoughLinesP 函数调用。
7.2.3 霍夫线变换的原理
(1)众所周知,一条直线在图像二维空间可由两个变量表示,有以下两种情 况。如图7.16所示。
- ①在笛卡尔坐标系:可由参数斜率和截距(m,b) 表示。
- ②在极坐标系:可由参数极径和极角 (r,θ) 表示。
对于霍夫变换,我们将采用第二种方式极坐标系来表示直线.因此,直线的
表达式可为:
化简便可得到:
r = x c o s θ + y s i n θ r=xcosθ+ysinθ r=xcosθ+ysinθ
(2)一般来说对于点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0), 可以将通过这个点的一族直线统一定义为:
r θ = x 0 ⋅ c o s θ + y 0 ⋅ s i n θ r_\theta=x_0·cosθ+y_0·sinθ rθ=x0⋅cosθ+y0⋅sinθ
这就意味着每一对 ( r θ , θ ) (r_{\theta},θ) (rθ,θ) 代表一条通过点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0) 的直线。
(3)如果对于一个给定点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0), 我们在极坐标对极径极角平面绘出所有通 过它的直线,将得到一条正弦曲线.例如,对于给定点 x 0 = 8 和 y 0 = 6 x_0=8 和 y_0=6 x0=8和y0=6 可以绘出如 7.17所示平面图。
只绘出满足下列条件的点 r > 0 r>0 r>0 和 0 < θ < 2 π 0 < θ < 2 π 0<θ<2π
(4)我们可以对图像中所有的点进行上述操作.如果两个不同点进行上述操 作后得到的曲线在平面θ—r 相交,这就意味着它们通过同一条直线。例如,接 上面的例子继续对点 x 1 = 9 , y 1 = 4 x₁=9,y₁=4 x1=9,y1=4 和 点 x 2 = 12 , y 2 = 3 x₂=12,y₂=3 x2=12,y2=3 绘图,得到图7.18。
这三条曲线在平面相交于点(0.925,9.6),坐标表示的是参数对θ-r 或者是 说点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0), 点 (x₁,y₁) 和 点(x₂,y₂) 组成的平面内的的直线。
(5)以上的说明表明,一般来说,一条直线能够通过在平面θ—r 寻找交于 一点的曲线数量来检测。而越多曲线交于一点也就意味着这个交点表示的直线由 更多的点组成。一般来说我们可以通过设置直线上点的阈值来定义多少条曲线交 于一点,这样才认为检测到了一条直线。
(6)这就是霍夫线变换要做的。它追踪图像中每个点对应曲线间的交点.如 果交于一点的曲线的数量超过了阈值,那么可以认为这个交点所代表的参数对 (θ, r θ r_\theta rθ) 在原图像中为一条直线。
7.2.4 标准霍夫变换:HoughLines()函数
此函数可以找出采用标准霍夫变换的二值图像线条。在OpenCV 中,我们可 以用其来调用标准霍夫变换SHT 和多尺度霍夫变换MSHT 的 OpenCV 内建算法。
void HoughLines(InputArray image, OutputArray lines, double rho,double theta, int threshold, double srn = 0, double stn = 0)
- 第一个参数,InputArray类型的image, 输入图像,即源图像。需为8位的 单通道二进制图像,可以将任意的源图载入进来,并由函数修改成此格式 后,再填在这里。
- 第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍 夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量(p,θ)
- 表示,其中,p 是离坐标原点(0,0)(也就是图像的左上角)的距离,θ 是弧度线条旋转角度(0度表示垂直线,π/2度表示水平线)。
- 第三个参数,double类型的rho, 以像素为单位的距离精度。另一种表述方 式是直线搜索时的进步尺寸的单位半径。(Latex 中/rho即表示p)
- 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种表述 方式是直线搜索时的进步尺寸的单位角度。
- 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为 图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold 的 线段才可以被检测通过并返回到结果中。
- 第六个参数,double 类型的srn, 有默认值0。对于多尺度的霍夫变换,这 是第三个参数进步尺寸 rho 的除数距离。粗略的累加器进步尺寸直接是第 三个参数rho, 而精确的累加器进步尺寸为rho/srn。
- 第七个参数,double 类型的 stn, 有默认值0,对于多尺度霍夫变换,srn 表示第四个参数进步尺寸的单位角度 theta 的除数距离。且如果srn 和 stn 同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
学完函数解析后,看一个以HoughLines 为核心的示例程序,就可以全方位了 解HoughLines 函数的使用方法。
void Test44() {Mat srcImage = imread("image.jpg");Mat grayImage, midImage, dstImage;cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);Canny(grayImage, midImage, 50, 200, 3);dstImage = midImage.clone();// 进行霍夫线变换std::vector<Vec2f> lines;HoughLines(midImage, lines, 1, CV_PI / 180, 150, 0, 0);// 在图像上绘制每一条检测到的直线for (size_t i = 0; i < lines.size(); ++i) {float rho = lines[i][0], theta = lines[i][1];Point pt1, pt2;double a = cos(theta), b = sin(theta);double x0 = a * rho, y0 = b * rho;pt1.x = cvRound(x0 + 1000 * (-b));pt1.y = cvRound(y0 + 1000 * (a));pt2.x = cvRound(x0 - 1000 * (-b));pt2.y = cvRound(y0 - 1000 * (a));// 使用彩色绘制直线line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, LINE_AA);}// 显示原始图像和带有霍夫变换线条的图像imshow("Original Image", srcImage);imshow("Edge Image", midImage);imshow("Detected Lines", dstImage);// 等待按键退出waitKey(0);}
7.2.5 累计概率霍夫变换:HoughLinesPO 函 数
此 函 数 在HoughLines 的基础上,在末尾加了 一 个代表Probabilistic ( 概 率 ) 的P, 表明它可以采用累计概率霍夫变换 (PPHT) 来找出二值图像中的直线。
void HoughLinesP(InputArray image, OutputArray lines, double rho,double theta, int threshold, double minLineLength = 0, double maxLineGap = 0)
- 第一个参数,InputArray 类型的image, 输入图像,即源图像。需为8位的 单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后, 再填在这里。
- 第二个参数,InputArray类型的lines,经过调用HoughLinesP函数后存储 了检测到的线条的输出矢量,每 一 条线由具有4个元素的矢量(x_1,y_1,x_2,y_2) 表示,其中,(x_1,y_1) 和(x_2,y_2) 是是每个检测到的线段的结束点
- 第三个参数,double类型的rho,以像素为单位的距离精度。另一种表述方 式是直线搜索时的进步尺寸的单位半径。
- 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种表述 方式是直线搜索时的进步尺寸的单位角度。
- 第五个参数,int类型的 threshold,累加平面的阈值参数,即识别某部分为 图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线 段才可以被检测通过并返回到结果中。
- 第六个参数,double类型的minLineLength,有默认值0,表示最低线段的 长度,比这个设定参数短的线段就不能被显现出来。
- 第七个参数,double类型的maxLineGap, 有默认值0,允许将同一行点与 点之间连接起来的最大的距离。
void Test45() {Mat srcImage = imread("image.jpg");Mat grayImage, midImage, dstImage;cvtColor(srcImage, grayImage, COLOR_BGR2GRAY);Canny(grayImage, midImage, 100, 300, 3);dstImage = midImage.clone();//霍夫线变换std::vector<Vec4i>lines;HoughLinesP(midImage, lines, 1, CV_PI / 180, 80, 50, 10);for (int i = 0; i < lines.size(); ++i) {Vec4i l = lines[i];line(dstImage, Point(l[0], l[1]), Point(l[2], l[3]),Scalar(186, 88, 255), 1, LINE_AA);}imshow("src", srcImage);imshow("mid", midImage);imshow("dst", dstImage);waitKey(0);}
7.2.6 霍夫圆变换
霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对 应的二维极径极角空间被三维的圆心点x、y和半径r 空间取代。说“大体上类似” 的原因是,如果完全用相同的方法的话,累加平面会被三维的累加容器所代替 — —在这三维中,一维是x, 一 维 是y, 另外一维是圆的半径r。这就意味着需要大 量的内存而且执行效率会很低,速度会很慢。
对直线来说,一条直线能由参数极径极角 ( r , θ ) (r,θ) (r,θ) 表示.而对圆来说,我们
需要三个参数来表示 一 个圆,也就是:
C : ( X c e n t e r , V c e n t e r , r ) C:(Xcenter,Vcenter,r) C:(Xcenter,Vcenter,r)
这里的(Xcener,Vcenter) 表示圆心的位置(下图中球心的点),而r 表示半径。 这样我们就能唯一的定义一个圆了。
7.2.7 霍夫梯度法的原理
霍夫梯度法的原理是这样的:
(1)首先对图像应用边缘检测,比如用canny 边缘检测。
(2)然后,对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel ( 函 数计算x 和 y 方 向 的Sobel 一阶导数得到梯度。
(3)利用得到的梯度,由斜率指定的直线上的每一个点都在累加器中被累加, 这里的斜率是从一个指定的最小值到指定的最大值的距离。
(4)同时,标记边缘图像中每一个非0像素的位置。
(5)然后从二维累加器中这些点中选择候选的中心,这些中心都大于给定阈 值并且大于其所有近邻。这些候选的中心按照累加值降序排列,以便于最支持像
素的中心首先出现。
(6)接下来对每一个中心,考虑所有的非0像素。
(7)这些像素按照其与中心的距离排序。从到最大半径的最小距离算起,选 择非0像素最支持的一条半径。
(8)如果一个中心收到边缘图像非0像素最充分的支持,并且到前期被选择 的中心有足够的距离,那么它就会被保留下来。
这个实现可以使算法执行起来更高效,或许更加重要的是,能够帮助解决三 维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。
人无完人,金无足赤。同样,这个算法也并不是十全十美的,还有许多需要 指出的缺点。
7.2.8 霍夫梯度法的缺点
(1)在霍夫梯度法中,我们使用Sobel 导数来计算局部梯度,那么随之而来 的假设是,它可以视作等同于一条局部切线,这并不是一个数值稳定的做法。在 大多数情况下,这样做会得到正确的结果,但或许会在输出中产生一些噪声。
(2)在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如 果把累加器的阈值设置偏低,算法将要消耗比较长的时间。此外,因为每一个中 心只选择一个圆,如果有同心圆,就只能选择其中的一个。
(3)因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过 于接近之前已经接受的中心的话,就不会被保留下来。且当有许多同心圆或者是 近似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较 极端的做法,因为在这里默认Sobel 导数会产生噪声,若是对于无穷分辨率的平 滑图像而言的话,这才是必须的。
7.2.9 霍 夫 圆 变 换 :HoughCircles() 函 数
HoughCircles 函数可以利用霍夫变换算法检测出灰度图中的圆。它相比之前 的HoughLines 和 HoughLinesP, 比较明显的一个区别是不需要源图是二值的,而 HoughLines和HoughLinesP 都需要源图为二值图像。
void HoughCircles(InputArray image, OutputArray circles, int method, double dp, double minDist, double paraml = 100, double param2 = 100, int minRadius = 0, int maxRadius = 0)
- 第一个参数,InputArray类型的image, 输入图像,即源图像,需为8位的 灰度单通道图像。
- 第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参 数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量 (x,y,radius)表示。
- 第三个参数,int类型的method, 即使用的检测方法,目前OpenCV 中就霍 夫梯度法一种可以使用,它的标识符为HOUGH_GRADIENT(OpenCV2 中 可写作CV_HOUGH_GRADIENT), 在此参数处填这个标识符即可。
- 第四个参数,double 类型的dp, 用来检测圆心的累加器图像的分辨率于输 入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。 例如,如果dp=1 时,累加器和输入图像具有相同的分辨率。如果dp=2,
累加器便有输入图像一半那么大的宽度和高度。 - 第五个参数,double 类型的minDist,为霍夫变换检测到的圆的圆心之间的 最小距离,即让算法能明显区分的两个不同圆之间的最小距离。这个参数 如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之, 这个参数设置太大,某些圆就不能被检测出来。
- 第六个参数,double类型的param1,有默认值100。它是第三个参数method 设置的检测方法的对应的参数。对当前唯 一 的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny 边缘检测算子的高阈值, 而低阈值为高阈值的一半。
- 第七个参数,double 类 型 的param2, 也有默认值100。它是第三个参数 method 设置的检测方法的对应的参数。对当前唯一 的方法霍夫梯度法 CV_HOUGH_GRADIENT, 它表示在检测阶段圆心的累加器阈值。它越小, 就越可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就 更加接近完美的圆形了。
- 第八个参数,int类 型 的minRadius, 有默认值0,表示圆半径的最小值。
- 第九个参数,int类型的maxRadius, 也有默认值0,表示圆半径的最大值。
需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不 到合适的圆半径。我们可以通过第八个参数 minRadius 和第九个参数 maxRadius 指定最小和最大的圆半径,来辅助圆检测的效果。或者,可以直接忽略返回半径, 因为它们都有着默认值0,只用HoughCircles 函数检测出来的圆心,然后用额外 的一些步骤来进一步确定半径。
需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不 到合适的圆半径。我们可以通过第八个参数 minRadius 和第九个参数 maxRadius 指定最小和最大的圆半径,来辅助圆检测的效果。或者,可以直接忽略返回半径, 因为它们都有着默认值0,只用HoughCircles 函数检测出来的圆心,然后用额外 的一些步骤来进一步确定半径。
void Test46() {Mat srcImage = imread("basketball.jpg");Mat midImage, dstImage;imshow("src", srcImage);cvtColor(srcImage, midImage, COLOR_BGR2GRAY);GaussianBlur(midImage, midImage, Size(9, 9), 2, 2);std::vector<Vec3f>circles;HoughCircles(midImage, circles, HOUGH_GRADIENT, 1.5, 10, 200, 100, 0, 0);for (int i = 0; i < circles.size(); ++i) {Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));int radius = cvRound(circles[i][2]);circle(srcImage, center, radius, Scalar(155, 50, 255), 3, 8, 0);}imshow("srcImage", srcImage);waitKey(0);
}
7.2.10 综合示例:霍夫变换
这次的综合示例,我们在HoughLinesP函数的基础上,为其添加了用于控制 其第五个参数阈值threshold的滚动条,因此可以通过调节滚动条来改变阈值,从 而动态地控制霍夫线变换检测的线条多少。
namespace test47 {Mat g_srcImage, g_dstImage ,g_grayImage;;std::vector<Vec4i> g_lines;int g_nthreshhold = 100; static void on_HoughLines(int, void*) {Mat dstImage = g_dstImage.clone(); Mat grayImage = g_grayImage.clone();std::vector<Vec4i> mylines;HoughLinesP(grayImage, mylines, 1, CV_PI / 180, g_nthreshhold + 1, 50, 10);for (int i = 0; i < mylines.size(); ++i) {Vec4i l = mylines[i];line(dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(23, 180, 55), 2, LINE_AA);}imshow("Hough", dstImage);}void Test() {g_srcImage = imread("house.jpg");if (g_srcImage.empty()) {std::cout << "Image not found!" << std::endl;return;}imshow("src", g_srcImage);cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);Canny(g_grayImage, g_dstImage, 50, 200, 3);g_grayImage = g_dstImage.clone();namedWindow("Hough");createTrackbar("Threshold", "Hough", &g_nthreshhold, 200, on_HoughLines);//on_HoughLines(g_nthreshhold, nullptr);waitKey(0);}
}void Test47() {test47::Test();
}
7.3 重映射
本节中,我们主要一起了解重映射的概念,以及OpenCV 中相关的实现函数 remap()。
7.3.1 重映射的概念
重映射,就是把一幅图像中某位置的像素放置到另一个图片指定位置的过程。 为了完成映射过程,需要获得一些插值为非整数像素的坐标,因为源图像与目标 图像的像素坐标不是一一对应的。一般情况下,我们通过重映射来表达每个像素 的位置(x,y), 像这样:
g ( x , y ) = f ( h ( x , y ) ) g(x,y)=f(h(x,y)) g(x,y)=f(h(x,y))
在这里,gO 是目标图像,f0 是源图像,而h(x,y) 是作用于(x,y) 的映射方法函数。 来看个例子。若有一幅图像I, 对其按照下面的条件作重映射:
h ( x , y ) = ( L . c o l s − x , y ) h(x,y)=(L.cols-x,y) h(x,y)=(L.cols−x,y)
图像会按照x 轴方向发生翻转。那么,源图像和效果图分别如图7.27和7.28 所示。
在OpenCV 中,可以使用函数remapO来实现简单重映射,下面我们就一起来 看看这个函数。
7.3.2 实现重映射:remapO函数
remapO函数会根据指定的映射形式,将源图像进行重映射几何变换,基于的 公式如下:
d s t ( x , y ) = s r c ( m a p r ( x , y ) , m a p , ( x , y ) ) dst(x,y)=src(mapr(x,y),map,(x,y)) dst(x,y)=src(mapr(x,y),map,(x,y))
需要注意,此函数不支持就地(in-place) 操作。看看其原型和参数。
void remap(InputArray src, OutputArraydst, InputArray map1,InputArray map2, int interpolation, intborderMode = BORDER_CONSTANT, const Scalar &borderValue = Scalar())
- 第一个参数,InputArray类型的src, 输入图像,即源图像,填Mat 类的对 象即可,且需为单通道8位或者浮点型图像。
- 第二个参数,OutputArray类型的dst,函数调用后的运算结果存在这里,即这 个参数用于存放函数调用后的输出结果,需和源图片有一样的尺寸和类型。
- 第三个参数,InputArray类型的mapl, 它有两种可能的表示对象。
- 表示点(x,y) 的第一个映射。
- 表示CV_16SC2、CV_32FC1 或 CV_32FC2 类型的X 值。
- 第四个参数,InputArray类型的map2, 同样,它也有两种可能的表示对象, 而且它会根据mapl 来确定表示那种对象。
- 若mapl 表示点(x,y) 时。这个参数不代表任何值。
- 表示CV_16UC1,CV_32FC1 类型的Y 值(第二个值)。
- 第五个参数,int类型的interpolation,插值方式,之前的resize()函数中有 讲到,需要注意,resize()函数中提到的INTER_AREA插值方式在这里是不 支持的,所以可选的插值方式如下(需要注意,这些宏相应的OpenCV2版 为在它们的宏名称前面加上“CV_” 前缀,比如 “INTER_LINEAR” 的 OpenCV2版为“CV_INTER_LINEAR”):
- INTER NEAREST——最近邻插值
- INTER_LINEAR——双线性插值(默认值)
- INTER_CUBIC—— 双三次样条插值(逾4×4像素邻域内的双三次插值) ■INTER_LANCZOS4——Lanczos 插值(逾8×8像素邻域的Lanczos插值)
- 第六个参数,int 类型的 borderMode,边界模式,有默认值 BORDER_CONSTANT, 表示目标图像中“离群点(outliers)” 的像素值不会被此函 数修改 。
- 第七个参数,const Scalar&类型的 borderValue,当有常数边界时使用的值, 其有默认值Scalar(), 即默认值为0。
void Test48() {Mat srcImage = imread("image.jpg");Mat dstImage, map_x, map_y;imshow("src", srcImage);map_x.create(srcImage.size(), CV_32FC1);map_y.create(srcImage.size(), CV_32FC1);for (int i = 0; i < srcImage.rows; ++i) {for (int j = 0; j < srcImage.cols; ++j) {map_x.at<float>(i, j) = static_cast<float>(j);map_y.at<float>(i, j) = static_cast<float>(srcImage.rows - i);}}//进行重映射操作remap(srcImage, dstImage, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));imshow("dst", dstImage);waitKey(0);}
7.3.4 综合示例程序:实现多种重映射
先放出以remap 为核心的综合示例程序,可以用按键控制四种不同的映射模 式。如图7.31所示。
void Test48() {Mat srcImage = imread("image.jpg");Mat dstImage, map_x, map_y;imshow("src", srcImage);map_x.create(srcImage.size(), CV_32FC1);map_y.create(srcImage.size(), CV_32FC1);for (int i = 0; i < srcImage.rows; ++i) {for (int j = 0; j < srcImage.cols; ++j) {map_x.at<float>(i, j) = static_cast<float>(j);map_y.at<float>(i, j) = static_cast<float>(srcImage.rows - i);}}//进行重映射操作remap(srcImage, dstImage, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));imshow("dst", dstImage);waitKey(0);}namespace test49 {Mat g_srcImage, g_dstImage;Mat g_map_x, g_map_y;// 初始化 g_map_x 和 g_map_yvoid init_maps() {g_map_x.create(g_srcImage.size(), CV_32FC1);g_map_y.create(g_srcImage.size(), CV_32FC1);}void update_map(int key) {for (int i = 0; i < g_srcImage.rows; ++i) {for (int j = 0; j < g_srcImage.cols; ++j) {switch (key) {case '1': if (j > g_srcImage.cols * 0.25 && j < g_srcImage.cols * 0.75 && i > g_srcImage.rows * 0.25 && i < g_srcImage.rows * 0.75) {g_map_x.at<float>(i, j) = static_cast<float>(2 * (j - g_srcImage.cols * 0.25) + 0.5);g_map_y.at<float>(i, j) = static_cast<float>(2 * (i - g_srcImage.rows * 0.25) + 0.5);}else {g_map_x.at<float>(i, j) = 0;g_map_y.at<float>(i, j) = 0;}break;case '2':g_map_x.at<float>(i, j) = static_cast<float>(j);g_map_y.at<float>(i, j) = static_cast<float>(g_srcImage.rows - i);break;case '3': g_map_x.at<float>(i, j) = static_cast<float>(g_srcImage.cols - j);g_map_y.at<float>(i, j) = static_cast<float>(i);break;case '4': g_map_x.at<float>(i, j) = static_cast<float>(g_srcImage.cols - j);g_map_y.at<float>(i, j) = static_cast<float>(g_srcImage.rows - i);break;}}}}void Test() {g_srcImage = imread("image.jpg");if (g_srcImage.empty()) {std::cerr << "Error loading image!" << std::endl;return;}imshow("src", g_srcImage);g_dstImage.create(g_srcImage.size(), g_srcImage.type());init_maps();namedWindow("transform");imshow("transform", g_srcImage);while (true) {int key = waitKey(0);if (key == 27) { break;}update_map(key);remap(g_srcImage, g_dstImage, g_map_x, g_map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));imshow("transform", g_dstImage);}}}
void Test49() {test49::Test();
}
7.4 仿射变换
本节中,我们将一起了解仿射变换的概念,以及OpenCV 中相关的实现函数 warpAffine和 getRotationMatrix2D。
7.4.1 认识仿射变换
仿射变换(Affine Transformation或 Affine Map),又称仿射映射,是指在几 何中,一个向量空间进行一次线性变换并接上一个平移,变换为另一个向量空间 的过程。它保持了二维图形的“平直性”(直线经过变换之后依然是直线)和“平 行性”(二维图形之间的相对位置关系保持不变,平行线依然是平行线,且直线上 点的位置顺序不变)。
一个任意的仿射变换都能表示为乘以一个矩阵(线性变换)接着再加上一个 向量(平移)的形式。
那么,我们能够用仿射变换来表示如下三种常见的变换形式:
- 旋转,rotation(线性变换)
- 平 移 ,translation(向量加)
- 缩放,scale(线性变换)
进行更深层次的理解,仿射变换代表的是两幅图之间的一种映射关系。 而我们通常使用2x3 的矩阵来表示仿射变换。
考虑到我们要使用矩阵 A 和 B 对二维向量 做变换,所以也能表示
为下列形式。
或者:
T = M ⋅ [ x , y , 1 ] ′ T=M·[x,y,1]' T=M⋅[x,y,1]′
即:
7.4.2 仿射变换的求法
我们知道,仿射变换表示的就是两幅图片之间的一种联系,关于这种联系的 信息大致可从以下两种场景获得。
- 已知X 和T, 而且已知它们是有联系的。接下来的工作就是求出矩阵M。
- 已知M 和X, 想求得T。只要应用算式T=M ·X 即可。对于这种联系的信 息可以用矩阵M 清晰地表达(即给出明确的2×3矩阵),也可以用两幅图片 点之间几何关系来表达。
形象地说明一下,因为矩阵M 联系着两幅图片,就以其表示两图中各三点直 接的联系为例。如图7.37所示。
图中,点1、2和3(在Imagel 中形成一个三角形)与Image2中的三个点是 一一映射的关系,且它们仍然形成三角形,但形状已经和之前不一样了。我们能 通过这样两组三点求出仿射变换(可以选择自己喜欢的点),接着就可以把仿射变 换应用到图像中去。
OpenCV 仿射变换相关的函数一般涉及到warpAffine和 getRotationMatrix2D 这两个函数:
- 使用OpenCV 函 数warpAffine 来实现一些简单的重映射。
- 使 用OpenCV 函 数getRotationMatrix2D来获得旋转矩阵。 下面分别对其进行讲解。
7.4.3 进行仿射变换:warpAffine(函数
warpAffine函数的作用是依据以下公式子,对图像做仿射变换。
d s t ( x , y ) = s r c ( M 11 x + M 12 y + M 13 , M 21 x + M 22 y + M 23 ) dst(x,y)=src(M_{11}x+M_{12}y+M_{13},M_{21}x+M_{22}y+M_{23}) dst(x,y)=src(M11x+M12y+M13,M21x+M22y+M23)
函数原型如下。
void warpAffine(InputArray src, OutputArray dst, InputArray M, Size dsize, int flags = INTER_LINEAR, intborderMode = BORDER_CONSTANT, const Scalar &borderValue = Scalar())
- 第 一 个参数,InputArray类 型 的src, 输入图像,即源图像,填Mat 类的对 象即可。
- 第二个参数,OutputArray 类 型 的dst, 函数调用后的运算结果存在这里, 需和源图片有一样的尺寸和类型。
- 第三个参数,InputArray类型的M,2×3 的变换矩阵。
- 第四个参数,Size 类型的dsize,表示输出图像的尺寸。
- 第五个参数,int 类 型 的 flags, 插值方法的标识符。此参数有默认值 INTER_LINEAR (线性插值),可选的插值方式如
表7.1所示。
表7.1 warpAffine函数可选的插值方式
标识符 | |
---|---|
INTER_NEAREST | 最近邻插值 |
INTER_LINEAR | 线性插值(默认值) |
INTER_AREA | 区域插值 |
INTER_CUBIC | 三次样条插值 |
INTER_LANCZOS4 | Lanczos插值 |
CV_WARP_FILL_OUTLIERS | 填充所有输出图像的象素。如果部分象素落在输入图像的边界 外,那么它们的值设定为fillval |
CV_WARP_INVERSE_MAP | 表示M为输出图像到输入图像的反变换。因此可以直接用来做 象素插值。否则,warpAffine函数从M矩阵得到反变换 |
- 第六个参数,int 类型的 borderMode,边界像素模式,默认值为 BORDER CONSTANT。
- 第七个参数,const Scalar&类型的borderValue,在恒定的边界情况下取的 值,默认值为Scalar(), 即 0 。
另外提 一 点,WarpAffine 函数与一个叫做cvGetQuadrangleSubPix (的函数类似,但是不完全相同。WarpAffine 要求输入和输出图像具有同样的数据类型,有 更大的资源开销(因此对小图像不太合适)而且输出图像的部分可以保留不变。 而cvGetQuadrangleSubPix 可以精确地从8位图像中提取四边形到浮点数缓存区 中,具有比较小的系统开销,而且总是全部改变输出图像的内容。
7.4.4 计算二维旋转变换矩阵: getRotationMatrix2DO 函 数
getRotationMatrix2DO函数用于计算二维旋转变换矩阵。变换会将旋转中心映射 到它自身 。
Mat getRotationMatrix2D(Point2fcenter,double angle,double scale)
- 第一个参数,Point2f类型的center,表示源图像的旋转中心。
- 第二个参数,double类型的angle,旋转角度。角度为正值表示向逆时针旋 转(坐标原点是左上角)。
- 第三个参数,double类型的scale,缩放系数。 此函数计算以下矩阵:
其 中 :
α = s c a l e ⋅ c o s a n g l e , β = s c a l e ⋅ s i n a n g l e α=scale·cos angle, \\ β=scale·sin angle α=scale⋅cosangle,β=scale⋅sinangle
7.4.5 示例程序:仿射变换
学 习 完 上 面 的 讲 解 和 函 数 实 现 , 下 面 是 一 个 以 warpAffine 和 getRotationMatrix2D 函 数 为 核 心 的 对 图 像 进 行 仿 射 变 换 的 示 例 程 序 。
void Test50() {Point2f srcTriangle[3];Point2f dstTriangle[3];Mat rotMat(2, 3, CV_32FC1);Mat warpMat(2, 3, CV_32FC1);Mat srcImage, dstImage_warp, dstImage_warp_rotate;srcImage = imread("universe.jpg");dstImage_warp = Mat::zeros(srcImage.rows, srcImage.cols, srcImage.type());//设置源图像三组点srcTriangle[0] = Point2f(0, 0);srcTriangle[1] = Point2f(static_cast<float>(srcImage.cols - 1), 0);srcTriangle[2] = Point2f(1, static_cast<float>(srcImage.rows - 1));//设置目标图像三组点dstTriangle[0] = Point2f(0, static_cast<float>(srcImage.rows * 0.33));dstTriangle[1] = Point2f(static_cast<float>(srcImage.cols * 0.65), static_cast<float>(srcImage.rows * 0.35));dstTriangle[2] = Point2f(static_cast<float>(srcImage.cols * 0.15), static_cast<float>(srcImage.rows * 0.6));//求得仿射变换warpMat = getAffineTransform(srcTriangle, dstTriangle);//对源图像应用仿射变换warpAffine(srcImage, dstImage_warp, warpMat, dstImage_warp.size());//对图像进行缩放后旋转Point center = Point(dstImage_warp.cols / 2, dstImage_warp.rows / 2);double angle = -30.0;double scale = 0.8;//获得旋转矩阵rotMat = getRotationMatrix2D(center, angle, scale);//旋转已缩放的图像warpAffine(dstImage_warp, dstImage_warp_rotate, rotMat, dstImage_warp.size());imshow("src", srcImage);imshow("dstImage_warp", dstImage_warp);imshow("dstImage_warp_rotate", dstImage_warp_rotate);waitKey(0);}
7.5 直方图均衡化
很多时候,我们用相机拍摄的照片的效果往往会不尽人意。这时,可以对这 些图像进行一些处理,来扩大图像的动态范围。这种情况下最常用到的技术就是 直方图均衡化。未经均衡化的图片范例如图7.41、7.42所示。
在图7.41中,我们可以看到,左边的图像比较淡,因为其数值范围变化比较 小,可以在这幅图的直方图(图7.42)中明显地看到。因为处理的是8位图像, 其亮度值是从0到255,但直方图值显示的实际亮度却集中在亮度范围的中间区 域。为了解决这个问题,就可以用到直方图均衡化技术,先来看看其概念。
7.5.1 直方图均衡化的概念和特点
直方图均衡化是灰度变换的一个重要应用,它高效且易于实现,广泛应用于 图像增强处理中。图像的像素灰度变化是随机的,直方图的图形高低不齐,直方 图均衡化就是用一定的算法使直方图大致平和的方法。均衡化效果示例如图7.43、 7.44所示。
简而言之,直方图均衡化是通过拉伸像素强度分布范围来增强图像对比度的 一种方法。
均衡化处理后的图像只能是近似均匀分布。均衡化图像的动态范围扩大了,但其本质是扩大了量化间隔,而量化级别反而减少了,因此,原来灰度不同的象 素经处理后可能变的相同,形成了一片相同灰度的区域,各区域之间有明显的边 界,从而出现了伪轮廓。
在原始图像对比度本来就很高的情况下,如果再均衡化则灰度调和,对比度 会降低。在泛白缓和的图像中,均衡化会合并一些象素灰度,从而增大对比度。 均衡化后的图片如果再对其均衡化,则图像不会有任何变化。如图7.45、7.46所 示。
通过图7.46可以发现,经过均衡化的图像,其频谱更加舒展,有效地利用了 0~255的空间,图像表现力更加出色。
7.5.2 实现直方图均衡化: equalizeHist ( 函 数
在 OpenCV 中,直方图均衡化的功能实现由equalizeHist 函数完成。我们一起 看看它的函数描述。
void equalizeHist(InputArray src,outputArray dst)
- 第一个参数,InputArray类型的src, 输入图像,即源图像,填Mat 类的对 象即可,需为8位单通道的图像。
- 第二个参数,OutputArray 类型的dst,函数调用后的运算结果存在这里, 需和源图片有一样的尺寸和类型。
采用如下步骤对输入图像进行直方图均衡化。
- 计算输入图像的直方图H。
- 进行直方图归一化,直方图的组距的和为255。
- 计算直方图积分: H ′ ( i ) = ∑ 0 ≤ j ≤ i H ( j ) H'(i)=\sum_{ 0 \leq j\leq i}H(j) H′(i)=∑0≤j≤iH(j)
- 以H 作为查询表进行图像变换: d s t ( x , y ) = H ( s r c ( x , y ) ) dst(x,y)=H(src(x,y)) dst(x,y)=H(src(x,y))
言而言之,由equalizeHist() 函数实现的灰度直方图均衡化算法,就是把直方 图的每个灰度级进行归一化处理,求每种灰度的累积分布,得到一个映射的灰度 映射表,然后根据相应的灰度值来修正原图中的每个像素。
7.5.3 示例程序:直方图均衡化
这一节将给大家演示的示例程序简明扼要而“字字珠玑”,非常直观地演示出 了如何用equalizeHist() 函数来进行图像的直方图均衡化,详细注释的代码如下。
void Test51() {Mat srcImage, dstImage;srcImage = imread("sky.jpg");//转换为灰度图cvtColor(srcImage, srcImage, COLOR_BGR2GRAY);imshow("srcImage", srcImage);//进行直方图均衡化equalizeHist(srcImage, dstImage);imshow("dstImage", dstImage);waitKey(0);
}
7.6 本章小结
在这章中我们学习了很多类型的图像变换方法。包括利用OpenCV 进行边缘 检测所用到的canny 算 子 、sobel 算 子 ,Laplace 算