资讯详情 深度学习毕设:遮挡视频行人重识别系统Python源码与GUI实现
📅 2026/10/10 23:05:18
简介这份资源是面向深度学习方向毕业设计与课程设计场景的遮挡视频行人重识别系统Python源码包适合具备一定深度学习基础、需要完成相关课题的学生与开发者使用。系统围绕视频输入与预处理、行人检测与特征提取、遮挡处理与特征增强、行人重识别匹配等环节展开并配有GUI界面便于直观操作与演示。压缩包共744个文件以715张jpg图像数据为主另含16个py源码文件、6个xml标注文件及若干txt说明、字体与工程配置文件整体约10.94MB结构紧凑、便于本地运行与二次开发。目前已有197人学习下载。读者可从中获得一套可运行的遮挡行人重识别完整方案涵盖视频帧提取、图像预处理、基于YOLO或Faster R-CNN的行人检测、ResNet等网络的特征提取以及注意力机制等遮挡特征增强思路同时可参考GUI交互逻辑与工程目录组织方式用于毕设复现、课设提交或算法改进实验。1. 遮挡视频行人重识别系统为什么它比普通 ReID 难一个量级行人重识别Person Re-IdentificationReID要解决的问题是给定一张目标行人的照片在其它摄像头拍到的画面里把这个人找出来。而遮挡视频行人重识别是在这个基础上再加两个约束——输入是视频序列而不是单帧图像且目标行人大概率被车、树、广告牌、其他行人挡住了一部分。这两个约束叠加之后难度不是线性上升而是直接换了一个量级。普通 ReID 在公开数据集上 rank-1 早就刷到 95% 以上但一放到真实监控场景里被遮挡的行人检索准确率经常掉到 50% 以下。原因很直接遮挡破坏了人体结构的完整性模型学到的全局特征被污染而视频又要求你在时间维度上做特征聚合遮挡帧如果权重给高了整个序列的特征就被带偏。这套「深度学习毕设-遮挡视频行人重识别系统python源码含GUI界面」要落地的正是把遮挡鲁棒性、时序聚合、可视化界面三件事串成一条能跑通的工程链路。这篇文章面向三类人正在做毕设、需要一套能跑通、能答辩、能讲清楚原理的系统已经做过普通 ReID、想补上遮挡和视频这两个短板以及想用 Python 快速搭一个带 GUI 的检索 Demo 的工程师。下面从数据、模型、训练、界面到排错按能复现的顺序讲。2. 遮挡视频 ReID 的数据组织与骨干选型先想清楚喂什么进去2.1 视频 ReID 的数据集结构和遮挡标注怎么处理视频 ReID 和图像 ReID 最大的区别在数据组织方式。图像 ReID 是(id, cam, image)三元组视频 ReID 是(id, cam, tracklet)一个 tracklet 是一段连续帧序列。常见的视频 ReID 数据集组织成这样的目录结构dataset/ ├── train/ │ ├── 0001/ # 行人ID │ │ ├── cam1/ # 摄像头编号 │ │ │ ├── 0001_00.jpg │ │ │ ├── 0001_01.jpg │ │ │ └── ... │ │ └── cam2/ │ └── 0002/ └── test/遮挡标注是这套系统的关键。公开数据集里带遮挡标注的不多常见做法是用人体关键点检测如 HRNet 或 OpenPose先跑一遍统计可见关键点比例低于阈值的帧标记为「遮挡帧」。我一般会写一个预处理脚本把每个 tracklet 的遮挡比例算出来存成 json训练时按帧采样权重import json import os import cv2 import numpy as np # 用关键点可见比例估计遮挡程度 def estimate_occlusion(keypoints, scores, vis_thr0.3): keypoints: (17, 2) 人体关键点坐标 scores: (17,) 每个关键点的置信度 返回: 可见关键点比例越低说明遮挡越严重 visible np.sum(scores vis_thr) return visible / len(scores) def build_occlusion_index(root, out_path): index {} for pid in sorted(os.listdir(root)): pid_dir os.path.join(root, pid) if not os.path.isdir(pid_dir): continue index[pid] {} for cam in sorted(os.listdir(pid_dir)): cam_dir os.path.join(pid_dir, cam) frames sorted(os.listdir(cam_dir)) ratios [] for f in frames: img cv2.imread(os.path.join(cam_dir, f)) # 这里接你的关键点检测器返回 kps 和 scores kps, scores run_pose_estimator(img) ratios.append(estimate_occlusion(kps, scores)) index[pid][cam] ratios with open(out_path, w) as fp: json.dump(index, fp) return index逻辑说明estimate_occlusion用可见关键点比例作为遮挡程度的代理指标比直接看像素遮挡更稳定因为它反映的是「人体结构还剩多少可辨识」。build_occlusion_index遍历整个数据集把每个 tracklet 每帧的遮挡比例存下来。参数上vis_thr0.3是经验值太低会把噪声关键点算成可见太高会误判正常帧为遮挡实际调的时候可以打印比例分布看直方图。提示如果数据集本身没有关键点标注这一步是必须自己补的。别跳过遮挡权重全靠它。2.2 骨干网络选型ResNet50-IBN 还是 ViT视频场景怎么选骨干网络决定了特征提取的上限。图像 ReID 里 ResNet50-IBN 是经典选择IBN 模块Instance-Batch Normalization对光照和风格差异更鲁棒。但视频 ReID 要考虑时序建模纯 CNN 骨干只能逐帧提特征时序聚合得另加模块。常见做法有三种方案骨干时序建模显存占用适合场景CNN 平均池化ResNet50-IBN帧特征平均低毕设快速跑通CNN 注意力聚合ResNet50-IBN时间注意力中遮挡场景推荐ViT 时空注意力VideoViT3D 注意力高有充足算力毕设场景我一般推荐第二种ResNet50-IBN 提帧特征加一个时间注意力模块做加权聚合。原因是显存友好单卡 8G 能跑而且时间注意力天然能压低遮挡帧的权重和遮挡问题正好对上。ViT 方案虽然理论上限高但训练数据量要求大毕设数据集往往撑不住容易过拟合。时间注意力的核心实现import torch import torch.nn as nn class TemporalAttention(nn.Module): def __init__(self, feat_dim, reduction8): super().__init__() # 用一个小 MLP 学每帧的重要性分数 self.attn nn.Sequential( nn.Linear(feat_dim, feat_dim // reduction), nn.ReLU(inplaceTrue), nn.Linear(feat_dim // reduction, 1) ) def forward(self, feat_seq, occlusion_ratioNone): feat_seq: (B, T, C) B个样本每个T帧每帧C维特征 occlusion_ratio: (B, T) 每帧遮挡比例可选 返回: (B, C) 聚合后的序列特征 scores self.attn(feat_seq).squeeze(-1) # (B, T) if occlusion_ratio is not None: # 遮挡越严重分数越低用 1-ratio 做软掩码 scores scores torch.log(1.0 - occlusion_ratio 1e-6) weights torch.softmax(scores, dim1) # (B, T) agg torch.bmm(weights.unsqueeze(1), feat_seq).squeeze(1) return agg, weights逻辑说明attn是一个两层 MLP把每帧特征映射成一个标量分数。occlusion_ratio作为先验加进 logits遮挡比例高的帧分数被压低softmax 之后权重自然小。参数reduction8控制中间层维度太大容易过拟合太小表达能力不够8 是个平衡点。返回的weights可以拿来做可视化答辩时能直接展示模型「看」了哪几帧。3. 训练策略与损失函数遮挡样本怎么不被当成噪声丢掉3.1 三元组损失在遮挡场景下的采样陷阱ReID 训练的核心损失是三元组损失Triplet Loss加 ID 分类损失。三元组损失要求 anchor、positive、negative 三个样本让 anchor 和 positive 靠近、和 negative 远离。问题在于如果 positive 样本恰好是遮挡严重的帧anchor 和 positive 的距离天然就大模型会被迫去拉近两个本来就不像的样本梯度方向就歪了。血泪经验是随机采样三元组在遮挡场景下翻车率很高。解决办法是「遮挡感知采样」——优先选遮挡程度相近的样本组成三元组避免拿一个清晰帧去匹配一个重度遮挡帧。import random import torch def occlusion_aware_triplet_sample(labels, occ_ratios, batch_size): labels: (N,) 每个样本的ID occ_ratios: (N,) 每个样本的遮挡比例 返回: anchor, positive, negative 的索引 idx list(range(len(labels))) anchors, positives, negatives [], [], [] for _ in range(batch_size): a random.choice(idx) # positive 选同ID且遮挡程度接近的 pos_candidates [i for i in idx if labels[i] labels[a] and i ! a] if not pos_candidates: continue pos_candidates.sort(keylambda i: abs(occ_ratios[i] - occ_ratios[a])) p pos_candidates[0] # 遮挡最接近的 # negative 选不同ID的 neg_candidates [i for i in idx if labels[i] ! labels[a]] n random.choice(neg_candidates) anchors.append(a); positives.append(p); negatives.append(n) return anchors, positives, negatives逻辑说明pos_candidates.sort按遮挡比例差值排序取最接近的作为 positive这样 anchor 和 positive 的遮挡程度一致距离差异主要来自身份而不是遮挡。参数batch_size是每个 batch 采多少组三元组一般设成 PK 采样里 P*K 的一半左右。这个采样策略比随机采样收敛更稳rank-1 通常能涨 2-4 个点。3.2 损失函数组合与权重设置单靠三元组损失不够ID 分类损失交叉熵提供类别判别力两者结合是标配。遮挡场景下还可以加一个「遮挡预测辅助任务」让模型在训练时顺便学会判断遮挡程度相当于多任务学习能提升特征对遮挡的鲁棒性。class ReIDLoss(nn.Module): def __init__(self, num_classes, feat_dim, triplet_weight1.0, id_weight1.0, occ_weight0.3): super().__init__() self.triplet nn.TripletMarginLoss(margin0.3) self.id_loss nn.CrossEntropyLoss() self.occ_head nn.Linear(feat_dim, 1) # 遮挡回归头 self.triplet_weight triplet_weight self.id_weight id_weight self.occ_weight occ_weight def forward(self, anchor, positive, negative, logits, labels, feat, occ_gt): loss_tri self.triplet(anchor, positive, negative) loss_id self.id_loss(logits, labels) # 遮挡回归用 MSE occ_pred self.occ_head(feat).squeeze(-1) loss_occ nn.functional.mse_loss(occ_pred, occ_gt) total (self.triplet_weight * loss_tri self.id_weight * loss_id self.occ_weight * loss_occ) return total, loss_tri, loss_id, loss_occ逻辑说明三个损失加权求和。margin0.3是三元组损失的边界太大收敛慢太小判别力不够0.3 是 ReID 常用值。occ_weight0.3是辅助任务权重不能给太高否则主任务被带偏。训练时建议打印三个损失的分量如果loss_occ一直不降说明遮挡标注有问题回头检查 2.1 的关键点估计。注意辅助任务只在训练时用推理阶段不需要遮挡标注模型照样能跑。4. GUI 界面与检索流程把模型封装成能演示的系统4.1 用 PyQt5 搭一个检索界面毕设答辩最怕的是「只有命令行老师看不到效果」。GUI 界面不是花架子它把「上传查询视频 → 提取特征 → 库内检索 → 返回 Top-K 结果」这条链路可视化答辩时直观得多。PyQt5 是 Python 里最稳的选择跨平台、文档全。界面核心就三块查询区选视频或图片、结果区展示 Top-K 匹配、日志区显示检索耗时和相似度。下面是一个最小可用的主窗口import sys import cv2 import torch from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog, QListWidget) from PyQt5.QtGui import QPixmap, QImage class ReIDWindow(QMainWindow): def __init__(self, model, gallery_feats, gallery_paths): super().__init__() self.model model self.gallery_feats gallery_feats # 库内特征 (M, C) self.gallery_paths gallery_paths # 库内图像路径 self.setWindowTitle(遮挡视频行人重识别系统) self.init_ui() def init_ui(self): central QWidget() layout QVBoxLayout() self.btn_query QPushButton(选择查询视频) self.btn_query.clicked.connect(self.on_query) self.lbl_query QLabel(查询预览) self.result_list QListWidget() layout.addWidget(self.btn_query) layout.addWidget(self.lbl_query) layout.addWidget(self.result_list) central.setLayout(layout) self.setCentralWidget(central) def on_query(self): path, _ QFileDialog.getOpenFileName( self, 选择视频, , Video (*.mp4 *.avi)) if not path: return # 抽帧 提特征 检索 feat self.extract_video_feature(path) sims torch.mm(feat.unsqueeze(0), self.gallery_feats.t()).squeeze(0) topk torch.topk(sims, k10) self.result_list.clear() for score, idx in zip(topk.values, topk.indices): self.result_list.addItem( f{self.gallery_paths[idx]} 相似度:{score:.3f}) def extract_video_feature(self, video_path): cap cv2.VideoCapture(video_path) frames [] while True: ret, frame cap.read() if not ret: break frames.append(frame) cap.release() # 均匀采样16帧走模型提特征 idxs torch.linspace(0, len(frames) - 1, 16).long() batch torch.stack([ self.preprocess(frames[i]) for i in idxs]) with torch.no_grad(): feat, _ self.model(batch.unsqueeze(0)) return feat.squeeze(0)逻辑说明on_query是主流程选视频后调extract_video_feature提特征再和库内特征做矩阵乘法算余弦相似度torch.topk取前 10。extract_video_feature里均匀采样 16 帧是折中方案太少时序信息不够太多推理慢。preprocess要做 resize、归一化和训练时保持一致否则特征分布对不上检索结果会莫名其妙地差。4.2 特征库构建与检索加速GUI 每次检索都要和库内所有特征算相似度库大了会卡。常见做法是提前把库内特征算好存成.npy启动时加载进内存。如果库超过几万条可以用 faiss 做近似最近邻检索把 O(M) 的暴力搜索降到亚线性。import numpy as np import faiss def build_gallery_index(feats): feats: (M, C) float32已做 L2 归一化 返回: faiss 索引 feats feats.astype(float32) faiss.normalize_L2(feats) dim feats.shape[1] # 内积索引配合归一化等价于余弦相似度 index faiss.IndexFlatIP(dim) index.add(feats) return index def search(index, query_feat, topk10): query_feat query_feat.astype(float32).reshape(1, -1) faiss.normalize_L2(query_feat) sims, idxs index.search(query_feat, topk) return sims[0], idxs[0]逻辑说明IndexFlatIP是内积索引特征先做 L2 归一化后内积就等于余弦相似度。faiss.normalize_L2原地归一化注意 query 和 gallery 都要归一化否则相似度没有可比性。参数topk10是返回结果数答辩演示 10 个够了。如果库只有几千条其实不用 faiss直接 torch 矩阵乘法就行别为了用而用。提示特征归一化这一步是检索系统的黑匣子忘了归一化相似度会全乱而且不报错排查起来很痛苦。5. 避坑与排查遮挡视频 ReID 最容易翻车的 5 个地方5.1 现象训练 loss 正常下降但 rank-1 一直上不去原因最常见的是数据泄漏——测试集的 ID 混进了训练集或者同一个 tracklet 的帧被分到了训练和测试两边。视频 ReID 里 tracklet 是整体划分的不能按帧随机分。解决检查划分脚本确保按(id, cam, tracklet)整体划分训练集和测试集的 ID 完全不重叠。写个断言assert set(train_ids) set(test_ids) set()。5.2 现象模型在清晰样本上表现好一遇遮挡就崩原因训练时遮挡样本权重没处理好模型实际上在「偷懒」只学清晰帧的特征遮挡帧被平均池化稀释掉了。解决换成时间注意力聚合把 2.1 算的遮挡比例作为先验加进去。同时检查三元组采样确保 positive 和 anchor 遮挡程度接近。可以做个消融实验对比平均池化和注意力聚合的 rank-1答辩时正好当亮点讲。5.3 现象GUI 检索结果和命令行不一致原因预处理不一致。GUI 里 resize 的尺寸、归一化的均值方差、抽帧策略和训练/评估脚本对不上特征分布偏移。解决把预处理逻辑抽成一个独立函数训练、评估、GUI 三处共用同一份代码。别在 GUI 里重新写一遍 resize这是最常见的翻车点。5.4 现象显存爆了batch size 上不去原因视频 ReID 一个样本是 T 帧显存占用是图像 ReID 的 T 倍。T16、batch32 的时候显存需求是图像 ReID 的 512 倍。解决用梯度累积模拟大 batch或者降低 T8 帧也能用或者用混合精度训练。torch.cuda.amp能省一半显存几乎不掉点from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for batch in loader: optimizer.zero_grad() with autocast(): loss model(batch) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()5.5 现象推理速度慢GUI 卡死原因提特征在主线程里跑阻塞了 Qt 的事件循环。解决把检索逻辑放到QThread里用信号槽回传结果。或者更简单提特征前先torch.no_grad()再开model.eval()能快不少。如果还慢考虑把骨干网络换成轻量版如 ResNet18毕设演示对精度要求没那么极致。6. 进阶技巧用重排序和可视化把系统做出「研究感」毕设想拿高分光跑通不够得有点自己的东西。两个投入产出比最高的进阶点重排序Re-Ranking和注意力可视化。重排序里最经典的是 k-reciprocal encoding不需要重新训练直接在检索结果上后处理rank-1 通常能涨 3-8 个点。核心思想是如果 A 的 Top-K 里有 BB 的 Top-K 里也有 A那 A 和 B 大概率是同一人用这个互惠关系重构距离矩阵。import numpy as np def k_reciprocal_rerank(query_feat, gallery_feat, k120, k26, lambda_value0.3): query_feat: (Q, C) 已归一化 gallery_feat: (M, C) 已归一化 返回: 重排序后的距离矩阵 (Q, M) # 原始距离余弦距离 dist 1 - np.dot(query_feat, gallery_feat.T) # 对每个 query 找 k1 近邻 initial_rank np.argsort(dist, axis1)[:, :k1] # 构建 k-reciprocal 集合展开到 k2 V np.zeros_like(dist) for i in range(len(query_feat)): forward initial_rank[i] # 反向验证gallery 的 k1 近邻里有没有 query for j in forward: back np.argsort(dist[:, j])[:k1] if i in back: V[i, j] 1.0 / len(forward) # Jaccard 距离 原始距离加权 jaccard 1 - V # 简化版完整版要做集合运算 final lambda_value * jaccard (1 - lambda_value) * dist return final逻辑说明k120是初始近邻数k26是扩展近邻数lambda_value0.3控制 Jaccard 距离和原始距离的权重。这段是简化版完整实现要做集合的并集运算但思路一致。重排序的代价是计算量库大了会慢可以只在 Top-100 候选里做。另一个加分项是注意力可视化。把 2.2 里时间注意力返回的weights画成曲线叠加在视频帧上答辩时直接展示「模型在遮挡帧上权重低、清晰帧上权重高」比讲一堆公式有说服力。我一般会写个小脚本把权重和帧一起导出成 GIF演示效果拉满。import matplotlib.pyplot as plt def visualize_temporal_attention(frames, weights, save_path): fig, axes plt.subplots(2, 1, figsize(10, 6)) axes[0].imshow(cv2.cvtColor(frames[len(frames)//2], cv2.COLOR_BGR2RGB)) axes[0].set_title(Middle Frame) axes[1].bar(range(len(weights)), weights) axes[1].set_xlabel(Frame Index) axes[1].set_ylabel(Attention Weight) plt.tight_layout() plt.savefig(save_path)这套系统我从数据预处理到 GUI 到重排序走了一遍最大的体会是遮挡视频 ReID 的坑不在模型结构而在数据组织和预处理一致性上。模型换个注意力模块涨两三个点但预处理对不上能让你掉二十个点还不报错。所以每次改完代码先跑一遍小规模过拟合测试——拿 10 个 ID 训练看能不能过拟合到 100% 准确率过拟合不了说明链路有问题别急着上大数据集。希望帮到你。本文还有配套的精品资源点击获取