600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > OpenCV入门学习笔记之Harris角点检测与SIFT特征匹配算法

OpenCV入门学习笔记之Harris角点检测与SIFT特征匹配算法

时间:2023-06-09 07:58:05

相关推荐

OpenCV入门学习笔记之Harris角点检测与SIFT特征匹配算法

1. 写在前面

这篇文章整理两个图像处理中非常重要的算法,一个是Harris角点检测算法,另一个是SIFT特征匹配算法,这两个算法本质上还是去找图像里面的关键特征点,帮助我们后续更好的理解图像以及做各种各样的分析。 由于这两个算法涉及到的数学原理会比较多,而我刚入门,所以只是从使用的角度,简单的描述到底在做什么事情,至于详细的数学细节或者推导,这里不过多整理,以掉包能完成任务为首要目的啦。

首先,先介绍Harris角点检测算法,角点在图像中是很重要的特征,信息含量很高,那么如何找到一个图像里面的角点呢? 这个算法就能轻松解决。但是呢?这个算法只考虑了旋转不变性,即一个角点旋转之后还是角点,没有考虑到尺度不变,尺度变化可能会导致角点变成边缘,所以有没有一种方法同时考虑这两个特性去找特征点呢? 这就是SIFT算法,这个算法也是用来侦测和描述影像中的局部性特征,基于位置,尺度,旋转不变性在空间尺度中寻找特征点。 这两个算法的使用场景非常广泛,比如图像配准,目标识别跟踪,图像对齐,图像拼接(全景图), 相机标定,三维场景重建等。最后,通过一个全景图拼接的Demo感受下这两个算法的魅力 😉

大纲如下:

Harris角点检测算法SIFT特征匹配算法特征匹配案例Demo: 全景图拼接

Ok, let’s go!

2. Harris角点检测算法

角点特征能在保留图像重要特征的同时,有效减少信息数据量,算是图像中较好的特征,比边缘特征更好的用于定位。 在图像处理中,检测角点特征的算法很多,最常用,最基础的就是Harris角点检测算法。

在说这个算法之前,先感受下啥子是角点:

如果我们人眼看的话,非常好理解,就是图像里面的物体的角呗。那么计算机看,定位这些角点可就不那么容易了。 如果想让计算机看,那么必须得搞明白一个事情: 这样的角点,与边界点,以及普通屏幕点有啥区别,即上图的红框,蓝框,黑框内的点在数值上会有啥区别呢?

Harris角点检测认为:特征点具有局部差异性。如何描述呢? 每一个点为中心,取一个窗口,比如窗口大小5×55\times55×5或者7×77\times77×7, 那么窗口描述了这个特征点周围的环境。

如果这个特定的窗口在图像各个方向移动时, 窗口内图像的灰度没有发生变化,那么窗口内不存在角点,比如蓝框如果窗口在某一个方向移动,窗口内图像灰度发生了较大变化,而另一些方向没有发生变化,那么,窗口内图像可能是一条直线边界, 黑框这个, 垂直方向移动就剧烈变化,水平方向就不变如果在各个方向上移动这个特征的小窗口,窗口内区域灰度都发生较大的变化,就认为这个窗口内有角点。

说是这么说,但具体应该怎么找到角点呢? Harris角点检测算法主要是三步:

当窗口同时向xxx和yyy两个方向移动时,计算窗口内部像素值变化量E(u,v)E(u,v)E(u,v)对于每个窗口,计算对应的角点响应函数RRR对于该函数阈值处理,如果R>thresholdR>thresholdR>threshold,表示该窗口对应一个角点特征

下面详细展开。

2.1 窗口移动,计算像素值变化量

这里的核心问题: 如何确定哪些窗口引起较大灰度值变化?

假设当前窗口中心的像素点位置(x,y)(x,y)(x,y), 这个点像素值I(x,y)I(x,y)I(x,y), 如果这个窗口分别向xxx和yyy方向分别移动了uuu和vvv步, 到了一个新的位置(x+u,y+v)(x+u, y+v)(x+u,y+v), 像素值I(x+u,y+v)I(x+u, y+v)I(x+u,y+v), 那么I[(x+u,y+v)−I(x,y)]I[(x+u,y+v)-I(x,y)]I[(x+u,y+v)−I(x,y)]就是中心点移动引起的灰度值的变化。

那我们这是个窗口呀,比如3×33\times33×3,那就有9个点,窗口一移动的话,这9个点都有引起的灰度值的变化,而把这个都加和,就得到了窗口在各个方向上移动(u,v)(u,v)(u,v)造成的像素灰度值变化,这个应该很好理解,公式如下:

E(u,v)=∑(x,y)∈W(x,y)w(x,y)×[I(x+u,y+v)−I(x,y)]2E(u, v)=\sum_{(x, y)\in W(x,y)} w(x, y) \times[I(x+u, y+v)-I(x, y)]^{2} E(u,v)=(x,y)∈W(x,y)∑​w(x,y)×[I(x+u,y+v)−I(x,y)]2

这里多出了一个w(x,y)w(x,y)w(x,y)函数,表示窗口内各个像素的权重,可以设定为窗口中心为原点的高斯分布,如果窗口中心点像素是角点,那么窗口移动前后,中心点灰度值变化非常强烈,那么这个权重就大一些,表示该点对灰度变化贡献大。 而离着窗口中心较远的点,灰度变化较小,于是权重小一些,对灰度变化的贡献小。

这个公式就是我们的目标函数,如果是角点,这个函数值会比较大,所以我们就是最大化这个函数来得到图像中的角点。

But, 上面这个函数计算E(u,v)E(u,v)E(u,v)会非常慢,比较涉及到了窗口内所有像素点的计算,所以,我们用泰勒,先对像素值函数进行近似。

I(x+u,y+v)=I(x,y)+Ix(x,y)u+Iy(x,y)v+O(u2,v2)≈I(x,y)+Ix(x,y)u+Iy(x,y)vI(x+u, y+v)=I(x, y)+I_{x}(x, y) u+I_{y}(x, y) v+O\left(u^{2}, v^{2}\right) \approx I(x, y)+I_{x}(x, y) u+I_{y}(x, y) v I(x+u,y+v)=I(x,y)+Ix​(x,y)u+Iy​(x,y)v+O(u2,v2)≈I(x,y)+Ix​(x,y)u+Iy​(x,y)v

