上一篇文章介绍的特征检测器已经可以较好地解决方向不变性问题,即图像旋转后仍能检测到相同的特征点。这篇文章介绍 SIFT 特征检测器,下一篇文章介绍对 SIFT 的改进 SURF 特征检测器,可以解决尺度不变性问题,即在任何尺度下拍摄的物体都能检测到一致的关键点,而且每个被检测的特征点都对应一个尺度因子。理想情况下,对比两幅图像中不同尺度的同一个物体点,计算得到的两个尺度因子之间的比率应该等于图像尺度的比率。
Scale-Invariant Feature Transform,尺度不变特征转换
尺度空间就是试图在图像领域中模拟人眼观察物体的概念与方法。例如:观察一棵树,关键在于我们想要观察的是树叶子还是整棵树:如果是一整棵树(相当于大尺度情况下观察),那么就应该去除图像的细节部分。如果是树叶(小尺度情况下观察),那么就应该观察局部细节特征。
高斯核函数是唯一的尺度不变核函数,对图像使用高斯滤波能够对图像进行模糊,使用不同的 “高斯核” 可得到不同模糊程度的图像,可以模拟人在距离目标由近到远时目标在视网膜上形成过程。高斯正态分布的标准差,称为尺度空间因子,反映了图像被模糊的程度,其值越大图像越模糊,对应的尺度也就越大。
尺度空间在实现时使用高斯金字塔表示,高斯金字塔的构建分为两步:
高斯金字塔构建过程中,一般首先将图像扩大一倍,将原始图像扩大一倍后再滤波能够保留更多的信息便于后续特征提取与匹配。然后在扩大的图像的基础上构建高斯金字塔,然后对该尺寸下图像进行不同参数的高斯模糊(每一幅模糊的图像称为一个层(layer)),几幅模糊图像的图像集合构成了一个组(octave)。然后对该组下的倒数第三张图像进行降采样(长宽分别缩小一倍)作为下一个组的初始图像,在初始图像的基础上完成属于这个组的高斯模糊处理,依次类推完成整个算法所需要的所有组构建,这样高斯金字塔就构建出来了,如下图所示
取倒数第三张图像是为了保证尺度空间的连续性,见以下 尺度变化的连续性
易知,高斯金字塔有多个组,每个组又有多层。一个组中的多个层之间的尺度是不一样的(也就是使用的高斯参数 σ \sigma σ 不同),相邻两层之间的尺度相差一个比例因子 k k k。如果每个组有 S S S 层,则 k = 2 1 S k = 2^{\frac{1}{S}} k=2S1 。上一个组的最底层图像是由下一个组中尺度为 2 σ 2\sigma 2σ 的图像进行 2 倍降采样得到的(高斯金字塔先从底层建立)。高斯金字塔的组数一般是
o = [ log 2 m i n ( m , n ) ] − a , a ∈ [ 0 , log 2 m i n ( m , n ) ) o = [\log_2 min(m, n)] - a, \; a \in [0, \log_2 min(m, n)) o=[log2min(m,n)]−a,a∈[0,log2min(m,n))
o o o 表示高斯金字塔的组数, m , n m,n m,n 分别是图像的行和列。 a a a 与具体需要的金字塔的顶层图像的大小有关。
高斯模糊参数 σ \sigma σ (尺度空间),可由下面关系式得到
σ ( o , s ) = σ 0 ⋅ 2 o + s S \sigma(o, s) = \sigma_0 \cdot 2^{o+\frac{s}{S}} σ(o,s)=σ0⋅2o+Ss
其中 o o o 为所在的组数, s s s 为所在的层, σ 0 \sigma_0 σ0 为初始的尺度, S S S 为每个组的层数
从上面可以得知同一个组内相邻曾的图像尺度关系:
σ s + 1 = k ⋅ σ s = 2 1 S ⋅ σ s \sigma_{s+1} = k \cdot \sigma_s = 2^{\frac{1}{S}} \cdot \sigma_s σs+1=k⋅σs=2S1⋅σs
相邻的组之间的尺度关系:
σ o + 1 = 2 σ o \sigma_{o+1} = 2 \sigma_o σo+1=2σo
构建尺度空间的目的是为了检测在不同的尺度下都存在的特征点,而检测特征点较好的算子是高斯拉普拉斯 LoG,使用 LoG 虽然能较好的检测到图像中的特征点,但是其运算量过大,通常可使用高斯差分 DoG 来近似 LoG。
对于使用 DoG 近似 LoG 可以查看这篇文章:OpenCV —— 边缘检测(Laplacian、LoG、DoG、Marr-Hildreth 边缘检测)
所以对同一个组的两幅相邻图像做差就得到了高斯差分金字塔。
特征点是由 DoG 空间的局部极值点组成的。为了寻找 DoG 函数的极值点,每一个像素点要和其图像域(同一尺度空间)和尺度域(相邻的尺度空间)的相邻点比较,当其大于(或者小于)所有相邻点时,该点就是极值点。特征点是由 DoG 空间的局部极值点组成的。如下图,中间的检测点和它同尺度的 8 个相邻点和上下相邻尺度对应的 9x2 个点共 26 个点比较,以确保在尺度空间和二维图像空间都检测到极值点。
从上面的描述可以知道,每一个组图像的第一层和最后一层是无法进行比较取得极值的,为了满足尺度变换的连续性,在每一个组的顶层继续使用高斯模糊生成 3 幅图像,高斯金字塔每个组有 S + 3 S+3 S+3 层图像,高斯差分金字塔的每个组有 S + 2 S+2 S+2 层图像。
为什么高斯金字塔中每个组有 S + 3 S+3 S+3 幅图像?
S S S 的意思是在差分高斯金字塔中求极值点的时候,要在每个组中求 S S S 层点(每一层极值点是与上下两个尺度空间比较得到的),因此为了获得 S S S 层点,那么在高斯差分金字塔中需要有 S + 2 S+2 S+2 层图像,所以在高斯金字塔中就需要有 S + 3 S+3 S+3 层图像(因为高斯差分金字塔是由高斯金字塔相邻两层相减得到的)。
为什么要在每个组中求 S S S 层点?
是为了保持尺度的连续性。下面进行详细的分析:
以第一个组且 S = 3 S=3 S=3 为例,高斯图像和差分高斯图像的尺度如下,每一层图像的尺度也在图像表示出来了
因此,当前组中各高斯图像的尺度依次为:
σ , 2 1 3 σ , 2 2 3 σ , 2 3 3 σ , 2 4 3 σ , 2 5 3 σ \sigma, 2^{\frac{1}{3}} \sigma,2^{\frac{2}{3}} \sigma, 2^{\frac{3}{3}} \sigma, 2^{\frac{4}{3}} \sigma, 2^{\frac{5}{3}} \sigma σ,231σ,232σ,233σ,234σ,235σ
当前组各高斯差分图像的尺度依次为:
σ , 2 1 3 σ , 2 2 3 σ , 2 3 3 σ , 2 4 3 σ \sigma, 2^{\frac{1}{3}} \sigma,2^{\frac{2}{3}} \sigma, 2^{\frac{3}{3}} \sigma, 2^{\frac{4}{3}} \sigma σ,231σ,232σ,233σ,234σ
同理可知下一个组高斯差分图像的尺度依次为:
2 σ , 2 ∗ 2 1 3 σ , 2 ∗ 2 2 3 σ , 2 ∗ 2 3 3 σ , 2 ∗ 2 4 3 σ 2 \sigma, 2*2^{\frac{1}{3}} \sigma,2 * 2^{\frac{2}{3}} \sigma, 2 * 2^{\frac{3}{3}} \sigma, 2 * 2^{\frac{4}{3}} \sigma 2σ,2∗231σ,2∗232σ,2∗233σ,2∗234σ
可见,其中当前组中高斯差分图像的尺度为 2 1 3 σ , 2 2 3 σ , 2 3 3 σ 2^{\frac{1}{3}} \sigma,2^{\frac{2}{3}} \sigma, 2^{\frac{3}{3}} \sigma 231σ,232σ,233σ 所在代表的层,而下一个组中高斯差分图像的尺度为 2 ∗ 2 1 3 σ , 2 ∗ 2 2 3 σ , 2 ∗ 2 3 3 σ 2*2^{\frac{1}{3}} \sigma,2 * 2^{\frac{2}{3}} \sigma, 2 * 2^{\frac{3}{3}} \sigma 2∗231σ,2∗232σ,2∗233σ 所在表的层,是高斯差分金字塔中获得极值点的层。这样,当前组中最后一层高斯差分的尺度为 2 3 3 σ 2^{\frac{3}{3}} \sigma 233σ,而下一个组第一层的高斯差分的尺度为 2 4 3 σ 2^{\frac{4}{3}} \sigma 234σ ,刚好可以连续起来,这一效果带来的直接好处是在尺度空间的极值点确定过程中,不会漏掉任何一个尺度上的极值点,而是弄够综合考虑量化的尺度因子所确定的每一个尺度。
通过比较检测得到的 DoG 的局部极值点是在离散的空间搜索得到的,由于离散空间是对连续空间采样得到的结果,因此在离散空间找到的极值点不一定是真正意义上的极值点,因此需要通过尺度空间 DoG 函数进行曲线拟合来精确定位特征点的位置和尺度,同时去除低对比度的特征点和不稳定的边缘响应点。
离散空间的极值点并不是真正的极值点,下图显示了二维函数离散空间得到的极值点与连续空间极值点的差别。利用已知的离散空间点插值得到的连续空间极值点的方法叫做子像素插值(Sub-pixel Interpolation)
为了提高特征点的稳定性,需要对尺度空间 DoG 函数进行曲线拟合。利用 DoG 函数在尺度空间的泰勒展开式(拟合函数)为:
D ( X ) = D + ∂ D T ∂ X X + 1 2 X T ∂ 2 D ∂ X 2 X D(X) = D + \frac{\partial D^T}{\partial X} X + \frac{1}{2} X^T \frac{\partial^2 D}{\partial X^2} X D(X)=D+∂X∂DTX+21XT∂X2∂2DX
其中, X = ( x , y , σ ) T X = (x, y, \sigma)^T X=(x,y,σ)T ,求导并让方程等于零,可以得到极值点的偏移量为:
X ^ = − ∂ 2 D − 1 ∂ X 2 ∂ D ∂ X \hat{X} = - \frac{\partial^2 D^{-1}}{\partial X^2} \frac{\partial D}{\partial X} X^=−∂X2∂2D−1∂X∂D
对应极值点,方程的值为:
D ( X ^ ) = D + 1 2 ∂ D T ∂ X X ^ D(\hat{X}) = D + \frac{1}{2} \frac{\partial D^T}{\partial X} \hat{X} D(X^)=D+21∂X∂DTX^
其中, X ^ = ( x , y , σ ) T \hat{X} = (x, y, \sigma)^T X^=(x,y,σ)T 代表相对插值中心的偏移,当它在任意维度上的偏移量大于 0.5 时,意味着插值中心已经偏移到它的临近点上,所以必须改变当前关键点的位置。同时在新的位置上反复插值直到收敛;有也可能超出所设定的迭代次数或者超出图像边界的范围,此时这样的点应该删除。另外, ∣ D ( x ) ∣ |D(x)| ∣D(x)∣ 过小的点易受噪声的干扰而变得不稳定,所以将 ∣ D ( x ) ∣ |D(x)| ∣D(x)∣ 小于某个经验值的极值点删除。同时,在此过程中获取特征点的精确位置(原位置上加上拟合的偏移量)以及尺度。
一个定义不好的高斯差分算子的极值在横跨边缘的地方有较大的主曲率,而在垂直边缘的方向有较小的主曲率。
DoG 算子会产生较强的边缘响应,需要提出不稳定的边缘响应点。获取特征点处的 Hessian 矩阵,主曲率通过一个 2x2 的 Hessian 矩阵 H 求出
H = [ D x x D x y D x y D y y ] H = \begin{bmatrix} D_{xx} & D{xy} \\ D_{xy} & D_{yy} \end{bmatrix} H=[DxxDxyDxyDyy]
其中 D x x , D x y , D y y D_{xx}, D_{xy}, D_{yy} Dxx,Dxy,Dyy 是候选点邻域对应位置的差分求得的。
为了避免求具体的值,可以使用 H 特征值的比例。设 α = λ m a x \alpha = \lambda_{max} α=λmax 为 H 的最大特征值, β = λ m i n \beta = \lambda_{min} β=λmin 为 H 的最小特征值,则
T r ( H ) = D x x + D y y = α + β Tr(H) = D_{xx} + D_{yy} = \alpha + \beta Tr(H)=Dxx+Dyy=α+β
D e t ( H ) = D x x D y y − ( D x y ) 2 = α ⋅ β Det(H) = D_{xx}D_{yy} - (D_{xy})^2 = \alpha \cdot \beta Det(H)=DxxDyy−(Dxy)2=α⋅β
其中 T r ( H ) Tr(H) Tr(H) 为矩阵 H 的迹, D e t ( H ) Det(H) Det(H) 为矩阵 H 的行列式。设 γ = α β \gamma = \frac{\alpha}{\beta} γ=βα 表示最大特征值和最小特征值的比值,则
T r ( H ) 2 D e t ( H ) = α + β α β = ( γ β + β ) 2 γ β 2 = ( γ + 1 ) 2 γ \frac{Tr(H)^2}{Det(H)} = \frac{\alpha + \beta}{\alpha \beta} = \frac{(\gamma \beta + \beta)^2}{\gamma \beta^2} = \frac{(\gamma + 1)^2}{\gamma} Det(H)Tr(H)2=αβα+β=γβ2(γβ+β)2=γ(γ+1)2
上式的结果与两个特征值的比例有关,和具体的大小无关,当两个特征值相等时其值最小,并且随着 γ \gamma γ 的增大而增大。值越大,说明两个特征值的比值越大,即在某个一个方向的梯度值越大,而在另一个方向的梯度值越小,而边缘恰恰就是这种情况。所以为了剔除边缘响应点,需要让该比值小于一定的阈值,因此为了检测主曲率是否在某个阈值 T γ T_{\gamma} Tγ 下,只需检测
T r ( H ) 2 D e t ( H ) < ( T γ + 1 ) 2 T γ \frac{Tr(H)^2}{Det(H)} < \frac{(T_{\gamma} + 1)^2}{T_{\gamma}} Det(H)Tr(H)2<Tγ(Tγ+1)2
如果上式成立,则特征点保留,否则剔除。(在 Lowe 论文中取 T γ = 10 T_{\gamma} = 10 Tγ=10)
经过上面的步骤已经找到了在不同尺度下都存在的特征点,为了实现图像旋转不变性,需要给特征点的方向进行赋值。利用特征点邻域像素的梯度分布特性来确定其方向参数,再利用图像的梯度直方图求取特征点局部结构的稳定方向。
找到了特征点,也就可以得到该特征点的尺度 σ \sigma σ ,也就可以得到特征点所在的尺度图像
L ( x , y ) = G ( x , y , σ ) ∗ I ( x , y ) L(x, y) = G(x, y, \sigma) * I(x, y) L(x,y)=G(x,y,σ)∗I(x,y)
计算以特征点为中心,以 3 × 1.5 σ 3 \times 1.5 \sigma 3×1.5σ 为半径的区域图像的幅角和幅值,每个点 L ( x , y ) L(x,y) L(x,y) 的梯度的模 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 m(x,y) = \sqrt{[L(x+1, y) - L(x-1, y)]^2 + [L(x, y+1) - L(x, y-1)]^2} 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 ) \theta(x, y) = \arctan \frac{L(x, y+1) - L(x, y-1)}{L(x+1, y) - L(x-1, y)} θ(x,y)=arctanL(x+1,y)−L(x−1,y)L(x,y+1)−L(x,y−1)
计算得到梯度方向后,就是用直方图统计特征点邻域内像素对应的梯度方向和幅值。梯度方向的直方图的横轴是梯度方向的角度(梯度方向的范围是 0 到 360 度,直方图每 36 度一个柱,共 10 个柱),纵轴是梯度方向对应梯度幅值的累加,在直方图的峰值就是特征点的主方向。
在 Lowe 的论文还提到了使用高斯函数对直方图进行平滑以增强特征点近的邻域点对特征点方向的作用,减少突变的影响。为了得到更精确的方向,通常还可以对离散的梯度直方图进行插值拟合。具体而言,特征点的方向可以由和主峰最近的三个柱值通过抛物线插值得到。在梯度直方图中,当存在一个相当于主峰值 80% 能量的柱值时,则可以将这个方向认为是该特征点的辅助方向。所以,一个特征点可能检测到多个方向(也可以理解为,一个特征点可能产生多个坐标、尺度相同,但是方向不同的特征点)。Lowe 在论文中指出 15% 的特征点具有多方向,而且这些点对匹配的稳定性很关键。
得到特征点的主方向后,对于每个特征点可以得到三个信息( x , y , σ , θ x, y, \sigma, \theta x,y,σ,θ)即位置、尺度和方向。由此可以确定一个 SIFT 特征区域,一个 SIFT 特征区域由三个值表示,中心表示特征点位置,半径表示特征点的尺度,箭头表示主方向。具有多个方向的特征点可以被复制成多份,然后将方向值分别赋值给复制后的特征点,一个特征点就产生了多个坐标、尺度相等,但是方向不同的特征点。
至此,将检测出的含有位置、尺度和方向的特征点即是该图像的 SIFT 特征点。
对于每一个特征点,都拥有位置、尺度以及方向三个信息。为每个特征点建立一个描述,用一组向量将这个特征点描述出来,使其不随各种变化而改变,比如光照变化、视角变化等等。这个描述子不但包括特征点,也包含特征点周围对其有贡献的像素点,并且描述符应该有较高的独特性,以便于提高特征点正确匹配的概率。
特征描述符的生成大致有三个步骤:
特征描述子与特征点所在的尺度有关,因此,对梯度的求取应在特征点对应的高斯图像上进行,将特征点附近的邻域划分为 d * d (Lowe 建议 d=4)个子区域,每个子区域作为一个种子点,每个种子点有 8 个方向。将坐标轴旋转为特征点的方向,以确保旋转不变性。
将邻域内的采样点分配到对应的子区域内,计算影响子区域的采样点的梯度和方向,将子区域内的梯度值分配到 8 个方向上,计算其权值。
以 2 x 2 共 4 个种子点为例,实际是 4x4 共16个种子点来描述。
旋转后以主方向为中心取 8 x 8 的窗口,下图所示,左图的中央为当前特征点的位置,每个小格代表为特征点邻域所在尺度空间的一个像素,求取每个像素的梯度幅值与梯度方向,箭头方向代表该像素的梯度方向,长度代表梯度幅值,然后利用高斯窗口对其进行加权运算。最后在每个 4 x 4 的小块上绘制 8 个方向的梯度直方图,计算每个梯度方向的累加值,即可形成一个种子点,如右图所示。每个特征点由四个种子点组成,每个种子有 8 个方向的向量信息。这种邻域方向性信息联合增强了算法的抗噪声能力,同时对于含有定位误差的特征匹配也提供了比较理性的容错性。
与求主方向不同,此时每个种子区域的梯度直方图在 0-360 之间划分为 8 个方向区间,每个区间为 45 度,即每个种子点有 8 个方向的梯度强度信息。
插值计算每个种子点八个方向的梯度。
如上统计的 4 * 4 * 8 = 128 个梯度信息即为该关键点的特征向量。特征向量形成后,为了去除光照对描述子的影响,对梯度直方图进行归一化处理。对于图像灰度值整体漂移,图像各点的梯度是邻域像素相减的,所以也能去除。
描述子向量门限。非线性光照,相机饱和度变化对造成某些方向的梯度值过大,而对方向的影响微弱。因此设置门限值(向量归一化后,一般取 0.2)截断较大的梯度值。然后,再进行一次归一化处理,提高特征的鉴别性。
按特征点的尺度对特征描述向量进行排序。
至此,SIFT 特征描述向量生成。
SIFT 在图像的不变特征提取方面拥有无与伦比的有事,但并不完美,仍然存在如下缺点:
所以 Bay 等人提出 SURF 对其进行改进。
SIFT::create
static Ptr<SIFT> cv::SIFT::create( int nfeatures = 0,
int nOctaveLayers = 3,
double contrastThreshold = 0.04,
double edgeThreshold = 10,
double sigma = 1.6
)
//Python:
retval = cv.SIFT_create([, nfeatures[, nOctaveLayers[, contrastThreshold[, edgeThreshold[, sigma]]]]])
在 OpenCV 4.4.0 以下版本,static Ptr
参数解释
参数 | 解释 |
---|---|
nfeatures | 保留的最好特征的数量。 如果是默认值 0 ,则算法将返回可以找到的所有特征。 |
nOctaveLayers | 每个组中的层数。 3 是 Lowe 论文中使用的值。 组的数量是根据图像分辨率自动计算的。 |
contrastThreshold | 用于过滤掉低对比度区域中的弱特征的对比度阈值。 阈值越大,检测器产生的特征越少。 |
edgeThreshold | 用于过滤边缘特征的阈值。 阈值越大,滤除的特征越少(保留的特征越多)。 |
sigma | 高斯的参数 σ \sigma σ ,输入图像的第 0 个组使用的。 如果您的图像是用弱镜头和软镜头拍摄的,则可能需要减少数量 |
C++示例
void mySIFT(Mat& image){
vector<KeyPoint> keypoints;
Mat descriptors;
// 创建 SIFT 特征检测器对象
cv::Ptr<cv::xfeatures2d::SIFT> ptrSIFT = cv::xfeatures2d::SIFT::create();
// 检测关键点
// ptrSIFT->detect(image, keypoints);
// 检测关键点并计算描述子
ptrSIFT->detectAndCompute(image, Mat(), keypoints, descriptors);
// 画出关键点
drawKeypoints(image, keypoints, image, cv::Scalar::all(-1), cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
}
int main()
{
string outDir = "./";
Mat image = imread("img7.jpg");
if (image.empty()) {
cout << "could not load image..." << endl;
return -1;
}
Mat imageGray;
cvtColor(image, imageGray, COLOR_BGR2GRAY);
mySIFT(image);
imwrite(outDir + "SIFT.jpg", image);
return 0;
}
效果图
使用 detect()
函数只是检测图中的特征点,使用 compute()
函数可以通过检测的特征点计算描述子,使用 detectAndCompute()
可以同时检测特征点并计算描述子。
- SIFT特征详解
- SIFT算法详解
- SIFT算法原理
- SIFT解析(一)建立高斯金字塔
- 基于C++和OpenCv的SIFT_图像局部特征检测算法代码的实现
- 不能错过!超强大的SIFT图像匹配技术详细指南(附Python代码)
- SIFT定位算法关键步骤的说明