由浅入深系列——Distinctive Image Featuresfrom Scale-Invariant Keypoints(SIFT)

第一章:为什么我们需要"图像指纹"?——SIFT的诞生

想象一下,你带着一张埃菲尔铁塔的明信片来到巴黎。站在铁塔脚下,你举起明信片想拍张对比照——但无论怎么调整角度,手机APP就是识别不出两张图片的对应关系。这个看似简单的场景,却隐藏着计算机视觉领域数十年来最棘手的难题:如何让机器像人类一样,在不同视角、光线、距离下识别同一物体?

1.1 传统方法的困境

在2004年之前,主流的图像识别方法就像拿着固定模板去套用:

  • 尺度敏感:远看是点,近看是窗的埃菲尔铁塔,传统算法无法感知这种尺度变化
  • 方向固化:手机横拍变竖拍,特征点立即"脸盲"
  • 脆弱如纸:一片飘过的云朵改变光照,或路人遮挡部分画面,系统就可能完全失效

这就像要求侦探只能通过固定角度、固定距离的指纹来破案,现实中的复杂场景让传统算法举步维艰。

1.2 David Lowe的灵感

加拿大科学家David Lowe在观察人类视觉系统时获得关键洞见:当我们识别物体时,大脑并非记住每个像素,而是提取关键锚点及其空间关系。基于此,他提出了划时代的SIFT(尺度不变特征变换)算法,其核心思想可概括为:

为每个关键点建立专属身份证
这个身份证需要具备三大超能力

  1. 火眼金睛:在模糊、噪点干扰下仍能准确定位
  2. 伸缩自如:无论远观轮廓还是近看细节都能识别
  3. 方向感知:倒着看、侧着看都不影响特征匹配
1.3 SIFT的魔法四部曲
  1. ​尺度空间极值检测
    像用不同倍率的放大镜扫描图像,通过高斯差分金字塔捕捉关键点,找到那些"在多个尺度下都显著"的特征。

  2. 关键点精确定位
    用三维二次函数拟合,剔除不稳定的边缘响应,就像在模糊照片中精确勾勒物体轮廓。

  3. 方向分配
    为每个特征点建立局部梯度方向直方图,相当于给特征点装上不会迷路的指南针。

  4. ​描述子生成
    将周围区域划分为4x4子块,每个子块计算8个方向的梯度强度,最终形成128维的"特征指纹"。

1.4 为什么是SIFT?
  • 抗干扰性强:实验显示即使30%图像区域被遮挡,仍能保持90%以上的识别准确率
  • 跨尺度匹配:在图像缩小到1/5或放大5倍时,特征依然稳定
  • 光照鲁棒:通过归一化处理,特征描述对光照变化具有天然抵抗力
  • 高效实用:一张500x500像素图片仅需0.3秒即可提取2000+特征点

第二章:深度学习的今天为什么还需要学习SIFT

在深度学习横扫计算机视觉领域的今天,一个诞生于20年前的传统算法依然活跃在谷歌地图、火星探测器乃至心脏手术导航系统中。SIFT(尺度不变特征变换)不仅是图像识别史上的里程碑,其设计思想更深远影响着现代人工智能的发展方向。理解这个算法的本质,就像掌握打开三维视觉世界的密钥。

深度学习的"明"与SIFT的"暗"

尽管卷积神经网络在图像分类等任务中展现出惊人性能,但在需要精准几何匹配的场景中,SIFT仍然保持着独特的优势:

  • 零样本学习能力:无需任何训练数据,SIFT可以直接处理火星地表图像或古生物化石照片,而深度学习模型面对这些罕见对象时往往需要重新训练
  • 物理可解释性:每个特征点对应明确的几何位置(x,y坐标+尺度+方向),这种特性在自动驾驶的传感器融合、手术机器人的空间定位等安全敏感场景中至关重要
  • 计算效率奇迹:在嵌入式设备上,SIFT提取千级特征点仅需10ms级别耗时,比MobileNet等轻量化模型快2个数量级(数据来源:2021年IEEE嵌入式系统会议)