其中, IxI_xIx​和IyI_yIy​是III偏微分, 在图像中是xxx和yyy方向上的梯度图,可通过cv2.Sobel()得到

Ix=∂I(x,y)∂x,Iy=∂I(x,y)∂yI_{x}=\frac{\partial I(x, y)}{\partial x}, \quad I_{y}=\frac{\partial I(x, y)}{\partial y} Ix​=∂x∂I(x,y)​,Iy​=∂y∂I(x,y)​

把上面的式子代入E(u,v)E(u,v)E(u,v)化简得:

E(u,v)=∑(x,y)∈W(x,y)w(x,y)×[I(x,y)+uIx+vIy−I(x,y)]2=∑(x,y)∈W(x,y)w(x,y)×(uIx+vIy)2=∑(x,y)∈W(x,y)w(x,y)×(u2Ix2+v2Iy2+2uvIxIy)\begin{aligned} E(u, v) &=\sum_{(x, y)\in W(x,y)} w(x, y) \times\left[I(x, y)+u I_{x}+v I_{y}-I(x, y)\right]^{2} \\ &=\sum_{(x, y)\in W(x,y)} w(x, y) \times\left(u I_{x}+v I_{y}\right)^{2} \\ &=\sum_{(x, y)\in W(x,y)} w(x, y) \times\left(u^{2} I_{x}^{2}+v^{2} I_{y}^{2}+2 u v I_{x} I_{y}\right) \end{aligned} E(u,v)​=(x,y)∈W(x,y)∑​w(x,y)×[I(x,y)+uIx​+vIy​−I(x,y)]2=(x,y)∈W(x,y)∑​w(x,y)×(uIx​+vIy​)2=(x,y)∈W(x,y)∑​w(x,y)×(u2Ix2​+v2Iy2​+2uvIx​Iy​)​

这个用矩阵来表示, 线代的二次型转换:

E(u,v)≈[u,v]M(x,y)[uv]E(u, v) \approx[u, v] M(x,y)\left[\begin{array}{l} u \\ v \end{array}\right] E(u,v)≈[u,v]M(x,y)[uv​]

其中, 矩阵MMM如下:

M(x,y)=∑w[Ix(x,y)2Ix(x,y)Iy(x,y)Ix(x,y)Iy(x,y)Iy(x,y)2]=[∑wIx(x,y)2∑wIx(x,y)Iy(x,y)∑wIx(x,y)Iy(x,y)∑wIy(x,y)2]=[ACCB]M(x, y)=\sum_{w}\left[\begin{array}{cc} I_{x}(x, y)^{2} & I_{x}(x, y) I_{y}(x, y) \\ I_{x}(x, y) I_{y}(x, y) & I_{y}(x, y)^{2} \end{array}\right]=\left[\begin{array}{cc} \sum_{w} I_{x}(x, y)^{2} & \sum_{w} I_{x}(x, y) I_{y}(x, y) \\ \sum_{w} I_{x}(x, y) I_{y}(x, y) & \sum_{w} I_{y}(x, y)^{2} \end{array}\right]=\left[\begin{array}{ll} A & C \\ C & B \end{array}\right] M(x,y)=w∑​[Ix​(x,y)2Ix​(x,y)Iy​(x,y)​Ix​(x,y)Iy​(x,y)Iy​(x,y)2​]=[∑w​Ix​(x,y)2∑w​Ix​(x,y)Iy​(x,y)​∑w​Ix​(x,y)Iy​(x,y)∑w​Iy​(x,y)2​]=[AC​CB​]

所以最终目标函数化成了:

E(x,y;u,v)≈Au2+2Cuv+Bv2A=∑wIx2,B=∑wIy2,C=∑wIxIy\begin{aligned} &E(x, y ;u, v) \approx A u^{2}+2 C u v+B v^{2} \\ &A=\sum_{w} I_{x}^{2}, B=\sum_{w} I_{y}^{2}, C=\sum_{w} I_{x} I_{y} \end{aligned} ​E(x,y;u,v)≈Au2+2Cuv+Bv2A=w∑​Ix2​,B=w∑​Iy2​,C=w∑​Ix​Iy​​

到这里,应该很好理解,无非就是泰勒近似,以及线代里面二次型化简的东西,注意MMM这里是一个协方差矩阵,主对角线可以看成是自己方向上梯度的方差,而副对角线是与其他方向梯度的协方差,并且这是一个对称矩阵。

下面说点不是很好理解的: 二次项函数本质上是椭圆函数,椭圆方程为:

[u,v]M(x,y)[uv]=1[u, v] M(x,y)\left[\begin{array}{l} u \\ v \end{array}\right]=1 [u,v]M(x,y)[uv​]=1

可视化出来如下:

这里的λ\lambdaλ就是实对称矩阵MMM的特征值。 这里可能并不是很好理解,我下面尝试从基变换的角度解释下。 首先,MMM是实对称矩阵,那么就一定满足:

RMR⊤=Λ=(λ100λ2)R M R^{\top}=\Lambda=\left(\begin{array}{llll} \lambda_{1} & 0 \\ 0& \lambda_{2} \end{array}\right) RMR⊤=Λ=(λ1​0​0λ2​​)

RRR是MMM的特征向量组合,λ1,λ2\lambda_1, \lambda_2λ1​,λ2​是MMM的特征值。把这个式子代入上面的式子:

[u,v]RMR⊤[uv]=[u,v]Λ[uv]=[u,v](λ100λ2)[uv]=λ1u2+λ2v2=u21λ1+v21λ2=1[u, v] R M R^{\top}\left[\begin{array}{l} u \\ v \end{array}\right] = [u, v] \Lambda\left[\begin{array}{l} u \\ v \end{array}\right]=[u, v] \left(\begin{array}{llll} \lambda_{1} & 0 \\ 0& \lambda_{2} \end{array}\right)\left[\begin{array}{l} u \\ v \end{array}\right]=\lambda_1u^2+\lambda_2v^2=\frac{u^2}{\frac{1}{\lambda_1}}+\frac{v^2}{\frac{1}{\lambda_2}}=1 [u,v]RMR⊤[uv​]=[u,v]Λ[uv​]=[u,v](λ1​0​0λ2​​)[uv​]=λ1​u2+λ2​v2=λ1​1​u2​+λ2​1​v2​=1

