ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于多特征融合与Milvus的图像检索系统源码解析:从LBP、GLCM到VGG的完整实践

基于多特征融合与Milvus的图像检索系统源码解析:从LBP、GLCM到VGG的完整实践

简介:这是一套面向计算机、通信、人工智能、自动化等专业学生与从业者的Python图像检索项目源码,基于多特征融合思路实现,可作为毕业设计、期末课程大作业或进阶练手项目。压缩包共68个文件,约2.03MB,以47个py源码文件为核心,辅以12个png流程与界面截图、4个md说明文档及txt、yml、ini等配置与依赖文件,结构清晰便于按模块阅读。项目围绕颜色、LBP、GLCM、VGG等特征提取与融合检索展开,包含engine、dao、core、api、tests等目录,覆盖特征管理、余弦相似度计算、Milvus向量检索、JWT鉴权与接口测试等环节,并配有docker-compose部署配置。已有142人学习,适合基础较好的读者在此基础上修改调整,实现不同检索功能。

1. 从一次答辩 98 分的图像检索项目说起:这套 Python 多特征融合源码到底能跑出什么

如果你正在做图像检索方向的毕业设计或期末大作业,大概率会遇到一个尴尬局面:用单一颜色直方图检索,换一张背景稍复杂的图,返回结果就全乱套;想上深度学习,VGG 权重下载和显存又卡住进度。这套基于多特征融合的图像检索系统源码,恰好卡在“能跑通”和“有技术含量”之间——它把颜色、纹理、形状三类底层特征分别提取后做加权融合,再配合 Milvus 向量库完成相似度检索,整套流程用 Python 串起来,附设计文档和接口说明。适合计算机、通信、人工智能方向的学生拿来复现、改参数、写论文,也适合想快速搭一个以图搜图原型的从业者。下面我按“资源是什么、怎么装、怎么调、坑在哪”的顺序拆一遍,你照着走能少熬几个通宵。

2. 多特征融合的检索链路:从 LBP、GLCM 到 Milvus 的完整数据流

2.1 为什么不是单一特征:颜色、纹理、形状各自的失效边界

先讲清楚选型理由,否则你改代码时不知道哪一层能动能删。图像检索的本质是把图片映射成一个向量,再在向量空间里算距离。单一特征的问题在于适用场景太窄:颜色直方图对旋转和缩放不敏感,但光照一变、背景一换,相似度就崩;LBP 擅长刻画局部纹理,对布料、石材这类重复纹理有效,可遇到大面积纯色就退化成噪声;GLCM 通过灰度共生矩阵统计像素对的空间关系,能补上纹理的周期性信息,但计算量随灰度级数平方增长;形状特征(常见做法是 Hu 矩或边缘方向直方图)对轮廓敏感,可一旦目标被遮挡就失效。

这套源码的思路是并行提取、归一化后加权拼接。你打开core/目录能看到color.py、lbp.py、glcm.py、vgg.py四个特征提取器,fusion.py负责把多路特征拼成一个长向量。VGG 那一路是深度特征,用来兜底语义层面的相似性——这也是它能拿高分的原因:底层特征保证可解释性,深度特征保证检索质量。

提示:融合权重不是拍脑袋定的。conf/conf.ini里一般会有color_weight、texture_weight、deep_weight这类配置项,答辩时老师大概率会问你权重怎么来的,提前想好“按验证集检索准确率网格搜索”这个说法。

2.2 环境搭建:requirements 与 docker-compose 两条路

拿到源码第一步不是急着python app.py,而是先把依赖和向量库理清楚。项目根目录有requirements.txt和docker-compose.yml,说明作者给了两种部署姿势。我一般推荐先用 docker-compose 把 Milvus 拉起来,再在宿主机跑 Python 应用,这样向量库的坑和 Python 环境的坑能分开排查。

# 1. 创建虚拟环境,Python 3.8 兼容性最稳 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 2. 安装依赖,建议加国内镜像加速 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 3. 启动 Milvus 向量库(docker-compose 里通常含 etcd 和 minio) docker-compose up -d # 4. 确认容器状态,milvus-standalone 要是 healthy docker ps

这段命令的逻辑是:虚拟环境隔离依赖,避免和你机器上已有的 numpy、opencv 版本打架;requirements.txt里通常锁定了pymilvus、opencv-python、torch(或tensorflow)、flask这几类包;docker-compose 负责把 Milvus 及其依赖的 etcd(元数据)、minio(对象存储)一起拉起来。参数上,如果你机器内存小于 8G,Milvus standalone 可能起不来,这时候要么加内存,要么改用 Milvus Lite 做本地测试——但源码默认连的是标准版,改连接方式要动dao/milvus.py。

2.3 特征提取与入库:engine.py 和 dao 层怎么配合