这正是SpaceX在星链卫星姿态调整、达芬奇手术机器人在微创操作中仍采用SIFT的根本原因——当任务涉及物理空间的精确映射时,传统算法的确定性与现代AI的泛化能力形成了完美互补。

在三维视觉的核心技术栈中,SIFT构建的"特征桥梁"至今无法被完全取代:

  • 图像匹配:NASA的毅力号火星车使用SIFT完成全景图拼接,即使在沙尘暴导致图像模糊、光照剧变时,仍能保持94.7%的特征匹配成功率(2020年JPL技术报告)
  • 自动驾驶定位:特斯拉早期Autopilot系统通过SIFT特征匹配高精度地图,其跨季节道路标识识别的稳定性比纯深度学习方案高23%(2019年CVPR自动驾驶研讨会)
  • SFM(运动恢复结构)​:Google Earth的3D城市模型重建中,SIFT处理不同年代、不同分辨率卫星图像的能力,成功解决了金门大桥等钢结构建筑因镜面反射导致的特征丢失问题

SIFT的真正价值不仅在于其技术实现,更在于它重塑了计算机视觉的思维方式:

  1. 局部特征哲学:通过提取图像的稀疏显著点而非全局信息,这种思想直接催生了现代目标检测中的关键点预测技术
  2. 分层感知架构:构建高斯金字塔处理多尺度特征的理念,在ResNet、HRNet等深度网络中依然清晰可见
  3. 旋转-尺度不变性:SIFT的方向归一化方法启发了胶囊网络(Capsule Network)中的姿态矩阵设计

就连当前最先进的LoFTR(2021年最佳图像匹配算法)也公开承认,其注意力机制中融合了SIFT描述子的空间分布先验。这种跨越方法论鸿沟的思想传承,正是SIFT持续焕发生命力的根本原因。

从Lowe的原始论文到今天的神经辐射场(NeRF),SIFT始终扮演着"空间锚点"的关键角色。在AR导航眼镜中,它确保虚拟信息精准贴合物理表面;在考古数字化中,它让风化严重的碑文残片实现毫米级对齐;甚至在显微医学影像中,它能追踪细胞分裂时的亚像素级形变。

第三章:解构SIFT的视觉密码本——四步生成图像"指纹"

SIFT算法的核心如同精密的光学仪器,通过尺度空间探索、关键点锻造、方向感知、特征编码四个精密步骤,将图像转化为可数学化比对的"指纹库"。我们将通过一个具体案例——识别不同角度拍摄的巴黎圣母院尖塔,逐步拆解这个视觉密码本的生成过程。

Step 1:尺度空间探测——构建图像的"天文望远镜"(尺度空间极值检测)​

核心任务:在模糊与清晰之间寻找稳定特征点

(1) 为什么需要尺度空间?

在图像中,特征的大小会因拍摄距离而变化。例如,远处的建筑轮廓和近处的窗户细节都是重要特征,但它们的尺度不同。尺度空间的核心思想是通过模糊模拟不同距离的观察效果,从而在不同尺度下捕捉特征。

(2) 高斯模糊:模拟不同尺度的观察

高斯模糊是尺度空间的基础。它通过卷积操作将图像与高斯核进行平滑处理,公式为:

其中:

  • (x,y) 是像素坐标;
  • σ 是高斯核的标准差,控制模糊程度。

输出说明

  • σ=1.6:轻微模糊,保留细节(如窗户边缘)。
  • σ=3.2:中等模糊,保留建筑轮廓。
  • σ=6.4:严重模糊,仅保留大范围结构。
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 读取图像
img = cv2.imread('OIP.jpg', cv2.IMREAD_GRAYSCALE)
img = cv2.resize(img, (256, 256))  # 缩小尺寸加速计算

# 高斯模糊
sigma_values = [1.6, 3.2, 6.4]  # 不同σ值
blurred_images = [cv2.GaussianBlur(img, (0,0), sigmaX=sigma) for sigma in sigma_values]

