图像检索这个方向,我前前后后做了差不多两年时间。最开始是在某高校实验室做遥感影像的快速检索课题,后来又在某公司的算法团队里做电商场景的以图搜图。期间用过不少工具,最顺手的反而是MATLAB——虽然工业界落地多用C++或者Python,但在做原型验证、特征策略对比、哪怕是给非技术同事演示检索效果的时候,MATLAB的交互式环境都让我省了太多时间。
如果你也正在摸索图像检索,想快速把想法跑通、想看清不同特征怎么影响检索结果,那这篇东西应该能帮你省不少弯路。我会把自己踩过的坑、验证过的套路、还有完整的可运行代码都交代出来。
1. 图像检索到底在解决什么问题
图像检索不是一个新概念。早在互联网图片还不多的时候,就有大量研究在讨论怎么从数据库里找出“看起来像”的图片。到今天,无论是淘宝拍照搜同款、百度识图,还是手机相册按人脸聚类,底层都离不开图像检索技术。
按技术路线,图像检索大体分两类:
- 基于文本的图像检索(TBIR):早期最常见的方案,靠人工给图片打标签,然后从标签文本里检索。问题很明显——人工标注成本高,而且文字描述很难覆盖图像的丰富语义,比如“一只趴在窗台上的橘猫”,要怎么打标签才算完整?
- 基于内容的图像检索(CBIR):直接分析图像本身的视觉内容,比如颜色、纹理、形状、空间布局等,从图像计算出特征向量,再通过向量相似度来检索。这就是我们现在聊的主体。
CBIR的优点在于不需要任何人工标注,完全靠图像自身的信息进行匹配。它的核心难题也从“文字怎么匹配”变成了“用什么特征来描述图像最有效”以及“怎么衡量两张图的特征相似度”。
用MATLAB来做CBIR,理由其实很实际。一来它的图像处理工具箱函数齐全,从读图、预处理到特征提取都有现成接口,不需要自己从底层像素级造轮子;二来脚本化的操作方式非常适合做算法实验,改一个特征参数看结果效果,几秒钟就能完成一轮迭代。特别是做研究的阶段,效率的重要性比运行性能更高。
2. 搭建检索系统前的核心思路
在写第一行代码之前,先想清楚整个检索系统的框架。实际工作中我见过太多人一上来就调函数调参,跑出一个貌似能用的效果,但换一批图测试就崩得不像样。原因就是没理清系统结构。
一个标准的CBIR系统,逻辑上分成两条链路:
1. 入库链路(离线部分)
对数据库里的每一张图片做特征提取,把提取出来的特征向量(对数据库的每张图来说是固定维度的数值数组)保存下来,建立索引。这个环节通常是离线的,耗时无所谓,关键是特征要稳定、可复现。
2. 查询链路(在线部分)
用户输入一张查询图片,系统用同样的特征提取逻辑算出查询图的特征向量,然后遍历数据库里存的每个特征向量,计算相似度,按相关度排序返回Top-K张结果。
这两条链路提取特征用的必须是同一套代码和参数,否则得到的特征不在同一个空间,相似度计算就没有意义了。这是CBIR最容易犯的低级错误,我身边就不止一个人因为查询时忘了做同样的预处理步骤,导致结果全乱。
选相似度度量方式之前,先看一个概念——特征空间。把每张图的特征向量想象成高维空间里的一个点,图像检索就是在这个空间里找与查询点“距离最近”或“夹角最小”的点。不同的度量方式,在这个空间里的几何含义完全不同,直接影响检索效果。
常见的度量方式有以下几种:
| 度量方式 | 适用场景 | 含义 |
|---|---|---|
| 欧氏距离 | 连续数值特征,各维度尺度相近 | 空间直线距离,L2 范数 |
| 曼哈顿距离 | 高维稀疏特征 | 各维度差异绝对值之和 |
| 余弦相似度 | 直方图、文本向量等高维但方向敏感的特征 | 向量夹角,忽略整体幅值 |
| 直方图相交距离 | 颜色直方图 | 统计直方图共同部分的累积占比 |
| 巴氏距离 | 直方图特征 | 两类分布的相似程度,范围 0 到 1 |
经验之谈,颜色直方图这类特征用巴氏距离或直方图相交距离的效果,普遍好于欧氏距离。原因很简单,直方图描述的是颜色的分布而非量级,两张图颜色比例接近但总体亮度不同,欧氏距离会被亮度差异带偏,而巴氏距离更关注分布形状的吻合程度。
3. 第一个能跑的版本:颜色直方图检索
早期做CBIR的人,几乎都是拿颜色特征练手的。颜色特征的理解门槛最低,而且对图像的缩放、旋转、平移不太敏感,是一种很基础的“全球特征”。我第一个能跑通的原型,就是基于HSV颜色直方图。
3.1 为什么用HSV而非RGB
我们日常接触的数字图像通常以RGB模式存储,但RGB三个通道之间的相关性很强,而且对光照变化很敏感——同一件衣服在暗处和白天的RGB数值差异可能非常大。
HSV模式把颜色分解为色调(Hue)、饱和度(Saturation)、明度(Value),更贴近人对颜色的感知逻辑。其中H分量表示色调,也就是我们说的“这图偏红、偏蓝”;S表示颜色的纯度;V表示明亮程度。检索时,通常把H和S两个分量量化后组合成直方图,V分量则可以根据实际场景决定是否保留。
实验时我习惯把V分量也一起纳入,因为有些场景需要区分不同亮度级别的同一色调区域。如果应用场景光照比较稳定、又想让特征更集中地表达色度信息,就可以舍弃V分量只统计H和S。
3.2 特征提取代码实现
下面这段代码,是我在原型阶段用的一套基础特征提取方案。
function hist = extractHSVHist(img, hBin, sBin, vBin) % 提取HSV颜色直方图特征 % 输入: % img - RGB图像矩阵 % hBin - 色调通道量化级数,默认16 % sBin - 饱和度通道量化级数,默认8 % vBin - 明度通道量化级数,默认8 % 输出: % hist - 一维特征向量,长度为 hBin*sBin*vBin if nargin < 2 hBin = 16; sBin = 8; vBin = 8; end hsvImg = rgb2hsv(img); % MATLAB中HSV分量取值范围均为0~1 hBinIdx = min(floor(hsvImg(:,:,1) * hBin) + 1, hBin); sBinIdx = min(floor(hsvImg(:,:,2) * sBin) + 1, sBin); vBinIdx = min(floor(hsvImg(:,:,3) * vBin) + 1, vBin); % 组合三个分量,形成一维索引 idx = sub2ind([hBin, sBin, vBin], hBinIdx(:), sBinIdx(:), vBinIdx(:)); hist = zeros(hBin * sBin * vBin, 1); for i = 1:numel(idx) hist(idx(i)) = hist(idx(i)) + 1; end % 归一化,使直方图不受图像尺寸影响 hist = hist / sum(hist); end这套代码有几个细节值得注意:
rgb2hsv后,HSV三个分量都在0到1范围内,所以量化时用分量乘以级数再取整,直接映射到分箱编号。sub2ind在这里用来把三维下标转换为一维索引,这样后面的累积统计就简单了。- 归一化是必须的,否则同一场景不同分辨率的图片,直方图数值会按像素总数放大,相似度计算就失真了。
3.3 批量入库与查询检索
数据库入库环节,核心就是遍历目录下所有图片,每张图调用特征提取函数,把结果存到一个矩阵变量里。MATLAB里最方便的做法是存成.mat文件。
function buildDatabase(imageDir, dbPath) % 遍历图像目录,提取特征并保存到数据库文件 % 输入: % imageDir - 存放图片的文件夹路径 % dbPath - 保存的特征数据库.mat路径 imgFiles = dir(fullfile(imageDir, '*.jpg')); numImages = length(imgFiles); featureMat = zeros(numImages, 16*8*8); imgPaths = cell(numImages, 1); for i = 1:numImages imgPath = fullfile(imageDir, imgFiles(i).name); img = imread(imgPath); if size(img, 3) == 1 img = repmat(img, [1 1 3]); % 灰度图转伪RGB end featureMat(i, :) = extractHSVHist(img)'; imgPaths{i} = imgPath; fprintf('已处理 %d / %d\n', i, numImages); end save(dbPath, 'featureMat', 'imgPaths'); end灰度图转为伪RGB这一步容易被忽视。有些图像库的图片是单通道,不走RGB读入,后面rgb2hsv就会报错。统一转成三通道是个省事的兜底方案。
查询部分的代码大致如下:
function [resultPaths, scores] = searchImage(featureMat, imgPaths, queryPath, topK) % 查询图像检索 % 输入: % featureMat - 数据库特征矩阵,每行是一张图的特征 % imgPaths - 数据库图片路径元胞数组 % queryPath - 查询图片路径 % topK - 返回前K张结果,默认10 % 输出: % resultPaths - 检索结果图片路径 % scores - 对应的相似度得分 if nargin < 4 topK = 10; end queryImg = imread(queryPath); if size(queryImg, 3) == 1 queryImg = repmat(queryImg, [1 1 3]); end queryHist = extractHSVHist(queryImg)'; % 计算巴氏距离,注意dtw中d=0代表完全匹配 distances = zeros(size(featureMat, 1), 1); for i = 1:size(featureMat, 1) distances(i) = bhattacharyya(queryHist, featureMat(i, :)); end [~, sortedIdx] = sort(distances, 'ascend'); topIdx = sortedIdx(1:min(topK, length(sortedIdx))); resultPaths = imgPaths(topIdx); scores = 1 - distances(topIdx); % 转成相似度,越大越相似 end function d = bhattacharyya(p, q) % 计算两个概率分布之间的巴氏距离 % p、q 需要是列向量,且各自和均为1 d = 1 - sum(sqrt(p .* q)); end我刻意没有用MATLAB内置的pdist2来算相似度,主要是想强调自定义度量方式更灵活。实际工程中完全可以用pdist2配合合适的距离类型替代内层循环。
注意:巴氏距离的原始定义是
d = -log(BC),其中BC = sum(sqrt(p*q))。这里我用的是简化版1 - BC,目的只是让距离范围映射到0~1之间。如果需要更严格的概率意义,建议使用原始的对数形式。
4. 进阶特征:让检索“看懂”纹理
颜色特征能解决一部分问题,但局限很明显——两件颜色相同但材质不同的衣服,颜色直方图会算出几乎一样的特征;草地和绿色的迷彩布,颜色特征也难以区分。这时候就需要引入纹理特征。
打个生活化的比方:颜色特征回答的是“这张图里有什么色彩”,而纹理特征回答的是“这些色彩的颗粒感、方向感、粗糙度如何”。看一个人的照片,你可以靠肤色识别出脸部区域,但要区分头发和毛衣的质感区别,就得依赖纹理信息。
4.1 灰度共生矩阵(GLCM)与特征统计量
灰度共生矩阵是纹理特征里最经典的做法之一。核心思想是统计图像中相距一定距离、成一定角度的两像素灰度的联合分布。
MATLAB里的graycomatrix和graycodisc可以直接实现:
function glcmFeature = extractGLCMFeature(img) % 提取灰度共生矩阵的统计特征 % 输入: % img - RGB图像,灰度图直接输入 % 输出: % glcmFeature - 4维特征向量 [对比度, 相关性, 能量, 同质性] if size(img, 3) == 3 grayImg = rgb2gray(img); else grayImg = img; end % 计算0度方向、距离为1的GLCM glcm = graycomatrix(grayImg, 'Offset', [0 1], 'NumLevels', 64, 'Symmetric', true); stats = graycodisc(glcm); % graycodisc返回的是结构体,包含Contrast, Correlation, Energy, Homogeneity等字段 glcmFeature = [stats.Contrast, stats.Correlation, stats.Energy, stats.Homogeneity]; end单独4维的GLCM统计量信息量有限。实践中更常用的做法是把多个方向(0°、45°、90°、135°)的GLCM统计量拼接起来,形成16维左右的特征向量,或者把GLCM本身直接当作矩阵特征送入后续处理。
我自己的经验是,GLCM对规则纹理(如砖墙、布料纹理)区分度很好;但对自然场景中的不规则纹理(比如树叶、云层)就不太给力。后者往往更依赖小波变换或Gabor滤波这类多尺度分析工具。
4.2 Gabor滤波器组的应用
Gabor滤波可以理解为一种能“扫描”图像特定方向和频率纹理的探针。用多个不同方向和频率的Gabor滤波器分别对图像滤波,得到的响应幅值统计特征(均值、方差)就能组成纹理特征向量。
MATLAB里写Gabor滤波器组相对简洁:
function gaborFeature = extractGaborFeature(img, scales, orientations) % 提取Gabor纹理特征 % 输入: % img - RGB图像或灰度图 % scales - 尺度数量,比如4 % orientations - 方向数量,比如6 % 输出: % gaborFeature - 特征向量,长度为 scales*orientations*2 if size(img, 3) == 3 grayImg = double(rgb2gray(img)) / 255; else grayImg = double(img) / 255; end g = gabor(scales, orientations); gaborMag = imgaborfilt(grayImg, g); % 返回的是幅度响应 gaborFeature = zeros(1, length(g) * 2); for i = 1:length(g) % 对每个滤波响应统计均值和方差作为特征 gaborFeature(2*i-1) = mean(gaborMag(:,:,i), 'all'); gaborFeature(2*i) = var(double(gaborMag(:,:,i)), 0, 'all'); end endMATLAB从R2015b之后内置了gabor函数和imgaborfilt函数,这极大简化了Gabor特征提取的复杂度。早年我做叶子图像分类的时候,还得自己写Gabor核的公式生成函数,现在一行调用就解决了。
4.3 LBP:局部二值模式
LBP(局部二值模式)是另一个异常好用的纹理特征,思路更直白——对图像中每个像素,比较它和周围邻域像素的大小关系,把比较结果编码成一个二进制数,然后统计这些二进制数的分布直方图。
LBP的伟大之处在于它的灰度不变性:光照变化导致所有像素值整体抬升或降低时,局部邻域的“大小关系”基本不变,所以LBP对照明变化有很强的鲁棒性。回想之前说RGB颜色特征对光照敏感,LBP的做法完全绕开了这个问题。
function lbpHist = extractLBPFeature(grayImg, radius, neighbors) % 提取LBP直方图特征 % 输入: % grayImg - 灰度图 % radius - 采样半径,常用1或2 % neighbors - 邻域点数,常用8或16 % 输出: % lbpHist - LBP直方图(按模式数归一化) if nargin < 3 radius = 1; neighbors = 8; end % 利用MATLAB的局部特征描述子接口 lbp = extractLBPFeatures(grayImg, 'Radius', radius, 'NumNeighbors', neighbors, 'Upright', false); lbpHist = lbp / sum(lbp); % 归一化 end这里直接用了Computer Vision Toolbox里的extractLBPFeatures,输出的是统计直方图。如果没这个工具箱,也可以自己实现,不难,但容易踩边界像素处理的坑。建议初学阶段先用内置函数跑通流程,再研究内部实现。
实际使用中,LBP配合颜色特征做融合,效果提升比单纯加GLCM明显得多。原因在于LBP捕获的是局部微观纹理,和颜色特征形成了互补。
5. 让特征更全面:形状与边缘信息
颜色、纹理都处理完,还有一个信息维度不能丢——形状。人们对“像不像”的直觉判断,很大程度取决于物体的轮廓结构。
形状特征的做法有很多流派,有基于轮廓的描述子(如傅里叶描述子、曲率尺度空间),也有基于区域的描述子(如Hu矩、Zernike矩)。但在图像检索的实际工程中,直接在原图上做精确的形状特征提取并不讨好,因为自然图像里的前景背景分割本身就很难。
一个更务实的思路是边缘方向直方图(EDH)。它不追求对物体轮廓的精确数学描述,而是统计图像中边缘像素的方向分布。比如“这张图里竖直方向的边缘很多”,本质上反映了图像中高耸建筑物的存在。
function edgeHist = extractEdgeOrientationHistogram(img, numBins) % 提取边缘方向直方图特征 % 输入: % img - RGB图像 % numBins - 方向分箱数,常用18(每20度一箱) % 输出: % edgeHist - 归一化的边缘方向直方图 if nargin < 2 numBins = 18; end grayImg = rgb2gray(img); % Canny边缘检测,阈值可以自适应调整 edgeMap = edge(grayImg, 'Canny'); % 计算梯度方向和幅值 [gradMag, gradDir] = imgradient(grayImg); % 只统计边缘像素的梯度方向 edgePixels = find(edgeMap); dirs = gradDir(edgePixels); dirs(dirs < 0) = dirs(dirs < 0) + 180; % 方向归一到0~180度(无向) edgeHist = histcounts(dirs, numBins, 'BinLimits', [0, 180]); edgeHist = edgeHist / sum(edgeHist); end梯度方向取0到180度的“无向”版本是有讲究的。在形状感知里,一条竖直边缘的方向究竟是“从左到右的竖边”还是“从右到左的竖边”并不重要,重要的是它是不是竖边。把方向压缩到180度区间,反而增强了特征的稳定性。
这个特征有个明显缺点——它对背景噪声敏感。如果图像的背景纹理比较丰富,边缘检测会把很多背景纹路也当成形状信息统计进来,干扰真正的物体轮廓。解决办法有两个方向:一是先用分割或显著性检测把前景抠出来再做边缘提取;二是把边缘方向直方图和颜色、纹理特征做融合,靠其他特征去弥补误判。
6. 多特征融合与动态调优
单一特征总有过不去的坎。颜色特征区分不了材质,纹理特征区分不了大面积色块,形状特征又依赖前景分割质量。解决思路很朴素:把多个特征串起来,构成一个更高维的特征向量。
6.1 最简串行拼接法
最直接的办法就是把颜色直方图特征、LBP特征、边缘方向直方图特征拼接成一个向量:
function combinedFeature = extractCombinedFeature(img) % 多特征串行拼接 hsvHist = extractHSVHist(img, 16, 8, 8); % 1024维 grayImg = rgb2gray(img); lbpHist = extractLBPFeature(grayImg, 1, 8); % 59维(均匀模式) edgeHist = extractEdgeOrientationHistogram(img); % 18维 combinedFeature = [hsvHist(:); lbpHist(:); edgeHist(:)]; end简单归简单,这做法有一个大坑——不同特征的数值范围不一样。HSV直方图的每个分量是归一化后的频率值,LBP直方图同理,但边缘方向直方图的某些分量可能很大,直接拼接后会导致相似度计算被数值大的维度主导。
因此,拼接之前必须对每个特征子块做单独的归一化,甚至可以考虑标准化(减去均值除以标准差)。实践中我通常对每个特征子块做L2归一化:
function vec = l2norm(vec) n = norm(vec, 2); if n > 0 vec = vec / n; end end把每个子块归一化到单位L2范数之后,再拼接在一起,相似度计算才不会被某一类特征“带节奏”。
6.2 特征加权与参数选择
拼接完成后,还可以为每个特征子块设置权重系数。这个在工程上极其常见,因为不同应用场景对不同特征维度的依赖程度不同。比如在服装检索场景,颜色权重可以高一些;在布料纹理检索场景,纹理权重就更关键。
加权的方式可以简单粗暴地乘一个常数,也可以更精细地通过训练数据学习最优权重。前者适合快速验证,后者适合追求检索精度的正式项目。
表:不同场景下的推荐权重分配(经验值)
| 应用场景 | 颜色特征权重 | 纹理特征权重 | 形状特征权重 |
|---|---|---|---|
| 通用相册检索 | 0.5 | 0.3 | 0.2 |
| 服装图片检索 | 0.6 | 0.25 | 0.15 |
| 织物纹理检索 | 0.2 | 0.7 | 0.1 |
| 遥感图像检索 | 0.4 | 0.3 | 0.3 |
这里没有“万能答案”。最可靠的做法是自己建一个验证集,跑不同的权重组合看检索准确率变化,用网格搜索或随机搜索来找最优权重。
6.3 降维处理与索引加速
串行拼接的特征维数往往上千维,这时不仅存储开销大,查询时的相似度计算也非常耗时。可以入手的方向有两个——降维和索引结构。
降维最常见的方法是PCA(主成分分析)。MATLAB里:
% 数据库全部特征矩阵 featureMat,每行一个样本 [coeff, score, ~, ~, explained] = pca(featureMat); % 保留贡献率达到95%的主成分 cumRatio = cumsum(explained); nPC = find(cumRatio >= 95, 1); featureMatReduced = score(:, 1:nPC);PCA适合在数据库特征整体离线的场景下做,因为需要统计全库的协方差结构。换一个视角,降维也是去除冗余信息、增强特征鲁棒性的手段。
检索加速方面,最经典的做法是构建KD树或倒排索引。不过MATLAB自带的knnsearch己经支持KDTree搜索了,可以直接用。数据库规模在几十万级别以内时,暴力线性扫描加向量化计算也完全能接受,因为MATLAB矩阵运算本身就比较高效。
7. 真实场景效果提升的实战技巧
理论讲完,来说说实操中我总结的几招。
7.1 预处理宁可多一步也不省
图像的输入质量对检索效果影响非常大。同一个检索系统,输入原始照片和输入经过归一化的照片,Top-1准确率可能差20%以上。
基础的预处理包括:统一尺寸缩放(避免直方图统计受到分辨率干扰)、去噪(高斯滤波或中值滤波)、光照均衡(直方图均衡化)。我自己的习惯是在特征提取函数前统一加上尺寸缩放和转灰度(按需),这样无论输入什么图都能稳定得到特征。
7.2 构建自己的评测集要留一手
做检索系统最容易陷入“自己调参自己满意”的陷阱。务必从数据库里选出一部分图片作为查询样例,人为构造相关的“标准答案”。这个标准答案怎么来?简单做法是从同一场景的不同角度、不同光照条件下拍摄的照片作为相关对。
如果没有办法获取多视角图,也可以用另一种思路:按类别文件组织图片库,从每个类别随机挑一张作为查询,其他同类图片作为期望结果。检索的准确率指标就用“检索结果中属于同类别图片的占比”来衡量。虽然粗暴,但能反映相对变化。
7.3 特征组合的动态选择
不同查询图对应的最优特征组合往往不一样。举个我遇到的真实例子:查询是一个纯色背景上的红色汽车,颜色特征占主导;但查询是一幅黑白街拍建筑,颜色特征基本没有判别力,这时候纹理和边缘特征才是主力。
可以设计一个简单的启发式逻辑:根据查询图的颜色丰富度(颜色直方图的有效分箱数),动态决定颜色特征对最终相似度的贡献。颜色分布越集中,说明颜色信息越有代表性,适当调高权重;反之则调低。
这种方式虽然不是最严谨的自适应方法,但工程收益很高,尤其适合小规模查询场景。
7.4 应对大面积遮挡和裁剪
真实场景里的查询图经常有物体被遮挡或包含大面积无关背景。这时候全图特征的表现力会明显下降。
更稳健的方案是分块特征(Block-based Feature)。把图像均匀切成若干块,每块提取特征,然后用块与块之间的局部匹配来算全局相似度。这相当于把目光从“整张图像不像”聚焦到“局部区域能不能对上”。
实现并不复杂,把前面的特征提取函数改成对子图像调用即可:
function blockFeature = extractBlockHSV(img, gridRows, gridCols) % 分块HSV直方图特征 % 将图像分成 gridRows * gridCols 块,对每块提取直方图并拼接 [height, width, ~] = size(img); blockFeature = []; for i = 1:gridRows for j = 1:gridCols rStart = round((i-1) * height / gridRows) + 1; rEnd = round(i * height / gridRows); cStart = round((j-1) * width / gridCols) + 1; cEnd = round(j * width / gridCols); subImg = img(rStart:rEnd, cStart:cEnd, :); blockFeature = [blockFeature; extractHSVHist(subImg)]; end end end分块的代价是特征维度成倍增加,比如把图分成3x3块后,单是HSV直方图特征就是原来的9倍。所以分块之后一般配合PCA做降维,或者改用更紧凑的特征类型。
8. 常见问题与排查技巧实录
我在代码分享、教学指导和项目迭代过程中,遇到过不少反复出现的坑。整理一份速查表出来,应该能帮你省不少排查时间。
| 问题现象 | 可能原因 | 排查与解法 |
|---|---|---|
| 检索结果全是乱序的“相似”图 | 特征提取时忘了同一套预处理 | 检查查询图和入库图的预处理流程是否完全一致 |
| 相似度计算结果全是同一个值 | 特征没有归一化,数值差异被淹没 | 确认每个特征子块做L2归一化后再拼接 |
| 灰度图入库时总是报错 | 单通道图像直接进rgb2hsv | 统一用repmat扩展为三通道 |
| 颜色特征不变的场景检索很差 | 特征维度覆盖不到的细节差异 | 增加纹理特征或LBP特征做融合 |
| 数据库大了以后查询很慢 | 全库线性扫描耗时增长 | 用KDTree或降维后建倒排索引 |
| 有关联结果但排序不对 | 相似度度量不适合该特征 | 换成直方图相交距离或余弦相似度对比一下 |
除了表格里的问题,我再补充几个不容易看出来的细节。
第一个是MATLAB的数据类型坑。imread读入的如果是uint8类型的图像,直接进行除法或乘小数,结果会被强行取整。在特征提取里计算归一化直方图时,如果不先转换成double,统计结果会匪夷所思。我建议在特征提取函数开头统一加上img = double(img)或对灰度图转double后除以255。
第二个是边界处理问题。提取LBP或梯度特征时,图像边缘像素周围可能没有完整的邻域。MATLAB内置函数一般会自动处理边界,但如果自己实现相关算法,很容易在边界处计算出错误特征值,导致后续相似度累计出微小偏差。排查时可以对比一下去掉边界像素前后的检索效果,如果变化不大,就说明边界处理不是关键因素。
第三个是特征维数选择。HSV直方图的bin数不是越多越好。我用16x8x8的参数跑过很多数据集,效果都算稳定。如果bin数太多(比如64x64x64),特征对噪声更敏感,相似度计算也更耗时;bin数太少又难以区分相近颜色。同理,LBP的采样半径和邻域点数也需要根据图像分辨率做调整,高分辨率图可以适当加大半径。
最后,分享一下我在实际项目里的几个体会。
第一个体会是,特征工程的投入产出比很高。很多人看了一些SOTA模型的论文,总觉得换一个“先进模型”就能解决所有问题。但在图像检索的落地环节,特征设计、预处理、度量方式这些“老一套”反而是效果提升最稳定的部分。做好基础,比盲目追新更可靠。
第二个体会是,MATLAB在做检索算法原型验证时,交互式脚本和可视化能力确实无可替代。我可以在几个小时之内对比十几组特征组合在评测集上的效果,然后用一张曲线图直观看出哪个方案最好,再拿最优方案到C++工程里复现。这种流程在别的环境里很难复制。
第三个体会是,不要忽略检索结果的人性化呈现。算法排名给出Top-10之后,用户看到一个折线排开的图片列表,脑子里会自动补一条“从最像到最不像”的判断。所以在开发过程中别光盯着评估指标,也要花心思做结果可视化,方便调试也方便向同事介绍。
如果你刚开始接触MATLAB图像检索,我的建议非常简单:先拿二十张图搭一个最小的完整系统,颜色特征起步,把入库、查询、可视化结果跑通,然后再逐步加纹理特征、形状特征、融合权重调优。路虽长,但每一步都能看到效果的变化,这个过程本身就很值得。