这样就转成了标准的椭圆方程了,而MMM的特征值平方根正好是表示着椭圆长短轴。那么特征值是啥? 其实就是将原始像素点映射到新的空间中(映射规则就是[u,v]R[u,v]R[u,v]R), 在每组基方向上的梯度方差或者叫变化程度。所以我们可以用这个特征值去衡量某个方向上像素点的波动程度。这里如果再不明白,可以看下我这篇文章,补一下向量表示与基变换的相关知识。

于是乎, 通过上面的一番操作,就把某个方向是灰度变化程度大小转成了看MMM矩阵的特征值上。

2.2 窗口的角点响应函数

这里定义每个窗口的角点响应函数

R=det⁡(M)−k(trace⁡(M))2=λ1λ2−k(λ1+λ2)2R=\operatorname{det}(M)-k(\operatorname{trace}(\mathrm{M}))^{2}=\lambda_1\lambda_2-k(\lambda_1+\lambda_2)^2 R=det(M)−k(trace(M))2=λ1​λ2​−k(λ1​+λ2​)2

这里的kkk是一个经验常数,范围在(0.04,0.06)(0.04,0.06)(0.04,0.06)之间。

2.3 非极大值抑制

根据 R 的值,将这个窗口所在的区域划分为平面、边缘或角点。为了得到最优的角点,我们还可以使用非极大值抑制

当然,因为特征值λ1\lambda_1λ1​和λ2\lambda_2λ2​决定了RRR的值,所以我们可以用特征值来决定一个窗口是平面、边缘还是角点:

平面:窗口在平坦区域上滑动,窗口内的灰度值基本不会发生变化,所以∣R∣|R|∣R∣非常小,在水平和竖直方向变化量都比较小,即IxI_xIx​和IyI_yIy​小,λ1\lambda_1λ1​和λ2\lambda_2λ2​小。边缘: R<0R<0R<0, 仅在水平或竖直方向有较大变化量,IxI_xIx​和IyI_yIy​有一个较大,即λ1>>λ2\lambda_1>>\lambda_2λ1​>>λ2​或者λ2>>λ1\lambda_2>>\lambda_1λ2​>>λ1​角点: RRR值很大,水平和竖直方向变化都较大的点,即IxI_xIx​和IyI_yIy​大,λ1\lambda_1λ1​和λ2\lambda_2λ2​大。

一图胜千言:

Harris 角点检测的结果是带有这些分数RRR的灰度图像,设定一个阈值,分数大于这个阈值的像素就对应角点。

注意:Harris 检测器具有旋转不变性,但不具有尺度不变性,也就是说尺度变化可能会导致角点变为边缘,如下图所示:

So, 如何找到旋转以及尺度不变的特征点呢?这就是SIFT算法干的事情了,但是介绍之前,先看看OpenCV中角点检测算法咋用。 理论一大推,但是用起来一个函数搞定。

2.4 OpenCV中的角点检测

这里的函数是cv2.cornerHarris():

img: 数据类型为float32的输入图像blockSize: 角点检测中指定区域的大小, 即www窗口大小ksize: Sobel求导中使用的窗口大小, 需要用sobel算子求梯度,这里是设置sobel算子求导的窗口k:取值参数为[0.04, 0.06]

看个例子:

img = cv2.imread('img/test_img.jpg')gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# 角点检测dst = cv2.cornerHarris(gray, 2, 3, 0.04) # 这个是每个像素点的E值,即平移后灰度级变换程度值img[dst>0.01*dst.max()] = [0, 0, 255]

下面是角点检测结果:

3. SIFT算法

Scale Invariant Feature Transform(SIFT): 尺度不变特征转换用来侦测与描述影像中的局部性特征, 基于位置,尺度和旋转不变性在空间尺度中寻找极值点。

特点:

SIFT特征是图像的局部特征,其对旋转、尺度缩放、亮度变化保持不变性,对视角变化、仿射变换、噪声也保持一定程度的稳定性独特性好,信息量丰富,适用于海量特征在数据库中快速,准确匹配多量性,即使少数几个物体也可以产生大量的SIFT特征向量高速性和可扩展性, 特征可以方便与其他向量联合

解决问题:目标自身状态,场景所处环境和成像器材的成像特性等因素影响图像配准/目标识别跟踪的性能。SIFT算法一定程度可解决:

目标旋转,缩放,平移图像仿射/投影变换光照影响目标遮挡杂物场景噪声

SIFT算法的实质是在不同的尺度空间上查找关键点(特征点),并计算出关键点的方向。SIFT所查找到的关键点是一些十分突出,不会因光照,仿射变换和噪音等因素而变化的点,如角点、边缘点、暗区的亮点及亮区的暗点等。

SIFT算法主要下面四步:

尺度空间极值检测: 搜索所有尺度上的图像位置,通过高斯微分函数来识别潜在的对于尺度和旋转不变的特征点关键点定位:在每个候选的位置上,通过一个拟合精细的模型来确定位置和尺度。关键点的选择依据于它们的稳定程度。方向确定:基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向。所有后面的对图像数据的操作都相对于关键点的方向、尺度和位置进行变换,从而提供对于这些变换的不变性。关键点描述:在每个关键点周围的邻域内,在选定的尺度上测量图像局部的梯度。这些梯度被变换成一种表示,这种表示允许比较大的局部形状的变形和光照变化。

由于这个算法理论上稍微复杂些,下面就简单整理了,首先,要先明白这个算法到底做的是什么事情?

我理解: 寻找图像中具有旋转,平移,以及尺度不变性的那些特征点,并且最终用一个向量表示出来。

究竟是怎么做到的呢? 大致上细节如下。

3.1 图像的尺度空间