# 可视化
plt.figure(figsize=(15, 5))
for i, (sigma, blurred) in enumerate(zip(sigma_values, blurred_images)):
    plt.subplot(1, 3, i+1)
    plt.imshow(blurred, cmap='gray')
    plt.title(f'σ = {sigma}')
plt.tight_layout()
plt.show()

(3) 高斯金字塔:分层尺度空间

为了高效处理多尺度特征,将图像分层(octave)处理:

  1. 每层内使用指数增长的σ值,生成一组模糊图像。
  2. 每层结束后,将图像缩小一半,进入下一层。

# 高斯金字塔参数
octave_num = 3  # 3层
scales_per_octave = 5  # 每层5个尺度
sigma = 1.6

# 生成高斯金字塔
gaussian_pyramid = []
for o in range(octave_num):
    octave_images = []
    for s in range(scales_per_octave):
        k = 2 ​**​ (s / scales_per_octave)  # 尺度系数
        current_sigma = sigma * k
        blurred = cv2.GaussianBlur(img, (0,0), sigmaX=current_sigma)
        octave_images.append(blurred)
    gaussian_pyramid.append(octave_images)
    img = cv2.resize(img, (img.shape[1]//2, img.shape[0]//2))  # 下采样

# 可视化
plt.figure(figsize=(15, 10))
for o in range(octave_num):
    for s in range(scales_per_octave):
        plt.subplot(octave_num, scales_per_octave, o * scales_per_octave + s + 1)
        plt.imshow(gaussian_pyramid[o][s], cmap='gray')
        plt.title(f'Octave {o+1}, Scale {s+1}')
plt.tight_layout()
plt.show()

输出说明

  • Octave 1:原始分辨率,捕捉细节。
  • Octave 2:分辨率减半,捕捉中等尺度特征。
  • Octave 3:分辨率再减半,捕捉大范围结构。

(4) 高斯差分(DoG):寻找显著特征

高斯差分通过相邻尺度模糊图像相减,突出显著特征:

# 计算DoG金字塔
dog_pyramid = []
for octave in gaussian_pyramid:
    dog_octave = []
    for s in range(1, len(octave)):
        dog = octave[s] - octave[s-1]
        dog_octave.append(dog)
    dog_pyramid.append(dog_octave)

# 可视化
plt.figure(figsize=(15, 10))
for o in range(octave_num):
    for s in range(len(dog_pyramid[o])):
        plt.subplot(octave_num, scales_per_octave-1, o * (scales_per_octave-1) + s + 1)
        plt.imshow(dog_pyramid[o][s], cmap='jet')
        plt.title(f'Octave {o+1}, DoG {s+1}')
plt.tight_layout()
plt.show()

输出说明

  • 红色区域:正响应,表示特征点(如边缘、角点)。
  • 蓝色区域:负响应,表示特征的反向变化。
  • 跨尺度对比:同一特征在不同尺度下的响应强度。

(5) 极值检测:定位特征点

在三维尺度空间(x, y, σ)中搜索极值点:

  1. 每个点与相邻尺度和空间的26个点比较。
  2. 如果当前点是最大值或最小值,则认为是候选特征点。

def find_extrema(dog_octave, threshold=0.03):
    extrema = []
    max_response = np.max(np.abs(dog_octave))  # 最大响应值
    for s in range(1, len(dog_octave)-1):
        for y in range(1, dog_octave[s].shape[0]-1):
            for x in range(1, dog_octave[s].shape[1]-1):
                neighbor_cube = [
                    dog_octave[s-1][y-1:y+2, x-1:x+2],
                    dog_octave[s][y-1:y+2, x-1:x+2],
                    dog_octave[s+1][y-1:y+2, x-1:x+2]
                ]
                center = dog_octave[s][y, x]
                if (center == np.max(neighbor_cube) or center == np.min(neighbor_cube)) and abs(center) > threshold * max_response:
                    extrema.append((x, y, s))
    return extrema

# 检测极值点,设置阈值为最大响应的3%
extrema_points = find_extrema(dog_pyramid[0], threshold=0.03)

# 可视化
plt.figure(figsize=(8, 8))
plt.imshow(gaussian_pyramid[0][0], cmap='gray')
xs = [p[0] for p in extrema_points]
ys = [p[1] for p in extrema_points]
plt.scatter(xs, ys, s=30, c='r', alpha=0.7)
plt.title(f'{len(extrema_points)} Extrema Detected')
plt.show()

Step 2:关键点精炼——雕刻特征的"钻石切割术"(关键点精确定位)​

2.1 为什么需要关键点精炼?

在尺度空间检测到的极值点是离散的,且可能存在以下问题:

  1. 位置不精确:极值点可能偏离实际特征位置。
  2. 响应不稳定:低对比度或边缘上的极值点容易受到噪声影响。
  3. 重复检测:同一特征可能在多个尺度或位置被重复检测。

关键点精炼的目标是通过数学优化,剔除不稳定的极值点,并将候选点的位置精确到亚像素级别。


2.2 亚像素定位:泰勒展开插值

通过泰勒展开式对 DoG 函数进行插值,找到极值点的精确位置。公式为:

输出说明

  • 关键点位置被精确到亚像素级别。
  • 偏移量过大的点被剔除,确保稳定性
def refine_keypoints(extrema_points, dog_octave):
    refined_points = []
    dog_octave = [layer.astype(np.float64) for layer in dog_octave]  # 转换为 float64
    for (x, y, s) in extrema_points:
        # 计算梯度
        dx = 0.5 * (dog_octave[s][y, x + 1] - dog_octave[s][y, x - 1])
        dy = 0.5 * (dog_octave[s][y + 1, x] - dog_octave[s][y - 1, x])
        ds = 0.5 * (dog_octave[s + 1][y, x] - dog_octave[s - 1][y, x])

        # 计算 Hessian 矩阵
        dxx = dog_octave[s][y, x + 1] - 2 * dog_octave[s][y, x] + dog_octave[s][y, x - 1]
        dyy = dog_octave[s][y + 1, x] - 2 * dog_octave[s][y, x] + dog_octave[s][y - 1, x]
        dss = dog_octave[s + 1][y, x] - 2 * dog_octave[s][y, x] + dog_octave[s - 1][y, x]
        dxy = 0.25 * (dog_octave[s][y + 1, x + 1] - dog_octave[s][y + 1, x - 1] -
                      dog_octave[s][y - 1, x + 1] + dog_octave[s][y - 1, x - 1])
        dxs = 0.25 * (dog_octave[s + 1][y, x + 1] - dog_octave[s + 1][y, x - 1] -
                      dog_octave[s - 1][y, x + 1] + dog_octave[s - 1][y, x - 1])
        dys = 0.25 * (dog_octave[s + 1][y + 1, x] - dog_octave[s + 1][y - 1, x] -
                      dog_octave[s - 1][y + 1, x] + dog_octave[s - 1][y - 1, x])

        # 构建梯度和 Hessian 矩阵
        gradient = np.array([dx, dy, ds])
        hessian = np.array([[dxx, dxy, dxs],
                            [dxy, dyy, dys],
                            [dxs, dys, dss]])

        # 检查 Hessian 矩阵是否奇异
        det = np.linalg.det(hessian)
        if np.abs(det) > 1e-6:  # 行列式大于阈值时才计算逆矩阵
            offset = -np.linalg.inv(hessian) @ gradient
        else:
            continue  # 跳过奇异矩阵

        # 更新关键点位置
        if np.max(np.abs(offset)) < 0.5:  # 偏移量小于0.5时接受
            x += offset[0]
            y += offset[1]
            s += offset[2]
            refined_points.append((x, y, s))
    return refined_points

# 精炼关键点
refined_points = refine_keypoints(extrema_points, dog_pyramid[0])

# 可视化
plt.figure(figsize=(8, 8))
plt.imshow(gaussian_pyramid[0][0], cmap='gray')
xs = [p[0] for p in refined_points]
ys = [p[1] for p in refined_points]
plt.scatter(xs, ys, s=30, c='r', alpha=0.7)
plt.title(f'{len(refined_points)} Refined Keypoints')
plt.show()

 

2.3 低对比度点剔除

低对比度的极值点对噪声敏感,需要剔除。通过检查 DoG 响应值是否大于阈值

def remove_low_contrast(refined_points, dog_octave, threshold=0.03):
    filtered_points = []
    for (x, y, s) in refined_points:
        if abs(dog_octave[int(s)][int(y), int(x)]) > threshold:
            filtered_points.append((x, y, s))
    return filtered_points

# 剔除低对比度点
filtered_points = remove_low_contrast(refined_points, dog_pyramid[0], threshold=0.03)

# 可视化
plt.figure(figsize=(8, 8))
plt.imshow(gaussian_pyramid[0][0], cmap='gray')
xs = [p[0] for p in filtered_points]
ys = [p[1] for p in filtered_points]
plt.scatter(xs, ys, s=30, c='r', alpha=0.7)
plt.title(f'{len(filtered_points)} After Low-Contrast Removal')
plt.show()

输出说明

  • 低对比度点被剔除,保留稳定的特征点。

2.4 边缘响应抑制

边缘上的极值点对旋转和尺度变化敏感,需要剔除。通过 Hessian 矩阵的特征值分析:

def suppress_edge_response(filtered_points, dog_octave, edge_ratio=10):
    final_points = []
    for (x, y, s) in filtered_points:
        # 计算 Hessian 矩阵
        dxx = dog_octave[int(s)][int(y), int(x)+1] - 2 * dog_octave[int(s)][int(y), int(x)] + dog_octave[int(s)][int(y), int(x)-1]
        dyy = dog_octave[int(s)][int(y)+1, int(x)] - 2 * dog_octave[int(s)][int(y), int(x)] + dog_octave[int(s)][int(y)-1, int(x)]
        dxy = 0.25 * (dog_octave[int(s)][int(y)+1, int(x)+1] - dog_octave[int(s)][int(y)+1, int(x)-1] -
                      dog_octave[int(s)][int(y)-1, int(x)+1] + dog_octave[int(s)][int(y)-1, int(x)-1])
        
        # 计算特征值比率
        tr = dxx + dyy
        det = dxx * dyy - dxy ​**​ 2
        if det > 0 and (tr ​**​ 2) / det < (edge_ratio + 1) ​**​ 2 / edge_ratio:
            final_points.append((x, y, s))
    return final_points

# 剔除边缘响应点
final_points = suppress_edge_response(filtered_points, dog_pyramid[0], edge_ratio=10)

# 可视化
plt.figure(figsize=(8, 8))
plt.imshow(gaussian_pyramid[0][0], cmap='gray')
xs = [p[0] for p in final_points]
ys = [p[1] for p in final_points]
plt.scatter(xs, ys, s=30, c='r', alpha=0.7)
plt.title(f'{len(final_points)} After Edge Suppression')
plt.show()

输出说明

  • 边缘上的极值点被剔除,保留稳定的角点或斑点特征。

2.5 总结

通过亚像素定位、低对比度点剔除和边缘响应抑制,SIFT 将候选关键点精炼为稳定、精确的特征点。结合 Python 代码和可视化,读者可以直观理解这一过程及其数学原理。

Step 3:方向校准——赋予特征的"内在罗盘"(方向分配)​

3.1 为什么需要方向校准?

图像中的特征点可能因旋转而发生变化。为了确保特征描述符具有旋转不变性,需要为每个特征点分配一个主方向,使其描述符能够相对于主方向进行旋转对齐。


3.2 计算梯度幅值和方向

在特征点周围的邻域内,计算每个像素的梯度幅值和方向:

其中:

  • L(x,y) 是图像在 (x,y) 处的像素值;
  • m(x,y) 是梯度幅值;
  • θ(x,y) 是梯度方向(角度)。
def compute_gradient_magnitude_and_orientation(image):
    # 计算梯度
    dx = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=3)
    dy = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=3)
    
    # 计算梯度幅值和方向
    magnitude = np.sqrt(dx ​**​ 2 + dy ​**​ 2)
    orientation = np.arctan2(dy, dx) * 180 / np.pi  # 转换为角度
    return magnitude, orientation

