基于VGG16的服装图像检索系统:特征提取、离线索引与Flask部署详解

发布时间:2026/10/12 18:11:24
基于VGG16的服装图像检索系统:特征提取、离线索引与Flask部署详解
简介这是一份基于服装数据的图像检索分类系统源码项目采用Python编写面向计算机、电子信息等专业学生可作为课程设计、期末大作业或毕业设计的参考实现也适合对图像特征提取与检索技术感兴趣的开发者入手学习。压缩包共86个文件大小仅1.05MB包含14个Python脚本、7个HTML页面以及配套的CSS样式、JS脚本、图片与字体资源。Python代码覆盖图像预处理、基于VGG16的卷积特征提取、索引构建、相似度检索与分类等完整流程HTML模板提供可视化交互界面目录结构清晰便于按模块阅读和调试。目前已有154人浏览学习。项目另附项目说明文档关键模块如特征提取脚本、检索测试脚本等均收于包内可直接运行读者借此可以掌握预训练卷积神经网络在服装图像上的应用方法并迁移到其他图像数据集。1. 服装图像检索分类系统VGG16 提取特征加离线索引查同款不需要训练分类器拿到这个「图像检索分类系统基于服装数据python 源码项目说明.zip」时很多人第一反应是想要检索和分类肯定得先训练一个服装分类模型。实际拆开看才发现核心思路是反的——用 Keras 里的 VGG16 预训练网络把每张服装图压成特征向量建好离线索引查询时对上传图做同样的特征提取再算一遍相似度就能返回同款。这套系统解决的是「以图搜图」和「按图归档」两件事适合做计算机视觉课程设计、期末大作业、毕设的学生也适合第一次接触以图搜图流程的从业者拿来做二次开发底子。源码包里预处理、特征提取、索引、检索、Flask 前端页面都齐了下面按我实际拆读的顺序把每个模块和踩过的坑讲清楚。2. 从图片到特征向量VGG16 特征提取与图像预处理决定检索上限2.1 为什么是 VGG16检索任务和分类任务的选型差异检索系统关心的是「两张图在特征空间里距离近不近」而不是「这张图属于哪个类」。分类网络最后一层 softmax 输出的类别概率对没见过的服装新款毫无办法但倒数第二层那个 4096 维特征向量可以。VGG16 在 ImageNet 上见过大量自然图像浅层特征边缘、纹理和深层特征部件、廓形都被压缩进了 fc6、fc7拿它做服装检索的骨架是迁移学习里性价比最高的方案。那么为什么是 VGG16 而不是 ResNet50我的判断有三个第一VGG16 结构线性特征直白feature map 语义容易解释答辩时好讲第二Keras 自带 VGG16 权重加载extract_cnn_vgg16_keras.py 这个命名已经说明了它就是 Keras 实现的 VGG16第三CPU 上单张推理也就几秒演示不会卡死。取哪一层特征也讲究fc6 输出 4096 维fc7 同样是 4096 维但更抽象也有人取 conv5_3 的 feature map 做全局池化得到 512 维。维度不是越高越好4096 维在几千张数据上内存和检索耗时都可控我一般优先用 fc6。提示后续想换网络时只需要替换特征提取器内部的输出层名索引和检索代码完全不用动。2.2 preprocess_img.py 的预处理细节尺寸、归一化与通道顺序预处理脚本是整套系统里最容易出错的地方。VGG16 的输入是 224×224 的 RGB 图但 Keras 自带的 VGG16 预训练权重有两种预处理约定tf 版本是 rescale 到 [0,1]caffe 风格是 RGB 转 BGR 再减均值。这个项目源码里 preprocess_img.py 走的是后一种因为 extract_cnn_vgg16_keras.py 加载的是 caffe 权重。# preprocess_img.py单张图预处理返回可直接喂给 VGG16 的数组 import numpy as np from keras.preprocessing import image def preprocess(img_path, target_size(224, 224)): img image.load_img(img_path, target_sizetarget_size) # 自动缩放到 224x224 x image.img_to_array(img) # (224, 224, 3) RGB x np.expand_dims(x, axis0) # (1, 224, 224, 3) x x[:, :, :, ::-1] # RGB - BGR x[:, :, :, 0] - 103.939 x[:, :, :, 1] - 116.779 x[:, :, :, 2] - 123.68 return x逻辑说明第四行的通道反转用切片完成不加这一行后面所有特征都相当于在错误颜色空间里提取检索结果会偏得莫名其妙。均值减法是 caffe 权重自带的统计值如果换成 tf 权重这套均值就不适用。另外 Keras 老版本里 image.load_img 已经内置 target_size 缩放不要在外部先 resize 再传进去两次缩放会造成特征细节损失。参数说明target_size 必须是 (224, 224)改动后 VGG16 输入维度不匹配直接报错函数返回 (1, 224, 224, 3) 的四维数组batch 维度为 1后面 model.predict 可以直接吃。如果手上有灰度图load_img 默认转三通道但 RGBA 的 PNG 透明通道会被忽略对白底服装图影响不大对透明背景抠图影响明显。预处理步骤取值对应代码目标尺寸224×224target_size(224, 224)通道顺序RGB→BGRx[:, :, :, ::-1]均值减除103.939 / 116.779 / 123.68三次逐通道减法归一化范围无caffe 风格不做 [0,1]不调用 rescaleprocess_image.py 和 preprocess_img.py 的分工从命名看一个负责单图入口、一个负责批量循环。批量时要注意VGG16 权重几百 MB每张图重新加载一次模型一万张图能把时间拖到不可接受。2.3 特征落盘npy 文件与目录组织方式特征提取完必须落盘否则每次检索都重算一遍等于灾难。项目里典型做法是每张图对应一个 npy 文件文件名取原图去掉扩展名这一步直接决定后期索引能不能对上图名。# extract.py批量提取并保存特征 import os import numpy as np def extract_to_npy(img_dir, feat_dir): os.makedirs(feat_dir, exist_okTrue) extractor get_vgg16_extractor() # 复用同一个模型实例 for name in sorted(os.listdir(img_dir)): if not name.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(img_dir, name) feat extractor.extract(path) # 返回 (1, 4096) float32 out os.path.join(feat_dir, name.rsplit(., 1)[0] .npy) np.save(out, feat) print(fprocessed {name}, shape{feat.shape})逻辑说明extractor 只初始化一次放在循环外sorted 排序保证处理顺序固定np.save 把 (1, 4096) 的二维数组原样存进去检索时 np.load 再 reshape 成一维。参数上4096 维 float32 每张 16KB3000 张图约 48MB 特征文件压力不大。为什么不直接存 picklenpy 是 numpy 原生格式加载省去反序列化开销后面 index.py 拼矩阵时可以直接 np.vstack。文件多了以后也可以把特征合并成单个 npy 再加一个 name_list.txt 记录顺序——这是我从第二版开始用的方式检索前一次性 np.load省掉几千次小文件 IO。3. 索引构建与检索排序从特征库到 Top-K 返回的完整链路3.1 extract_cnn_vgg16_keras.py特征提取器的封装方式这个文件名把技术栈暴露得很清楚CNN 特征提取 VGG16 Keras。一个典型的封装是类初始化时加载预训练模型提取时只走一次 forward# extract_cnn_vgg16_keras.py核心结构 from keras.applications.vgg16 import VGG16 from keras.models import Model class VGG16FeatureExtractor: def __init__(self, weightsimagenet): base_model VGG16(weightsweights, include_topTrue) # 取 fc6 输出丢掉 softmax 分类头 self.model Model(inputsbase_model.input, outputsbase_model.get_layer(fc6).output) def extract(self, img_path): x preprocess(img_path) # 调用 2.2 的预处理 feat self.model.predict(x) # (1, 4096) return feat逻辑说明VGG16(include_topTrue) 加载完整模型再把输出接到 fc6 层等于手动截断分类头。这样可以避开 softmax 的强类别假设特征向量更适合做相似度检索。参数上weightsimagenet 首次运行会下载约 500MB 权重务必确认网络能正常访问也可以先把权重文件放到 ~/.keras/models/ 再运行避免启动时卡住。单张 predict 存在大量重复计算开销我一般按 batch 喂# 批量提取一次喂 32 张图 feats model.predict(np.vstack([preprocess(p) for p in batch_paths]), batch_size32)注意 batch 模式下每张图的预处理仍然要独立完成堆叠维度保持 (N, 224, 224, 3)。CPU 机器上 batch_size 调成 8 到 16 更保险GPU 可以给 32 或 64。项目里还带了一个 debug.log说明原开发环境也经历过反复调试出现报错先看日志尾部比猜原因快得多。3.2 index.py向量拼接与相似度度量方式索引模块的核心是把所有特征拼成一个矩阵再对查询向量做相似度计算。余弦相似度和欧氏距离的抉择直接影响服装检索的召回效果。我推荐 cosine原因很直白服装图受光照和拍摄角度影响时整体亮度变化会让欧氏距离变大但方向变化较小cosine 只关心方向天然抗亮度偏移。# index.py构建索引矩阵 import os import numpy as np def build_index(feat_dir): feats, names [], [] for fn in sorted(os.listdir(feat_dir)): if fn.endswith(.npy): feats.append(np.load(os.path.join(feat_dir, fn)).reshape(-1)) names.append(fn.replace(.npy, .jpg)) mat np.vstack(feats).astype(np.float32) # (N, 4096) mat / np.linalg.norm(mat, axis1, keepdimsTrue) # L2 归一化 np.save(index_matrix.npy, mat) with open(index_names.txt, w, encodingutf-8) as f: f.write(\n.join(names)) return mat, names逻辑说明L2 归一化之后矩阵点积等价于余弦相似度检索阶段直接用 mat query 一次矩阵乘法拿到所有相似度。参数说明astype(np.float32) 是为省内存4096 维 float32 一万张约 160MB如果特征文件是 float64 存的这一步转换能明显降低后续计算耗时。度量方式对亮度变化的敏感度适用场景余弦相似度不敏感服装检索光照、曝光变化欧氏距离敏感特征严格对齐、无亮度变化曼哈顿距离中等稀疏特征还有个容易忽视的点索引矩阵的行顺序必须和 names 列表一一对应我在此类项目里栽过跟头——二维特征保存时沿用了 (1, 4096)vstack 后多出的维没压干净矩阵行数和 names 对不上检索结果张冠李戴。解决方法是存文件前统一 reshape(-1)。3.3 retrieval.py检索流程与返回数量控制检索过程就是一条流水线加载索引 → 读取查询特征 → 算相似度 → 排序 → 返回映射。retrieval.py 负责这部分retrieval_test.py 则是离线测试入口。# retrieval.py给定查询特征返回 Top-K import numpy as np def retrieve(query_feat, index_mat, index_names, top_k5): q query_feat.reshape(-1).astype(np.float32) q / np.linalg.norm(q) # 查询向量同样做 L2 归一化 sims index_mat q # (N,) 余弦相似度 rank np.argsort(-sims) # 从大到小排序 results [(index_names[i], float(sims[i])) for i in rank[:top_k]] return results逻辑说明argsort(-sims) 是关键argsort 默认升序取负号才能得到相似度从高到低的排名。返回结构是 (文件名, 相似度) 列表前端 show.html 直接拿它渲染图片。参数说明top_k 一般取 5 到 10演示 5 张足够取太大时后面几名的相似度会掉到 0.3 以下视觉上明显不相关。检索耗时上一万张图的索引矩阵和查询向量做点积numpy 一条命令毫秒级返回瓶颈在特征提取不在检索。所以 query_online.py 里真正慢的是上传图走一遍 VGG16那两三秒是正常的前端需要给个 loading。另外阈值控制也是这里的一个隐藏需求相似度低于 0.6 的结果宁可不要硬凑进去反而让演示显得不靠谱。4. Flask 服务与前端页面query_online.py 把离线检索变成在线系统4.1 query_online.py上传、提取、检索的接口设计项目里有 templates 和 static 目录说明后端是 Flask 渲染模板不是前后端分离。query_online.py 是 Flask 入口承担上传图片、调用特征提取、检索、渲染结果页整个链路。# query_online.pyFlask 核心路由 from flask import Flask, request, render_template import numpy as np import os app Flask(__name__) extractor VGG16FeatureExtractor() # 全局初始化一次 index_mat np.load(index_matrix.npy) index_names open(index_names.txt).read().splitlines() app.route(/, methods[GET, POST]) def index(): if request.method POST: f request.files[query_img] save_path os.path.join(uploads, os.path.basename(f.filename)) f.save(save_path) feat extractor.extract(save_path) # 提取查询特征 results retrieve(feat, index_mat, index_names, top_k5) return render_template(show.html, resultsresults) return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)逻辑说明extractor 和索引矩阵在模块加载时初始化一次避免每次请求重新加载权重os.path.basename 处理上传文件名防止 Windows 下带路径前缀的坑render_template 把检索结果传给 show.html 渲染。参数说明uploads 目录需要提前创建不然后台静默报错debugFalse 是线上习惯课程演示开不开随意。templates 里有 index.html、show.html、classify.html、bar.html、editor.htmlstatic 下有 bootstrap、jquery、dropzone.js、flexslider说明前端是 Bootstrap 风格加拖拽上传。base.html 是公共母板其余页面继承它Flask 渲染时模板路径默认指向 templates 目录不用额外配置。4.2 classify.py 分类模块与 classify.html 的配合除了检索系统还有分类。classify.py 在项目里负责给查询图或检索结果打标签。常见实现是检索出 Top-K 后对 K 个结果的类别投票或者单独训练服装类别分类器。对于这套资源最简单可行的方案是复用检索特征做 KNN 式投票# classify.py用检索到的 Top-K 做类别投票 def classify_by_vote(results_with_labels, k5): votes {} for name, label in results_with_labels[:k]: votes[label] votes.get(label, 0) 1 return max(votes, keyvotes.get)逻辑说明每条检索结果带一个类别标签来自索引时的标注文件取前 K 个做票数统计票数最多的作为查询图的类别。参数 k 与检索 top_k 一致K 太小投票不稳K 太大容易混入尾部噪声。这个方案零训练成本很适合课程设计里的「分类」功能演示。clothes2 目录里是 id_00000051_01_1_front.jpg 这种命名是 DeepFashion 风格的数据集id 是服装 ID后面是视角front/back。索引时把视角字段切出来当标签classify.py 就能直接判断「这是正面还是背面」演示效果很直观。注意这里的「分类」和前面说的「不训练分类器」不冲突——检索做主链路分类用投票做辅助输出共用同一套特征不用额外训练。4.3 模板与静态资源base.html、index.html、show.html 的渲染链路模板渲染问题最常见的现象是图片不显示。Flask 模板里引用静态资源必须走 url_for(static, filename...)路径写死相对路径放到子目录或二级路由时会断掉。show.html 大概长这样{% for item in results %} div classcard img src{{ url_for(static, filenameimg/ item[0]) }} alt{{ item[0] }} p相似度{{ %.3f % item[1] }}/p /div {% endfor %}逻辑说明Flask 把 results 列表传进模板循环渲染每张结果图。这里必须有 url_for否则浏览器会从当前路由拼路径大概率 404。参数说明相似度格式化到三位小数方便答辩讲解。index.html 里的上传组件用的 dropzone.js它默认走 ajax 提交如果路由只支持 form 表单需要配置 acceptedFiles 和 url 参数。static/img 目录对应展示图片目录一种做法是把上传图缓存到 static/img 下前端直接引用另一种是传 base64 字符串。前者实现简单缺点是垃圾文件只增不减我一般加一个定时清理 uploads 的逻辑。bar.html 和 editor.html 这俩页面从命名看像是图表展示和结果微调入口bar 可能用于显示相似度分布属于加分项不影响主流程。另外Flask 的 host 参数建议设 0.0.0.0这样局域网里手机和另一台电脑都能访问演示页面比默认 127.0.0.1 实用得多。5. 避坑指南服装检索项目最容易翻车的五个现场5.1 预处理不一致离线提取和在线查询各写一套现象离线索引和在线查询用的是同一份代码但检索结果像随机排序查询图本身都排不进前三。原因最常见的是离线批量脚本里自己写了预处理在线路由里又改了一版两版在尺寸、通道顺序或归一化上差了一个细节另一个原因是 Keras 老版本里 image.load_img 和 cv2.resize 的插值方式不同特征对不上。解决把预处理收敛成一个函数离线、在线都调同一个 preprocess(img_path)不要各写各的。改完预处理逻辑后做一次指纹验证对同一张图分别走离线预处理和在线预处理对比 np.array_equal 是否完全一致不一致就说明两套管路没对齐。5.2 Keras / TensorFlow 版本不匹配import 直接失败现象按 README 装完依赖运行 extract_cnn_vgg16_keras.py 报错常见是 keras.backend 找不到、tf 内部 AttributeError或者 get_layer(fc6) 报层名不存在。原因项目源码在老环境里写的Keras 从独立包切到 tf.keras 后VGG16 的层名在不同版本有差异还有一种情况是装了新版 Keras 3.x不再带 applications.vgg16 模块。解决先看 import 语句是 from keras.applications.vgg16 还是 from tensorflow.keras.applications.vgg16再匹配环境。我习惯用 python 3.8 tensorflow 2.4 keras 2.4 的组合VGG16 层名兼容性最好。权重文件提前放到 ~/.keras/models/ 下避免启动时现场下载卡住。项目自带的 debug.log 值得先看一眼里面很可能记录了原环境的报错轨迹。5.3 特征文件与图像列表顺序错位现象检索返回的图名和图内容对不上点开一看是另一件衣服。原因索引阶段用 os.listdir 遍历后没有排序文件系统层面顺序不保证或者特征矩阵里混入了 (1, 4096) 的二维数组vstack 后维度和 names 数量不等。解决遍历目录一律 sorted()保存前统一 feat.reshape(-1)。索引重建后打印矩阵行数和 names 行数两个数相等再继续。这是我印象里最便宜的检查能省掉一个晚上的排查。5.4 批处理内存与性能失衡现象提取特征时把几千张图一次性堆进 np.vstack内存直接吃满机器卡死或者 batch_size 设太大CPU 假死。原因每张图预处理后是 (1, 224, 224, 3) 的 float32 数组3000 张就超过 1.8GB全堆在内存里显然不现实batch 太大会触发 CPU 资源争抢。解决批量提取按 32 张一组组内 vstack 喂给 model.predict特征结果落盘后原始图像数组及时释放。想观察内存峰值可以用 resource 模块打印控制在物理内存一半以内比较保险。5.5 灰度图、透明 PNG 和损坏图片现象某些图跑预处理时报错或者检索出的图被拉伸变形。原因数据集混了灰度图单通道、带 alpha 的 PNG或者尺寸极端的横图。image.load_img 能自动转 RGB但 cv2.imread 读灰度图默认返回 (H, W) 形状通道维对不上 VGG16 输入。解决预处理第一步统一转 RGB用 PIL 的 image.convert(RGB) 兜底PNG 透明度直接忽略 alpha 通道。对损坏图片加 try-except记录文件名跳过别让一张坏图中断整个提取流程。我在批量脚本里固定加一个 skip 列表处理完打印失败清单。6. 进阶用法用离线批量验证把 Top-K 变成可信的评估结果检索系统最怕的不是跑不起来而是「看起来能出结果」但不知道准不准。把 retrieval_test.py 用起来构建一个最小的验证集准备一个 query 目录放 20 张查询图再维护一张期望命中表 query_to_target.txt每行是查询图名和目标同类图名。批量跑检索后统计 Top-1 准确率和 Top-5 召回率# evaluation.pyTop-K 召回统计 def evaluate(query_dir, gt_file, index_mat, index_names, top_k5): gt dict(line.strip().split() for line in open(gt_file)) hits 0 for qname in sorted(os.listdir(query_dir)): if qname not in gt: continue res retrieve(load_feat(qname), index_mat, index_names, top_ktop_k) top_names [name for name, _ in res] if gt[qname] in top_names: hits 1 print(fTop-{top_k} recall: {hits}/{len(gt)} {hits / len(gt):.2%})跑一次就知道相似度 0.9 的到底是真同款还是仅撞色阈值卡在 0.7 还是 0.8 合适。我见过最典型的现象是 Top-1 相似度 0.95 但完全是另一件衣服——VGG16 的 fc6 特征对颜色特别敏感纯色同色系很容易误命中。查这种 case 时把命中的相似度分布画一下能看清阈值边界在哪。从那以后我每次改预处理或者换特征层都强制走一遍这个批量验证流程解压这份 python 源码项目说明后建议先把 README 对一遍环境依赖再按目录把图像数据放进 clothes2跑通后补上这个评估脚本你就知道这套索引到底值不值得信任。检索代码再简单验证环节不能省。希望帮到你。本文还有配套的精品资源点击获取