ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于VGG-16的图像检索系统:特征提取与Top3相似图召回实战

基于VGG-16的图像检索系统:特征提取与Top3相似图召回实战 简介本资源面向人工智能与信息检索方向的学习者与开发者提供一套基于VGG-16的图像检索系统完整项目实践。项目以深度学习特征替代传统颜色、形状、纹理等手工特征采用Keras预训练模型对图像库逐张抽取特征并存入h5文件完成索引化检索阶段以夹角余弦度量相似度对测试图片特征与库中向量逐一计算距离并排序返回相似度Top3结果覆盖特征提取、索引构建、相似度定义与检索排序的完整链路。压缩包共253个文件以241张jpg图像样本为主另含5个xml标注、3个py脚本、2个pyc编译文件、1个h5特征库及1个iml工程配置整体约41.13MB目录结构便于按模块查阅与复现。目前已有194人学习下载适合希望理解图像检索原理、动手搭建可运行检索流程的读者参考借鉴。1. 基于 VGG-16 的图像检索系统从特征提取到 Top3 相似图召回手头有一批图片想按“长得像”来找而不是靠文件名或标签这件事在信息检索里叫基于内容的图像检索CBIR。传统做法是提颜色直方图、纹理、形状、能量这些手工特征换个光照或角度就崩。这个项目直接上深度学习模型提特征用 Keras 预训练 VGG-16 把每张图压成一个高维向量存进 h5 文件当索引库检索时算夹角余弦、排序取 Top3。它适合做人工智能大作业、毕业设计选题也适合想搞懂“特征向量 相似度”这条检索主线的工程师。下面按我拆包的顺序讲清楚怎么复现、参数怎么调、坑在哪。2. 特征提取选型为什么是 VGG-16 而不是颜色纹理2.1 手工特征的天花板在哪颜色、形状、纹理、能量这些特征本质是人在替模型做归纳。颜色直方图对整体色调敏感但两张构图完全不同、色调相近的图会被判成相似纹理特征比如 LBP、GLCM对局部模式有反应可一旦图像旋转或缩放统计分布就漂了。信息检索场景里用户要的是“语义相似”——同一类物体、同一场景而不是像素统计接近。手工特征在简单数据集上还能看图片一多、类别一杂召回结果就开始玄学。深度学习特征不一样。VGG-16 在 ImageNet 上预训练过卷积层学到的是一层层从边缘、纹理到部件、物体的抽象表示。把一张图喂进去取全连接层之前的输出得到的就是一个语义向量。这个向量对光照、轻微位移的鲁棒性远好于手工特征这也是这个项目选它的核心理由。摘要里提到还支持 resnet50、densenet121思路一致换模型就是换特征提取器索引和检索逻辑不动。2.2 VGG-16 的结构与特征层选择VGG-16 的结构很规整13 个卷积层 3 个全连接层。卷积部分全是 3x3 小卷积核堆叠池化用 2x2 最大池化。Keras 里加载预训练权重时include_topFalse会把最后三层全连接去掉输出的是最后一个卷积块之后的特征图。常见做法是再接一个全局平均池化GlobalAveragePooling2D把(7, 7, 512)的特征图压成(512,)的向量。这样每张图就是一个 512 维向量存起来轻量算余弦也快。为什么不直接取Flatten后的 25088 维维度太高h5 文件膨胀快检索时逐一算距离也慢而且高维里很多分量是冗余的。全局平均池化相当于对每个通道做空间平均保留通道语义、丢掉空间位置对检索这种“整体像不像”的任务更合适。这是我一般会采用的方案也是这个项目能跑通的关键一步。2.3 用 Keras 抽取特征并落盘下面这段是特征提取的核心逻辑我按项目里的vgg_featureCNN.h5命名习惯写import os import numpy as np from keras.applications.vgg16 import VGG16, preprocess_input from keras.preprocessing import image from keras.models import Model from keras.layers import GlobalAveragePooling2D # 加载 VGG-16去掉顶层全连接输入尺寸固定 224x224 base_model VGG16(weightsimagenet, include_topFalse, input_shape(224, 224, 3)) # 接全局平均池化输出 512 维向量 x GlobalAveragePooling2D()(base_model.output) model Model(inputsbase_model.input, outputsx) def extract_feat(img_path): # 统一缩放到 224x224保持与预训练输入一致 img image.load_img(img_path, target_size(224, 224)) arr image.img_to_array(img) arr np.expand_dims(arr, axis0) # VGG 专用的预处理通道均值减法BGR 顺序 arr preprocess_input(arr) feat model.predict(arr) # 归一化方便后面直接点积算余弦 feat feat / np.linalg.norm(feat) return feat.flatten()逻辑说明include_topFalse是必须的否则输出是 1000 类分类概率不是特征。preprocess_input做的是 VGG 训练时的通道均值减法不做这一步特征分布和预训练时对不上检索质量会掉。最后做 L2 归一化是因为余弦相似度在向量归一化后等价于点积后面算距离能省一次除法。参数上target_size必须和input_shape一致改成 448 会报维度错除非你同时改input_shape但那样显存和耗时都涨。3. 索引化数据集把特征存进 h5 与检索实现3.1 为什么要建索引而不是每次现算图像库如果有几千张图每次检索都重新跑一遍 VGG-16一张图前向传播在 CPU 上大概几十到上百毫秒几千张就是几分钟完全没法交互。索引化的意义就是把“提特征”这个重活提前做一次存成文件检索时只做向量比对。项目里用 h5 存是因为 h5 支持大数据集、读写快、Python 生态里 h5py 用起来简单。常见做法是存两个数据集一个是特征矩阵(N, 512)一个是文件名列表保证顺序一一对应。3.2 构建索引库的完整脚本import h5py import os import numpy as np from extract_feat import extract_feat # 上一节的函数 IMG_DIR ./images H5_PATH vgg_featureCNN.h5 def build_index(): names [] feats [] for fname in os.listdir(IMG_DIR): if not fname.lower().endswith((.jpg, .png, .jpeg)): continue path os.path.join(IMG_DIR, fname) try: feat extract_feat(path) names.append(fname) feats.append(feat) except Exception as e: # 损坏图或非图像文件跳过不让整个索引挂掉 print(skip, fname, e) feats np.array(feats, dtypefloat32) names np.array(names, dtypeh5py.special_dtype(vlenstr)) # 写入 h5特征和文件名分开存 with h5py.File(H5_PATH, w) as f: f.create_dataset(features, datafeats) f.create_dataset(names, datanames) print(indexed, len(names), images) if __name__ __main__: build_index()逻辑说明dtypefloat32比默认 float64 省一半空间精度对检索够用。文件名用vlenstr存变长字符串避免固定长度截断。异常捕获是血泪经验——图像库里混进一张损坏 jpg整个索引脚本就崩了加个 try 跳过最后打印跳过了哪些。参数上IMG_DIR换成你的图片目录H5_PATH就是项目里那个vgg_featureCNN.h5。3.3 夹角余弦检索与 Top3 召回import h5py import numpy as np from extract_feat import extract_feat def search(query_path, topk3): with h5py.File(vgg_featureCNN.h5, r) as f: feats f[features][:] names f[names][:] q extract_feat(query_path) # 特征已 L2 归一化点积即余弦相似度 sims np.dot(feats, q) # 从大到小排序取前 topk idx np.argsort(-sims)[:topk] return [(names[i], float(sims[i])) for i in idx] if __name__ __main__: for name, score in search(430453.jpg): print(name, round(score, 4))逻辑说明np.dot(feats, q)一次矩阵乘法算出查询向量和库中所有向量的余弦比 for 循环快几个数量级。argsort(-sims)取负号实现降序。返回文件名和分数分数越接近 1 越相似。参数topk默认 3对应摘要里的 Top3想多看几张改成 5 或 10 都行。项目正文里那串430453.jpg、430448.jpg这些就是检索命中的结果文件名说明库里的图是按编号命名的。4. 避坑与排查检索结果不对时先查这几处4.1 现象检索出来的图完全不相关原因最常见的是预处理没对齐。VGG-16 的preprocess_input做的是 BGR 通道均值减法如果你用img_to_array后直接除以 255特征分布和预训练时差很远向量就失去语义。解决确认提取特征和建库用的是同一个extract_feat函数别一个用preprocess_input一个用rescale1./255。4.2 现象h5 文件读出来文件名是乱码或截断原因存文件名时用了固定长度的dtypeS20之类长文件名被截。解决用h5py.special_dtype(vlenstr)存变长字符串读的时候f[names][:]直接拿到字符串数组。如果已经存错了只能重建索引。4.3 现象查询图和库里的图明明是同一张相似度却不到 0.9原因查询图在提取时被 resize 到了 224x224如果原图和库里的图分辨率差异大resize 后的插值方式不同会引入差异。另外 JPEG 压缩质量也会影响。解决统一用image.load_img的默认插值别混用 OpenCV 的 resize。相似度 0.85 以上通常就算高度相似不必强求 1.0。4.4 现象图片一多建索引越来越慢甚至内存爆原因feats列表把所有特征堆在内存里几万张图就是几万乘 512 的 float32大概几百 MB加上 VGG 模型本身占显存容易顶不住。解决分批写入 h5用f.create_dataset(..., maxshape(None, 512), chunksTrue)然后逐批 append别一次性np.array。或者换 resnet50特征维度一样但前向更快。4.5 现象检索时np.dot报维度不匹配原因查询特征没 flatten形状是(1, 512)库是(N, 512)点积维度对不上。解决extract_feat最后加.flatten()保证查询向量是(512,)。这个坑很小但很常见报错信息也不直观。5. 进阶技巧换模型、调 topk 与检索质量验证5.1 换 resnet50 / densenet121 只改一行这个项目的摘要提到支持 resnet50、densenet121换模型确实简单改导入和加载即可from keras.applications.resnet50 import ResNet50, preprocess_input base_model ResNet50(weightsimagenet, include_topFalse, input_shape(224, 224, 3))注意 resnet50 的preprocess_input和 VGG 不同它做的是通道均值减法但顺序是 RGB别混用。densenet121 同理。换完模型必须重建索引因为特征空间变了旧 h5 和新查询向量不在一个空间里算出来的相似度没有意义。这是我见过最容易翻车的地方——有人换了模型忘了重建库结果检索全乱。5.2 检索质量怎么验证没有标注数据时我一般用“同类查询”做粗验证拿一张猫的图去查看 Top3 里是不是猫拿一张风景图去查看是不是风景。有标注的话算 Top3 命中率Recall3。具体做法是每类抽几张当查询看返回结果里同类占比。如果 Recall3 明显偏低先查预处理再查特征层选择最后才怀疑模型本身。验证项做法合格线预处理一致性建库和查询用同一函数必须一致特征归一化检查向量模长是否为 1必须为 1Recall3每类抽 5 张查询简单数据集 0.7 以上响应时间单次检索耗时千张库 100ms 内5.3 一个具体技巧查询扩展如果单张查询图效果不稳可以拿 Top3 结果的特征均值再查一次相当于用“查询 初步结果”共同表达意图。这个技巧在信息检索里叫查询扩展实现就几行# 第一次检索 first search(query_path, topk3) # 取 Top3 特征均值作为新查询 with h5py.File(vgg_featureCNN.h5, r) as f: feats f[features][:] names list(f[names][:]) avg_q np.mean([feats[names.index(n)] for n, _ in first], axis0) avg_q avg_q / np.linalg.norm(avg_q) sims np.dot(feats, avg_q) idx np.argsort(-sims)[:3]逻辑说明均值前先归一化避免某张图模长大主导。这个技巧对“一张图里有多个物体”的场景提升明显但对单一物体可能引入噪声建议先跑基线再决定要不要加。从那以后我每次换特征提取器都强制走一遍“重建索引 → 单张验证 → Recall3 抽检”这三步不跳过任何一步。希望帮到你。本文还有配套的精品资源点击获取
返回列表