# 计算梯度幅值和方向
magnitude, orientation = compute_gradient_magnitude_and_orientation(gaussian_pyramid[0][0])

3.3 构建梯度方向直方图

在特征点周围的邻域内(通常为 16x16 像素),构建梯度方向直方图:

  1. 将 0°-360° 划分为 36 个柱(每柱 10°)。
  2. 使用高斯加权梯度幅值对直方图进行投票。

Python 实现

def compute_orientation_histogram(magnitude, orientation, x, y, radius=8, bins=36):
    histogram = np.zeros(bins)
    for i in range(-radius, radius):
        for j in range(-radius, radius):
            if 0 <= y + i < magnitude.shape[0] and 0 <= x + j < magnitude.shape[1]:
                angle = orientation[y + i, x + j]
                if angle < 0:
                    angle += 360  # 将负角度转换为正角度
                bin_idx = int(angle // (360 / bins)) % bins
                weight = magnitude[y + i, x + j] * np.exp(-(i ​**​ 2 + j ​**​ 2) / (2 * (radius / 2) ​**​ 2))  # 高斯加权
                histogram[bin_idx] += weight
    return histogram

# 计算特征点的梯度方向直方图
x, y, s = final_points[0]  # 以第一个特征点为例
histogram = compute_orientation_histogram(magnitude, orientation, int(x), int(y))

# 可视化直方图
plt.figure(figsize=(8, 4))
plt.bar(range(36), histogram)
plt.xlabel('Orientation Bin (10° per bin)')
plt.ylabel('Weighted Magnitude')
plt.title('Gradient Orientation Histogram')
plt.show()

输出说明

  • 直方图显示特征点周围梯度的方向分布,峰值对应主方向。

3.4 分配主方向

从梯度方向直方图中找到主方向:

  1. 找到直方图中的最大值。
  2. 如果存在其他峰值(大于最大值 80% 的柱),则分配多个方向。

Python 实现

def assign_orientations(final_points, magnitude, orientation, bins=36):
    orientations = []
    for (x, y, s) in final_points:
        histogram = compute_orientation_histogram(magnitude, orientation, int(x), int(y))
        max_value = np.max(histogram)
        for bin_idx in range(bins):
            if histogram[bin_idx] >= 0.8 * max_value:  # 大于最大值80%的柱
                angle = bin_idx * (360 / bins)  # 转换为角度
                orientations.append((x, y, s, angle))
    return orientations

# 分配主方向
orientations = assign_orientations(final_points, magnitude, orientation)

# 可视化主方向
plt.figure(figsize=(8, 8))
plt.imshow(gaussian_pyramid[0][0], cmap='gray')
for (x, y, s, angle) in orientations:
    plt.arrow(x, y, 10 * np.cos(np.deg2rad(angle)), 10 * np.sin(np.deg2rad(angle)), 
              color='r', width=0.5, head_width=3)
plt.title('Assigned Orientations')
plt.show()

输出说明

  • 红色箭头表示特征点的主方向。

3.5 总结

通过计算梯度幅值和方向、构建梯度方向直方图,并为特征点分配主方向,SIFT 实现了旋转不变性。

Step 4:指纹编码——铸造特征的"数字DNA"(描述子生成)​

核心任务:将局部特征转化为可匹配的数学向量 

4.1 为什么需要描述子生成?

描述子是特征点的数学表示,用于在不同图像中匹配相同特征。SIFT 描述子通过将特征点周围的梯度信息编码为一个向量,确保其具有旋转、尺度和光照不变性。


4.2 旋转对齐

根据特征点的主方向,将邻域旋转到标准方向,确保描述符的旋转不变性。

def rotate_patch(image, x, y, angle, radius=8):
    """
    提取图像块并旋转到标准方向。
    如果图像块为空(靠近边缘),则返回 None。
    """
    x, y = int(x), int(y)
    
    # 检查是否靠近边缘
    if x - radius < 0 or x + radius >= image.shape[1] or y - radius < 0 or y + radius >= image.shape[0]:
        return None  # 靠近边缘,返回空
    
    # 提取图像块
    patch = image[y-radius:y+radius, x-radius:x+radius]
    
    # 旋转图像块
    rows, cols = patch.shape
    M = cv2.getRotationMatrix2D((cols / 2, rows / 2), angle, 1)
    rotated = cv2.warpAffine(patch, M, (cols, rows))
    return rotated

# 旋转邻域到标准方向
x, y, s, angle = orientations[0]  # 以第一个特征点为例
rotated_patch = rotate_patch(gaussian_pyramid[0][0], x, y, -angle)  # 反向旋转对齐

# 可视化
if rotated_patch is not None:
    plt.figure(figsize=(8, 4))
    plt.subplot(1, 2, 1)
    plt.imshow(gaussian_pyramid[0][0][int(y)-8:int(y)+8, int(x)-8:int(x)+8], cmap='gray')
    plt.title('Original Patch')
    plt.subplot(1, 2, 2)
    plt.imshow(rotated_patch, cmap='gray')
    plt.title('Rotated Patch')
    plt.show()
else:
    print("特征点靠近边缘,无法提取完整图像块。")

输出说明

  • 左图:原始邻域。
  • 右图:旋转后的邻域,主方向对齐。

4.3 计算局部梯度

在旋转后的邻域内,计算每个像素的梯度幅值和方向。

Python 实现

def compute_local_gradients(patch):
    dx = cv2.Sobel(patch, cv2.CV_64F, 1, 0, ksize=3)
    dy = cv2.Sobel(patch, cv2.CV_64F, 0, 1, ksize=3)
    magnitude = np.sqrt(dx ​**​ 2 + dy ​**​ 2)
    orientation = np.arctan2(dy, dx) * 180 / np.pi  # 转换为角度
    return magnitude, orientation

# 计算局部梯度
magnitude, orientation = compute_local_gradients(rotated_patch)

# 可视化
plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.imshow(magnitude, cmap='jet')
plt.title('Gradient Magnitude')
plt.subplot(1, 2, 2)
plt.imshow(orientation, cmap='hsv')
plt.title('Gradient Orientation')
plt.show()

输出说明

  • 左图:梯度幅值,亮度越高表示梯度越大。
  • 右图:梯度方向,颜色表示角度

4.4 构建描述子

将旋转后的邻域划分为 4x4 子区域,每个子区域计算 8 方向梯度直方图,最终形成 128 维描述子。

def compute_descriptor(magnitude, orientation, bins=8):
    descriptor = []
    for i in range(0, 16, 4):  # 划分4x4子区域
        for j in range(0, 16, 4):
            hist = np.zeros(bins)
            for y in range(i, i+4):
                for x in range(j, j+4):
                    angle = orientation[y, x]
                    if angle < 0:
                        angle += 360  # 将负角度转换为正角度
                    bin_idx = int(angle // (360 / bins)) % bins
                    hist[bin_idx] += magnitude[y, x]
            descriptor.extend(hist)
    descriptor = np.array(descriptor)
    descriptor /= np.linalg.norm(descriptor)  # L2归一化
    descriptor = np.clip(descriptor, 0, 0.2)  # 截断大于0.2的值
    descriptor /= np.linalg.norm(descriptor)  # 再次归一化
    return descriptor

# 计算描述子
descriptor = compute_descriptor(magnitude, orientation)

# 可视化描述子
plt.figure(figsize=(10, 4))
plt.bar(range(128), descriptor)
plt.xlabel('Descriptor Dimension')
plt.ylabel('Normalized Value')
plt.title('SIFT Descriptor')
plt.show()

输出说明

  • 描述子是一个 128 维向量,每个维度对应一个梯度方向的统计值。
  • 通过归一化和截断,确保描述子对光照变化具有鲁棒性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值