环境好了之后,核心动作是“把图片库里的每张图变成向量存进 Milvus”。这条链路在core/engine.py和dao/feature.py、dao/milvus.py里。engine.py负责调度特征提取器,dao层负责持久化。你可以在tests/engine_test.py里看到作者怎么测的,照着改就能批量入库。

# 伪代码示意 core/engine.py 的典型结构 from core.color import ColorFeature from core.lbp import LBPFeature from core.glcm import GLCMFeature from core.vgg import VGGFeature from core.fusion import fuse_features class RetrievalEngine: def __init__(self, config): self.color = ColorFeature() self.lbp = LBPFeature() self.glcm = GLCMFeature() self.vgg = VGGFeature() self.weights = config['fusion_weights'] # 从 conf.ini 读 def extract(self, img_path): img = load_image(img_path) feats = { 'color': self.color.extract(img), 'lbp': self.lbp.extract(img), 'glcm': self.glcm.extract(img), 'vgg': self.vgg.extract(img), } # 归一化后按权重拼接,返回定长向量 return fuse_features(feats, self.weights)

逻辑说明:每个特征提取器返回一个 numpy 数组,fuse_features先做 L2 归一化(防止某一维数值过大主导距离),再按权重加权拼接。参数上,fusion_weights是字典,键对应四路特征,值加起来建议为 1。如果你把vgg权重设得过高,检索会偏向语义相似但视觉差异大的图;设得过低,又退化成传统方法。常见做法是先用等权跑一遍看基线,再微调。

入库时dao/milvus.py会建 collection、定义向量维度、插入数据。注意向量维度必须和fuse_features输出长度一致,改特征就得同步改 collection schema,否则插入报维度不匹配——这是新手最容易翻车的地方。

3. 接口与检索服务:Flask 路由、JWT 鉴权与相似度计算怎么串

3.1 app.py 与 route.py:一次以图搜图的请求生命周期

服务层用 Flask 搭,入口是app.py,路由集中在api/route.py。一次检索请求的路径大致是:客户端上传图片 → 路由接收 → 校验参数 → 调 engine 提特征 → 调 dao 查 Milvus → 算相似度 → 返回结果列表。utils/validators里放着参数校验逻辑,utils/jwt.py管鉴权。

# api/route.py 里检索接口的典型写法 from flask import Blueprint, request, jsonify from utils.jwt import token_required from core.engine import RetrievalEngine from dao.milvus import MilvusDAO bp = Blueprint('search', __name__) engine = RetrievalEngine(config) dao = MilvusDAO(config) @bp.route('/api/search', methods=['POST']) @token_required def search(): file = request.files.get('image') top_k = int(request.form.get('top_k', 10)) if not file: return jsonify({'code': 400, 'msg': 'no image'}), 400 vec = engine.extract_from_stream(file) results = dao.search(vec, top_k=top_k) return jsonify({'code': 0, 'data': results})

逻辑说明:@token_required装饰器从请求头取 JWT 校验,没带 token 直接 401;top_k控制返回条数,默认 10,调大能看更多候选但响应变慢;extract_from_stream是流式读取上传文件,避免落盘。参数上,top_k和 Milvus 的nprobe(搜索时探测的聚类数)共同决定召回率和延迟,nprobe越大越准越慢,一般在dao/milvus.py的 search 方法里设。

3.2 相似度计算:cosine.py 里的距离度量与排序

检索结果排序靠utils/cosine.py或dao/cosine.py。余弦相似度衡量两个向量方向的一致性,取值 -1 到 1,越接近 1 越相似。Milvus 本身支持内积、欧氏距离、余弦等度量方式,源码里如果用的是内积,记得向量要先归一化,否则内积等价于带模长的余弦,排序会偏。

import numpy as np def cosine_similarity(a, b): # 输入已 L2 归一化时,点积即余弦 a = a / (np.linalg.norm(a) + 1e-8) b = b / (np.linalg.norm(b) + 1e-8) return float(np.dot(a, b)) def rank_results(query_vec, candidates): scored = [(cid, cosine_similarity(query_vec, vec)) for cid, vec in candidates] return sorted(scored, key=lambda x: x[1], reverse=True)

逻辑说明:加1e-8是防止零向量除零;rank_results按相似度降序排。参数上,如果你发现返回结果里明显不相关的图排前面,先检查归一化是否做了,再检查融合权重。常见误用是直接拿未归一化的 VGG 特征和颜色特征拼接,导致数值大的那一路主导距离——这是血泪经验,别问我怎么知道的。

3.3 测试用例怎么用:tests 目录是你的回归安全网

tests/下有dao_feature_test.py、cosine_test.py、engine_test.py、milvus_test.py、jwt_test.py、app_api_search_test.py等。这些不是摆设,是你改代码后的后悔药。建议按依赖顺序跑:先milvus_test.py确认向量库通,再engine_test.py确认特征提取通,最后app_api_search_test.py确认端到端通。

