简介:面向毕设与课设场景的遮挡视频行人重识别系统源码包,基于Python实现并配有GUI操作界面,适合计算机视觉方向的学生快速搭建完整项目。系统覆盖视频导入与帧提取、图像预处理、行人检测、深度特征提取、遮挡区域检测与注意力特征增强、重识别匹配等关键环节,代码结构清晰,可作为毕业设计或课程设计的直接参考。包内共744个文件,压缩包约10.94MB,其中16个py源文件对应主程序、模型构建与界面逻辑,715张jpg为行人数据集与检测结果样例,另有xml、txt等标注和配置文档,便于理解数据组织。已有196人学习下载,适合需要动手复现或扩展遮挡鲁棒性研究的初学者与进阶者,拿到后可根据自带说明快速运行,并围绕特征融合、损失函数等方向做二次开发。
1. 遮挡视频行人重识别到底在做什么:毕设选它的理由
把“遮挡视频行人重识别”拆开看,它就是三件日常的事:先找到视频里每一帧的人(检测),再把同一个人的历史帧连成一条轨迹(跟踪),最后拿这条轨迹去库里比对身份(重识别)。这条流水线是深度学习视觉方向毕业设计的标配,模块拆得开,既能单独换模型做分析实验,又能用 python 源码接 GUI 界面撑起系统展示。
但真正动手你会发现,难点全在“遮挡”两个字上。人被人挡、被车挡、被广告牌挡,检测框里的特征就不完整,跟踪容易断,重识别更容易认错人。这里不讲虚的,直接给出一条可复现的落地路线:从模型选型、数据集处理、训练参数,到把视频流接进 GUI 界面,再到血泪踩坑记录。这套方案适合想拿 80 分以上成绩、且需要交付完整演示系统的毕设项目。
2. 拆解系统架构:检测、跟踪与重识别怎么串成流水线
先说结论:遮挡视频行人重识别系统从来不是单个模型的项目,而是一条三阶段的流水线。任何一个阶段失败,最终识别精度都会崩掉。这也是为什么直接套用一个开源 ReID 模型到视频上会严重翻车——因为开源模型大多只在干净的裁剪图上评测,没考虑检测框的抖动和遮挡噪声。
2.1 三步走:从YOLO检测到ByteTrack跟踪,再到ReID特征比对
常见实现做法是:用 YOLO 系列做检测,用 ByteTrack 做跟踪,再用 ResNet 或 OSNet 做 ReID 特征提取。为什么这么选?YOLO 的检测框质量高,召回率稳定;ByteTrack 是纯后处理算法,不需要额外训练,只用检测框的置信度和 IOU 就能生成轨迹;ReID 单独训练,特征提取与检测完全解耦。三者互相独立,对毕设来说最友好,也最容易在答辩时讲清楚。
检测和跟踪的衔接一般长这样:
# demo_track.py - 检测结果送入 ByteTrack 生成轨迹 import numpy as np from yolox.tracker.byte_tracker import BYTETracker # args 里需要包含 track_thresh, match_thresh 等参数 tracker = BYTETracker(args, frame_rate=30) def run_tracking(frame): # dets 是检测结果:每个元素为 [x1, y1, x2, y2, score] dets = run_yolo_detector(frame) # ByteTrack 接收的 tlbrs 坐标格式为左上右下 tlbrs = np.array([d[:4] for d in dets]) scores = np.array([d[4] for d in dets]) online_targets = tracker.update(tlbrs, scores, frame.shape[:2]) tracks = [] for t in online_targets: tracks.append((t.tlbr, t.track_id)) return tracks这段代码的关键不在 YOLO 和 ByteTrack 本身,而在tracker.update()的输入输出格式。ByteTrack 接收(tlbr, scores, img_size),输出STrack对象列表,含tlbr坐标和track_id。实际写的时候最常见的报错是坐标顺序问题——YOLO 输出[x1, y1, x2, y2],但某些版本的 ByteTrack 内部转成了[x, y, w, h],统一格式只需要五分钟,否则跟踪轨迹会画成乱麻。
跟踪拿到之后,截取每个人的检测框,缩放到固定尺寸,再过一遍 ReID 模型提取特征。这个特征一般是 512 维或 2048 维向量,最终通过余弦距离或欧氏距离排序检索,输出“这个人是谁”。整个串联逻辑一句话:检测出人,跟踪稳住轨迹,特征确认身份。
2.2 为什么遮挡是重识别的头号难题:从部件缺失到特征混淆
遮挡分两种:场景遮挡(电线杆、广告牌)和行人相互遮挡(人群交错)。两种都会让检测框不完整或混入大量背景。对 ReID 来说,特征向量会退化——全局池化把遮挡部分的背景噪声平均进去,原本该区分行人的浅色上衣被树干颜色冲散。
我在实际训练中有一个很直观的体感:在 Market1501 这种公开数据集上训练好的模型,测试 mAP 能到 80% 以上,一旦直接拿到视频里跑,首当其冲的就是遮挡帧。公开数据集的框是人工打的,目标完整;而视频里 YOLO 框在遮挡时会歪,甚至只框住半边身体。
解决这个问题的常见做法有两条。一是训练时做 RandomErasing 增强,随机遮掉图像的一部分,让模型学会“脑补”;二是推理时引入 Tracklet-Level ReID,不比对单帧特征,而是把一整条轨迹的特征向量做平均或加权融合再比对。后者对视频 ReID 尤其关键,因为单帧的坑可以由相邻帧补回来。
3. 跑通源码:从数据集准备到训练环境搭建
拿到一份遮挡视频行人重识别 python 源码,第一件事不是跑训练,而是把数据准备好。很多人一上来就训练,结果 loss 乱跳,最后发现连目录结构都是错的。
3.1 数据集准备:Market1501与遮挡子集的取舍,以及目录结构
绝大多数据开源 ReID 源码都遵循 Market1501 或 DukeMTMC 的目录约定。目录结构必须严格匹配,否则代码里的os.listdir路径会对不上:
Market1501/ bounding_box_train/ # 训练集,约 751 个 ID bounding_box_test/ # 测试集,约 750 个 ID query/ # 查询集,来自不同摄像头文件名格式如0001_c1s1_001051_00.jpg:第一位0001是行人 ID,c1s1是摄像头和场景编号,001051是帧号。写数据集类时,解析顺序不能错,否则标签全乱。
# reid_dataset.py - 按 Market1501 目录结构读取数据 import os from PIL import Image import torch from torch.utils.data import Dataset class MarketDataset(Dataset): def __init__(self, root, transform=None): self.paths, self.ids = [], [] for fname in os.listdir(root): if not fname.endswith('.jpg'): continue # 文件名格式: 0001_c1s1_001051_00.jpg parts = fname.split('_') if len(parts) < 2: continue pid = int(parts[0]) self.paths.append(os.path.join(root, fname)) self.ids.append(pid) self.transform = transform def __len__(self): return len(self.paths) def __getitem__(self, index): img = Image.open(self.paths[index]).convert('RGB') if self.transform: img = self.transform(img) return img, torch.tensor(self.ids[index]), index这段逻辑看起来简单,实际有两个坑。第一,有些源码把query和gallery混在一起读,结果检索时会把自己的同帧图片当成目标,mAP 虚高。第二,遮挡场景建议额外准备一个遮挡子集,比如把行人框随机裁剪掉 20% 再测试,否则无法体现系统对“遮挡”的鲁棒性。
3.2 训练配置与启动命令:YOLO权重、ReID骨干网络和损失函数的参数详解
ReID 训练通常不做端到端训练检测器和识别器,而是单独训练一个 ReID 模型。骨干网常用 ResNet50,去掉最后的分类层,换成 512 维的特征层加上一个 ID 分类器。
# train.py - ResNet50 骨干 + ID Loss 与 Triplet Loss 联合训练 import torch import torch.nn as nn import torch.optim as optim import torchvision.transforms as T from torchvision.models import resnet50 from torch.utils.data import DataLoader from reid_dataset import MarketDataset transform_train = T.Compose([ T.Resize((256, 128)), T.RandomHorizontalFlip(p=0.5), T.RandomErasing(p=0.5, scale=(0.02, 0.2)), # 模拟遮挡,关键 T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_set = MarketDataset('Market1501/bounding_box_train', transform_train) train_loader = DataLoader(train_set, batch_size=64, shuffle=True, num_workers=4) model = resnet50(pretrained=True) # 常见做法:把最后的 fc 替换成 512 维特征层 + BN + ReLU model.fc = nn.Sequential( nn.Linear(2048, 512), nn.BatchNorm1d(512), nn.ReLU(inplace=True) ) id_classifier = nn.Linear(512, 751) # 751 是训练集的 ID 数量 optimizer = optim.Adam([ {'params': model.parameters(), 'lr': 3e-4}, {'params': id_classifier.parameters(), 'lr': 3e-4}, ]) id_loss_fn = nn.CrossEntropyLoss() triplet_loss_fn = nn.TripletMarginLoss(margin=0.3) for imgs, ids, _ in train_loader: imgs, ids = imgs.cuda(), ids.cuda() features = model(imgs) logits = id_classifier(features) loss = id_loss_fn(logits, ids) # Triplet loss 需要构造三元组,常见做法是 batch-hard 采样 # 这里先用 ID Loss 做热身,再逐步加入 triplet loss optimizer.zero_grad() loss.backward() optimizer.step()几个必调参数先说清楚。batch_size不要低于 32,最好 64,因为 Triplet Loss 依赖 batch 内采样到足够的正负样本对,batch 太小训练会抖成波浪线。margin一般在 0.3 到 0.5 之间,太大会让模型对难样本过度敏感。RandomErasing的遮挡概率我习惯开 0.5,遮挡比例控制在 0.02 到 0.2,太小没效果,太大模型学不到完整特征。
3.3 特征提取与检索:Gallery和Query的推理流程拆解
训练完成后,需要把所有测试集图片过一遍模型,得到特征库,然后对每张查询图做检索排序。
# infer.py - 提取 query 和 gallery 特征,计算余弦距离 import torch import numpy as np def extract_features(model, data_loader, device): model.eval() feats = [] with torch.no_grad(): for imgs, _, _ in data_loader: imgs = imgs.to(device) feat = model(imgs) feats.append(feat.cpu().numpy()) return np.concatenate(feats, axis=0) def compute_scores(query_feats, gallery_feats): # 余弦相似度 = 归一化后做内积 q_norm = query_feats / np.linalg.norm(query_feats, axis=1, keepdims=True) g_norm = gallery_feats / np.linalg.norm(gallery_feats, axis=1, keepdims=True) scores = np.dot(q_norm, g_norm.T) return scores这里有个容易忽略的细节:训练时用了RandomHorizontalFlip,推理时也必须保持同一套归一化参数,但不能再加随机增强。很多新手把训练时的RandomErasing也带到推理里,结果每一次识别的结果都不一样,还以为是模型问题。
4. 给视频加上连续身份:跟踪关联与GUI界面的落地实现
训练完模型只是第一步,毕设更看重的是整套系统能不能跑起来。视频流里最忌讳逐帧独立识别,因为单帧抖动太严重,必须结合轨迹。
4.1 把单帧检测变成跨帧轨迹:IOU匹配与卡尔曼滤波
ByteTrack 的原理说穿了就是两步:先用 IOU 匹配相邻帧的检测框,再对没匹配上的低分框做二次补偿。这套策略在遮挡场景很好用,因为行人短暂被挡后重新出现,检测置信度会骤降,但 IOU 匹配能靠运动预测把轨迹接回去。
我的经验是,将检测到的行人框截取后送入 ReID 模型,但不逐帧去跟全量库匹配。而是先按轨迹聚合特征,再比对。也就是把视频里同一个track_id的每一帧特征全部存下来,做一次平均,得到这条轨迹的“身份卡”,然后用这个“身份卡”去和总库匹配。
# aggregator.py - 轨迹特征聚合 import numpy as np from collections import defaultdict class TrackletAggregator: def __init__(self): # track_id -> 特征列表 self.track_buffers = defaultdict(list) def update(self, track_id, feature, count=20): """每帧调用,保存最新 N 帧特征""" buf = self.track_buffers[track_id] buf.append(feature) if len(buf) > count: buf.pop(0) # 只保最近 20 帧,避免存储膨胀 def get_tracklet_feature(self, track_id): buf = self.track_buffers[track_id] return np.mean(buf, axis=0) # 均值融合这里参数值很关键。count=20在 30 帧视频里约等于 0.67 秒,足够覆盖一次短暂遮挡,又不会因为行人走远导致特征变化过大。想更鲁棒,可以把np.mean换成加权平均,给最近帧更高权重。
4.2 PyQt5界面:视频流、实时识别结果与线程防卡死的写法
GUI 是本项目的重要加分项,但几乎所有人都踩过界面卡死的坑。原因很简单:视频检测是个重计算任务,直接写在 PyQt5 的主线程里,界面必然白屏。
# gui.py - 用 QThread 跑视频推理,主线程只管刷新界面 from PyQt5.QtCore import QThread, pyqtSignal import cv2 class VideoProcessThread(QThread): frame_ready = pyqtSignal(object, list) # 信号发射帧和识别结果 def __init__(self, video_path, model, tracker, aggregator): super().__init__() self.video_path = video_path self.model = model self.tracker = tracker self.aggregator = aggregator self._running = True def run(self): cap = cv2.VideoCapture(self.video_path) while self._running and cap.isOpened(): ret, frame = cap.read() if not ret: break tracks = run_tracking(frame) # 调用检测 + 跟踪 results = [] for tlbr, tid in tracks: x1, y1, x2, y2 = tlbr crop = frame[y1:y2, x1:x2] if crop.size == 0: continue feat = extract_crop_feature(self.model, crop) self.aggregator.update(tid, feat) traj_feat = self.aggregator.get_tracklet_feature(tid) results.append((tlbr, tid, traj_feat)) # 交给主线程展示 self.frame_ready.emit(frame, results) cap.release() def stop(self): self._running = False这段代码的核心思想是“生产者-消费者”。QThread负责生产帧和结果,通过pyqtSignal把数据交给主线程的QImage渲染。很多初学者喜欢把cv2.VideoCapture直接写在while True里再用QTimer刷新,那种写法在低帧率下还能凑活,一旦跑检测就会卡死。
一个值得注意的细节是frame_ready信号里传了list对象,但 PyQt5 的信号槽是跨线程的,list会被深拷贝,不用担心变量覆盖。真正要担心的是线程结束后忘了调stop(),导致摄像头句柄没释放,第二次点击识别会黑屏。
5. 毕设翻车重灾区:遮挡视频ReID的5个踩坑与排查套路
以下每条都是实际交付项目时血泪换来的记录,按“现象 -> 原因 -> 解决”写,照着排能省两天时间。
5.1 现象:模型在公开测试集 mAP 很高,视频里一塌糊涂
这是最常见的情况。模型在 Market1501 上 mAP 75%,一上视频就乱跳。原因是公开数据集是人工裁剪的完整框,视频里 YOLO 检测框在遮挡时是残缺的,特征里混入了背景。解决:第一,确认预处理有没有对齐,Resize 尺寸和训练时是否一致;第二,引入RandomErasing后重新训练;第三,推理时用轨迹平均特征,不要用单帧特征。
5.2 现象:遮挡后人一出现,ID 就变了
原因是跟踪匹配丢失后,重新进入画面的人被分配了新 ID,ReID 特征又在遮挡后漂移了。排查时先看 tracker 的输出——如果track_id在遮挡后从 3 变成了 10,说明是跟踪层断了,而不是 ReID 认错人。解决:调大BYTETracker的track_buffer,给轨迹更长的“失忆”时间,比如默认 30 帧的 buffer 调到 50 帧。同时检查match_thresh,通常设 0.8 比较稳妥。
5.3 现象:GUI 点击“开始识别”就白屏卡死
原因百分之百是一股脑把检测循环写在了主线程里,事件循环被阻塞,界面失去响应。解决:按前面 4.2 节的QThread写法来处理,把视频推理丢到子线程,主线程只接收frame_ready信号刷新标签和列表控件。还要注意退出时先thread.stop()再thread.wait(),否则会报“线程仍在运行”的崩溃错误。
5.4 现象:训练 loss 不下降或收敛极慢
原因通常是 batch size 太小,Triplet 在 batch 内采不到足够的正样本对。我用 64 的 batch_size 很顺,降到 16 时 loss 基本是条水平线。解决:第一,优先用 ID Loss(交叉熵)做 start-up,别一上来就用 Triplet,否则 margin 设置不当会带来很大噪声;第二,给学习率加 warmup,比如前 10 个 epoch 从 1e-4 线性升到 3e-4,后面再用余弦退火;第三,确认RandomErasing概率不要超过 0.7,否则输入被过度破坏,模型学不进去。
5.5 现象:显存不足或 CPU 跑视频特别慢
视频 ReID 比单帧检索吃显存,因为检测、跟踪、ReID 都要同时驻留。解决:把输入分辨率从 1080p 降到 720p,对结果影响很小;检测模型用轻量版,比如 YOLOv5s;ReID 的 ResNet50 可以换成 ResNet18,特征降到 256 维,识别速度提升四到五倍,代价是准确率会掉几个点。如果连 GPU 都没有,建议 add 一个帧间隔策略,比如每秒只处理 10 帧,用跟踪在中间帧补轨迹,而不是每帧都跑检测。
6. 把准确率从70%拉到85%的三个进阶操作
前面是基础跑通,这一章说三个能立刻看到分数变化的技巧,也是答辩时最容易被问到的点。
6.1 加一个Re-Ranking:K-reciprocal编码带来的稳定提升
Re-Ranking 的思路很简单:两个 ID 在各自 top-K 邻居里互相包含对方,就认为它们更像。这个操作可以有效压制遮挡带来的假阳性。常见实现是标准 K-reciprocal 编码 + Jaccard 距离融合,跑一遍会把 mAP 凭空拉高 5 到 8 个点。只需在得到初始距离矩阵后调用一次,不用改任何模型结构,是我最推荐先在验证集上试的操作。
6.2 在视频上做时空约束:用轨迹平滑修正单帧误判
这是我每次做遮挡视频必加的一步。具体做法:把同一个人的轨迹特征按时间窗口做滑动平均,窗口大小设为 5 帧。遮挡最严重的那几帧会被前后正常帧拉回来,身份识别一下子稳了。
# smooth.py - 轨迹滑动窗口平滑 import numpy as np def smooth_track_features(track_features, window=5): """ track_features: [T, D] 按时间顺序排列的特征矩阵 返回同样形状的平滑结果 """ T = track_features.shape[0] smoothed = np.zeros_like(track_features) radius = window // 2 for i in range(T): start = max(0, i - radius) end = min(T, i + radius + 1) smoothed[i] = np.mean(track_features[start:end], axis=0) return smoothed注意,平滑幅度不能过强。窗口超过 9 帧后,两个人交错时特征会被对方污染,ID 反而会锁错人。5 帧是一个稳健的折中值。
6.3 模型层面的最后一步:从全局特征到局部特征
如果做完上面两步还不满足,就要在模型结构上动手。常见做法是把 ResNet50 最后一层特征图做水平切分,比如切成 6 块,每块分别做池化和分类,再把局部特征拼接起来。这就是常说的 PCB 结构,对遮挡场景特别有效,因为遮挡往往只毁了局部部件,其他部件的特征仍然可用。注意,引入局部特征后,Retrieval 时要同时加权全局和局部特征的距离,权重比一般取 1:1 或 2:1。
我的习惯是每次改完都先在单条遮挡视频上肉眼检查十帧,而不是只看 mAP 数字。mAP 高只能说明你的检索排序好,不代表你的跟踪轨迹稳。视频 ReID 最终交付的是“连续不跳 ID”的观感,这一条一定要守住。希望这些踩坑经验帮你在毕设答辩前少走弯路。
本文还有配套的精品资源,点击获取