在一定范围内,无论物体时大还是小,人眼都可以分辨出来,然而计算机要有相同能力却很难,所有要让机器能够对物体在不同尺度下有一个统一的认知,就需要考虑图像在不同的尺度下都存在的特点。

尺度空间的获取通常使用高斯模糊来实现。

高斯模糊是一种图像滤波器,它使用正态分布(高斯函数)计算模糊模板,并使用该模板与原图像做卷积运算,达到模糊图像的目的。公式如下:

G(r)=12πσ2Ne−r2/(2σ2)G(r)=\frac{1}{\sqrt{2 \pi \sigma^{2}} N} e^{-r^{2} /\left(2 \sigma^{2}\right)} G(r)=2πσ2​N1​e−r2/(2σ2)

σ\sigmaσ参数是标准差,指定的越大,说明像素的变化幅度会越大,越偏离原始图像,即图像就会越模糊。rrr模糊半径,指模板元素到模板中心的距离,假如二维模板大小维m∗nm*nm∗n, 模板上元素(x,y)(x,y)(x,y)对应的高斯计算公式:

G(x,y)=12πσ2e−(x−m/2)2+(y−n/2)22σ2G(x, y)=\frac{1}{2 \pi \sigma^{2}} e^{-\frac{(x-m / 2)^{2}+(y-n / 2)^{2}}{2 \sigma^{2}}} G(x,y)=2πσ21​e−2σ2(x−m/2)2+(y−n/2)2​

分布不为零的像素组成的卷积矩阵与原始图像做变换。每个像素的值都是周围相邻像素值的加权平均。原始像素的值有最大的高斯分布值,所以有最大的权重,相邻像素随着距离原始像素越来越远,其权重也越来越小。这样进行模糊处理比其它的均衡模糊滤波器更高地保留了边缘效果。

不同σ\sigmaσ的模糊效果如下:

3.2 多分辨率金字塔

尺度空间使用高斯金字塔表示, 尺度规范化LoG(Laplacion of Gaussian)算子具有真正尺度不变性,Lowe使用高斯差分金字塔近似LoG算子,在尺度空间检测稳定关键点。

尺度空间在实现时,使用高斯金字塔表示,高斯金字塔构建主要分为两部分:

对图像做不同尺度的高斯模糊对图像做降采样点构建金字塔

高斯金字塔应该不陌生了:

图像的金字塔模型是指,将原始图像不断降阶采样,得到一系列大小不一的图像,由大到小,从下到上构成的塔状模型。原图像为金子塔的第一层,每次降采样所得到的新图像为金字塔的一层(每层一张图像),每个金字塔共nnn层。金字塔的层数根据图像的原始大小和塔顶图像的大小共同决定,其计算公式如下:

n=log⁡2{min⁡(M,N)}−t,t∈[0,log⁡2{min⁡(M,N)})n=\log _{2}\{\min (M, N)\}-t, t \in\left[0, \log _{2}\{\min (M, N)\}\right) n=log2​{min(M,N)}−t,t∈[0,log2​{min(M,N)})

其中M,NM,NM,N为原图像的大小,ttt为塔顶图像的最小维数的对数值。

3.3 DoG空间极值点检测

在实际计算时,使用高斯金字塔每组中相邻上下两层图像相减,得到高斯差分图像。

这个就是高斯金字塔中的每一层的图片,进行差分,这样得到的结果中,像素点相差较大的位置,就是不同尺度的图片之间的不同。DoG公式定义如下:

D(x,y,σ)=[G(x,y,kσ)−G(x,y,σ)]∗I(x,y)=L(x,y,kσ)−L(x,y,σ)D(x, y, \sigma)=[G(x, y, k \sigma)-G(x, y, \sigma)] * I(x, y)=L(x, y, k \sigma)-L(x, y, \sigma) D(x,y,σ)=[G(x,y,kσ)−G(x,y,σ)]∗I(x,y)=L(x,y,kσ)−L(x,y,σ)

这个公式也非常好理解, 差分结果就等于第一次高斯滤波的结果,与第二次高斯滤波结果之差。

为了寻找尺度空间的极值点, 每个像素点要和其图像域(同一尺度空间)和尺度域(相邻的尺度空间)的所有相邻点进行比较, 当其大于(或者小于)所有相邻点时,该点就是极值点。 如下图所示, 中间的检测点要和其所在图像的3×33\times 33×3邻域的8个像素点,以及其相邻的上下两层的3×33\times 33×3领域的18个像素点,共26个像素点进行比较。

由于要在相邻尺度进行比较,如上上面那个图,每组含4层高斯差分金字塔,只能中间两层中进行两个尺度的极值点检测,为了在每组中检测SSS个尺度的极值点,则DOG金字塔每组需要S+2S+2S+2层图像, 而DOG金字塔由高斯金字塔相邻两层相减得到, 则高斯金字塔每组需S+3S+3S+3层图像, 实际计算时SSS在3-5之间。

当然这样产生的极值点并不全都是稳定的特征点,因为某些极值点响应较弱,而且DoG算子会产生较强的边缘响应。

值得一提的是,选出的高斯差分金字塔极值点只是候选的特征点。虽然高斯差分金字塔极值点已经能够较好地代表图像的特征并且具有尺度不变性,但在选取过程中没有考虑图像特征点对于图像噪声的鲁棒性,这样确定出的图像特征点在实际应用时易出现图像匹配不当等问题。

3.4 关键点的精确定位

以上方法检测到的极值点是离散空间的极值点,以下通过拟合三维二次函数来精确确定关键点的位置和尺度,同时去除低对比度的关键点和不稳定的边缘响应点(因为DoG算子会产生较强的边缘响应),以增强匹配稳定性、提高抗噪声能力。

为了稍微好理解一点,先看看一维情况下,如何通过离散值拟合曲线:

放到三维情况下:

