OpenCV - SIFT 算法

9 阅读3分钟

SIFT(Scale-Invariant Feature Transform,尺度不变特征变换) ,由 David Lowe 在 1999 年提出,2004 年完善。 核心亮点:图像特征具有尺度不变性、旋转不变性,光照、视角小幅变化也比较鲁棒,广泛用于图像匹配、拼接、目标识别、三维重建。

专利已于 2020 年过期,现在可以自由商用。

一、SIFT 四大步骤

1. 构建尺度空间,检测极值点(关键点候选)

目的:找到图像在不同尺度下都存在的特征点,实现尺度不变。

  1. 高斯金字塔:对原图不断高斯模糊 + 下采样,得到多分辨率图像,每一层称为一个八度 (octave) 。
  2. DOG(Difference of Gaussian,高斯差分) :同一八度内,相邻两层高斯图像相减,得到 DOG 图像。

D(x,y,σ)=L(x,y,kσ)−L(x,y,σ)D(x,y,\sigma)=L(x,y,k\sigma)-L(x,y,\sigma) 3. 空间极值检测:每个像素和同层 8 邻域 + 上下两层各 9 个点,一共 26 个点比较,若是最大 / 最小值,即为候选关键点。

DOG 近似高斯拉普拉斯 LoG,计算更快。

2. 关键点精确定位 + 剔除不稳定点

候选极值很多是噪声、边缘弱点:

  1. 用泰勒展开拟合 DOG 函数,对候选点做亚像素精度修正,得到精确坐标、尺度。
  2. 去除低对比度点:泰勒展开值过小,容易受噪声干扰。
  3. 去除边缘响应点:利用 Hessian 矩阵,计算主曲率比值,滤除沿着边缘的不稳定点(这类点定位差)。

3. 关键点方向分配(旋转不变性来源)

在关键点所在尺度的高斯图像上,取局部邻域:

  1. 计算邻域像素梯度幅值与梯度方向

m=(Lx)2+(Ly)2,θ=arctan⁡2(Ly,Lx) m=\sqrt{(L_{x})^2+(L_{y})^2},\quad \theta=\arctan2(L_y,L_x)

  1. 梯度方向做直方图统计(36 个 bin,每 10°),直方图峰值为主方向;大于峰值 80% 的作为辅方向。

一个关键点可以有多个方向,提升鲁棒性。后续描述子生成时,邻域坐标按这个主方向旋转,保证旋转不变。

4. 生成 SIFT 特征描述子

对关键点邻域,按主方向旋转,取 16×16 像素窗口,划分成 4×4 个子块; 每个子块内统计 8 个方向梯度,得到 4×4×8=1284\times4\times8 = \boldsymbol{128} 维向量。

  • 向量归一化,降低光照影响;
  • 截断大于 0.2 的值再归一化,抑制对比度突变。

👉 最终:每个关键点 = (坐标x,y,尺度σ,方向θ,128维描述子)

二、特征匹配

两张图提取 SIFT 特征后做匹配:

  1. 对图 A 的每个描述子,在图 B 描述子集合中找最近邻、次近邻;
  2. Lowe 比率测试:最近邻距离 / 次近邻距离 < 阈值(常用0.7),认为是正确匹配;
  3. 再用 RANSAC 剔除外点(误匹配对),求解单应矩阵 H。

三、优缺点

✅ 优点

  • 尺度、旋转不变;对光照、微小视角变化鲁棒;
  • 特征点位置稳定,适合图像拼接、SLAM、三维重建。

❌ 缺点

  • 计算量大,速度慢,不适合实时场景;
  • 对大视角形变、严重遮挡效果一般;
  • 描述子维度高 (128 维),存储和匹配开销大。

四、相关衍生算法

  • SURF:加速版 SIFT,用盒式滤波近似高斯,速度更快;
  • ORB:FAST 关键点 + BRIEF 描述子,二进制描述子,速度远快于 SIFT,无专利,SLAM 最常用;
  • AKAZE:非线性尺度空间,对边缘保持更好。

五、OpenCV 极简 Python 示例

OpenCV 4.4+ 可直接使用 SIFT

import cv2
import numpy as np

img1 = cv2.imread("img1.jpg", 0)
img2 = cv2.imread("img2.jpg", 0)

# 创建sift
sift = cv2.SIFT_create()
# 检测关键点+计算描述子
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)

# FLANN匹配器(适合高维描述子,比暴力匹配快)
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50)
flann = cv2.FlannBasedMatcher(index_params, search_params)

matches = flann.knnMatch(des1, des2, k=2)

# Lowe比率筛选
good = []
for m, n in matches:
    if m.distance < 0.7 * n.distance:
        good.append(m)

# 绘制匹配结果
res = cv2.drawMatches(img1, kp1, img2, kp2, good, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
cv2.imshow("SIFT match", res)
cv2.waitKey(0)
cv2.destroyAllWindows()