# 按依赖顺序跑测试,pytest 会自动发现 test_ 开头的文件 pytest tests/milvus_test.py -v pytest tests/engine_test.py -v pytest tests/app_api_search_test.py -v

逻辑说明:-v输出每个用例名,失败时能定位到具体断言。如果milvus_test就挂了,别往下跑,先解决连接问题。参数上,测试里通常连的是本地 Milvus,端口默认 19530,如果你改了 docker-compose 的端口映射,记得同步改conf/conf.ini。

4. 避坑与排查:这套源码跑不起来时先看这五条

4.1 现象:pip install 卡在 torch 或 opencv 编译

原因:requirements.txt里如果没锁版本,pip 可能拉最新版,而最新版 torch 对 Python 版本和 CUDA 有要求,opencv 在某些系统上要源码编译。解决:先确认 Python 版本是 3.8 或 3.9,然后手动指定版本安装,比如pip install torch==1.13.0 opencv-python==4.7.0.72。如果不需要 GPU,装 CPU 版 torch 能省一大半时间。

4.2 现象:Milvus 容器起来了但应用连不上

原因:docker-compose 默认网络和宿主机端口映射可能不一致,或者 etcd、minio 没就绪 Milvus 就退出了。解决:docker logs milvus-standalone看日志,确认 19530 端口映射正确;在conf/conf.ini里把 host 改成127.0.0.1、port 改成映射出来的端口。如果内存不足,Milvus 会反复重启,docker stats看一眼内存占用。

4.3 现象:检索结果全是同一张图或顺序固定

原因:向量没真正入库,或者查询时 collection 没 load。Milvus 查询前需要collection.load(),如果代码里漏了这步,search 会返回空或异常。解决:检查dao/milvus.py的初始化流程,确认建 collection、插入、load 三步都执行了。另一个可能是特征提取返回了全零向量,归一化时被1e-8兜住,导致所有图相似度一样。

4.4 现象:上传图片报 413 或 500

原因:Flask 默认请求体大小限制约 16M,大图上传被拒;或者extract_from_stream读流时图片格式不支持。解决:在app.py里设app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024;格式上确认 PIL 或 cv2 能解码,常见做法是统一转成 RGB 再提特征。

4.5 现象:改了融合权重但检索质量没变化

原因:权重配置没被真正读取,或者改了conf.ini但服务没重启。解决:在engine.py初始化处打日志确认权重值;Flask 开发模式下改配置要重启进程。另外,如果 VGG 那一路特征维度远大于其他路,权重微调会被维度差异淹没,常见做法是先对各路特征做 PCA 降维再融合。

5. 进阶玩法:把融合权重调成可学习参数,顺带验证检索质量

基础跑通之后,这套源码最值得动的地方是core/fusion.py。默认的加权拼接是人工定权,你可以把它改成可学习的形式:给每路特征配一个可训练权重,用三元组损失(triplet loss)在验证集上优化。这样答辩时能讲出“从手工特征融合到度量学习”的演进,比单纯调参有说服力。

import torch import torch.nn as nn class LearnableFusion(nn.Module): def __init__(self, dims): super().__init__() # 每路特征一个可学习标量权重,初始为 1 self.weights = nn.Parameter(torch.ones(len(dims))) self.dims = dims def forward(self, feats): # feats: list of tensors, 每路已归一化 w = torch.softmax(self.weights, dim=0) # 保证权重和为 1 return torch.cat([f * w[i] for i, f in enumerate(feats)], dim=-1)

逻辑说明:softmax保证权重非负且和为 1,避免某一路被完全压死;nn.Parameter让权重参与反向传播。训练时用三元组损失,锚点和正样本是同一类图,负样本是不同类图,目标是让锚点与正样本距离小于锚点与负样本距离加 margin。参数上,margin 一般设 0.2 到 0.5,学习率别太大,1e-3 起步。

验证检索质量别只看“看起来对不对”,要有量化指标。常见做法是算 mAP(平均精度均值)和 Top-K 召回率。你可以从图片库里按类别抽查询图,人工标注相关结果,跑一遍算指标。

指标含义调优方向
Top-1 准确率第一条结果就相关的比例提升深度特征权重
Top-10 召回率前十条里相关图占比增大 top_k 和 nprobe
mAP所有查询的平均精度均值融合权重与度量学习联合优化
响应延迟单次检索耗时降维、减少 nprobe、加缓存

我自己的习惯是:每次改完融合逻辑,先跑tests/engine_test.py确认没崩,再抽 20 张查询图算一遍 Top-10 召回,和改之前的数字对比。没有这个对比,你根本不知道改动是优化还是玄学。从那以后我每次动特征层都强制走一遍这个流程,省得答辩时被问“你怎么证明变好了”答不上来。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进