简介:这份资源是面向深度学习方向毕业设计与课程设计场景的遮挡视频行人重识别系统Python源码包,适合具备一定深度学习基础、需要完成相关课题的学生与开发者使用。系统围绕视频输入与预处理、行人检测与特征提取、遮挡处理与特征增强、行人重识别匹配等环节展开,并配有GUI界面,便于直观操作与演示。压缩包共744个文件,以715张jpg图像数据为主,另含16个py源码文件、6个xml标注文件及若干txt说明、字体与工程配置文件,整体约10.94MB,结构紧凑、便于本地运行与二次开发。目前已有197人学习下载。读者可从中获得一套可运行的遮挡行人重识别完整方案,涵盖视频帧提取、图像预处理、基于YOLO或Faster R-CNN的行人检测、ResNet等网络的特征提取,以及注意力机制等遮挡特征增强思路,同时可参考GUI交互逻辑与工程目录组织方式,用于毕设复现、课设提交或算法改进实验。
1. 遮挡视频行人重识别系统:为什么它比普通 ReID 难一个量级
行人重识别(Person Re-Identification,ReID)要解决的问题是:给定一张目标行人的照片,在其它摄像头拍到的画面里把这个人找出来。而遮挡视频行人重识别,是在这个基础上再加两个约束——输入是视频序列而不是单帧图像,且目标行人大概率被车、树、广告牌、其他行人挡住了一部分。这两个约束叠加之后,难度不是线性上升,而是直接换了一个量级。
普通 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_thr=0.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_thr=0.3是经验值,太低会把噪声关键点算成可见,太高会误判正常帧为遮挡,实际调的时候可以打印比例分布看直方图。
提示:如果数据集本身没有关键点标注,这一步是必须自己补的。别跳过,遮挡权重全靠它。
2.2 骨干网络选型:ResNet50-IBN 还是 ViT,视频场景怎么选
骨干网络决定了特征提取的上限。图像 ReID 里 ResNet50-IBN 是经典选择,IBN 模块(Instance-Batch Normalization)对光照和风格差异更鲁棒。但视频 ReID 要考虑时序建模,纯 CNN 骨干只能逐帧提特征,时序聚合得另加模块。
常见做法有三种:
| 方案 | 骨干 | 时序建模 | 显存占用 | 适合场景 |
|---|---|---|---|---|
| CNN + 平均池化 | ResNet50-IBN | 帧特征平均 | 低 | 毕设快速跑通 |
| CNN + 注意力聚合 | ResNet50-IBN | 时间注意力 | 中 | 遮挡场景推荐 |
| ViT + 时空注意力 | VideoViT | 3D 注意力 | 高 | 有充足算力 |
毕设场景我一般推荐第二种:ResNet50-IBN 提帧特征,加一个时间注意力模块做加权聚合。原因是显存友好,单卡 8G 能跑,而且时间注意力天然能压低遮挡帧的权重,和遮挡问题正好对上。ViT 方案虽然理论上限高,但训练数据量要求大,毕设数据集往往撑不住,容易过拟合。
时间注意力的核心实现:
import torch import torch.nn as nn class TemporalAttention(nn.Module): def __init__(self, feat_dim, reduction=8): super().__init__() # 用一个小 MLP 学每帧的重要性分数 self.attn = nn.Sequential( nn.Linear(feat_dim, feat_dim // reduction), nn.ReLU(inplace=True), nn.Linear(feat_dim // reduction, 1) ) def forward(self, feat_seq, occlusion_ratio=None): """ 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, dim=1) # (B, T) agg = torch.bmm(weights.unsqueeze(1), feat_seq).squeeze(1) return agg, weights逻辑说明:attn是一个两层 MLP,把每帧特征映射成一个标量分数。occlusion_ratio作为先验加进 logits,遮挡比例高的帧分数被压低,softmax 之后权重自然小。参数reduction=8控制中间层维度,太大容易过拟合,太小表达能力不够,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(key=lambda 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_weight=1.0, id_weight=1.0, occ_weight=0.3): super().__init__() self.triplet = nn.TripletMarginLoss(margin=0.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逻辑说明:三个损失加权求和。margin=0.3是三元组损失的边界,太大收敛慢,太小判别力不够,0.3 是 ReID 常用值。occ_weight=0.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, k=10) 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, topk=10): 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 都要归一化,否则相似度没有可比性。参数topk=10是返回结果数,答辩演示 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 倍。T=16、batch=32 的时候,显存需求是图像 ReID 的 512 倍。
解决:用梯度累积模拟大 batch,或者降低 T(8 帧也能用),或者用混合精度训练。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 里有 B,B 的 Top-K 里也有 A,那 A 和 B 大概率是同一人,用这个互惠关系重构距离矩阵。
import numpy as np def k_reciprocal_rerank(query_feat, gallery_feat, k1=20, k2=6, lambda_value=0.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, axis=1)[:, :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逻辑说明:k1=20是初始近邻数,k2=6是扩展近邻数,lambda_value=0.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% 准确率,过拟合不了说明链路有问题,别急着上大数据集。希望帮到你。
本文还有配套的精品资源,点击获取