D(Δx,Δy,Δσ)=D(x,y,σ)+[∂Dx∂Dy∂Dσ][ΔxΔyΔσ]+12[ΔxΔyΔσ][∂2D∂x2∂2D∂x∂y∂2D∂x∂σ∂2D∂y∂x∂2D∂y2∂2D∂y∂σ∂2D∂σ∂x∂2D∂σ∂y∂2D∂σ2][ΔxΔyΔσ]D(\Delta x, \Delta y, \Delta \sigma)=D(x, y, \sigma)+\left[\begin{array}{lll} \frac{\partial D}{x} & \frac{\partial D}{y} & \frac{\partial D}{\sigma} \end{array}\right]\left[\begin{array}{c} \Delta x \\ \Delta y \\ \Delta \sigma \end{array}\right]+\frac{1}{2}\left[\begin{array}{lll} \Delta x & \Delta y & \Delta \sigma \end{array}\right]\left[\begin{array}{ccc} \frac{\partial^{2} D}{\partial x^{2}} & \frac{\partial^{2} D}{\partial x \partial y} & \frac{\partial^{2} D}{\partial x \partial \sigma} \\ \frac{\partial^{2} D}{\partial y \partial x} & \frac{\partial^{2} D}{\partial y^{2}} & \frac{\partial^{2} D}{\partial y \partial \sigma} \\ \frac{\partial^{2} D}{\partial \sigma \partial x} & \frac{\partial^{2} D}{\partial \sigma \partial y} & \frac{\partial^{2} D}{\partial \sigma^{2}} \end{array}\right]\left[\begin{array}{c} \Delta x \\ \Delta y \\ \Delta \sigma \end{array}\right] D(Δx,Δy,Δσ)=D(x,y,σ)+[x∂D​​y∂D​​σ∂D​​]⎣⎡​ΔxΔyΔσ​⎦⎤​+21​[Δx​Δy​Δσ​]⎣⎢⎡​∂x2∂2D​∂y∂x∂2D​∂σ∂x∂2D​​∂x∂y∂2D​∂y2∂2D​∂σ∂y∂2D​​∂x∂σ∂2D​∂y∂σ∂2D​∂σ2∂2D​​⎦⎥⎤​⎣⎡​ΔxΔyΔσ​⎦⎤​

这里的Δx\Delta xΔx表示相对窗口中心的偏移量,等价于上一节中的uuu。 如果对上面进行求导,并令导数等于0,就可以得到较为准确的极值点和极值。

D(x)=D+∂DT∂xΔx+12ΔxT∂2DT∂x2ΔxΔx=−∂2D−1∂x2∂D(x)∂xD(x)=D+\frac{\partial D^{T}}{\partial x} \Delta x+\frac{1}{2} \Delta x^{T} \frac{\partial^{2} D^{T}}{\partial x^{2}} \Delta x \quad \Delta x=-\frac{\partial^{2} D^{-1}}{\partial x^{2}} \frac{\partial D(x)}{\partial x} D(x)=D+∂x∂DT​Δx+21​ΔxT∂x2∂2DT​ΔxΔx=−∂x2∂2D−1​∂x∂D(x)​

这里将每个候选极值点求出相应的导数,然后代入,得到D(x)D(x)D(x), 把结果值非常小的(比如小于0.03)的先进行首轮剔除。

3.5. 消除边界响应

DoG算子会产生较强的边缘响应,即容易保留边界点,所以下面需要剔除不稳定的边缘响应点。具体做法如下:

首先,获取每个特征点出的Hessian矩阵, 这里其实和角点检测那里是一样的:

H(x,y)=[Dxx(x,y)Dxy(x,y)Dxy(x,y)Dyy(x,y)]H(x, y)=\left[\begin{array}{ll} D_{x x}(x, y) & D_{x y}(x, y) \\ D_{x y}(x, y) & D_{y y}(x, y) \end{array}\right] H(x,y)=[Dxx​(x,y)Dxy​(x,y)​Dxy​(x,y)Dyy​(x,y)​]

令α\alphaα是最大的特征值,β\betaβ是最小的特征值,下面找一个边界值:

Tr⁡(H)=Dxx+Dyy=α+βTr⁡(H)2Det⁡(H)=(α+β)2αβ=(γ+1)2γDet⁡(H)=DxxDyy−(Dxy)2=αβ\begin{array}{cl} \operatorname{Tr}(H)=D_{x x}+D_{y y}=\alpha+\beta & \frac{\operatorname{Tr}(H)^{2}}{\operatorname{Det}(H)}=\frac{(\alpha+\beta)^{2}}{\alpha \beta}=\frac{(\gamma+1)^{2}}{\gamma} \\ \operatorname{Det}(H)=D_{x x} D_{y y}-\left(D_{x y}\right)^{2}=\alpha \beta & \end{array} Tr(H)=Dxx​+Dyy​=α+βDet(H)=Dxx​Dyy​−(Dxy​)2=αβ​Det(H)Tr(H)2​=αβ(α+β)2​=γ(γ+1)2​​

导数由采样点相邻差估计得到. 这个比值越大,说明两个特征值的比值越大,即在某一个方向的梯度值越大,而在另一个方向的梯度值越小,而边缘恰恰就是这种情况。所以为了剔除边缘响应点,需要让该比值小于一定的阈值。所以,对于每个特征点,再通过下面公式:

Tr⁡(H)2Det⁡(H)<(r+1)2r\frac{\operatorname{Tr}(H)^{2}}{\operatorname{Det}(H)}<\frac{(r+1)^{2}}{r} Det(H)Tr(H)2​<r(r+1)2​

这个不等式成立的关键点保留下来,反之剔除掉。论文中γ=10\gamma=10γ=10

3.6 特征点的主方向

根据上面的一顿操作,就能找到比较好的一些候选关键点,但是在建立高斯差分金字塔以选取图像特征点时,算法考虑了关键点的尺度不变性。而对于图像特征而言,与尺度不变性同等重要的还有旋转不变性。

为了使描述具有旋转不变性,需要利用图像的局部特征为给每一个关键点分配一个主方向,该“主方向”以生成特征点周围的局部区域的梯度方向基准,使图像在旋转后仍能与旋转前保持相同的特征描述。

