SIFT(Scale-Invariant Feature Transform,尺度不变特征变换) ,由 David Lowe 在 1999 年提出,2004 年完善。 核心亮点:图像特征具有尺度不变性、旋转不变性,光照、视角小幅变化也比较鲁棒,广泛用于图像匹配、拼接、目标识别、三维重建。
专利已于 2020 年过期,现在可以自由商用。
一、SIFT 四大步骤
1. 构建尺度空间,检测极值点(关键点候选)
目的:找到图像在不同尺度下都存在的特征点,实现尺度不变。
- 高斯金字塔:对原图不断高斯模糊 + 下采样,得到多分辨率图像,每一层称为一个八度 (octave) 。
- DOG(Difference of Gaussian,高斯差分) :同一八度内,相邻两层高斯图像相减,得到 DOG 图像。
3. 空间极值检测:每个像素和同层 8 邻域 + 上下两层各 9 个点,一共 26 个点比较,若是最大 / 最小值,即为候选关键点。
DOG 近似高斯拉普拉斯 LoG,计算更快。
2. 关键点精确定位 + 剔除不稳定点
候选极值很多是噪声、边缘弱点:
- 用泰勒展开拟合 DOG 函数,对候选点做亚像素精度修正,得到精确坐标、尺度。
- 去除低对比度点:泰勒展开值过小,容易受噪声干扰。
- 去除边缘响应点:利用 Hessian 矩阵,计算主曲率比值,滤除沿着边缘的不稳定点(这类点定位差)。
3. 关键点方向分配(旋转不变性来源)
在关键点所在尺度的高斯图像上,取局部邻域:
- 计算邻域像素梯度幅值与梯度方向
- 梯度方向做直方图统计(36 个 bin,每 10°),直方图峰值为主方向;大于峰值 80% 的作为辅方向。
一个关键点可以有多个方向,提升鲁棒性。后续描述子生成时,邻域坐标按这个主方向旋转,保证旋转不变。
4. 生成 SIFT 特征描述子
对关键点邻域,按主方向旋转,取 16×16 像素窗口,划分成 4×4 个子块; 每个子块内统计 8 个方向梯度,得到 维向量。
- 向量归一化,降低光照影响;
- 截断大于 0.2 的值再归一化,抑制对比度突变。
👉 最终:每个关键点 = (坐标x,y,尺度σ,方向θ,128维描述子)
二、特征匹配
两张图提取 SIFT 特征后做匹配:
- 对图 A 的每个描述子,在图 B 描述子集合中找最近邻、次近邻;
- Lowe 比率测试:
最近邻距离 / 次近邻距离 < 阈值(常用0.7),认为是正确匹配; - 再用 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()