多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

图像颜色特征全解析:从颜色直方图到颜色相关图的算法与实践

图像颜色特征全解析:从颜色直方图到颜色相关图的算法与实践 说实话做图像检索和内容理解的从业者迟早都要回到图像特征这个基本功上。而颜色特征又是所有特征里最直观、最容易上手、也最容易踩坑的一类。这个系列我准备从颜色特征讲起覆盖颜色直方图、颜色矩、颜色集、颜色聚合向量和颜色相关图这五个经典方案。无论你是刚接触计算机视觉的初学者还是想在检索、分类、去重等场景里快速选型的工程师这篇文章都会给你一套完整、可落地的思路和代码参考。颜色特征的核心价值在于它不需要任何语义理解只用像素的数值分布就能描述一张图的内容。光照变了、分辨率变了、小幅度旋转裁剪颜色分布依然有很强的稳定性。这也是为什么几十年来从传统图像检索到现在的预处理环节颜色直方图依然没有被彻底淘汰。但它的局限也很明显——完全没有空间位置信息一只橘猫和一块橘子皮的直方图可能非常接近。于是才有了后面四种特征对它的逐步改进。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)icolor(p2)j ]实际代码中自相关图已经足够好用γ_i(d) 表示距离为d且颜色为i的像素对数量。颜色量化到8个主色距离取从1到5个级别那么自相关图维度就是8×540维。相比原始相关图K×K×d要小得多。计算相关图时最容易踩坑的是距离计算时间复杂度。幼稚实现是两两像素对遍历复杂度O(N²)一张百万像素图根本算不完。我实际用的是优化方法固定距离d后对每个像素只检查距离为d的固定邻域偏移比如d1时只看右方和下方像素这样复杂度降为O(N)性能可以接受。相关图适合什么场景呢它保留了颜色的空间相对位置关系对纹理和结构有一定刻画能力。比如红色在绿色旁边和红色离绿色很远两种图像相关图差异很明显。但它的计算开销和维度远高于前几种特征实际系统中很少全量使用。我主要把它当成精排阶段的强特征或者在数据量可控的小型检索系统中使用。3. 实操用Python实现五套颜色特征3.1 环境准备与数据简化这一节我把五套特征串在一个Python脚本里实现方便你直接照着跑。依赖只需要OpenCV和NumPyimport cv2 import numpy as np为了演示方便我会把图像统一缩放到较小的尺寸一方面提高计算速度另一方面减少高频噪声。这里选的是600x600以内的长边缩放def load_image(path, max_size600): 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)), interpolationcv2.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~179S和V在0~255需要先归一化再乘量化级别数避免直接取整时边界错乱def color_histogram_hsv(img_rgb, h_bins16, s_bins8, v_bins8): 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(), minlengthh_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:(i1)*cell_h, j*cell_w:(j1)*cell_w] feats.append(color_histogram_hsv(block)) return np.concatenate(feats)注意拼接后维度是1024×99216维这个维度对后续存储和检索来说不算小建议在特征入库前做PCA降维。我做商品检索时通常降到128~256维效果依然稳定。3.3 颜色矩与颜色集的实现颜色矩代码比较直接对每个通道计算均值、标准差和偏度。偏度通常用三次方根压缩数值尺度否则数值会特别大。数值稳定性上对std接近0的通道要加一个很小的epsilondef 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, dtypenp.float32)这样得到9维颜色矩。如果你想加入RGB通道就是18维。实测下来9维HSV颜色矩在图像主色调非常明显的场景下区分度不错比如黄衣服的人和蓝衣服的人。颜色集实现时我用HSV量化到16×4×4256个桶设定激活阈值为总像素数的0.3%。然后输出二值向量def color_set(img_rgb, threshold_ratio0.003): hist color_histogram_hsv(img_rgb, h_bins16, s_bins4, v_bins4) threshold threshold_ratio return (hist threshold).astype(np.uint8)要注意颜色集里阈值不能设得过高否则特征会非常稀疏无法区分图像。我测试时发现阈值在0.1%~0.5%之间比较合理。颜色集可以用于快速过滤两个颜色集没有交集的话图像一定不相似。这个特性在构建检索索引时特别有用。3.4 颜色聚合向量与颜色相关图的实现颜色聚合向量的实现核心是连通域分析。我先把图像量化到少量主色然后对每个颜色桶生成掩码用OpenCV的connectedComponentsWithStats计算连通域再按面积阈值划分聚合与非聚合像素def color_coherence_vector(img_rgb, h_bins16, s_bins4, v_bins4, area_ratio0.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, dtypenp.float32) beta np.zeros(h_bins * s_bins * v_bins, dtypenp.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, connectivity8) 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连通。颜色相关图的实现我直接给自相关图的简化版本。假设量化后主色数K8最大距离D5def color_autocorrelogram(img_rgb, num_colors8, max_distance5): 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), dtypenp.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接近0H无所谓。所以我建议在提取特征前先做一次简单的亮度校正或者至少把亮度极低的像素统一处理避免噪声进入特征。另外OpenCV里RGB和HSV的通道范围跟教科书不一致。OpenCV的H是0到179而数学定义是0到360S和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两者顺序错位导致所有检索结果混乱。这种问题很难排查但一旦发生就让人对特征管线的顺序一致性产生深刻敬畏。
返回列表