
那年我接到一个商品图按颜色检索的需求第一版做出来之后被业务方吐槽搜蓝色毛衣居然把蓝底白花的图也排上来了。我当时满脑子都是颜色直方图后来才发现颜色特征这个家族远比我想象的大颜色直方图、颜色矩、颜色集、颜色聚合向量、颜色相关图每一种解决的问题和适用的场景都不太一样。这篇文章是图像特征系列的第一篇我就把这些颜色特征从头到尾捋一遍讲清楚原理、实现思路、局限性和选型建议给正准备做图像检索、以图搜图、图像分类或者拿颜色信息做数据清洗的朋友做个参考。颜色特征之所以能成为图像处理里最先讲的内容是因为它最贴近人眼直觉、计算成本低、而且对平移旋转缩放这些变换相对不敏感。但相对不敏感不等于完全鲁棒不同颜色特征之间差异很大有的只统计全局颜色分布有的能兼顾颜色的空间关系。这篇文章我会用比较工程化的语言来讲尽量少堆公式把每一步为什么这么设计讲明白方便你直接照着去实现。1. 从按颜色找图说起颜色特征为什么是图像检索的第一块基石我刚开始做图像检索那会儿总觉得深度学习一上来就是端到端直接用CNN提特征就行根本没必要去研究传统的颜色特征。后来遇到一个实际问题库里有一批老商品图没有标签、没有文本描述想要做一个找同色系商品的功能。这时候我发现如果直接上深度特征第一是提取成本高第二是检索结果往往因为语义差异把颜色相近的图排得很靠后。反而是纯颜色特征最直接、最稳定。颜色特征的本质是把一张图映射成一个数字向量这个向量能够描述图像中颜色的大致构成或分布规律。为什么它能成为图像检索的第一块基石因为颜色在绝大多数情况下是全局共享的——不管物体是正着放、斜着放、大一点还是小一点颜色分布基本不变。这意味着我可以用非常低的计算量先在全库中过滤掉一批完全不相关的图再由后续更精细的特征去排序。当然颜色特征也不是万能的。它怕光照变化怕同色背景干扰更怕颜色完全相同但内容完全不同这种情况。所以从做工程的角度来讲颜色特征更适合做粗召回和初筛而不是唯一排序依据。理解这一点后面学任何颜色特征都会比较顺。在这篇文章里我按这个逻辑来组织从最简单的全局统计方法开始逐步往带空间关系的方向走每一步都是为了弥补上一步丢失的信息。1.1 两个绕不开的问题颜色空间与量化讲具体特征之前必须先讲两个决策点因为后面所有颜色特征都会受到这两个选择的影响。第一个是颜色空间。RGB是我们在程序里最常碰到的格式但它并不适合直接拿来做相似度比较。原因很简单RGB三个通道的相关性很强而且欧氏距离在RGB里并不能很好对应人类的颜色感知差异。实际做颜色特征时更常用的是HSV和Lab。HSV把色调(H)、饱和度(S)、明度(V)分开符合人对颜色的认知方式尤其在光照变化比较大的场景下可以直接丢弃V通道只保留H和S来减少亮度干扰。Lab用L表示亮度、a表示红绿轴、b表示黄蓝轴它的设计目标就是让颜色之间的欧氏距离尽可能接近感知距离所以在做颜色相似度排序时非常稳。第二个是量化。原始图像的颜色数量可以达到1600万级别256×256×256如果不做量化直方图维度会高到没法用计算量也完全不可接受。量化的方式一般有两种一是均匀量化比如RGB三通道每通道只保留8个区间得到512维向量二是非均匀量化比如在HSV空间里把色相分成8份、饱和度分3份、明度分3份组合成8×3×372个区间。量化太粗会丢失区分度量化太细又会带来维度灾难这个平衡后面每一种特征里都会遇到。1.2 五种颜色特征的关系从直方图到相关图如果把这五种颜色特征放在一条线上看其实有一条很清晰的递进逻辑。颜色直方图解决的是图里有哪些颜色、各占多少比例但它完全不关心颜色出现在哪里颜色矩就是想用很少的几个统计量比如均值、方差来近似描述整个颜色分布颜色集进一步把颜色分布压缩成一个有没有的位图表示适合做大规模快速查询颜色聚合向量是在直方图基础上把像素按空间上是否连成片分成聚合和非聚合两类这样就带上了局部空间信息颜色相关图则走了更远一步它统计的是距离为d的两个像素颜色分别是ci和cj的概率直接把颜色和空间位置揉在一起。所以你看这五种方法并不是并列的五个独立算法更像是一层一层往上叠加信息的过程。理解了这条主线遇到具体项目时你就知道该在哪个环节选哪种特征。2. 颜色直方图统计全局颜色分布但代价是丢掉位置信息2.1 原理把图像扔进颜色空间里数像素颜色直方图是所有颜色特征里最朴素也最常用的一种。思路简单得不能再简单把颜色空间划分成若干个区间然后统计每个区间里有多少像素。归一化之后每个区间的值表示该类颜色在整张图中的占比。这里的区间就是量化bin。比如用HSV空间H通道设8个binS通道设8个binV通道设8个bin整个三维颜色空间就是8×8×8512个格子。每个格子对应一个维度最终得出来的是一个512维的向量。这个向量天然具有平移、旋转、缩放不变性因为不管物体挪到哪、转成什么角度图像里这些颜色的总占比并不会改变。但它也有个特别明显的盲区丢掉空间位置。一张左边半红右边半白的图和一张上半红下半白的图在直方图看来可能完全一样。我在实际项目中就踩过这个坑用直方图做以图搜图结果搜索红色汽车的时候把一张红底白字海报也排到了前面因为两者的大色块占比接近但构图完全不同。2.2 手写实现与OpenCV的calcHist了解原理之后自己动手实现一遍直方图并不是什么难事大致流程是读图、转色彩空间、量化、统计、归一化。用Python写得话大概是这个样子import cv2 import numpy as np img cv2.imread(demo.jpg) # OpenCV读入的是BGR这里转成HSV hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 定义bin数量H分16份S分8份V分8份 hist cv2.calcHist( [hsv], channels[0, 1, 2], maskNone, histSize[16, 8, 8], ranges[0, 180, 0, 256, 0, 256] ) # 归一化到[0, 1] hist cv2.normalize(hist, hist).flatten()这里有几个容易出错的细节。第一H通道在OpenCV里的取值范围是0到180而不是0到360所以计算区间的时候容易对不上第二calcHist返回的是一个维度数组如果你不flatten后面跟特征向量做点积、求距离会很别扭第三归一化这一步不要省尤其当你要比较的两张图尺寸不一致时没归一化的直方图对比结果会被图像大小严重干扰。我自己在工程里更习惯手写一个简单版来保证逻辑可控因为calcHist在图像非常大、又需要批量处理的时候内存占用会有点吓人。一个替代方案是先用np.bincount结合量化后的像素值来做统计代码更灵活速度更快。2.3 特征对比时的距离度量与常见坑直方图算出来了接下来就是怎么比较两个直方图的相似度。常用的方法有这么几类欧氏距离、余弦相似度、卡方距离、直方图相交距离和巴氏距离。这里我建议优先考虑直方图相交和余弦相似度两者计算简单而且在颜色特征这种稀疏度中等、向量非负的场景下表现比较稳。卡方距离在直方图匹配里也挺好用但要注意它要求直方图严格归一化否则结果会失真。再讲两个常见的坑。第一个是bin数选择。bin太少颜色区分度太差不同颜色的图会被混在一起bin太多维度高、计算慢而且对噪声更敏感。我个人的经验是在做全局颜色检索时HSV空间下H16、S8、V8通常是个不错的起点总共1024维。如果图像数量很大做个PCA降到128维也不亏。第二个坑是光照敏感。任何直方图都直接受光照影响同一件衣服在暖光灯和冷光灯下拍出来的直方图差异很大。解决思路有三个一是换到HSV空间后直接把V通道扔掉只用H和S做统计二是做光照归一化比如对图像做灰度世界假设调整白平衡三是用Lab空间并考虑只取ab通道。具体用哪种得看你的图像来源是否可控。3. 颜色矩用9个数字给一张图的颜色画像3.1 为什么需要低维度特征直方图动辄几百上千维但它真的包含了这么多有效信息吗很多时候不是。大量bin里的值是很接近的真正把不同图像区分开来的往往只是整体分布的形态差异。颜色矩的核心思路是不要逐个bin去统计而是直接计算颜色分布的几个统计特征量。这个想法最早来自Stricker和Orengo在1995年发表的论文。他们用一阶矩、二阶矩和三阶矩来描述图像的颜色分布每个颜色通道算3个值。如果使用RGB三个通道总共就是9个数字使用HSV同样也是9个数字。9维的向量做检索计算量几乎可以忽略不计非常适合拿来给海量图像做第一轮粗筛。当然低维度也意味着信息量有限。颜色矩的区分度通常要比直方图弱一些对复杂纹理图像基本无能为力。它真正的价值在于快和稳尤其是在视频关键帧提取、大规模图像去重这些对实时性要求很高的场景里。3.2 一阶矩、二阶矩、三阶矩怎么算一阶矩就是均值反映图像某个通道的整体明亮程度或色调倾向二阶矩是标准差反映颜色分布的对比度标准差越大说明颜色层次越丰富三阶矩是偏度反映颜色分布是否对称偏度为正说明图像中有少量像素颜色特别突出。我一般这样实现import numpy as np import cv2 def color_moments(img): # 转成Lab空间或者HSV都可以项目里我用Lab更多 lab cv2.cvtColor(img, cv2.COLOR_BGR2LAB) moments [] for i in range(3): channel lab[:, :, i] mean np.mean(channel) std np.std(channel) # 三阶矩偏度开了三次根方便数值比较 skewness np.mean((channel - mean) ** 3) ** (1/3) moments.extend([mean, std, skewness]) return np.array(moments)这里有几个细节值得说一下。第一三阶矩开三次根是因为原始偏度值的量纲是像素值的三次方数值会变得很大开根之后便于和其他特征放一起做距离计算第二每个通道的数值范围不一样如果后续要跟其他特征拼接最好先对9维向量做一次标准化第三Lab空间里L通道对光照非常敏感如果你发现检索结果总是受亮度干扰一个简单办法是去掉L通道只用a和b通道这样特征就变成6维了。3.3 颜色矩的工程经验和局限我在大数据量场景下有个习惯把颜色矩当作过滤器来用而不是当作排序主特征。举个例子一个图库有几百万张图我不可能对每张图做深度特征检索但我可以用9维颜色矩先粗排取前1000张再对这1000张用直方图或者深度特征精排。这样做最大的好处是粗排阶段的内存开销几乎可以忽略4字节一个float几百万张图也只需要几十MB。颜色矩的局限也很明显。它只描述颜色分布的统计形态对于空间布局毫无感知同时它对颜色分布相似但内容完全不同的图没有区分能力。由于统计特征天然对局部小区域的颜色变化不敏感当你需要检索图中某个局部区域的颜色时颜色矩基本帮不上忙。4. 颜色集把颜色分布压缩成比特位走查重和粗筛路线4.1 颜色集到底在做什么颜色集这个概念最早是Smith和Chang在做大规模图像数据库检索时提出来的。它的核心想法很直接既然一张图可能出现的颜色种类很多那我干脆不记录每种颜色占多少比例只记录这张图里有哪些颜色用一组布尔值来表示。说人话就是对图像做颜色量化之后颜色直方图记录的是每个颜色bin的像素数量颜色集则把这些数量全部丢掉只保留每个颜色bin是不是非空。这样一来一张图像的颜色特征就变成了一个由0和1组成的位向量。假设量化成48个bin那这个向量只有48位也就是6个字节比动辄几千字节的直方图特征小了好几个数量级。有人可能会问信息这么少它能干什么答案是它能做大规模的快速否定。在图像检索系统里第一步往往不是找出最相似的图而是快速排除掉绝大多数明显不相关的图。颜色集就是干这个事的它可以快速判断一张图里有没有红色有没有蓝色并且用位运算来做多条件的AND、OR查询。4.2 二值图和颜色索引表实现颜色集的一般流程分三步。第一步把图像变换到合适的颜色空间并量化比如HSV空间分成若干个区间第二步对每个像素判断它落在哪个颜色区间然后把该区间标记为1第三步把所有区间的0/1值拼成一个位向量。更进一步颜色集还可以把整个图像按空间划分成若干个区域分别统计每个区域的位向量。这样当用户想要查询某个区域是否包含某色的时候系统也能快速定位。这个设计实际上为后来很多基于区域的图像检索方法打了底子。在实际代码里我一般直接用numpy处理import numpy as np import cv2 def color_set(img, hist_size[8, 4, 4]): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 量化直接用整除映射到bin q_h hsv[:, :, 0] // (180 / hist_size[0]) q_s hsv[:, :, 1] // (256 / hist_size[1]) q_v hsv[:, :, 2] // (256 / hist_size[2]) flat (q_h.astype(int) * hist_size[1] q_s.astype(int)) * hist_size[2] q_v.astype(int) # bit位向量 bits np.zeros(hist_size[0] * hist_size[1] * hist_size[2], dtypebool) bits[np.unique(flat)] True return bits4.3 适用场景大规模的快速否定颜色集真正发挥价值的地方是那些宁可放过少数也不愿错杀大量的前置场景。比如我在做商品图库去重时会先用颜色集排除掉完全没有重叠颜色的图对再对剩下的图对做更精细的感知哈希或直方图比较。这一步过滤一般能砍掉80%以上的无关计算量。不过要注意颜色集本身并不适合直接做相似度排序原因很简单它只比较有哪些颜色完全不区分颜色比例。两张图都有红色、蓝色、绿色但颜色分布比例完全不同颜色集给出的结果可能一模一样。所以在落地时我建议把它放在检索管道的最前面配合后面的精排特征一起使用。5. 颜色聚合向量把空间上连不连补回到直方图里5.1 聚合像素和非聚合像素一个连通域的简单故事颜色直方图最大的问题是没有空间信息。针对这个问题Pass等人提出了颜色聚合向量。它的思想很有画面感同一张图里相同颜色的像素有的成片出现有的则零零散散地分布在不同位置。成片出现的像素称为聚合像素零散的称为非聚合像素。如果把颜色直方图的每个bin进一步拆成聚合像素占比和非聚合像素占比那么最终特征向量就比普通直方图多了一倍信息而这些额外信息恰恰能反映颜色的空间分布模式。举个例子一张图的左侧是纯蓝色背景右侧有无数细小的蓝色斑点。从颜色直方图看两张图蓝色像素占比相同完全区分不开但颜色聚合向量里纯蓝背景那幅图的蓝色像素基本都是聚合的而细密斑点那幅图蓝色像素大多是离散的、非聚合的两者特征差异一下就出来了。那种大色块更多的图聚合比例会更高那种细碎纹理更多的图非聚合比例会更高。这个特征对背景简单、主体大块的商品图、海报图特别有效。5.2 计算流程与阈值选择颜色聚合向量的计算流程大致有四步对图像做颜色量化得到离散颜色索引。对每个颜色通道的0/1掩膜做连通域分析找到所有连通分量。设定面积阈值τ连通分量像素数大于等于τ的标记为聚合类小于τ的标记为非聚合类。对每个颜色bin分别统计聚合像素数和非聚合像素数。连通域分析在OpenCV里直接用connectedComponentsWithStats就能做代码并不复杂def color_coherence_vector(img, tau50, hist_size[8, 4, 4]): hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) H, W hsv.shape[:2] q_h (hsv[:, :, 0] / (180 / hist_size[0])).astype(np.uint8) q_s (hsv[:, :, 1] / (256 / hist_size[1])).astype(np.uint8) q_v (hsv[:, :, 2] / (256 / hist_size[2])).astype(np.uint8) flat (q_h.astype(int) * hist_size[1] q_s.astype(int)) * hist_size[2] q_v.astype(int) num_bins hist_size[0] * hist_size[1] * hist_size[2] ccv np.zeros(num_bins * 2, dtypenp.float32) for c in range(num_bins): mask (flat c).astype(np.uint8) # 连通域分析 n, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) # 统计聚合与非聚合像素数 if n 1: continue for i in range(1, n): # 0是背景 if stats[i, cv2.CC_STAT_AREA] tau: ccv[2*c] stats[i, cv2.CC_STAT_AREA] else: ccv[2*c1] stats[i, cv2.CC_STAT_AREA] ccv / (H * W) return ccv阈值τ的选择会直接影响特征表现。τ选得太小几乎所有像素都被判定为聚合非聚合信息形同虚设τ选得太大聚合信息也会失真。按照我的经验τ取图像面积的0.5%到1%比较合适比如一张1000×1000的图像τ取5000到10000。当然如果所有图像尺寸统一固定一个像素阈值也行。5.3 为什么它比纯直方图更能区分画面对比不同的图颜色聚合向量能区分直方图无法区分的图像这个特性在实际业务里很实用。比如搜索蓝色背景的产品图时普通直方图会把蓝色天空的风景图也排进来因为蓝色像素占比高但颜色聚合向量会计算蓝色像素的聚集程度产品图的蓝色背景通常非常规整、连片而风景图的蓝色分布则更分散两者特征容易拉开距离。不过颜色聚合向量也不是万能的。它对连通域分析的计算量比直方图大不少图像分辨率较高时connectedComponentsWithStats的耗时会上涨。我一般会先把图缩放到统一较短边比如256像素再做这个特征既能控制计算量也不会丢失太多连通结构信息。另外它对噪声点是敏感的图像里如果有很多彩色噪点这些噪点会被划分到非聚合类里虽然不会完全破坏特征但会引入干扰。6. 颜色相关图不只是有什么颜色还要看颜色之间的空间搭配6.1 核心思想一个距离条件下的颜色共现概率颜色相关图是五种特征里空间感最强的一种。它的核心思想不是单看某个颜色占比多少而是统计在相隔一定距离的两个像素点中颜色对(ci, cj)同时出现的概率。换句话说它描述的是图像中颜色之间的空间搭配关系。举个例子如果一张图里红色和黄色经常间隔出现颜色相关图就会在这个颜色对上给出较高的概率值而如果红色和黄色分别出现在图像的两个不同区域相关图里的对应值就会很低。这种信息在检索有纹理、有规律图案的图像时特别有用。6.2 公式理解与自相关图/带限相关图严格来说颜色相关图的定义是给定距离d统计图中一个像素颜色为ci、另一个像素颜色为cj并且两者欧氏距离为d的联合概率。当ci等于cj时它刻画的是同色像素在某个距离上的共现情况这个特例单独提出来叫颜色自相关图。这里要注意完整相关图的计算量非常大。如果量化后颜色种类是m考虑的距离等级是d个那特征维度就是m×m×d。m取32、d取4已经是4096维而且每一维都要对全图像素对做统计代价非常高。所以在工程落地时通常会用两个变体带限相关图只考虑少数几个距离等级。作者原论文里取的是距离1、3、5、7这基本覆盖了短距离范围内的纹理信息颜色自相关图把颜色对限制在同类颜色上也就是只统计同色像素在距离d上的共现维度降为m×d计算量大幅下降。6.3 复杂度和工程落地技巧直接计算完整相关图的朴素实现方式复杂度接近O(N²)对于一张百万像素图像来说完全不可行。常见的优化方式是先对图像做网格化只统计邻域范围内的像素对而不是全图所有距离的像素对。具体来说设定最大距离d_max后对每个像素只计算它周围2×d_max范围内像素的贡献这样复杂度就从O(N²)降到了O(N×d_max²)。我在做颜色相关图的时候通常会先把图像缩放到64×64这种极小尺寸再做一次量化颜色种类控制在16以内。这时候即使使用朴素实现速度也还在可以接受的范围。同时我会在检索时把它和颜色直方图联合使用直方图负责全局相似度相关图负责补充纹理和空间布局信息效果比单独用任何一种都稳定。颜色相关图最大的优势是对空间布局敏感但它也因此失去了平移旋转不变性。图像发生了旋转颜色对之间的距离关系会变化相关图特征也会偏离。如果业务场景里有大量旋转或镜像图片这个特征的表现就会打折。7. 五个特征放一起比维度、开销、适用场景各不同先整体看一张对比表把五种特征的差异拉齐特征维度量级是否含空间信息计算开销主要适用场景颜色直方图中高几十到上千无低全局颜色相似度、场景分类、粗排颜色矩极低9维左右无最低视频关键帧、大规模粗筛颜色集极低位向量弱可分区近似最低大规模数据库快速否定、去重颜色聚合向量中2×量化级别有聚合/非聚合中大色块图像、商品图、海报图颜色相关图高m²×d带限后可降强高纹理图案、空间布局敏感的图像从这个表可以很直观地看出从直方图到相关图特征包含的信息越来越丰富但计算复杂度也越来越高、鲁棒性越来越难控制。没有一种特征是绝对最优的关键是看你的任务处在检索管道的哪个位置。我自己的选型经验通常是这么几种组合如果只是给图库做按颜色找相似款优先用HSV颜色直方图配合余弦相似度速度快且效果直观如果图像数量几百万甚至上千万需要在召回之前先做一次粗筛优先用颜色矩做粗排再用颜色集做快速否定两个特征一起能把计算量砍掉九成如果图片是商品图、海报这种大色块占比高、背景干净的类型颜色聚合向量往往比直方图效果更好如果检索目标有明显纹理或重复图案比如花布、瓷砖、地图那颜色相关图或颜色自相关图值得一试但要接受它的计算成本和旋转敏感。另外还有一个非常实用的思路把特征拼接起来用。常见做法是把9维颜色矩拼上256维直方图得到265维的联合特征因为向量内部不同部分的数值量级差异可能很大拼接后要做一次维度归一化才好用。我在实践里用这种联合特征做商品图检索准确率比单独用任意一种都有明显提升。8. 实际项目里我积累的一些经验这些经验不算论文里的标准结论但都是我在生产环境里踩过坑之后留下的印象分享出来供你参考。颜色空间的选择比特征方法更影响效果。同样一个直方图在RGB空间和HSV空间里的表现差了非常多。我只认一个原则如果项目里有明显的光照变化就别用RGB优先去HSV的V通道或者Lab的L通道再去统计颜色。曾经有个数据集是手机在室内不同灯光下拍的商品图直接RGB直方图检索效果一塌糊涂换成HSV去V之后检索准确率翻了一倍都不止。量化bin不要贪多。我看到很多人一上来就把直方图的bin调到32×32×32结果维度两万多检索又慢效果也没好到哪去。颜色信息本身是低秩的几百维足够表达绝大多数情况。bin数量超过1000以后带来的更多是过拟合噪声而不是有效区分度。连通域类特征对图像分辨率很敏感。颜色聚合向量和颜色相关图都不是标准的尺度和旋转不变特征。当图像尺寸差异很大时差不多内容的图在不同分辨率下算出来的特征差异也很大所以提取这类特征前一定要先统一图像的短边长度并且尽量固定下来不要今天用256、明天用512。做好特征的后续处理也很关键。颜色特征提取出来只是第一步如果直接拿原始特征去建索引往往会被某些取值特别大的维度支配距离计算。我常用的做法是先做一次标准化把每个维度缩放到0到1之间条件允许的情况下再做一次PCA降到合适的维度再建立索引这样无论是用向量检索库还是直接暴力检索速度和稳定性都会有改善。最后说一句关于测试的体会判断颜色特征好坏不要只看一两张图的检索结果最好准备一个带相似度人工标注的小测试集每次调整完特征之后跑一遍量化出Top K准确率再决定是否替换方案。颜色特征看似简单实际上每一个细节——颜色空间、量化方法、距离度量、归一化方式——都会显著影响最终效果凭感觉挑参数很容易踩坑。这五个方法理解透了接下来的纹理特征、形状特征学起来就会顺手很多。