算法将关键点指定大小领域中的所有点计算梯度方向与赋值,并统计所有梯度方向对应的赋值和,作关键点周围iii邻域梯度方向直方图。梯度的模值m(x,y)m(x,y)m(x,y)和方向(θ(x,y)(\theta(x,y)(θ(x,y)计算公式如下:

m(x,y)=[L(x+1,y)−L(x−1,y)]2+[L(x,y+1)−L(x,y−1)]2θ(x,y)=arctan⁡L(x,y+1)−L(x,y−1)L(x+1,y)−L(x−1,y)\begin{aligned} &m(x, y)=\sqrt{[L(x+1, y)-L(x-1, y)]^{2}+[L(x, y+1)-L(x, y-1)]^{2}} \\ &\theta(x, y)=\arctan \frac{L(x, y+1)-L(x, y-1)}{L(x+1, y)-L(x-1, y)} \end{aligned} ​m(x,y)=[L(x+1,y)−L(x−1,y)]2+[L(x,y+1)−L(x,y−1)]2​θ(x,y)=arctanL(x+1,y)−L(x−1,y)L(x,y+1)−L(x,y−1)​​

这样,对于每个关键点, 就有了三个信息(x,y,σ,θ)(x,y,\sigma, \theta)(x,y,σ,θ), 即位置,尺度和方向。但关键点的主方向到底是啥呢?

对于一个关键点, 要统计它邻域内各个点的梯度直方图,

下图直方图为简化版本(只有8个bin,实际操作时算法会统计从0到360°步长为10°的共计36个梯度方向的幅值和,共有36个bin)。梯度方向直方图中最高的bin对应的方向即定义为该关键点的主方向,若存在任一方向的幅值大于主方向幅值的80%,则将其作为辅方向。所以,对于同一个关键点,可能有多个方向,这种情况在相同位置和尺度将会有多个关键点被创建但方向不同。实际编程实现中,就是把该关键点复制成多份关键点,并将方向值分别赋给这些复制后的关键点。

直方图的横轴是方向,纵轴是邻域内各个点对应方向梯度的累加和。

3.7 生成特征描述

得到特征点二维位置、尺度位置、主方向的具体信息后,算法需要解决的最后一个问题就是生成关键点信息的描述子,即用一个向量描述图像中的特征点信息。使其不随各种变化而改变,比如光照变化、视角变化等等。这个描述子不但包括关键点,也包含关键点周围对其有贡献的像素点,并且描述符应该有较高的独特性,以便于提高特征点正确匹配的概率。

为了保证特征矢量的旋转不变性, 要以特征点为中心, 在附近领域内将坐标轴旋转θ\thetaθ角度,即将坐标轴旋转为特征点的主方向。

各个像素点的坐标变换是用下面的公式:

[x′y′]=[cos⁡θ−sin⁡θsin⁡θcos⁡θ][xy]\left[\begin{array}{l} x^{\prime} \\ y^{\prime} \end{array}\right]=\left[\begin{array}{cc} \cos \theta & -\sin \theta \\ \sin \theta & \cos \theta \end{array}\right]\left[\begin{array}{l} x \\ y \end{array}\right] [x′y′​]=[cosθsinθ​−sinθcosθ​][xy​]

旋转完之后,算法将特征点周围的16×1616\times1616×16邻域分为4个8×88\times88×8的区域,再将8×88\times88×8的区域划为2×22\times22×2区域,即每个小区域为4×44\times44×4的范围。统计每个4×44\times44×4区域的梯度方向直方图(直方图为8个bin,8个方向),故共计4∗4∗8=1284*4*8=1284∗4∗8=128个bin。对应生成128维向量(值为梯度方向的幅值)。该128维向量即该点的特征描述子。

每个像素的梯度幅值和方向(箭头方向代表梯度方向,长度代表梯度幅值), 然后利用高斯窗口对其加权运算(离中心点距离不一样), 最后在每个4×44\times44×4的小块上绘制8个方向的梯度直方图, 计算每个梯度方向的累加值, 即可形成一个种子点, 即每个特征点的由4×44\times44×4个种子点组成, 每个种子点由8个方向的向量信息。

论文中建议对每个关键点使用4×44\times44×4共16个种子点来描述,每个种子点由888个方向的梯度直方图描述,即8维向量, 这样一个关键点就会产生128维的SIFT特征向量

OK, 到这里,就把SIFT算法的大致流程整理了下,不过上面有些乱,下面集中总结下:

原始图片,先用高斯滤波做模糊操作的尺度变换,通过改变高斯核的标准差,得到SSS图片

为了让后序的特征表达更加丰富,把这SSS张图片,做成高斯金字塔

接下来,对于高斯金字塔的每一个level的SSS张图片, 相邻图片进行差分运算,得到高斯差分图像,S−1S-1S−1张

基于高斯差分图像,去寻找候选的极值点,所谓极值点,就是对差分图像中,不是首尾层的图像中的每个像素点,去对比不同尺度空间(上下相邻两层邻域)和同一尺度空间周围像素点的值与当前这个像素点值大小,拿到极大值点,作为候选的关键点。

定位精确的关键点,对尺度空间中DOG函数进行曲线拟合,计算其极值点,从而实现关键点的精确定位。这里用到了泰勒近似,拟合出函数来之后,把当前关键点代入,得到的函数值过小的(比如小于0.03)的关键点去掉。

消除边缘效应,对剩下的关键点,获取特征点处的梯度矩阵,使用的有限差分法求导,得到这个矩阵之后,根据特征值的比例再进行筛选,水平方向梯度与垂直方向梯度差不多的保留下来,相差很大,说明是边缘, 这种关键点要去掉,即消除边缘效应

通过上面步骤,就把关键点给选择了出来,但是仅仅从尺度不变性角度进行的考虑,接下来考虑平移不变性, 为每个关键点选择一个主方向

这个就是考虑关键点邻域内的所有像素点,用一个直方图去统计这个邻域内所有像素点的梯度方向以及梯度累加值,梯度方向由于是360度,这里进行了分桶操作,划分成了8个bins。拿到直方图之后,把梯度累加值最大的那个方向作为关键点的主方向, 当然次大的还可以作为辅方向,这样每个关键点就同时有了位置,尺度,方向三个特征描述(如果关键点还有辅方向的,就需要把这个观测点复制一份,保持位置,尺度不变,该变方向即可)。

每个观测点有了三种属性描述,接下来,需要转成特征,即想用一个向量来描述

首先,对于每个观测点值,先把邻域内的所有像素点的坐标值修改,修改原则是坐标轴要保持和当前观测点的主方向一致修改完毕之后, 统计这个邻域内的像素点的梯度直方图,依然是8个bins,每个bins中是像素点梯度的累加值,这样对于一个关键点,锁定一个邻域就能得到8个值。 这算是一个种子区间内的特征描述真正做的时候,是先对于一个关键点给定一个大的邻域,然后把这个大的邻域进行划分成小的邻域,作为一个个的种子区间,然后得到种子区间的特征关键点最后的特征描述,就是所有种子矩阵特征的Concat值,上面是一个128维的向量表示,这样就得到了关键点尺度和平移不变的特征描述

金字塔中所有关键点,都得到特征描述,返回结果

下面就可以看代码了。

3.8 OpenCV下的SIFT算法

对于SIFT算法, OpenCV中直接也是一个函数搞定。

img = cv2.imread('img/test_1.jpg')gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)# opencv版本高于3.4.3, 这个sift算法使用变成cv2.SIFT_create(), 在这之前的版本是cv2.xfeatures2d.SIFT_create()# SIFT检测器sift = cv2.SIFT_create()# 找出图像中的关键点kp = sift.detect(gray, None)# 在图中画出关键点img = cv2.drawKeypoints(gray, kp, img) # 计算关键点对应的SIFT特征向量kp, des = pute(gray, kp) # 上面的两步,也可以用下面的一步sift = cv2.SIFT_create()kp1, des1 = sift.detectAndCompute(gray, None) # 这里还能一步到位,直接算出关键点以及关键向量

看下效果:

4. 特征匹配

拿到图像中的关键点,以及也能用向量来描述这个关键点的特征了,那么给定两张图像之后,怎么看出这两张图片中哪些关键点比较相似呢?这就是看关键点向量之间的差异了,也是特征匹配在做的事情。

4.1 Brute-Force蛮力匹配

暴力匹配,两个图像中的关键点的特征向量,一个个的计算差异, 即两层for循环了。 这里直接看怎么用:

首先, 读入两张图片,然后拿到各自的关键点以及关键点向量。

img1 = cv2.imread('img/box.png', 0)img2 = cv2.imread('img/box_in_scene.png', 0)# sift算法,得到每张图的关键点以及关键向量sift = cv2.SIFT_create()kp1, des1 = sift.detectAndCompute(img1, None) # 这里还能一步到位,直接算出关键点以及关键向量kp2, des2 = sift.detectAndCompute(img2, None)

下面进行特征匹配:

1对1的匹配: 即图片A中的一个向量匹配图片B中的一个向量

# crossCheck表示两个特征点要互相匹配,例如A中的第i个特征点与B中的第j个特征点最近的,并且B中的第j个特征点到A中的第i个特征点也是最近的# NORM_L2: 归一化数组的欧几里得距离, 如果其他特征计算方法需要考虑不同的匹配计算方式bf = cv2.BFMatcher(crossCheck=True)# 1对1的匹配matches = bf.match(des1, des2)matches = sorted(matches, key=lambda x: x.distance)

如果这里指定属性crossCheck为False, 得到的结果是604,如果为True,得到的结果是206,所以我基于这个,盲猜下暴力匹配以及这个属性的意义。

暴力匹配的话,就是对于A图像中的每个观测点的向量, 我遍历一遍B图像中每个观测点的向量,然后求欧几里得距离,拿到最小的。这样对于A图像中每个观测点,就得到了B图像里面的最佳匹配。

然后再对于B图像中的每个观测点,也同样用上面的方式走一遍,这样对于B图像中的每个观测点,也得到了A图像中的最佳匹配。

此时,如果是:

bf.match(des1, des2): 返回的就是A图像中的每个观测点的最佳匹配,个数是A中关键点的个数bf.match(des2, des1): 返回的是B图像中每个观测点的最佳匹配, 个数是B中关键点的个数如果设置crossCheck为True的话:bf.match(des1, des2)bf.match(des2, des1)都是260, 表示的其实是A和B中,关键点互相匹配的那些,比如A中的第j个观测点,最近点是B中的i,那么B中的i,也要对应A中的j,即<i,j> VS <j, i>

img3 = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None, flags=2)

画10个最近的匹配看看:

K对最佳匹配: 对于一个关键点, 找K个最相似的向量。

bf = cv2.BFMatcher()matches = bf.knnMatch(des1, des2, k=2) # 相当于对于A中的每个关键点,不是找某一个最相似,而是K相似

这里可以根据近邻之间的相似比例对关键点筛选

good = []for m, n in matches:if m.distance < 0.75 * n.distance:good.append([m])

也可以可视化一下:

img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, good, None, flags=2)

