查看全课大纲(第 48 / 93 节)
- 1.计算机视觉概述
- 2.计算机视觉应用场景
- 3.困难与挑战
- 4.开源库介绍
- 5.图像显示与存储
- 6.CLAHE
- 7.腐蚀膨胀与开闭运算
- 8.滤波与边缘填充
- 9.各种滤波
- 10.图像金字塔
- 11.傅里叶变换
- 12.短时傅里叶变换与小波变换
- 13.颜色特征与颜色直方图
- 14.几何特征_边缘
- 15.几何特征-角点与斑点
- 16.局部特征-SIFT特征描述子
- 17.局部特征-SURF特征描述子
- 18.局部特征-ORB特征描述子
- 19.其他特征描述子
- 20.传统图像分割技术
- 21.GraphCut
- 22.GrabCut
- 23.人脸检测的传统方法
- 24.行人检测的传统方法HOG+SVM
- 25.行人检测DPM
- 26.人工神经网络的架构
- 27.目标函数与梯度下降
- 28.误差反向传播
- 29.可手算的神经网络
- 30.改进目标函数与克服梯度消散
- 31.神经网络之传统神经网络与深度网络的差异
- 32.各种梯度下降算法
- 33.如何选择合适的梯度下降算法
- 34.避免过适应
- 35.CNN的架构
- 36.CNN的误差反向传播_卷积层
- 37.CNN的误差反向传播_池化层
- 38.分类所用的数据集
- 39.Alexnet详解
- 40.VGG详解
- 41.GoogLeNet_v1
- 42.BatchNormalization
- 43.GoogLeNet_v3v4
- 44.ResNet
- 45.ResNeXt
- 46.CNN设计准则
- 47.图像检索与推土机距离
- 48.基于颜色相似性
- 49.基于形状相似性
- 50.基于纹理相似性
- 51.基于局部特征相似性
- 52.DL的二值哈希编码
- 53.高维空间中检索KDTree
- 54.高维空间中检索LSH
- 55.目标识别任务综述
- 56.RCNN
- 57.SPP-Net
- 58.Fast R-CNN
- 59.Faster R-CNN
- 60.R-CNN系列模型回顾
- 61.R-FCN的设计思路
- 62.R-FCN的实现
- 63.YOLOv1
- 64.YOLOv2_9000
- 65.YOLOv3
- 66.图像分割应用场景与数据集
- 67.上采样的实现方式
- 68.FCN网络架构的由来
- 69.FCN构建细节
- 70.DeepLabv1
- 71.CRF
- 72.DeepLabv2
- 73.DeepLabv3
- 74.特定的医学影像分割任务
- 75.U-Net
- 76.3D U-Net
- 77.V-Net
- 78.DenseNet
- 79.病理切片分析任务及数据集
- 80.病理切片分析
- 81.图说任务及其传统处理方法
- 82.深度语言模型与RNN
- 83.LSTM及其变种
- 84.NIC模型
- 85.SAT模型
- 86.图说的性能指标
- 87.判别模型与生成模型
- 88.KL散度与JS散度
- 89.VAE
- 90.GAN的基本原理
- 91.DCGAN与CGAN
- 92.Wasserstein GAN
- 93.SRGAN
基于颜色相似性
基于颜色相似性的图像检索
小象实战讲义 · 基于深度学习的计算机视觉
在图像检索任务中,除了关注物体的形状和纹理,颜色本身就是一个非常直观且强大的检索线索。想象一下,你想在电商平台找一件“酒红色”的毛衣,或者在相册里找出所有“夕阳”主题的照片,颜色相似性检索就能派上用场。本节我们将学习如何利用颜色直方图来量化图像的颜色特征,并引入符合人眼视觉感知的色差度量方法,最终通过“推土机距离”来比较图像之间的颜色相似度,从而实现精准的检索。
💡 核心导读
- 颜色特征提取:学习如何将一张图像的丰富色彩信息,通过颜色直方图或聚类直方图,压缩成一个高维特征向量。
- Lab 颜色空间:理解为何 RGB 空间不适合直接度量颜色差异,以及 Lab 颜色空间如何更好地模拟人眼视觉感知。
- 色差度量 CIEDE2000:了解用于度量 Lab 空间中两个颜色视觉相似度的标准方法,它考虑了人眼对不同颜色的敏感度差异。
- 推土机距离 (EMD):掌握如何将两个颜色直方图之间的相似度比较,转化为一个经典的“运输问题”,并求解最小成本。
- 检索流程实战:从特征提取到相似度计算与排序,完整走一遍基于颜色相似性的图像检索流程。
颜色特征提取:从像素到直方图
图像检索的第一步是特征提取。对于颜色特征,最经典的方法就是颜色直方图。
颜色直方图统计的是一幅图像中各种颜色出现的频率。如果我们把 RGB 图像中每一个可能的 (R, G, B) 组合都看作一种颜色,那么直方图的横坐标就是所有可能的颜色,纵坐标就是该颜色在图像中出现的像素数量。这样,一张图像就被表示成了一个高维度的特征向量。
import numpy as np
import cv2
# 示例:构造一个简单的3x3 RGB图像,并计算其颜色直方图(简化版)
# 假设颜色空间被量化为8个等级(0-31, 32-63, ..., 224-255),那么共有8^3=512种颜色
# 这里我们手动统计一个极简的例子
# 构造一个3x3的RGB图像,每个像素值在[0, 255]之间
# 为了演示,我们使用整数并假设已经量化到8个等级(实际计算时会先量化)
image_data = np.array([
[[200, 50, 50], [50, 200, 50], [50, 50, 200]],
[[200, 200, 50], [50, 200, 200], [200, 50, 200]],
[[100, 100, 100], [150, 150, 150], [200, 200, 200]]
], dtype=np.uint8)
print("原始图像像素值(3x3x3):")
print(image_data)
print()
# 量化:将0-255范围压缩到0-7(8个等级)
# 实际应用中,量化等级可以是16、32等,这里用8级简化演示
quantized = image_data // 32 # 256/8=32
print("量化后的像素值(等级0-7):")
print(quantized)
print()
# 将三维颜色索引转换为一维直方图索引
# 公式:index = R * (bins^2) + G * bins + B,其中bins=8
bins = 8
hist_index = (quantized.astype(np.int32)[:, :, 0] * (bins**2)
+ quantized.astype(np.int32)[:, :, 1] * bins
+ quantized.astype(np.int32)[:, :, 2])
print("每个像素对应的一维直方图索引:")
print(hist_index)
print()
# 计算直方图(统计每个索引出现的次数)
hist, _ = np.histogram(hist_index.flatten(), bins=range(bins**3 + 1))
print(f"颜色直方图向量(长度为{bins**3}={bins**3}):")
# 只打印非零的部分以节省空间
non_zero_indices = np.where(hist > 0)[0]
for idx in non_zero_indices:
# 将一维索引解码回RGB等级
r = idx // (bins**2)
g = (idx % (bins**2)) // bins
b = idx % bins
print(f" 颜色等级(R={r}, G={g}, B={b}) 出现次数: {hist[idx]}")原始图像像素值(3x3x3):
[[[200 50 50]
[ 50 200 50]
[ 50 50 200]]
[[200 200 50]
[ 50 200 200]
[200 50 200]]
[[100 100 100]
[150 150 150]
[200 200 200]]]
量化后的像素值(等级0-7):
[[[6 1 1]
[1 6 1]
[1 1 6]]
[[6 6 1]
[1 6 6]
[6 1 6]]
[[3 3 3]
[4 4 4]
[6 6 6]]]
每个像素对应的一维直方图索引:
[[393 113 78]
[433 118 398]
[219 292 438]]
颜色直方图向量(长度为512=512):
颜色等级(R=1, G=1, B=6) 出现次数: 1
颜色等级(R=1, G=6, B=1) 出现次数: 1
颜色等级(R=1, G=6, B=6) 出现次数: 1
颜色等级(R=3, G=3, B=3) 出现次数: 1
颜色等级(R=4, G=4, B=4) 出现次数: 1
颜色等级(R=6, G=1, B=1) 出现次数: 1
颜色等级(R=6, G=1, B=6) 出现次数: 1
颜色等级(R=6, G=6, B=1) 出现次数: 1
颜色等级(R=6, G=6, B=6) 出现次数: 1从输出可以看到,我们的小图像包含了9种不同的颜色(对应9个非零的直方图条目)。然而,直接使用所有可能的颜色(例如256^3约1678万种)会导致直方图维度爆炸,且许多颜色可能根本不出现。为了解决这个问题,我们通常采用两种策略:
- 颜色量化:将连续的RGB颜色空间划分为有限个区间(如8x8x8=512个),将落在同一区间的颜色视为同一种。
- 颜色聚类:使用聚类算法(如K-means++)对图像中的所有像素颜色进行聚类,用聚类中心代表主要的颜色成分,形成颜色聚类直方图。这尤其适用于Lab这类颜色数量不可枚举的颜色空间。
Lab颜色空间与视觉感知均匀性
为什么我们更倾向于在 Lab 颜色空间 中进行颜色相似度的计算,而不是直接使用 RGB 空间?
RGB 颜色模型是基于硬件(如显示器发光)的,其三个分量(红、绿、蓝)的欧氏距离并不能很好地反映人眼感知到的颜色差异。例如,人眼对绿色色调的细微变化不敏感,但对蓝色色调的微小变化却非常敏感。
Lab 颜色空间 是由国际照明委员会(CIE)制定的,旨在数字化地描述人眼的视觉感应,它是一个感知上更均匀的颜色空间。在Lab空间中:
- L 分量代表明度(Lightness),范围从0(纯黑)到100(纯白)。
- a 分量代表从绿色(负值)到红色(正值)的色轴。
- b 分量代表从蓝色(负值)到黄色(正值)的色轴。
Lab 空间能描述正常人眼可见的所有颜色,其范围比 RGB 更广。更重要的是,在 Lab 空间中,两个颜色点之间的欧氏距离,比在 RGB 空间中更能反映人眼感知到的色差。
然而,即使是 Lab 空间,其均匀性仍不完美。因此,CIE 制定了更复杂的色差公式,如 CIEDE2000,它进一步修正了不同颜色区域人眼敏感度的差异,是目前工业界度量小色差的标准。
import numpy as np
import cv2
# 演示RGB到Lab空间的转换
# 注意:OpenCV 的 8 位 Lab 编码为 L: [0, 255](对应 L* 0~100),a/b: [0, 255](偏移 128)
# 颜色1/2:绿色通道 +30;颜色3/4:蓝色通道 +30
color1_rgb = np.uint8([[[0, 100, 0]]])
color2_rgb = np.uint8([[[0, 130, 0]]])
color3_rgb = np.uint8([[[0, 0, 100]]])
color4_rgb = np.uint8([[[0, 0, 130]]])
# 转换到Lab空间
color1_lab = cv2.cvtColor(color1_rgb, cv2.COLOR_RGB2Lab).squeeze()
color2_lab = cv2.cvtColor(color2_rgb, cv2.COLOR_RGB2Lab).squeeze()
color3_lab = cv2.cvtColor(color3_rgb, cv2.COLOR_RGB2Lab).squeeze()
color4_lab = cv2.cvtColor(color4_rgb, cv2.COLOR_RGB2Lab).squeeze()
print("颜色值对比(OpenCV 8位 Lab 编码):")
print(f"颜色1 (深绿) - RGB: {color1_rgb.squeeze()}, Lab: {color1_lab}")
print(f"颜色2 (浅绿) - RGB: {color2_rgb.squeeze()}, Lab: {color2_lab}")
print(f"颜色3 (深蓝) - RGB: {color3_rgb.squeeze()}, Lab: {color3_lab}")
print(f"颜色4 (浅蓝) - RGB: {color4_rgb.squeeze()}, Lab: {color4_lab}")
print()
print("可以看到:RGB 中同样是 +30 的变化,在 Lab 的 L、a、b 三个分量上并不等价;")
print("因此不能直接用 RGB 数值差来代表视觉色差,需要在 Lab 上再用 CIEDE2000 做加权。")颜色值对比(OpenCV 8位 Lab 编码):
颜色1 (深绿) - RGB: [ 0 100 0], Lab: [ 92 85 170]
颜色2 (浅绿) - RGB: [ 0 130 0], Lab: [120 76 178]
颜色3 (深蓝) - RGB: [ 0 0 100], Lab: [ 21 167 74]
颜色4 (浅蓝) - RGB: [ 0 0 130], Lab: [ 34 176 63]
可以看到:RGB 中同样是 +30 的变化,在 Lab 的 L、a、b 三个分量上并不等价;
因此不能直接用 RGB 数值差来代表视觉色差,需要在 Lab 上再用 CIEDE2000 做加权。注意:OpenCV 的 8 位 Lab 是编码值(L 按 255/100 缩放,a/b 偏移 128),并非直接的 Lab* 物理量;本示例只演示 RGB→Lab 的转换,不直接比较欧氏距离。真正的 CIEDE2000 会在 Lab 上引入对明度(L)、彩度(C)和色相(h)的加权函数,以及针对中性色、蓝色区域等的修正项,使其与大规模人眼视觉实验高度吻合。其公式较复杂,我们只需知道它是当前度量小色差的工业标准即可。
推土机距离:比较颜色直方图
当我们有了两幅图像的颜色直方图(或聚类直方图)后,如何度量它们的相似度?直接计算直方图的欧氏距离或余弦相似度可能不合适,因为它没有考虑颜色本身之间的相似性。例如,一个以深红色为主的直方图和一个以浅红色为主的直方图,在颜色内容上应该是相似的,但如果把红色和蓝色当作完全无关的维度,它们的距离就会很大。
推土机距离(Earth Mover’s Distance, EMD),又称 Wasserstein 距离,完美地解决了这个问题。它将两个分布之间的差异度量,转化为一个经典的运输问题:
想象有两个土堆分布,代表第一幅图像的颜色直方图;另有一些土坑,代表第二幅图像的颜色直方图。每个土堆的土量是某种颜色的频率,每个土坑的容量是另一种颜色的频率。将土从土堆运到土坑需要成本,成本就是两种颜色之间的色差(例如用CIEDE2000计算)。EMD 要解决的问题是:找到一种运输方案,以最小的总成本将所有的土从土堆填入土坑。这个最小总成本就是 EMD 距离。
EMD 的优势在于它同时考虑了分布“形状”的差异和基础元素(颜色)之间的相似性。即使两个直方图的峰值没有严格对齐,只要它们的颜色在视觉上是相近的,EMD 距离也会很小。
import numpy as np
# 简化版EMD计算演示(仅用于理解概念,实际使用需调用优化库如`pyemd`)
# 假设我们有两幅图像的简化颜色直方图,每种颜色用一个聚类中心代表(这里用一维索引简化)
# 以及一个预定义的颜色距离矩阵。
# 图像A的颜色直方图 (土堆)
hist_a = np.array([0.5, 0.3, 0.2]) # 三种颜色,总质量为1
# 图像B的颜色直方图 (土坑)
hist_b = np.array([0.4, 0.4, 0.2]) # 三种颜色,总容量为1
# 颜色距离矩阵 (假设是CIEDE2000距离,这里用简化的数字示意)
# D[i][j] 表示将颜色i的土运到颜色j的坑的成本
distance_matrix = np.array([
[0.0, 2.0, 5.0], # 颜色0到颜色0,1,2的成本
[2.0, 0.0, 3.0], # 颜色1到颜色0,1,2的成本
[5.0, 3.0, 0.0], # 颜色2到颜色0,1,2的成本
])
print("图像A直方图(土堆):", hist_a)
print("图像B直方图(土坑):", hist_b)
print("颜色距离矩阵:")
print(distance_matrix)
print()
# 手动给出一个运输方案(后文可验证:它恰好就是最优方案)
# 方案:尽量将土运到同色坑,缺口再按最低单价补
# 从颜色0运0.4到颜色0(成本0),运0.1到颜色1(成本2)
# 从颜色1运0.3到颜色1(成本0)
# 从颜色2运0.2到颜色2(成本0)
# 注意:颜色1的坑还差0.1,由颜色0运来的0.1填补。
flow = np.array([
[0.4, 0.1, 0.0],
[0.0, 0.3, 0.0],
[0.0, 0.0, 0.2]
])
print("假设的土方运输流量矩阵(flow[i][j]):")
print(flow)
print("检查: 每行之和应等于hist_a")
print(" 行和:", flow.sum(axis=1))
print("检查: 每列之和应等于hist_b")
print(" 列和:", flow.sum(axis=0))
print()
# 计算该方案的总成本
total_cost = np.sum(flow * distance_matrix)
print(f"该运输方案的总成本: {total_cost}")
print("可以验证:颜色1的坑需要0.4,而同色土堆只有0.3,缺口0.1只能从颜色0(单位成本2)")
print("或颜色2(单位成本3)补足,所以任何可行方案的成本都不低于 0.1*2=0.2。")
print("因此上面这个flow恰好就是最优运输方案,0.2 就是这两幅图像颜色直方图的真实EMD。")图像A直方图(土堆): [0.5 0.3 0.2]
图像B直方图(土坑): [0.4 0.4 0.2]
颜色距离矩阵:
[[0. 2. 5.]
[2. 0. 3.]
[5. 3. 0.]]
假设的土方运输流量矩阵(flow[i][j]):
[[0.4 0.1 0. ]
[0. 0.3 0. ]
[0. 0. 0.2]]
检查: 每行之和应等于hist_a
行和: [0.5 0.3 0.2]
检查: 每列之和应等于hist_b
列和: [0.4 0.4 0.2]
该运输方案的总成本: 0.2
可以验证:颜色1的坑需要0.4,而同色土堆只有0.3,缺口0.1只能从颜色0(单位成本2)
或颜色2(单位成本3)补足,所以任何可行方案的成本都不低于 0.1*2=0.2。
因此上面这个flow恰好就是最优运输方案,0.2 就是这两幅图像颜色直方图的真实EMD。完整检索流程与实战总结
结合以上知识点,基于颜色相似性的图像检索流程可以归纳为以下四步:
特征提取:对于查询图像和图像库中的所有图像,提取其颜色特征。通常做法是:
- 将图像转换到 Lab 颜色空间。
- 对像素的 Lab 值进行聚类(如 K-means++),得到 K 个主要颜色簇。
- 统计每个簇中的像素数量,归一化后得到 K 维的颜色聚类直方图。
特征存储:将图像库中所有图像的颜色直方图特征预先计算并存储起来。
相似度计算:
- 当用户提交查询图像时,同样提取其颜色直方图。
- 计算查询图像直方图与库中每个图像直方图之间的 EMD 距离。
- EMD 计算中需要的基础颜色距离,使用 CIEDE2000 公式计算聚类中心之间的色差。
结果排序:根据 EMD 距离由小到大对库中图像进行排序。EMD 距离越小,表示两幅图像的颜色组成在视觉上越相似,排名就越靠前。
这个过程的核心思想是:用符合人眼视觉的色差度量(CIEDE2000)作为“运费单价”,用推土机距离(EMD)作为“总运费”来计算两幅图像颜色分布的差异,从而实现以人类感知为基础的相似性排序。
📝 动手练一练
- 概念辨析:在基于颜色直方图的检索中,为什么使用 EMD 距离比使用欧氏距离更合理?请从直方图比较的本质和颜色相似性两个角度阐述。
👉 点击查看参考答案
从直方图比较的本质看:直方图本质上是概率分布。欧氏距离计算的是两个向量在每个维度上的数值差异,它假设所有维度是独立且正交的。对于直方图,这意味着它把“红色增多”和“蓝色减少”视为完全独立的事件,忽略了分布的整体形态和对应关系。而 EMD 距离将比较视为一个分布匹配问题,寻找一个最小成本的“流动”方案将一个分布变成另一个,更能反映两个分布整体的相似性。
从颜色相似性看:欧氏距离在颜色直方图的应用中,隐含假设了“颜色索引i和颜色索引j之间的距离是固定的”(例如,索引1代表红色,索引2代表蓝色,它们的距离就是1)。但这不符合视觉事实,深红和浅红是相似的,深红和深蓝则差异巨大。EMD 距离允许我们定义一个更合理的颜色距离矩阵(如使用 CIEDE2000),在这个基础上计算分布匹配的最小成本,从而将颜色本身的视觉相似性融入到了相似度度量中。
- 流程理解:假设我们有一个包含3种主色(聚类中心)的图像库,查询图像Q的直方图为
[0.6, 0.3, 0.1],库中图像A的直方图为[0.5, 0.4, 0.1],图像B的直方图为[0.1, 0.3, 0.6]。已知颜色距离矩阵中,颜色0和颜色1很接近(距离小),颜色0和颜色2差异很大(距离大)。在不进行精确计算的情况下,请判断查询图像Q更可能与图像A还是图像B更相似?为什么?
👉 点击查看参考答案
查询图像Q更可能与图像A更相似。
推理过程:
- 比较Q和A:两者的直方图形状相似,主要质量都集中在颜色0和颜色1上,且颜色0和1本身很接近。因此,将Q的“土”运到A的“坑”时,大部分土(颜色0和1)可以以很低的成本运到相同或相近颜色的坑里,总成本(EMD)会很小。
- 比较Q和B:两者的直方图形状几乎相反。Q的质量主要在颜色0,而B的质量主要在颜色2,且颜色0和2差异巨大。为了将Q的分布“变成”B的分布,需要将大量“颜色0的土”以极高的成本运到“颜色2的坑”里,总成本(EMD)会非常大。
- 因此,EMD距离
D(Q, A) < D(Q, B),Q与A更相似。
本章小结
本节我们深入探讨了基于颜色相似性的图像检索技术。我们了解到,简单的颜色统计(直方图)是强大的特征,但为了使其有效,必须考虑人眼的视觉特性。这引导我们使用 Lab 颜色空间和 CIEDE2000 色差公式来度量颜色间的感知差异。最后,通过推土机距离(EMD),我们能够以一种同时考虑分布形态和元素相似性的方式,来比较两幅图像的整体颜色构成,从而实现符合人类直觉的图像检索。
📋 行动清单
- 理解颜色空间转换的意义:动手写几行代码,将 RGB 图像转换为 Lab 空间,观察三个通道的含义。
- 查阅 CIEDE2000 公式:虽然无需记忆,但可以浏览其 Wikipedia 页面,了解它为了解决人眼感知不均匀性做了哪些修正(如亮度、彩度、色相加权,蓝色区域修正等)。
- 思考 EMD 的应用:除了颜色直方图,EMD 还可以用于比较哪些类型的特征分布?(例如:纹理基元分布、词袋模型中的视觉单词分布等)。
—— 小象教研组