Python以图搜图实战:VGG16特征提取+PyQt桌面工具开发 简介基于Python和VGG16预训练模型构建的以图搜图软件带PyQt界面输入一张图片即可在本地图库中检索相同或相似图片并展示支持对本地目录图片进行离线索引与实时查询适合批量图片管理、相似图去重、个人相册检索等应用场景。资源包约331MB包含完整项目源码、测试图片、使用文档、配置文档以及一键爬取百度图片的脚本源码附带详细注释同时打包了Python 3.7.2与VS Code相关环境素材方便按文档从环境配置到运行检索完整走通。已有2708人学习使用通过该资源既能深入理解VGG16特征提取与相似度匹配原理也能学习PyQt界面搭建方式更适合在此基础上扩展图像检索功能也可用于课程设计与毕业设计参考。1. 以图搜图到底怎么搜先用 VGG16 给每张图生成“指纹”做个人照片库整理时我碰到过一段很狼狈的经历存了上万张壁纸和截图想找一张“记得大概样子但记不清文件名”的图翻了半天一无所获。后来我决定自己做一个类似百度识图的本地工具输入一张图去图片库找出相同或相似的图片用 PyQt 搭界面用 VGG16 预训练模型做特征提取。这套“python_以图搜图_pyqt_vgg16”方案就是这样跑通的它不依赖云端接口模型权重离线加载能让你彻底掌控从特征提取到检索展示的完整链路。它解决的核心问题很简单人眼能一眼看出两张图“像”但计算机面对的是像素矩阵必须先把它压缩成一个可比较的向量再在这个向量空间里算距离。这套方案把一张张图片变成固定长度的特征向量入库、查询、排序最后在 PyQt 界面里展示 Top N 结果。适合谁正在学视觉检索的开发者、想用预训练模型落地一个完整项目的学生以及只想把手头图片库整理明白的普通 Python 爱好者。2. VGG16 做特征提取为什么选预训练模型而不是自己训网络2.1 特征到底取哪一层从卷积层到全连接层的取舍VGG16 是 2014 年 ImageNet 比赛上的经典网络结构不复杂13 个卷积层加 3 个全连接层但它的迁移学习效果非常稳定。拿来即用的关键原因是预训练权重已经在 ImageNet 上见过上千万张图学到的边缘、纹理、形状等低级特征和轮廓、部件等高级语义特征可以被复用到任意图像任务上。自己从零训练一个像样的分类网络动辄几天而迁移学习只需要前向推断一次这就是选它的最大理由。但“用 VGG16”不等于“用整个 VGG16”。不同层输出的语义粒度和维度差别很大直接影响检索效果和检索速度。下面是我实测下来常用的特征层选择对比特征层输出维度语义粒度检索表现备注block3_pool28×28×256纹理、局部边缘对颜色敏感误检率高可用于风格近似检索block5_pool7×7×512物体部件、轮廓中等粒度泛化好需做池化压缩配合 PCA 加速常用fc14096全局语义偏抽象对“同类不同物”区分好参数量大内存占用高fc24096全局语义更抽象相似度排序较稳本方案采用适合中等规模图库我一般会优先用 fc2 输出因为它是全连接层已经把卷积特征压扁成全局描述子对缩放、平移、轻微旋转的容忍度比卷积层高。截取层的写法很直接用 Keras 的函数式 API 从原模型尾部切一刀from tensorflow.keras.applications import VGG16 from tensorflow.keras.models import Model base_model VGG16(weightsimagenet, include_topTrue) feature_model Model(inputsbase_model.input, outputsbase_model.get_layer(fc2).output) # 测试一张图特征形状应该是 (1, 4096) import numpy as np dummy np.random.rand(1, 224, 224, 3).astype(float32) feature feature_model.predict(dummy) print(feature.shape)这段代码把 VGG16 原模型加载进来然后从fc2层截断构造一个新的feature_model。注意include_topTrue因为我们要用到全连接层不能像做分类微调那样丢弃顶层。get_layer(fc2)拿到的就是第二个全连接层。测试时用随机数据走一遍前向推断确认输出维度是(1, 4096)说明模型通路已经打通。2.2 相似度计算余弦相似度比欧氏距离好在哪有了特征向量剩下的问题是怎么判断“像不像”。欧氏距离算的是向量在特征空间中的直线距离但 VGG16 提取的特征向量各维度的数值范围差异很大有的维度接近几百有的只有几十直接用欧氏距离会被大数值维度主导。余弦相似度计算的是两个向量的夹角余弦值更关注方向一致性对特征的绝对大小不敏感所以更像是“语义是否对齐”。实际检索时我会对特征向量先做 L2 归一化再做矩阵乘法一次算出查询向量与库中全部向量的余弦相似度def cosine_similarity(query_feat, library_feats): # library_feats: (N, 4096) 归一化后的特征矩阵 query_norm query_feat / np.linalg.norm(query_feat) lib_norm library_feats / np.linalg.norm(library_feats, axis1, keepdimsTrue) scores np.dot(lib_norm, query_norm.reshape(-1, 1)).flatten() return scores这里先把查询向量和库向量都做了 L2 归一化np.dot(lib_norm, query_norm)得到的就是一个长度为 N 的余弦相似度向量。归一化的目的是让计算出来的分数严格落在 -1 到 1 之间方便后面统一用阈值过滤。keepdimsTrue很关键保证广播时维度对齐不然np.linalg.norm对二维矩阵会输出形状(N,)除矩阵时会有隐患。2.3 构建特征库npy 矩阵存向量路径列表存索引以图搜图的“图库”本质上不是一个图片文件夹而是一个“特征矩阵 路径索引”的组合。图片本身存在磁盘上特征矩阵用npy文件保存这样启动时只需要加载一个几 MB 到几十 MB 的矩阵不用把成百上千张图片全部读进内存。import numpy as np import json, os def build_library(feature_model, image_dir, save_npyfeatures.npy, save_jsonpaths.json): feats [] paths [] for fname in sorted(os.listdir(image_dir)): fpath os.path.join(image_dir, fname) img load_and_preprocess(fpath) # 统一缩放为 (224, 224, 3) feat feature_model.predict(img)[0] # 单张特征 feats.append(feat) paths.append(fpath) feats np.array(feats) np.save(save_npy, feats) with open(save_json, w, encodingutf-8) as f: json.dump(paths, f, ensure_asciiFalse) return feats, paths我先遍历图库目录对每张图调用统一的预处理函数再经过特征模型得到 4096 维向量。收集完所有向量后合并成二维矩阵把路径列表单独存成 JSON这样后续检索时按相似度分数下标就能直接定位图片文件。注意特征矩阵和路径列表的索引顺序必须严格一一对应一旦出现过删改图片的情况最好重新生成一次特征库不要手动拼 JSON。3. 把项目跑起来环境配置、爬虫脚本与批量特征提取3.1 环境搭配Python 3.7.2 VSCode PyQt5 TensorFlow这个项目里自带的 Python 版本是 3.7.2我建议你直接用它因为在 TensorFlow 2.x 和 PyQt5 的兼容性组合里3.7 系是最省心的区间。VSCode 做编辑和调试也够用装好 Python 扩展之后F5 就能跑起来。关键依赖大概是下面这张表依赖库推荐版本用途tensorflow2.6.2加载 VGG16 预训练模型前向推断numpy1.19.5特征矩阵运算PyQt55.15.4桌面界面opencv-python4.5.3.56读取和处理图片requests2.26.0爬取百度图片pillow8.3.2格式兼容处理尤其处理 RGBA 图安装命令用 pip 一把梭就行pip install tensorflow2.6.2 numpy1.19.5 PyQt55.15.4 opencv-python4.5.3.56 requests2.26.0 pillow8.3.2这里我想提醒一个点Python 3.7 下不要装最新版 numpy 和 tensorflow新版 numpy 在高版本上会出现 API 删改而且 tensorflow 2.10 之后就不再提供 3.7 的 wheel 包了。如果你在 VSCode 里配置环境时选错了解释器跑起来会直接报No module named tensorflow那多半是解释器选成了全局环境而不是项目虚拟环境不是包没装成功。3.2 一键爬取百度图片requests 拼接 URL 与 UA 伪装资源包里带的爬虫脚本能按关键词批量拉取百度图片本质上是请求百度图片的公开接口拿到 JSON 再解析图片地址。这个脚本非常适合快速构造图库但要注意它只能用于个人学习别拿去扒付费图库也要遵守目标网站的 robots 协议。import requests import json import os def crawl_baidu_images(keyword, save_dirimages, max_num50): os.makedirs(save_dir, exist_okTrue) url https://image.baidu.com/search/acjson headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://image.baidu.com/ } count 0 for page in range(max_num // 30 1): params { tn: resultjson_com, word: keyword, pn: page * 30, rn: 30, } resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() for item in data.get(data, []): img_url item.get(thumbURL) or item.get(hoverURL) if not img_url: continue try: img_resp requests.get(img_url, headersheaders, timeout10) ext img_url.split(.)[-1].split(?)[0] or jpg fname os.path.join(save_dir, f{keyword}_{count}.{ext}) with open(fname, wb) as f: f.write(img_resp.content) count 1 if count max_num: return except Exception as e: print(f下载失败: {img_url}, {e})这段爬虫的核心是百度图片搜索接口的acjson参数里word是搜索词pn是翻页偏移量rn是每页条数。拿到 JSON 后优先取thumbURL作为缩略图地址取不到再退而求其次用hoverURL。下载时我把图片内容直接以二进制写入文件扩展名从 URL 里推断split(?)[0]是为了去掉 URL 里的查询参数。注意加Referer因为百度会对裸请求做防盗链检查没有 Referer 很容易返回空列表。3.3 批量提取特征并保存特征库预处理函数必须严格统一图库图片来源五花八门有 jpg、png也有 webp尺寸从几百像素到几千像素都有。如果每张图预处理方式不一样后续检索必然出问题。我在代码里把预处理单独抽成一个函数保证入库和查询走同一条路径。import cv2 def load_and_preprocess(fpath, target_size(224, 224)): img cv2.imread(fpath) if img is None: raise ValueError(f图片读取失败: {fpath}) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # OpenCV 默认 BGR模型要 RGB img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) img img.astype(float32) # VGG16 的 preprocess_input 会做 RGB-BGR 并且减均值 img img[..., ::-1] # RGB 转回 BGR 顺序 img - [103.939, 116.779, 123.68] return img.reshape(1, 224, 224, 3)这里有个非常典型的坑cv2.imread读出来的图像是 BGR 通道顺序而 VGG16 预训练权重是在 RGB 图像上训练的。我先把 BGR 转成 RGB再送进模型但 Keras 的preprocess_input内部还会再做一次 BGR 转换和减均值操作所以我干脆手动模拟这个流程保证输入分布和预训练时一致。缩放的插值方式我选了INTER_AREA它对缩小图像更友好不容易出现摩尔纹。如果你发现检索结果“看着差不多但总差一点”十有八九就是通道顺序或者均值减法这里出了问题。4. PyQt 界面实战把检索流程封装成“百度识图”式桌面工具4.1 界面结构左侧选图、右侧结果列表PyQt 界面我拆得比较克制顶部是一个选择图片的按钮和当前图片路径显示中间左侧放查询图片预览右侧放检索结果的缩略图列表底部是状态栏显示特征库规模、检索耗时等信息。结果展示用QListWidget设置成图标模式每项上面显示缩略图下面用文本标注相似度分数。from PyQt5.QtWidgets import QListWidget, QListWidgetItem from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap, QIcon class ResultList(QListWidget): def __init__(self, parentNone): super().__init__(parent) self.setViewMode(QListWidget.IconMode) self.setIconSize(150, 150) self.setResizeMode(QListWidget.Adjust) self.setSpacing(8) def add_result(self, img_path, score): item QListWidgetItem() pixmap QPixmap(img_path) item.setIcon(QIcon(pixmap.scaled(150, 150, Qt.KeepAspectRatio))) item.setText(f{score:.3f}) self.addItem(item)IconMode让列表按网格排列缩略图setIconSize((150, 150))统一格子的图片尺寸setResizeMode(Adjust)让窗口拉大时列表自动重排。我故意把分数显示在文本上这样你能直观看到相似度排序的梯度如果结果分数普遍都在 0.6 以下说明图库内容比较杂或者需要调特征层。图片加载用QPixmap直接读文件路径对 png 和 jpg 都适用但如果图片是 CMYK 模式或 16 位深度这里可能显示异常需要在读图前用 Pillow 做一次转码。4.2 核心检索逻辑从点击按钮到结果展示界面按钮的槽函数承担了整个检索流程核心步骤是选图、预处理、提特征、算相似度、排序展示。这里我不建议把特征库加载也放在按钮事件里否则点击一次就要等好几秒体验很差而是放到程序初始化时后台加载。import sys import numpy as np from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QFileDialog, QLabel, QVBoxLayout, QWidget class SearchWindow(QMainWindow): def __init__(self, feature_model, features, paths): super().__init__() self.feature_model feature_model self.features features # (N, 4096) 特征矩阵 self.paths paths # 路径列表 self.btn QPushButton(选择查询图片, self) self.btn.clicked.connect(self.on_search) self.preview QLabel(查询图预览, self) self.result_list ResultList(self) layout QVBoxLayout() layout.addWidget(self.btn) layout.addWidget(self.preview) layout.addWidget(self.result_list) container QWidget() container.setLayout(layout) self.setCentralWidget(container) def on_search(self): fpath, _ QFileDialog.getOpenFileName(self, 选择图片, , Images (*.png *.jpg *.jpeg *.webp)) if not fpath: return pixmap QPixmap(fpath) self.preview.setPixmap(pixmap.scaled(300, 300, Qt.KeepAspectRatio)) query_feat self.feature_model.predict(load_and_preprocess(fpath))[0] scores cosine_similarity(query_feat, self.features) top_idx np.argsort(scores)[::-1][:20] # 取前 20 个 self.result_list.clear() for idx in top_idx: self.result_list.add_result(self.paths[idx], scores[idx])on_search里先用文件对话框选图更新预览图然后提取查询特征调相似度函数得到全库分数。np.argsort(scores)[::-1]得到从大到小的排名取前 20 个结果展示。这里有个参数建议展示条数不要贪多20 到 30 张足够因为相似度排序越靠后参考价值越低还拖慢界面。load_and_preprocess必须和建库时用的是同一个函数否则查询特征和库特征分布不一致排序结果就是乱的。4.3 主线程卡顿问题用加载状态缓解启动等待PyQt 界面是事件循环驱动的如果在主线程里做特征库矩阵加载程序会短暂无响应。特征库矩阵(N, 4096)在 N 等于一万时大约是 150 MB加载和归一化都需要时间。常见做法是在窗口初始化时先显示“正在加载特征库”加载完成后再刷新状态。我在实际项目里会先用QApplication.processEvents()强制刷新界面然后再执行耗时加载至少让用户看到程序没有死掉。更彻底的方案是QThread不过对小规模图库来说有点杀鸡用牛刀。如果你后面把图库规模做到几万张建议再上线程否则加载期间的界面白屏会被误认为崩溃。5. 常见问题与排查从“跑不起来”到“结果不对”的 5 个坑5.1 环境依赖没装全运行时提示找不到模块现象启动脚本后直接报ModuleNotFoundError: No module named PyQt5.sip或者ImportError: DLL load failed。 原因PyQt5 安装不完整或者 Python 解释器选错常见于 VSCode 里选择了全局 Python 而不是项目虚拟环境。PyQt5.sip是 PyQt5 的底层支撑模块pip 安装时崩溃或中断会导致它缺失。 解决先确认解释器路径再强制重装。命令行里where python看当前解释器然后跑pip install --force-reinstall PyQt5 PyQt5-sip。装完后在 Python 交互式环境里import PyQt5验证加载如果 DLL 报错优先检查是否装了 32 位 Python 而系统是 64 位换成 64 位解释器重新建虚拟环境。5.2 特征向量维度对不上检索时矩阵广播失败现象特征库构建成功但检索时np.dot报ValueError: operands could not be broadcast together with shapes (N, 512) (4096,)。 原因建库时用的特征层是block5_pool加池化后的 512 维查询时却用了fc2的 4096 维或者反过来。更隐蔽的原因是两个阶段分别加载了不同的模型文件一个include_topTrue一个include_topFalse。 解决在建库和查询的代码入口顶部统一加一个断言assert query_feat.shape[1] self.features.shape[1]维度不一致时立刻报错不要等矩阵运算炸掉。同时确认feature_model定义只出现一次查询时直接 import 建库时保存的同一个模型对象。5.3 相似图片排到后面检索结果“看着像但排序不对”现象图库里明明有同一场景的图结果却排到第十五名开外前面的结果五花八门。 原因全连接层特征虽然语义抽象但对图像中的背景占比非常敏感背景相似但主体不同的图得分反而高。另外如果特征没有做 L2 归一化余弦相似度计算会失真。 解决第一先对库特征和查询特征都强制 L2 归一化这步很多人会漏。第二如果归一化后还是结果偏移把特征层从fc2换到block5_pool然后做全局平均池化降成 512 维这一步能保留更多空间局部性对“相同场景不同角度”的召回更友好。我一般会写一个小脚本分别用两个特征层做同一批查询的排序肉眼对比前十条的准确率再决定用哪个。5.4 爬虫抓到一堆打不开的图片特征库被污染现象爬虫脚本跑完图片目录里多了很多文件名正常但打不开的文件OpenCV 读图返回 None建库时直接抛异常中断。 原因百度图片返回的thumbURL缩略图地址有时会失效请求回来的是一个 HTML 错误页但由于 HTTP 状态码是 200脚本不知道内容已经不是图片。另外部分重试逻辑会把同一个失败地址写进不同文件名重复污染目录。 解决在下载函数里加文件头校验不要只靠扩展名判断。图片文件的头部字节是固定的JPEG 以\xFF\xD8\xFF开头PNG 以\x89PNG开头。下载后先读前三个字节判断不匹配就直接删掉文件。这样能保证进入图库的每个文件都是可用图片。5.5 界面启动时卡顿点击按钮后窗口转圈现象程序双击启动后黑屏几秒加载完才好点“选择图片”时窗口像卡死一样标题栏显示“未响应”。 原因特征库矩阵加载、图片预处理和模型前向推断都在主线程执行阻塞了 PyQt 的事件循环。尤其是模型第一次predict时会触发权重加载和计算图优化耗时可能长达几秒到十几秒。 解决把模型加载和特征库加载挪到程序入口处在窗口显示之前完成让“卡顿”发生在出现界面之前而不是之后。检索按钮事件里只做特征提取和矩阵运算这两步在 CPU 上大约几百毫秒不会让用户察觉明显卡顿。如果图库规模上万再考虑用QThread把检索放到子线程通过信号把结果显示回主线程。6. 让检索更准的进阶技巧特征层对比与阈值验证前面说了fc2和block5_pool的区别这里给一个可复现的验证方法。我建议你拿测试图里的十张图分别设置成“库外查询图”每张去跑完整召回统计 Top 10 里真正相关的图片数量对比两个特征层的命中率。这个实验做一次比你凭感觉调参靠谱得多。# 对比脚本骨架用同一张查询图分别用两个特征模型做检索 models { fc2: feature_model_fc2, pool5: feature_model_pool5, } for name, model in models.items(): q_feat model.predict(load_and_preprocess(query_path))[0] scores cosine_similarity(q_feat, library_feats[name]) top_idx np.argsort(scores)[::-1][:10] print(f{name}: {top_idx.tolist()})另外我养成了一个习惯先用一张完全不在图库里的图片做查询看返回结果的相似度分数分布。如果最高分都在 0.8 以上说明图库和查询图来源高度一致阈值可以调高如果最高分只有 0.4 左右那要么查询图和图库主题差异太大要么特征层选取不合适。这个分数分布本身就是你的“指路牌”。工程上当好分数普遍太低时优先换特征层而不是去调排序算法因为排序算法的调整空间远没有特征层面的影响大。还有一个小技巧值得试如果图库规模超过两万直接用全量 4096 维特征做矩阵乘法单次查询大约几十毫秒但内存占用会逐渐变大。可以先用 PCA 降维到 256 维检索速度提升明显准确率下降可以接受。我之前就是在一万五千张图的新库里直接跑全量内存涨了快 400 MB后来做了 PCA 才把启动时间从 8 秒压到 3 秒。从那以后我每次做以图搜图都强制走一遍“查全率对比 分数分布检查”这两步这个习惯帮我少走了很多弯路。希望帮到你。本文还有配套的精品资源点击获取