结果如下:

这里会发现一个问题,对于上面的匹配结果,大部分结果还可以,但有某些匹配错误的点,这种情况怎么弥补呢? 可以使用RANSAC(Random sample consensus)随机抽样一致算法。 这个东西可以简单看下原理。

4.2 RANSAC(Random sample consensus)算法

这个算法也是一个拟合算法, 和最小二乘对比如下:

思路是这样: 先选择初始样本点进行拟合, 给定一个容忍的范围,不断进行迭代。

每一次拟合之后, 容差范围内都有对应的数据点数, 找出数据点个数最多的情况,就是最终的拟合结果。所以这种算法在拟合的时候,先随机抽样初始点,然后进行拟合的时候,会考虑容忍范围(能拟合的数据点个数)

那么,这东西干啥用呢? 下面全景拼接的Demo会用到。

5. 全景拼接Demo

全景拼接大家肯定都玩过,相机里就有这个功能, 这个的原理大概是这样, 假设有两张图片要进行拼接,大概流程如下:

通过SIFT算法拿到两张图片的关键点以及关键向量

根据关键向量做特征匹配

根据特征匹配点,对图片进行一些仿射变换,比如平移,选择,缩放等,这里是保证能无缝衔接上,如果不做处理,那肯定拼接不是。

既然是仿射变换,本质是乘变换矩阵,所以核心就是这个变换矩阵求解

