由浅入深系列——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 * kblurred = 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]  # 转换为 float64for (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) @ gradientelse: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 + dyydet = dxx * dyy - dxy ​**​ 2if 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)) % binsweight = magnitude[y + i, x + j] * np.exp(-(i ​**​ 2 + j ​**​ 2) / (2 * (radius / 2) ​**​ 2))  # 高斯加权histogram[bin_idx] += weightreturn 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.shapeM = 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)) % binshist[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 维向量,每个维度对应一个梯度方向的统计值。
  • 通过归一化和截断,确保描述子对光照变化具有鲁棒性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/25743.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

spring注解开发(Spring整合MyBatis——Mapper代理开发模式、(Spring、MyBatis、Jdbc)配置类)(6)

目录 一、纯MyBatis独立开发程序。 &#xff08;1&#xff09;数据库与数据表。 &#xff08;2&#xff09;实体类。 &#xff08;3&#xff09;dao层接口。&#xff08;Mapper代理模式、无SQL映射文件——注解配置映射关系&#xff09; &#xff08;4&#xff09;MyBatis核心配…

Redis缓存一致性难题:如何让数据库和缓存不“打架”?

标题&#xff1a;Redis缓存一致性难题&#xff1a;如何让数据库和缓存不“打架”&#xff1f;&#xff08;附程序员脱发指南&#xff09; 导言&#xff1a;当数据库和缓存成了“异地恋” 想象一下&#xff1a;你刚在美团下单了一份麻辣小龙虾&#xff0c;付款后刷新页面&#…

eMMC安全简介

1. 引言 术语“信息安全”涵盖多种不同的设计特性。一般而言&#xff0c; 信息安全是指通过实践防止信息遭受未经授权的访问、使用、披露、中断、篡改、检查、记录或销毁。 信息安全的三大核心目标为 机密性&#xff08;Confidentiality&#xff09;、完整性&#xff08;Integr…

如何用python画一棵分形树

这个代码会生成一个彩色的分形树图案&#xff0c;可以通过调整draw_tree函数中的参数来改变树的形状和大小 import turtle import random# 递归函数绘制分形树 def draw_tree(branch_len, t):if branch_len > 5:t.color(random.choice(colors))t.pensize(branch_len / 10)t…

linux中安装部署Jenkins,成功构建springboot项目详细教程

参考别人配置Jenkins的git地址为https&#xff0c;无法连上github拉取项目&#xff0c;所以本章节介绍通过配置SSH地址来连github拉取项目 目录&#xff1a; 1、springboot项目 1.1 创建名为springcloudproject的springboot项目工程 1.2 已将工程上传到github中&#xff0c;g…

sqli-lab

没了解sql注入的知识&#xff0c;本篇更像是复现 数据库层面 │ ├── 数据库1&#xff08;Database 1&#xff09; │ │ ├── 表1&#xff08;Table 1&#xff09; │ │ │ ├── 主键字段&#xff08;Primary Key&#xff09;&#xff1a;唯一标识表中的每…

线程间的通信

一、实现方式 各线程之间栈区独享、并且与进程共享文本段、数据段、堆区&#xff0c;所以可以通过全局变量实现多线程间通信。 int num 0; //创建一个全局变量void *thread1(void *arg) {while (1){num 100; …

Deepseek开源周第四天:从 DualPipe 到 EPLB

Deepseek开源周第四天&#xff1a;从 DualPipe 到 EPLB 前言 上周deepseek宣布&#xff0c;将在本周陆续发布五个开源项目&#xff0c;这些库已经在生产环境中经过了记录、部署和实战测试。 今天是deepseek开源周的第四天&#xff0c;deepseek发布了三个开源项目&#xff0c;…

字符函数和字符串函数1

1、字符分类函数 C语⾔中有⼀系列的函数是专⻔做字符分类的&#xff0c;也就是⼀个字符是属于什么类型的字符的。 这些函数的使⽤都需要包含⼀个头⽂件是 ctype.h 在这里介绍一下islower函数&#xff0c;其他的非常相似。 函数原型&#xff1a; 1 int islower(int c); islo…

算法系列之排序算法-堆排序

