查看全课大纲(第 51 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

基于局部特征相似性

小象实战讲义 · 基于深度学习的计算机视觉

在上一节中,我们学习了基于颜色、纹理和形状等全局特征的图像检索方法。然而,这些方法在面对复杂场景、遮挡或视角变化时,其检索能力会受到限制。本节我们将深入到图像的局部,学习如何利用局部特征及其相似性进行更鲁棒、更精确的图像检索。掌握本节内容后,你将能够理解并实现基于SIFT等局部特征的图像匹配,以及更高级的视觉词袋模型,从而构建语义层面更丰富的图像检索系统。

💡 核心导读

  • 局部特征匹配:回顾SIFT/SURF等特征描述子,学习如何通过计算两幅图像间特征点的匹配对数来衡量图像相似性。
  • 视觉词袋模型:借鉴自然语言处理中的词袋思想,将图像中的局部特征聚类为“视觉单词”,从而将图像表示为一个词频向量。
  • 从局部到语义:理解视觉词袋模型如何将底层局部特征聚合,形成对图像内容更宏观、更具语义信息的描述,提升检索的准确性。

局部特征相似性检索

在计算机视觉中,局部特征指的是图像中那些具有显著性的点或区域,例如角点、边缘交点、斑点等。这些特征通常对光照变化、尺度缩放、旋转甚至轻微的视角变化具有较好的不变性。

局部特征提取:SIFT与SURF

我们之前详细介绍过SIFT(尺度不变特征变换)和SURF(加速稳健特征)等算法。它们的核心流程分为两步:

  1. 特征点检测:在图像的不同尺度空间上,寻找稳定的极值点作为特征点。每个特征点有其位置 (x,y)(x, y) 和尺度 σ\sigma。
  2. 特征描述:以特征点为中心,计算其邻域内像素梯度的方向直方图,形成一个高维的描述向量(如SIFT为128维)。
import cv2
import numpy as np

# 示例:使用OpenCV的SIFT检测器(需OpenCV >= 4.4.0)
# 注意:此处仅为演示API,实际检索中会提取大量特征点
np.random.seed(42)
# 构造一个简单的8x8灰度“角点”图案作为示例图像
demo_img = np.zeros((8, 8), dtype=np.uint8)
demo_img[2:6, 2:6] = 100  # 一个亮块
demo_img[4, 4] = 255      # 中心最亮,模拟角点

# 创建SIFT检测器
sift = cv2.SIFT_create()
# 检测关键点并计算描述子
keypoints, descriptors = sift.detectAndCompute(demo_img, None)

print(f"在示例图像中检测到 {len(keypoints)} 个关键点。")
if len(keypoints) > 0:
    # 打印第一个关键点的部分信息(位置、尺度)
    kp = keypoints[0]
    print(f"第一个关键点: 位置 ({kp.pt[0]:.1f}, {kp.pt[1]:.1f}), 尺度 {kp.size:.2f}")
    # 打印其描述子的前10个维度(共128维)
    if descriptors is not None:
        print(f"描述子前10维: {descriptors[0, :10]}")
在示例图像中检测到 5 个关键点。
第一个关键点: 位置 (4.1, 4.1), 尺度 2.34
描述子前10维: [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]

除了基础的SIFT,还有许多改进版本,如考虑颜色的Color-SIFT、适应仿射变换的Affine-SIFT、密集采样的Dense-SIFT以及使用主成分分析降维的PCA-SIFT等,它们都是为了在特定条件下获得更好的特征表示。

基于特征匹配的图像相似性

得到两幅图像的特征点集及其描述子后,如何衡量图像的相似性?一个直观的方法是进行特征匹配。

核心思想:对于图像A中的每一个特征点,在图像B的所有特征点中寻找其描述子距离(如欧氏距离)最近的点。如果这个最近距离小于某个阈值,且与次近距离的比值也满足一定条件(如Lowe’s ratio test),则认为这两个点是一对匹配点。最终,两幅图像之间的匹配点对数可以作为它们相似性的一个度量。

双向匹配:为了增加匹配的可靠性,可以进行双向检查。即不仅从A到B寻找匹配,也从B到A寻找匹配,只保留那些互为最近邻的匹配对。

import numpy as np

# 模拟两幅图像的SIFT描述子(为了演示,我们构造简单的数据)
# 假设每幅图像提取了3个128维的特征
np.random.seed(123)
# 图像A的特征
descriptors_A = np.random.randn(3, 128)
# 图像B的特征:构造两个与A相似,一个随机
descriptors_B = np.vstack([
    descriptors_A[0] + 0.01 * np.random.randn(128),  # 与A[0]相似
    descriptors_A[1] + 0.01 * np.random.randn(128),  # 与A[1]相似
    np.random.randn(128)                             # 随机特征
])

def match_features(desc1, desc2, ratio_thresh=0.8):
    """简单的最近邻匹配(演示原理,非最优实现)"""
    matches = []
    for i, d1 in enumerate(desc1):
        # 计算d1与desc2中所有特征的距离
        distances = np.linalg.norm(desc2 - d1, axis=1)
        # 获取最近和次近的距离索引
        idx_sorted = np.argsort(distances)
        nearest_idx, second_nearest_idx = idx_sorted[0], idx_sorted[1]
        nearest_dist, second_nearest_dist = distances[nearest_idx], distances[second_nearest_idx]
        
        # Lowe's ratio test: 最近距离/次近距离 < 阈值
        if nearest_dist < ratio_thresh * second_nearest_dist:
            matches.append((i, nearest_idx))
    return matches

# 进行匹配
matched_pairs = match_features(descriptors_A, descriptors_B)
print(f"匹配到的特征点对数量: {len(matched_pairs)}")
for pair in matched_pairs:
    print(f"  图像A的特征点 {pair[0]} <-> 图像B的特征点 {pair[1]}")
匹配到的特征点对数量: 2
  图像A的特征点 0 <-> 图像B的特征点 0
  图像A的特征点 1 <-> 图像B的特征点 1

这种方法对于颜色差异大但结构相似的图像(如不同光照下的同一件衬衫、不同角度拍摄的同一建筑物)非常有效。匹配点越多,表明两幅图像在局部结构上越相似。

视觉词袋模型:从局部特征到视觉单词

基于特征点直接匹配的方法虽然有效,但当图像库非常庞大时,逐对比较所有特征点的计算成本很高。此外,这种方法更关注局部几何一致性,缺乏对图像整体语义内容的宏观描述。

视觉词袋模型(Bag of Visual Words, BoVW)正是为了解决这些问题而提出的。它借鉴了自然语言处理中词袋模型的思想。

自然语言处理中的词袋模型

在NLP中,词袋模型忽略文本的语法和语序,仅将文档表示为其包含的单词出现的频率向量。

例如,有两个句子:

  1. “John likes to watch movies. Mary likes movies too.”
  2. “John also likes to watch football games.”

首先构建一个词典(所有不重复的单词): [‘John’, ‘likes’, ‘to’, ‘watch’, ‘movies’, ‘Mary’, ‘too’, ‘also’, ‘football’, ‘games’]

然后,每个句子就可以表示为一个10维的词频向量:

  • 句子1: [1, 2, 1, 1, 2, 1, 1, 0, 0, 0]
  • 句子2: [1, 1, 1, 1, 0, 0, 0, 1, 1, 1]

这样,复杂的文本就被压缩成了固定长度的向量,便于后续的相似度计算(如计算余弦相似度)。

构建视觉词典

在计算机视觉中,我们将图像类比为文档,将局部特征(如SIFT描述子)类比为单词。但是,图像中的“单词”不是现成的,我们需要从图像集合中“学习”出来。

构建视觉词典的步骤:

  1. 特征提取:对于一个包含 M 幅图像的数据集,对每幅图像提取大量(比如几百上千个)局部特征描述子(如128维SIFT)。假设总共提取了 N 个特征。
  2. 特征聚类:使用聚类算法(最常用的是K-means)对这 N 个高维特征向量进行聚类。设定聚类数目为 K(即词典大小)。
  3. 生成词典:聚类后得到的 K 个聚类中心,就是我们要的 K 个视觉单词。这 K 个中心向量构成了我们的“视觉词典”或“码本”。
import numpy as np

# ========== 步骤1 & 2: 模拟特征提取与聚类(构建视觉词典) ==========
np.random.seed(42)
# 假设我们从3类物体(人脸、自行车、提琴)的图像中提取了共60个128维特征
# 为了演示,我们简化:用3个高斯分布生成3类特征,每类20个
num_features_per_class = 20
dim = 128  # SIFT描述子维度

# 生成3类特征的中心(模拟3类视觉模式)
class_centers = np.array([
    [1.0] * dim,   # 类1中心
    [0.0] * dim,   # 类2中心
    [-1.0] * dim   # 类3中心
])

all_features = []
for center in class_centers:
    # 围绕每个中心生成一些特征点,加入噪声
    features = center + 0.5 * np.random.randn(num_features_per_class, dim)
    all_features.append(features)
all_features = np.vstack(all_features)  # 形状 (60, 128)
print(f"总共提取了 {all_features.shape[0]} 个特征,每个特征维度 {all_features.shape[1]}")

# 使用K-means聚类(这里使用简化的模拟,实际应用中使用cv2.kmeans或sklearn)
# 为演示,我们直接指定聚类中心为预设的类中心(实际是迭代求出的)
visual_vocabulary = class_centers  # 这就是我们的视觉词典,K=3
print(f"视觉词典(视觉单词)的形状: {visual_vocabulary.shape} (K={visual_vocabulary.shape[0]})")
总共提取了 60 个特征,每个特征维度 128
视觉词典(视觉单词)的形状: (3, 128) (K=3)

重要理解:视觉单词(聚类中心)并没有一个像“眼睛”、“车轮”这样明确的、人类可解释的物理含义。它只是一个高维空间中的点,代表了某一种频繁出现的局部特征模式。课件中用“眼睛”、“鼻子”等图示仅仅是为了方便解释。

图像编码与检索

构建好视觉词典后,对于任何一幅新的图像(包括数据库中的图像和查询图像),我们都可以将其编码为一个词频向量:

  1. 提取该图像的所有局部特征。
  2. 对于每一个特征,计算它与视觉词典中所有 K 个视觉单词的距离。
  3. 将该特征分配给距离最近的那个视觉单词。
  4. 统计整幅图像中,每个视觉单词被分配到的次数,形成一个 K 维的词频直方图。这个直方图就是该图像的BoVW表示。
import numpy as np

# 为保持代码块自包含,重新构造与上文相同的视觉词典
np.random.seed(42)
dim = 128
class_centers = np.array([
    [1.0] * dim,
    [0.0] * dim,
    [-1.0] * dim
])
visual_vocabulary = class_centers

# ========== 步骤3: 将图像编码为词频直方图 ==========
def encode_image(features, vocabulary):
    """将一幅图像的特征编码为视觉单词词频直方图。"""
    K = len(vocabulary)
    histogram = np.zeros(K, dtype=int)

    for feat in features:
        # 计算特征到每个视觉单词的距离
        distances = np.linalg.norm(vocabulary - feat, axis=1)
        # 找到最近的视觉单词的索引
        nearest_word_idx = np.argmin(distances)
        # 对应词频加1
        histogram[nearest_word_idx] += 1
    return histogram

# 模拟三幅测试图像的特征
# 图像1:主要包含“类1”模式
img1_feats = class_centers[0] + 0.3 * np.random.randn(15, dim)
# 图像2:主要包含“类2”模式
img2_feats = class_centers[1] + 0.3 * np.random.randn(15, dim)
# 图像3:包含“类1”和“类3”模式
img3_feats = np.vstack([
    class_centers[0] + 0.3 * np.random.randn(8, dim),
    class_centers[2] + 0.3 * np.random.randn(7, dim)
])

# 编码图像
hist1 = encode_image(img1_feats, visual_vocabulary)
hist2 = encode_image(img2_feats, visual_vocabulary)
hist3 = encode_image(img3_feats, visual_vocabulary)

print("图像1的词频直方图:", hist1)
print("图像2的词频直方图:", hist2)
print("图像3的词频直方图:", hist3)
图像1的词频直方图: [15  0  0]
图像2的词频直方图: [ 0 15  0]
图像3的词频直方图: [8 0 7]

现在,每幅图像都被表示为一个固定的 K 维向量。图像检索就变成了在向量空间中寻找最近邻的问题:

  1. 将查询图像编码为词频向量。
  2. 计算该向量与图像库中所有图像向量的距离(如 L1、L2 距离)或相似度(如余弦相似度)。
  3. 按距离由近到远(用相似度时则按由大到小)排序,返回最相似的图像。

BoVW模型的优势在于:

  • 计算高效:将数量不定的局部特征压缩为固定维度的向量,大大减少了相似度比较的计算量。
  • 更具语义:词频直方图反映了图像中不同视觉模式的统计分布,比单纯的局部匹配更能捕捉图像的整体和语义内容。
  • 对局部噪声更鲁棒:个别错误匹配的特征点对整体词频分布影响较小。

📝 动手练一练

  1. 概念辨析:基于局部特征点直接匹配的图像检索,与基于视觉词袋模型的检索,核心思想有何不同?各自适用于什么场景?
👉 点击查看参考答案
  • 直接匹配:核心思想是计算两幅图像间可匹配的局部特征点对数。它侧重于图像间局部几何结构的一致性,对于存在视角变化、遮挡但局部结构相似的图像非常有效。适用于精确匹配、图像拼接、运动结构恢复等场景。
  • 视觉词袋模型:核心思想是将局部特征量化为有限的视觉单词,用图像的词频分布来表示图像。它侧重于图像内容的统计分布,将底层特征提升到了更宏观的“语义”层面。适用于大规模图像库的分类和检索,因为它将图像表示为固定长度的向量,便于索引和快速比较。
  • 简单比喻:直接匹配像是比较两篇文章里完全相同的句子有多少;而词袋模型像是比较两篇文章用词风格和主题词分布的相似度。
  1. 动手计算:假设一个视觉词典只有3个单词。图像A的词频向量为 [10, 2, 0],图像B为 [2, 10, 0],图像C为 [0, 1, 9]。请计算图像A与B、A与C之间的L2距离(欧氏距离)。根据距离,哪两幅图像更相似?
👉 点击查看参考答案

计算L2距离:

  • A与B的距离:(10−2)2+(2−10)2+(0−0)2=64+64+0=128≈11.31\sqrt{(10-2)^2 + (2-10)^2 + (0-0)^2} = \sqrt{64 + 64 + 0} = \sqrt{128} \approx 11.31
  • A与C的距离:(10−0)2+(2−1)2+(0−9)2=100+1+81=182≈13.49\sqrt{(10-0)^2 + (2-1)^2 + (0-9)^2} = \sqrt{100 + 1 + 81} = \sqrt{182} \approx 13.49

A与B的距离(~11.31)小于A与C的距离(~13.49),因此图像A与图像B更相似。从向量可以看出,A和B的主要成分都集中在第一个和第二个单词上,只是权重不同;而C的分布则完全不同。

本章小结

本节我们深入探讨了基于局部特征的图像检索技术。我们首先回顾了SIFT等局部特征描述子,并学习了如何通过特征点匹配来衡量图像相似性。接着,我们引入了更强大的视觉词袋模型,它通过聚类局部特征构建视觉词典,并将图像编码为词频直方图,从而实现了从局部特征到图像整体语义描述的飞跃。这种方法不仅提高了检索效率,也增强了检索结果在语义层面的相关性。

📋 行动清单

  • 使用OpenCV的SIFT接口,对两幅不同的图像提取特征并尝试进行匹配,观察匹配结果。
  • 在小型图像数据集上,尝试用K-means聚类SIFT特征构建一个简易的视觉词袋模型,并比较不同图像的词频向量。
  • 思考:视觉词袋模型中的“视觉单词”没有明确物理含义,这是它的缺点吗?为什么?

—— 小象教研组