如果需要做变换,目的就是求变换矩阵, 至少有4对匹配好的特征点才行。那么思路就是,先用随机采样算法采出四对匹配点,然后解8个方程得到上面的变换矩阵, 然后基于这个变换矩阵算其他的匹配点是否能匹配上,定义一个loss函数为匹配上的点的对数,这样就能基于当前的四对匹配点算出一个loss值。 然后再随机采样四对, 再求变换矩阵,再看看其他匹配点能匹配上的个数,得到损失。依次迭代多次, 取loss函数最小的那个变换矩阵。

找到变换矩阵,对某张图片先进行变换,然后进行拼接即可。

这么说比较抽象,下面通过一个Demo把这个过程带起来。

5.1 读入图像,拿到SIFT特征

代码如下:

image_left = cv2.imread('img/left_01.png')image_right = cv2.imread('img/right_01.png')

这两张图片如下:

显然,这样拼接是无法拼接上的。

下面拿到关键点和特征向量

def detectAndDescribe(image):# 将彩色图片转成灰度图gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# SIFT生成器destriptor = cv2.SIFT_create()(kps, features) = destriptor.detectAndCompute(image, None)# 结果转成numpy数组kps = np.float32([kp.pt for kp in kps])return (kps, features)# 检测A, B图片的SIFT特征关键点,得到关键点的表示向量(kps_left, features_left) = detectAndDescribe(image_left) # kpsA (关键点个数, 坐标) features(关键点个数,向量)(kps_right, features_right) = detectAndDescribe(image_right)

5.2 特征匹配

这里依然是写个函数:

def matchKeyPoints(kpsA, kpsB, featuresA, featuresB, ratio=0.75, reprojThresh=4.0):# 建立暴力匹配器matcher = cv2.BFMatcher()# KNN检测来自两张图片的SIFT特征匹配对rawMatches = matcher.knnMatch(featuresA, featuresB, 2)matches = []for m in rawMatches:# 当最近距离跟次近距离的比值小于ratio时,保留此配对# (<DMatch 000001B1D6B605F0>, <DMatch 000001B1D6B60950>) 表示对于featuresA中每个观测点,得到的最近的来自B中的两个关键点向量if len(m) == 2 and m[0].distance < m[1].distance * ratio: # 存储两个点在featuresA, featuresB中的索引值matches.append([m[0].trainIdx, m[0].queryIdx]) # 这里怎么感觉只用了m[0]也就是最近的那个向量啊,应该没用到次向量# 这个m[0].trainIdx表示的时该向量在B中的索引位置, m[0].queryIdx表示的时A中的当前关键点的向量索引# 当筛选后的匹配对大于4时,可以拿来计算视角变换矩阵if len(matches) > 4:# 获取匹配对的点坐标ptsA = np.float32([kpsA[i] for (_, i) in matches])ptsB = np.float32([kpsB[i] for (i, _) in matches])# 计算视角变换矩阵 这里就是采样,然后解方程得到变换矩阵的过程(H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)return (matches, H, status)# 匹配结果小于4时,返回Nonereturn None

主要逻辑是从图片B中给图片A中的关键点拿最近的K个匹配向量,然后基于规则筛选, 保存好匹配好的关键点的两个索引值,通过索引值取到匹配点的坐标值,有了多于4对的坐标值,就能得到透视变换矩阵。 这里返回的主要就是那个变换矩阵。

# 匹配两张图片的所有特征点,返回匹配结果 注意,这里是变换right这张图像,所以应该是从left找与right中匹配的点,然后去计算right的变换矩阵M = matchKeyPoints(kps_right, kps_left, features_right, features_left)if not M:# 提取匹配结果(matches, H, status) = M

这里要一定要注意好,到底是对哪张图片做变换,比如给图片B做变换,那么就从A中找与B中特征点匹配的特征向量,求的是让图片B变换的透视矩阵。 这里是对right做变换,所以从left中给right的关键点找匹配点,给right计算透视矩阵,接下来,变换right

# 图片right进行视角变换, result是变换后的图片result = cv2.warpPerspective(image_right, H, (image_left.shape[1] + image_right.shape[1], image_right.shape[0]))cv_imshow('result', result)

这里看下变换之后的结果:

接下来,进行拼接即可:

# 将图片A传入result图片最左端result[0:image_right.shape[0], 0:image_right.shape[1]] = image_left

结果如下:

这样就拼接起来了,除了亮度不太一样外,效果还是不错的。

5.3 画出匹配图像

这里还可以在图像上画出匹配的向量:

def drawMatches(imageA, imageB, kpsA, kpsB, matches, status):# 初始化可视化图片,将A、B图左右连接到一起(hA, wA) = imageA.shape[:2](hB, wB) = imageB.shape[:2]vis = np.zeros((max(hA, hB), wA + wB, 3), dtype="uint8")vis[0:hA, 0:wA] = imageAvis[0:hB, wA:] = imageB# 联合遍历,画出匹配对for ((trainIdx, queryIdx), s) in zip(matches, status):# 当点对匹配成功时,画到可视化图上if s == 1:# 画出匹配对ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))cv2.line(vis, ptA, ptB, (0, 255, 0), 1)# 返回可视化结果return visvis = drawMatches(image_left, image_right, kps_left, kps_right, matches, status)

结果如下:

6. 小总

这篇文章主要是整理了在图像处理中重要且常用的找特征点的两个算法Harris和SIFT算法,包括算法的数学原理以及如何使用,然后是整理了下特征匹配与一致性采样算法,这俩东西其实为了后面的透视变换矩阵服务。 最后通过一个全景图拼接的Demo感受了下算法的魅力。

当然这只是冰山下面的一小角哈,因为后面要做一个停车场车位识别的一个项目, 会用到这里面的这些知识。另外就是这些方法依然是普适性的算法,所以也想沉淀下,方便后面回看。

参考

OpenCV入门到实战课程角点检测:Harris 与 Shi-TomasiSIFT算法详解SIFT算法简述及Python标记SIFT特征检测实践

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。