在数据结构中&#xff0c;堆&#xff08;Heap&#xff09;是一种特殊的树形数据结构&#xff0c;通常用于实现优先队列。堆分为最大堆和最小堆两种类型。最大堆的每个节点的值都大于或等于其子节点的值&#xff0c;而最小堆的每个节点的值都小于或等于其子节点的值。Java 提供了…

Mercury、LLaDA 扩散大语言模型

LLaDA 参考&#xff1a; https://github.com/ML-GSAI/LLaDA https://ml-gsai.github.io/LLaDA-demo/ 在线demo&#xff1a; https://huggingface.co/spaces/multimodalart/LLaDA Mercury 在线demo&#xff1a; https://chat.inceptionlabs.ai/ 速度很快生成

YOLO - pose detect 输入输出接口与执行效率测试

0.参考资料&#xff1a; Pose - Ultralytics YOLO Docs 下面仅对这个模型的输入输出接口和效率做了判断&#xff0c;尚不涉及训练。 pose和segment 相对class detect是相对自然的扩展。object box内部的 subclass就是seg&#xff0c;object box 内部的point array 就是Pose。…

DeepSeek 开源狂欢周(一)FlashMLA:高效推理加速新时代

上周末&#xff0c;DeepSeek在X平台&#xff08;Twitter&#xff09;宣布将开启连续一周的开源&#xff0c;整个开源社区为之沸腾&#xff0c;全球AI爱好者纷纷为关注。没错&#xff0c;这是一场由DeepSeek引领的开源盛宴&#xff0c;推翻了传统推理加速的种种限制。这周一&…

MySQL数据库基本概念

目录 什么是数据库 从软件角度出发 从网络角度出发 MySQL数据库的client端和sever端进程 mysql的client端进程连接sever端进程 mysql配置文件 MySql存储引擎 MySQL的sql语句的分类 数据库 库的操作 创建数据库 不同校验规则对查询的数据的影响 不区分大小写 区…

【洛谷贪心算法】P1106删数问题

这道题可以使用贪心算法来解决&#xff0c;核心思路是尽量让高位的数字尽可能小。当我们逐步删除数字时&#xff0c;会优先删除高位中相对较大的数字。具体做法是从左到右遍历数字序列&#xff0c;当发现当前数字比它后面的数字大时&#xff0c;就删除当前数字&#xff0c;直到…

【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目

解决idea至少创建jdk17项目 问题 idea现在只能创建最少jdk17&#xff0c;不能创建java8了吗?解决 问题 idea现在只能创建最少jdk17&#xff0c;不能创建java8了吗 我本来以为是 IDEA 版本更新导致的 Bug&#xff0c;开始还没在意。 直到我今天自己初始化项目时才发现&am…

MyBatis 操作数据库(详细入门详细)

本章⽬标 1. 使⽤MyBatis完成简单的增删改查操作, 参数传递. 2. 掌握MyBatis的两种写法: 注解 和 XML⽅式 3. 掌握MyBatis 相关的⽇志配置 铺垫 在应⽤分层学习时, 我们了解到web应⽤程序⼀般分为三层&#xff0c;即&#xff1a;Controller、Service、Dao . 之前的案例中…

C# 基于.NET Framework框架WPF应用程序-MQTTNet库实现MQTT消息订阅发布

C# 基于.NET Framework框架WPF应用程序-MQTTNet库实现MQTT消息订阅发布 MQTT简述MQTTNet简述创建项目&#xff08;基于.NET Framework框架&#xff09;安装MQTTNet库项目源码运行效果 MQTT简述 mqtt官网 MQTTNet简述 MQTTnet MQTTnet 是一个强大的开源 MQTT 客户端库&#…

武汉大学生命科学学院与谱度众合(武汉)生命科技有限公司举行校企联培座谈会

2025年2月21日下午&#xff0c;武汉大学生命科学学院与谱度众合&#xff08;武汉&#xff09;生命科技有限公司&#xff08;以下简称“谱度众合”&#xff09;在学院学术厅举行校企联培专业学位研究生合作交流会。武汉大学生命科学学院副院长刘星教授、生命科学学院周宇教授、产…

【JSON2WEB】15 银河麒麟操作系统下部署JSON2WEB

【JSON2WEB】系列目录 【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSO…