1. 检索方案整体设计:为什么是颜色+纹理双剑合璧
1.1 单一特征的局限
做图像检索,最直接的思路是比颜色。早年我也图省事,直接把一张图缩小到 8×8,取 RGB 均值,拿一个低维向量去做匹配。实验做下来,遇到白底商品图和自然风景图时,效果还能看;一旦碰上光照变化、物体旋转、背景杂乱,检索结果就乱套。原因不复杂:纯颜色特征丢弃了空间结构,红色毛衣和红色汽车的颜色直方图可能高度相似,而纹理特征完全没用到;反过来,纯纹理特征只关心局部灰度变化,对颜色信息无感,蓝色天空和灰色天空在纹理上几乎没有差别,但语义上可能不是一类东西。
所以就想到把两类特征叠在一起。颜色负责描述“画面里有什么颜色、大概占多少”,纹理负责描述“局部灰度模式怎么重复”,两者在信息层面互补。实际检索任务里,这种互补带来的收益非常直接:查一张森林图片时,颜色特征会拉回很多绿油油的图,纹理特征会进一步把“叶片纹理密集”的图片排在“大片平滑草地”前面。
1.2 MATLAB作为实现平台的优势
Matlab做这类图像检索实验,最大的优势是“从算法到结果的距离短”。图像直方图计算、矩阵归一化、距离计算、结果可视化,都是几个函数调完的事;不像 OpenCV 那种还需要考虑 Mat 类型转换、内存管理,也不像 Python 需要自己配环境。
我用的版本是 R2023b,需要装两个工具箱:Image Processing Toolbox 和 Computer Vision Toolbox。前者提供rgb2hsv、histcounts等基础函数,后者直接提供extractLBPFeatures,省去手写 LBP 的很多坑。如果你的 MATLAB 版本比较老,没有extractLBPFeatures也不用慌,我在第 2 节会给一套纯基础函数实现的 LBP 代码,R2016b 以上都能跑。
1.3 融合策略的选择
特征融合不是简单拼起来就完事。我在项目里优先采用“串联融合 + 归一化”的方案:把颜色直方图和纹理直方图首尾相接成为一个长向量,然后整体归一化。这样做的好处是思路透明、调试方便,颜色特征维度也不容易被纹理特征“淹没”。
更进阶的融合方式有加权串联、打分融合、距离度量学习。加权串联需要人为调权重,我做过一组对照:颜色权重 0.4、纹理权重 0.6 时,对 Corel 数据集某些类别效果好,换到另一类又变差。所以入门阶段建议先做等权重串联,跑通检索闭环后再去调权重。打分融合则是分别计算颜色距离和纹理距离,再用一个公式合成最终分数,适合两类特征的量纲差异比较大的场景。
2. 环境准备与数据集预处理
2.1 MATLAB环境与工具箱配置
打开 MATLAB 后,先检查工具箱是否齐全。在命令行窗口输入:
ver看列表里有没有Image Processing Toolbox和Computer Vision Toolbox。缺了的话,在 APPS 菜单里能找到 Add-On Explorer,搜名字安装。推荐 R2023b 是因为extractLBPFeatures的旋转不变性选项在后续版本里更稳定,但老版本也能用。
数据集我选了 Corel 10 类图像库,每类 100 张,涵盖花、马、恐龙、大象、汽车等,总共 1000 张。如果你手头没有标准库,自己建库也行:找几个有明显视觉差别的类别,每类至少 50 张,不然检索结果没有统计意义。
2.2 数据集预处理
预处理这一步很多人容易跳过,但这是最影响检索效果的一环。我用imageDatastore统一管理所有图片,好处是不用把所有图一次性读入内存,按需迭代读取:
imds = imageDatastore('D:/imageDB', 'FileExtensions', {'.jpg','.png'});然后做三个标准化操作:
- 尺寸统一:所有图片缩放到 256×256。不需要太精细,因为颜色直方图和 LBP 特征对绝对尺寸不敏感,但图片尺寸差距过大会导致纹理尺度漂移。缩放方式建议用双三次插值
imresize(I, [256, 256], 'bicubic')。 - 颜色空间转换:RGB 直方图在光照变化时抖动很厉害,我统一转到 HSV 空间,只取 H 和 S 分量参与颜色特征。HSV 的 H 分量本身就是色调角度,S 是饱和度,两者对线性光照变化比 RGB 健壮得多。理解这一点,可以想象一张暗红色的桌子在不同台灯亮度下,RGB 数值可能差 50 以上,但 H 分量基本都是 0 附近。
- 纹理图生成:纹理特征在灰度图上提取,所以把原图转灰度
rgb2gray。这里注意,做完尺寸统一再转灰度,顺序不能反,否则imresize在灰度图上的插值效果和彩色图不一致。
预处理完成后,建议把所有图片存成.mat文件或者直接生成特征矩阵。我在项目里把 1000 张图的特征一次性计算并保存为featureDB.mat,后续检索直接load,不用每次重算。
3. 特征提取核心实现
3.1 颜色特征:HSV空间颜色直方图
颜色直方图的关键是“量化”。如果不量化,直接对每个像素的 HSV 值做histcounts,维度会爆炸,而且对微小扰动特别敏感。我采用的量化方案:
- H 通道量化成 8 bin
- S 通道量化成 3 bin
- V 通道量化成 3 bin
- 特征总维度 = 8×3×3 = 72
为什么不是 16×4×4?因为检索任务关注的是“大致颜色分布”,过度细化会让两张视觉很接近的图片因为亮度差一点而直方图分开。我用 8×3×3 做出来的结果,在 Corel 数据集上已经能明显区分花和汽车。72 维特征向量也很适合后续距离计算。
实现代码:
function featColor = extractColorHist(img) img = imresize(img, [256, 256], 'bicubic'); hsvImg = rgb2hsv(img); h = hsvImg(:,:,1); s = hsvImg(:,:,2); v = hsvImg(:,:,3); % 量化:h映射到0~7,s映射到0~2,v映射到0~2 hQ = min(floor(h * 8), 7); sQ = min(floor(s * 3), 2); vQ = min(floor(v * 3), 2); idx = hQ * 9 + sQ * 3 + vQ + 1; % 等价于三维直方图拉平 featColor = histcounts(idx, 1, 1:72) / numel(h); end注意最后一行归一化:用像素总数除以直方图,让特征变成“颜色比例”,这样不同尺寸的图片才能公平比较。这一步最简单,但也是我最容易踩坑的点——一开始直接histcounts不归一化,结果一张 1024×768 的图永远比 256×256 的图“颜色更多”,检索前排名全是高分辨率图。
3.2 纹理特征:均匀LBP直方图
纹理特征我用的是 LBP(局部二值模式)。LBP 的原理是:对每个像素,取它周围半径 R 内的 P 个邻居,比较邻居灰度与中心灰度的大小,大于中心的记为 1,否则记为 0,得到一个 P 位二进制数,也就是该像素的 LBP 码。整张图所有像素的 LBP 码构成直方图,就是纹理特征。
为什么选 LBP 而不选灰度共生矩阵(GLCM)?GLCM 需要设定方向和距离参数,一张图往往要算 4 个方向的矩阵再取均值,计算量更大;LBP 天然带灰度不变性,对光照变化不敏感,而且提取特征速度很快。对于 256×256 的图,纯 MATLAB 实现 LBP 大约 0.3 秒,GLCM 一般要 1 秒以上。
我采用半径为 1、邻域点数 8 的经典设置。为了让特征对旋转更鲁棒,使用“均匀模式”:把 8 位二进制的循环跳变次数超过 2 的码归为一类,这样维度从 256 降到 59。旋转不变均匀 LBP 更极端,会降到 10 维,但实验下来检索效果不如 59 维的均匀模式,因为后者保留了更多结构信息。
手写代码最稳定的版本:
function featLBP = extractLBP(img) img = imresize(img, [256, 256], 'bicubic'); gray = double(rgb2gray(img)); [rows, cols] = size(gray); lbpImage = zeros(rows-2, cols-2); % 半径为1,8邻域 offsets = [-1 -1; -1 0; -1 1; 0 -1; 0 1; 1 -1; 1 0; 1 1]; for i = 1:rows-2 for j = 1:cols-2 center = gray(i+1, j+1); binaryVal = false(1,8); for k = 1:8 neighbor = gray(i+1+offsets(k,1), j+1+offsets(k,2)); binaryVal(k) = neighbor > center; end code = binaryVal * [128 64 32 8 4 2 1]'; % 逆时针顺序,只是示意 % 这里省略均匀模式映射,实际用自带函数更快 lbpImage(i,j) = code; end end featLBP = histcounts(lbpImage(:), 0:256) / numel(lbpImage); end这个双重循环跑起来偏慢,所以我实际开发中用extractLBPFeatures:
featLBP = extractLBPFeatures(rgb2gray(imresize(img, [256,256], 'bicubic')), ... 'Radius', 1, 'NumNeighbors', 8, 'Upright', false);Upright设为 false 意味着使用旋转不变均匀模式;设为 true 则是非旋转不变的均匀模式。我建议先用Upright=false做第一版,因为检索库里同一物体可能横着、竖着、斜着拍,旋转不变性能显著提升召回率。
3.3 特征融合与归一化
颜色特征 72 维,LBP 旋转不变均匀模式 10 维,加起来 82 维。直接串联会有一个问题:颜色特征维度是纹理的 7 倍,距离计算时颜色分量天然占据主导地位。所以串联后必须整体归一化。
归一化方法我对比过两种:
- min-max 归一化到一个区间:对所有特征向量按每个维度做 min-max,让每个维度都落在 [0,1]。但这个方法受离群值影响大,有一张极端图片会把整个维度的尺度带偏。
- 向量模长归一化:把每个特征向量除以它的 L2 范数,也就是让每张图的特征向量模长变为 1。这样做后,欧氏距离和余弦相似度在排序上等价,而且光线变化导致的整体亮度差异会被压缩。
我最终选择“L2 归一化后再串联”,因为实现简单、稳定:
feat = [featColor, featLBP]; feat = feat / (norm(feat) + eps);注意eps防除零,虽然是小事,但数据量一大总会碰上纯黑图。把所有图片的特征存成矩阵后,我是这样组织数据的:
features = zeros(numImages, 82); for i = 1:numImages img = readimage(imds, i); featColor = extractColorHist(img); featLBP = extractLBPFeatures(rgb2gray(imresize(img, [256,256], 'bicubic')), ... 'Radius', 1, 'NumNeighbors', 8, 'Upright', false); features(i, :) = [featColor, featLBP]; features(i, :) = features(i, :) / (norm(features(i, :)) + eps); end4. 相似度度量与检索测试
4.1 距离度量选择
特征提取完,检索的核心就是把待查图片和数据库图片之间的距离排序。我试过三种度量:
| 度量方式 | 公式示意 | 适合场景 | 我实验中的表现 |
|---|---|---|---|
| 欧氏距离 | (d = \sqrt{\sum (x_i - y_i)^2}) | 特征向量各维度独立同权重 | 融合特征上中规中矩 |
| 余弦相似度 | (sim = \frac{x \cdot y}{|x||y|}) | 高维稀疏向量、光照变化 | 融合特征上最好 |
| 直方图相交 | (sim = \sum \min(x_i, y_i)) | 颜色直方图 | 单独颜色特征时最好 |
实验结论是:在串联融合特征上,余弦相似度的平均检索准确率比欧氏距离高约 5%。原因在于 L2 归一化后的特征向量,余弦相似度等价于内积,它更关注方向差异而不是绝对数值差异。比如两张图内容完全相同但一张亮度高、一张亮度低,归一化后向量方向接近,余弦相似度会给高分,欧氏距离则会因为数值差异拉开距离。
4.2 检索流程与结果分析
我的检索流程分三步:
- 读入查询图片,预处理并提取特征。
- 计算查询特征与数据库中所有特征的距离。
- 按距离升序排,返回前 K 张图。
代码实现:
function resultIDs = searchImage(queryImg, features, imds, K) qFeat = extractColorHist(queryImg); qLBP = extractLBPFeatures(rgb2gray(imresize(queryImg, [256,256], 'bicubic')), ... 'Radius', 1, 'NumNeighbors', 8, 'Upright', false); qFeat = [qFeat, qLBP]; qFeat = qFeat / (norm(qFeat) + eps); % 余弦距离 = 1 - 余弦相似度 dist = 1 - features * qFeat'; [~, idx] = sort(dist, 'ascend'); resultIDs = idx(1:K); end这里我直接用了矩阵乘法features * qFeat',因为特征已经做过 L2 归一化,向量内积就是余弦相似度。1000 张图的库,一次排序耗时不到 1 毫秒,比循环逐张算距离快得多。
我在 Corel 数据集上做了一个直观实验:拿一张“花”类图片做查询,颜色单特征返回的前 10 张里只有 6 张是同类,融合特征后前 10 张里有 8 张是同类。更系统一点的评估是用 PR 曲线:对每类图片轮流作为查询,统计前 20 个返回结果中同类的比例。融合特征的均值准确率在 3 类测试集上分别比颜色单特征高 8% 到 15%,比纹理单特征高 12% 到 20%。这个差距在背景杂乱的图片上尤其明显,因为颜色特征会被大面积背景干扰,纹理特征则能把前景物体的结构“捞”出来。
5. 踩坑记录与参数调优
5.1 常见问题与排查表
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 检索结果全是同分辨率的大图 | 颜色直方图没有归一化 | 除以像素总数 |
| 加了纹理特征后效果反而变差 | 颜色和 LBP 特征尺度差太大 | 先各自归一化再串联,或改用余弦距离 |
| LBP 特征提取非常慢 | 用了双重循环实现 | 换用extractLBPFeatures |
| 同一张图不同旋转角度检索不到彼此 | LBP 未开启旋转不变 | Upright(false),或人为旋转增强训练集 |
| 检索结果对光照敏感 | 用了 RGB 直方图 | 改成 HSV,且只取 H 和 S 分量 |
| 特征矩阵太大,内存溢出 | 一次性把所有图读到内存 | 用imageDatastore边读边算,特征存single类型 |
5.2 经验技巧
第一,不要把颜色直方图做得太细。我做过 16×4×4 的量化,准确率比 8×3×3 反而低 2% 左右。原因不难理解:量化越细,对拍摄角度、光照的细微变化越敏感,泛化能力反而下降。检索任务不是图像分类,不需要像素级精确,够用就好。
第二,先粗筛再精排。当图片库超过 1 万张时,逐张计算 82 维特征的距离也能接受,但如果你想把检索延迟控制在 0.5 秒内,可以先算一个非常简单的 9 维“平均颜色分块特征”,粗筛出最像的 500 张,再在候选集里用完整特征精排。我在 5000 张图上试过,粗筛阶段能保持 95% 的同类图片进入候选集,速度提高约 8 倍。
第三,归一化的顺序是容易被忽视的坑。如果先拼接颜色特征和 LBP 特征,再统一做 min-max 归一化,遇到一个维度上的极端值,会压缩其他维度的有效范围。我踩过一次:有一张几乎全黑的图,LBP 特征大部分维度是 0,但其中一个纹理维度特别大,min-max 直接把这一维度压到接近 0,导致所有黑色图都聚在一起。后来改用 L2 归一化,问题消失。
第四,验证一个特征是否有效,别只看总准确率。要分错误案例看。我在实验中发现,融合特征对“花”类和“汽车”类提升明显,但对“恐龙”类几乎没提升,因为恐龙图片的背景大多是纯灰绿色,纹理信息本来就弱。这种分析能帮你理解特征是哪里起作用,也方便后续针对性地调参。
6. 后续扩展思路
颜色+纹理融合是一个经典基线,但它的天花板很明显:颜色直方图没有空间位置信息,LBP 也没有全局结构信息。如果要在真实业务里进一步提升检索效果,有两条可走的路。
一是引入空间信息。把图片分割成 3×3 的块,每块单独提取颜色直方图和 LBP,再拼成一个 9 倍长的特征。这样“天空在图片上方、草地在下方的风景图”才能和“上下颠倒的同样风景”区分开。缺点是维度从 82 涨到 738,检索速度变慢,但准确率能再提 10% 左右。
二是结合深度特征。用预训练的 CNN 提取最后一层卷积特征作为补充,或者干脆用 Caffe/PyTorch 的预训练模型提 2048 维特征。MATLAB 里可以直接用alexnet、googlenet的activations方法提取特征,代码量很小。不过在课程设计或小数据集场景下,颜色+纹理融合已经是个性价比很高的解决方案。
我个人的建议是,先把这个经典基线跑通,把检索闭环做出来,然后再思考自己的数据有什么特性。比如你的图像库主要是文档扫描件,那纹理特征的比重可以调高;如果是商品图较多,颜色特征的权重可以更大。这个调优过程本身,会比单纯套一个深度学习模型有意思得多,也更能锻炼对图像特征的理解。