说实话,做图像检索和内容理解的从业者,迟早都要回到图像特征这个基本功上。而颜色特征,又是所有特征里最直观、最容易上手、也最容易踩坑的一类。这个系列我准备从颜色特征讲起,覆盖颜色直方图、颜色矩、颜色集、颜色聚合向量和颜色相关图这五个经典方案。无论你是刚接触计算机视觉的初学者,还是想在检索、分类、去重等场景里快速选型的工程师,这篇文章都会给你一套完整、可落地的思路和代码参考。
颜色特征的核心价值在于:它不需要任何语义理解,只用像素的数值分布就能描述一张图的内容。光照变了、分辨率变了、小幅度旋转裁剪,颜色分布依然有很强的稳定性。这也是为什么几十年来,从传统图像检索到现在的预处理环节,颜色直方图依然没有被彻底淘汰。但它的局限也很明显——完全没有空间位置信息,一只橘猫和一块橘子皮的直方图可能非常接近。于是才有了后面四种特征对它的逐步改进。
1. 颜色特征的整体设计思路
1.1 为什么先从颜色特征讲起
我这些年做图像相关的项目,小到电商图片去重,大到千万级的以图搜图,发现一个规律:颜色是图像最底层的数字指纹。你在读一张图时,第一眼接收到的往往是色调和明暗,而不是边缘和纹理。算法也是一样,颜色特征的提取成本最低、抗干扰能力最强,适合做粗筛。
举一个实际例子。你在做商品图去重时,同一件衣服在不同店铺有不同背景、不同模特、不同拍摄角度。纹理特征会受到褶皱和装饰影响,SIFT类局部特征会因为角度不同而失配。但颜色直方图或者颜色聚合向量,只要衣服主体颜色没变,匹配结果就很稳。这个"先粗筛再精排"的思路,是所有检索系统的通用架构。
另一个原因是,颜色特征是理解局部特征的桥梁。很多人在学SIFT、HOG时觉得抽象,是因为没有建立"把图像信息表达成向量"的直觉。颜色特征就是建立这个直觉的最好入口:把高维像素压缩成低维向量,再在向量空间里做相似度计算。这个过程一旦想通了,后面所有特征都会触类旁通。
颜色特征的通用流程可以拆成三步:
- 颜色空间选择:RGB、HSV、Lab等。
- 颜色量化:把连续颜色值映射到有限个颜色桶。
- 特征表达:用直方图、矩、集合、聚合向量或相关图的形式形成特征向量。
这三步的每一步都有讲究,我会在后面逐一展开。
1.2 五个代表思路:从全局到局部的进化
市面上颜色特征的方法非常多,但这五个最经典,因为它们代表了完全不同的五条技术路线,理解了它们等于理解了颜色特征的整个演进史。
- 颜色直方图:统计各颜色桶的出现频率,表达全局颜色分布,计算最简单,但丢失空间信息。
- 颜色矩:用均值和方差等低阶统计量描述颜色分布,特征维度极低,适合做粗匹配或辅助特征。
- 颜色集:把颜色量化后转成二值索引集合,更像一种查询结构,非常适用于大规模图像检索的倒排索引。
- 颜色聚合向量:在直方图基础上加入连通区域信息,回答"这种颜色是成片聚集还是星星点点"的问题。
- 颜色相关图:更进一步,描述不同颜色像素在空间上相距多远,最终形成颜色对随距离变化的联合分布。
这五个方法在信息量和计算复杂度上逐步上升。直方图是全局统计的起点,矩是对统计量的再压缩,颜色集是检索导向的离散表达,聚合向量补充了局部空间结构,相关图则把距离维度也纳了进来。实际选型时要按场景来,并不一定越复杂越好。
我对这五个特征的总体判断是:如果你追求速度和鲁棒性,用直方图或颜色矩就够;如果你做检索系统且对精度有要求,颜色聚合向量性价比最高;只有当数据规模和质量都需要精细化匹配时,才值得上颜色相关图。
2. 颜色特征的核心原理
2.1 颜色直方图:全局统计的起点
颜色直方图其实是概率密度函数的离散估计。假设一幅图像总共有N个像素,量化后有K个颜色桶,第k个桶里的像素数量是n_k,那么直方图定义为:
h(k) = n_k / N,其中k = 1, 2, ..., K
这表示每个颜色桶出现的频率。直方图的好处是平移、旋转、缩放不变,因为坐标变化不会改变像素值分布。这里要注意:缩放不变的前提是插值算法没有引入太多颜色偏移,实际代码里最好统一缩放后再提取特征。
颜色空间的选择会直接影响直方图的效果。我做过一组对比实验:RGB直方图对光照变化很敏感,同一物体在阴影里和阳光下匹配度下降明显。HSV空间下,把H(色相)单独拿出来做直方图,光照影响就小得多,因为H保留了色相信息,S和V变化主要影响饱和度和亮度。所以实际工程中,我建议优先用HSV,并把H通道的量化数量设得比S、V多一些,比如H量化16~32级,S和V各自量化8级。
还有一个容易忽略的细节:直方图该用全图还是分块。全图直方图的优势是抗旋转,但容易把主体颜色和背景颜色混在一起。分块直方图把图像划分为若干区域(如3x3),每块单独统计,然后拼接成一个特征向量。这种做法提升了空间区分性,但引入了对旋转和移位的敏感性。我用分块直方图做商品分类时发现,分块数量从1x1到3x3效果提升明显,再到5x5反而下降,因为块越多,主体位置的干扰越严重。推荐从3x3开始调。
2.2 颜色矩:用数学期望压缩信息
颜色直方图最大的问题是维度太高,一个量化到64个桶的直方图就有64维。颜色矩的思路是用几个低阶统计量来压缩颜色分布,最早由Stricker和Orengo在1995年提出,核心假设是颜色分布的主要信息集中在前三阶矩。
定义如下:对图像某个颜色通道,假设第i个像素值为p_i(像素总数为N),那么一阶矩表示均值,二阶矩表示标准差,三阶矩表示偏度:
一阶矩(均值):μ = (1/N) * Σ p_i 二阶矩(标准差):σ = sqrt( (1/N) * Σ (p_i - μ)^2 ) 三阶矩(偏度):s = cbrt( (1/N) * Σ (p_i - μ)^3 )
颜色矩的缺点很明显:只保留了全局颜色分布的均值、方差和偏度,空间信息也丢失了。优点是特征维度极低,RGB三通道加上HSV三通道,每通道3个矩,总共18维,做相似度计算非常快。而且矩对光照变化的容忍度比直方图更高,因为统计量对噪声有平滑作用。
我一般在两种场景下用颜色矩:第一,作为检索系统的第一级粗筛,先用18维特征把候选集缩小到几千张,再用高维特征精排;第二,用于颜色分布差异非常明显的场景,比如区分蓝天、森林、沙漠这类风景图,几个矩就能拉开差距。单独用颜色矩做精细分类是不够的,但它作为辅助特征非常出色。
2.3 颜色集:从连续空间到二值化检索
颜色集由Smith和Chang提出,目标很明确:服务大规模图像检索。它的思路先量化颜色空间,再把图像中出现的颜色表示成一个二值索引集合,出现的颜色标为1,没出现的标为0。这个集合还能配合区域划分使用:把图像分成若干块,对每块统计出现的主颜色集合,再根据颜色集做匹配。
颜色集最关键的一步是颜色量化。实际操作中,我通常把HSV空间的H量化到8~16级,S和V量化到4~8级,合并成一个一维索引。比如H量化16级、S量化8级、V量化8级,总桶数就是16乘8乘8等于1024个。对每个像素计算索引后,用阈值判断这个桶是否被激活。阈值可以是固定值,比如像素数大于某个绝对数量,也可以是比例阈值,比如超过总像素的0.5%。
颜色集的优势在检索效率:因为特征是二值形式,可以用二进制位运算做匹配,甚至建立倒排索引,查询速度远超浮点距离计算。但代价是信息损失很大,两张颜色分布接近但细节不同的图像,集合表现几乎一样。所以这个特征现在很少单独作为最终特征,更多用于数据库分库分桶,或者作为语义标签的中间表示。
2.4 颜色聚合向量:把空间信息补回来
颜色聚合向量是Pass等在1998年提出的,它解决的是直方图的一个经典问题:两个直方图相同,但空间分布截然不同的图像。比如一张图的红色像素全部聚在中心位置,另一张图的红色像素均匀撒在四周,直方图看不出来差异,但肉眼一眼就能区分。
聚合向量的计算分三步:
- 将颜色量化,得到每个颜色桶。
- 对每个颜色桶内的像素做连通域分析,标记连通分量。
- 对每个连通分量,判断它的大小是否超过设定阈值T(通常取图像总像素数的某个比例,如0.5%),超过则这些像素记为聚合像素,否则为非聚合像素。
最终每个颜色桶用两个值表示:(α_i, β_i),其中α_i表示颜色桶i的聚合像素数,β_i表示非聚合像素数。整个特征向量维度是颜色桶数的两倍。
我在实现时发现一个关键细节:连通域的判定方式和阈值T的选择直接决定了特征质量。用4邻域还是8邻域结果差异很大。做商品图检索时,8邻域会把噪声点连成片,导致聚合比例偏高;4邻域更保守,对噪点更敏感但更稳定。我的建议是优先试4邻域,阈值T取图像总像素的0.5%~1%之间,再根据数据集微调。
颜色聚合向量对纹理密度的描述很有效:聚合比例高的图像通常有面积较大的均匀色块,适合描述背景简洁的商品图;非聚合比例高的图像则纹理复杂,适合描述碎花图案或者森林草地。这一点在实际应用中经常能带来意想不到的区分效果。
2.5 颜色相关图:颜色与距离的联合分布
颜色相关图是颜色特征里信息最丰富的一种,由Huang等提出。核心思想是:图像中相距为d的两个像素,颜色分别是i和j的概率。简化到只用同一颜色时,就是颜色自相关图。
严格定义比较复杂,实际计算时可以这样理解。设图像中有若干像素对,两个像素的空间距离恰好为d(通常用L1或L2距离),统计距离为d且颜色分别为i和j的像素对数量,再除以总像素对数,得到条件概率。公式为:
γ_ij(d) = Pr[ |p1 - p2| = d,且 color(p1)=i,color(p2)=j ]
实际代码中,自相关图已经足够好用:γ_i(d) 表示距离为d且颜色为i的像素对数量。颜色量化到8个主色,距离取从1到5个级别,那么自相关图维度就是8×5=40维。相比原始相关图(K×K×d)要小得多。
计算相关图时最容易踩坑的是距离计算时间复杂度。幼稚实现是两两像素对遍历,复杂度O(N²),一张百万像素图根本算不完。我实际用的是优化方法:固定距离d后,对每个像素只检查距离为d的固定邻域偏移(比如d=1时只看右方和下方像素),这样复杂度降为O(N),性能可以接受。
相关图适合什么场景呢?它保留了颜色的空间相对位置关系,对纹理和结构有一定刻画能力。比如"红色在绿色旁边"和"红色离绿色很远"两种图像,相关图差异很明显。但它的计算开销和维度远高于前几种特征,实际系统中很少全量使用。我主要把它当成精排阶段的强特征,或者在数据量可控的小型检索系统中使用。
3. 实操:用Python实现五套颜色特征
3.1 环境准备与数据简化
这一节我把五套特征串在一个Python脚本里实现,方便你直接照着跑。依赖只需要OpenCV和NumPy:
import cv2 import numpy as np为了演示方便,我会把图像统一缩放到较小的尺寸,一方面提高计算速度,另一方面减少高频噪声。这里选的是600x600以内的长边缩放:
def load_image(path, max_size=600): img = cv2.imread(path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w = img.shape[:2] scale = max_size / max(h, w) if scale < 1.0: img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) return img这里有个工程细节:缩放时用INTER_AREA,它对缩小的处理比默认的INTER_LINEAR更平滑,能减少采样锯齿,有效避免颜色直方图出现不必要的高频桶噪声。别看细节小,对后续量化结果影响挺大的。
还需要一个HSV转换函数,后续多次会用到:
def to_hsv(rgb_img): return cv2.cvtColor(rgb_img, cv2.COLOR_RGB2HSV)3.2 颜色直方图的完整实现
先写全局直方图,HSV空间量化。我采用H量化16级、S和V各量化8级。这里要注意,HSV在OpenCV里的数值范围是H在0~179,S和V在0~255,需要先归一化再乘量化级别数,避免直接取整时边界错乱:
def color_histogram_hsv(img_rgb, h_bins=16, s_bins=8, v_bins=8): hsv = to_hsv(img_rgb).astype(np.float32) h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2] h_bin = np.minimum((h / 180.0 * h_bins).astype(np.int32), h_bins - 1) s_bin = np.minimum((s / 256.0 * s_bins).astype(np.int32), s_bins - 1) v_bin = np.minimum((v / 256.0 * v_bins).astype(np.int32), v_bins - 1) flat_index = h_bin * (s_bins * v_bins) + s_bin * v_bins + v_bin hist = np.bincount(flat_index.ravel(), minlength=h_bins * s_bins * v_bins) hist = hist / hist.sum() return hist.astype(np.float32)这段代码的关键是用bincount代替OpenCV的calcHist,速度更快,而且可以直接向量化。返回的直方图是归一化频率,做相似度计算时可以用巴氏距离或相关系数。这里我特意用int32类型去承接索引,是因为OpenCV的HSV通道是uint8,直接进行乘除可能会导致截断错误,这个坑我在最初实现时踩过,现在都习惯先转float32再处理。
分块直方图的实现也不难,就是把图像切成3x3或者4x4的块,每块单独调用上面的函数,然后把结果拼接:
def block_histogram(img_rgb, grid=(3, 3)): h, w = img_rgb.shape[:2] cell_h, cell_w = h // grid[0], w // grid[1] feats = [] for i in range(grid[0]): for j in range(grid[1]): block = img_rgb[i*cell_h:(i+1)*cell_h, j*cell_w:(j+1)*cell_w] feats.append(color_histogram_hsv(block)) return np.concatenate(feats)注意拼接后维度是1024×9=9216维,这个维度对后续存储和检索来说不算小,建议在特征入库前做PCA降维。我做商品检索时通常降到128~256维,效果依然稳定。
3.3 颜色矩与颜色集的实现
颜色矩代码比较直接,对每个通道计算均值、标准差和偏度。偏度通常用三次方根压缩数值尺度,否则数值会特别大。数值稳定性上,对std接近0的通道要加一个很小的epsilon:
def color_moments(img_rgb): hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV).astype(np.float32) channels = [hsv[..., 0], hsv[..., 1], hsv[..., 2]] eps = 1e-8 features = [] for ch in channels: mean = ch.mean() std = ch.std() skew = np.cbrt(((ch - mean) ** 3).mean()) features.extend([mean, std, skew]) return np.array(features, dtype=np.float32)这样得到9维颜色矩。如果你想加入RGB通道,就是18维。实测下来,9维HSV颜色矩在图像主色调非常明显的场景下区分度不错,比如"黄衣服的人"和"蓝衣服的人"。
颜色集实现时,我用HSV量化到16×4×4=256个桶,设定激活阈值为总像素数的0.3%。然后输出二值向量:
def color_set(img_rgb, threshold_ratio=0.003): hist = color_histogram_hsv(img_rgb, h_bins=16, s_bins=4, v_bins=4) threshold = threshold_ratio return (hist >= threshold).astype(np.uint8)要注意颜色集里阈值不能设得过高,否则特征会非常稀疏,无法区分图像。我测试时发现阈值在0.1%~0.5%之间比较合理。颜色集可以用于快速过滤:两个颜色集没有交集的话,图像一定不相似。这个特性在构建检索索引时特别有用。
3.4 颜色聚合向量与颜色相关图的实现
颜色聚合向量的实现核心是连通域分析。我先把图像量化到少量主色,然后对每个颜色桶生成掩码,用OpenCV的connectedComponentsWithStats计算连通域,再按面积阈值划分聚合与非聚合像素:
def color_coherence_vector(img_rgb, h_bins=16, s_bins=4, v_bins=4, area_ratio=0.005): hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV).astype(np.float32) h, w = hsv.shape[:2] total_pixels = h * w threshold = total_pixels * area_ratio h_bin = np.minimum((hsv[..., 0] / 180.0 * h_bins).astype(np.int32), h_bins - 1) s_bin = np.minimum((hsv[..., 1] / 256.0 * s_bins).astype(np.int32), s_bins - 1) v_bin = np.minimum((hsv[..., 2] / 256.0 * v_bins).astype(np.int32), v_bins - 1) idx = h_bin * (s_bins * v_bins) + s_bin * v_bins + v_bin alpha = np.zeros(h_bins * s_bins * v_bins, dtype=np.float32) beta = np.zeros(h_bins * s_bins * v_bins, dtype=np.float32) for k in range(h_bins * s_bins * v_bins): mask = (idx == k).astype(np.uint8) if mask.sum() == 0: continue num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) coherent = 0 for lab in range(1, num_labels): area = stats[lab, cv2.CC_STAT_AREA] if area >= threshold: coherent += area alpha[k] = coherent beta[k] = mask.sum() - coherent alpha = alpha / total_pixels beta = beta / total_pixels return np.concatenate([alpha, beta])循环里对256个桶逐个做连通域分析,性能不算最优,但逻辑清晰,适合学习和调优。如果要在生产环境跑,建议用更低的分辨率输入,比如缩到200x200以内。8连通和4连通的选择很关键,我分别试过,8连通对噪声的鲁棒性更好但会模糊细小纹理,4连通细节更丰富但对噪声敏感。图像本身降噪做得好时,我推荐8连通。
颜色相关图的实现,我直接给自相关图的简化版本。假设量化后主色数K=8,最大距离D=5:
def color_autocorrelogram(img_rgb, num_colors=8, max_distance=5): hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV) h_bin = np.minimum(hsv[..., 0] // (180 // num_colors), num_colors - 1).astype(np.int32) h, w = h_bin.shape feat = np.zeros((num_colors, max_distance + 1), dtype=np.float32) for d in range(1, max_distance + 1): # 只检查右方和下方两个方向,避免重复计数 if d < w: right = h_bin[:, d:] left = h_bin[:, :w - d] for c in range(num_colors): count = np.sum((right == c) & (left == c)) feat[c, d] += count if d < h: down = h_bin[d:, :] up = h_bin[:h - d, :] for c in range(num_colors): count = np.sum((down == c) & (up == c)) feat[c, d] += count total = max(1, h * w * 2 * max_distance) feat = feat / total return feat.ravel().astype(np.float32)这段代码的时间复杂度是O(KDH*W),实际跑下来几百像素的图完全够用。需要注意的是,我用了"右方和下方"两个方向来统计,避免同一条边被重复计算。更严谨的开放实现还会考虑距离d的对称性,但对实际匹配效果影响不大。
颜色相关图的记忆点在于它描述的是"同色像素在空间上的接近程度"。如果某种颜色大面积连片,那么近距离的相关值会比较高;如果某种颜色均匀散布,近距离和远距离的相关值差异不大。这一点在区分规则纹理和随机噪声上有奇效。
3.5 特征之间的对比与选型
先给出一张对比表,方便快速决策:
| 特征名称 | 维度示例 | 空间信息 | 计算复杂度 | 适用场景 |
|---|---|---|---|---|
| 颜色直方图 | 1024维 | 无 | 低 | 粗筛、大类区分 |
| 颜色矩 | 9~18维 | 无 | 极低 | 快速粗筛、辅助特征 |
| 颜色集 | 256位 | 无 | 低 | 索引过滤、大规模检索 |
| 颜色聚合向量 | 512维 | 弱到中 | 中 | 商品图、主体明确场景 |
| 颜色相关图 | 40~100维 | 中 | 高 | 纹理结构区分、精细检索 |
选型逻辑我给你一个简单粗暴的经验法则:
- 如果只是快速区分"蓝天、绿地、沙漠"这种宏观差异,颜色矩就够。
- 如果需要做图像去重、相似商品匹配,优先试颜色聚合向量。
- 如果候选集特别大、需要实时响应,用颜色集做倒排索引,再配合直方图精排。
- 如果图像纹理复杂且颜色关系重要,再加颜色相关图作为精排特征。
我在一个实际的服装检索项目里,最终方案是"颜色集预筛 + 颜色聚合向量初排 + 颜色相关图精排"。整套管线的特征提取时间每张图大约在几十毫秒,检索时间控制在百毫秒级,效果远超单一直方图方案。
4. 常见问题与排查技巧实录
4.1 颜色空间选择的坑
很多人一开始直接用RGB做直方图,遇到光照变化就翻车。同一个物体在室内灯光和室外阳光下,RGB值差异巨大,直方图匹配度会急剧下降。换成HSV以后,H通道对光照变化更鲁棒,但也不是完全免疫。光照过低时,H值会变得不稳定,整张图偏黑,S和V接近0,H无所谓。所以我建议在提取特征前先做一次简单的亮度校正,或者至少把亮度极低的像素统一处理,避免噪声进入特征。
另外,OpenCV里RGB和HSV的通道范围跟教科书不一致。OpenCV的H是0到179,而数学定义是0到360;S和V是0到255。我第一次做量化时没注意,直接对H除以360,导致高色相区间全部量化到最后一个桶。这个问题很隐蔽,建议量化时统一转为float,然后按实际范围归一化,不要假设OpenCV的通道跟公式完全一致。
4.2 量化参数怎么定
量化桶数太小,颜色分辨率不够,相似度区分度差;量化桶数太大,维度爆炸,且对噪声过度敏感。我总结了一个经验区间:HSV空间下,H量化8到32级,S和V各自量化2到8级,总计64到1024个桶比较常见。
如果特征用于粗分类,比如区分风景、人物、商品大类,可以稍微激进一点,桶数少一些,泛化更好。如果用户是精细检索,比如找同款衣服,桶数可以多一些,但一定要配合PCA降维。还有一个实用技巧:先统计数据集所有图像的直方图分布,找到那些几乎为0的桶,记录下来并在提取特征时直接置0,可以省存储和计算量。
4.3 相似度度量怎么配
特征提取完毕,最后的相似度计算也经常被轻视。颜色直方图比较适合用巴氏距离、卡方距离和相关系数,不适合直接用欧氏距离,因为直方图是分布数据,欧氏距离对桶与桶之间的大小差异过于敏感。我实测下来,巴氏距离在大量数据集上的排序效果最稳定。
颜色矩和颜色聚合向量用欧氏距离或者余弦距离都可以。颜色聚合向量是比例数据,用L1距离往往比L2更稳,因为L1对异常桶更不敏感。颜色相关图维度不高,也推荐用L1或卡方距离。距离度量的选择跟特征分布紧密相关,最好在数据集里抽一批正负样本,先计算特征分布范围,再确定用哪种度量和对数归一化。
4.4 工程落地时的小建议
最后分享几个工程经验。第一,特征提取之前一定要统一预处理管线:缩放尺寸、转颜色空间、亮度校正,任何一步不一致都会导致线上和线下特征分布漂移。第二,千万像素级别的大图不要直接提取特征,先缩略图,否则耗时和内存都扛不住。第三,特征入库时最好保存双精度版本和量化版本,量化版本用于快速过滤,双精度版本用于精排。第四,定期用少量人工标注数据回测特征区分度,颜色特征在数据集分布变化大的场景会慢慢失效,别指望一套特征打天下。
我在实际项目里最惨的一次教训是:线上用BGR读图并直接提取RGB直方图,线下预处理把图片转成了RGB,两者顺序错位,导致所有检索结果混乱。这种问题很难排查,但一旦发生,就让人对特征管线的"顺序一致性"产生深刻敬畏。