ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

遮挡视频行人重识别系统实战:从YOLO检测到ReID特征融合的毕设完整方案

遮挡视频行人重识别系统实战:从YOLO检测到ReID特征融合的毕设完整方案 简介面向毕设与课设场景的遮挡视频行人重识别系统源码包基于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_rate30) 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是行人 IDc1s1是摄像头和场景编号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, transformNone): 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(p0.5), T.RandomErasing(p0.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_size64, shuffleTrue, num_workers4) model resnet50(pretrainedTrue) # 常见做法把最后的 fc 替换成 512 维特征层 BN ReLU model.fc nn.Sequential( nn.Linear(2048, 512), nn.BatchNorm1d(512), nn.ReLU(inplaceTrue) ) 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(margin0.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, axis0) def compute_scores(query_feats, gallery_feats): # 余弦相似度 归一化后做内积 q_norm query_feats / np.linalg.norm(query_feats, axis1, keepdimsTrue) g_norm gallery_feats / np.linalg.norm(gallery_feats, axis1, keepdimsTrue) 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, count20): 每帧调用保存最新 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, axis0) # 均值融合这里参数值很关键。count20在 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 就变了原因是跟踪匹配丢失后重新进入画面的人被分配了新 IDReID 特征又在遮挡后漂移了。排查时先看 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对结果影响很小检测模型用轻量版比如 YOLOv5sReID 的 ResNet50 可以换成 ResNet18特征降到 256 维识别速度提升四到五倍代价是准确率会掉几个点。如果连 GPU 都没有建议 add 一个帧间隔策略比如每秒只处理 10 帧用跟踪在中间帧补轨迹而不是每帧都跑检测。6. 把准确率从70%拉到85%的三个进阶操作前面是基础跑通这一章说三个能立刻看到分数变化的技巧也是答辩时最容易被问到的点。6.1 加一个Re-RankingK-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, window5): 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], axis0) return smoothed注意平滑幅度不能过强。窗口超过 9 帧后两个人交错时特征会被对方污染ID 反而会锁错人。5 帧是一个稳健的折中值。6.3 模型层面的最后一步从全局特征到局部特征如果做完上面两步还不满足就要在模型结构上动手。常见做法是把 ResNet50 最后一层特征图做水平切分比如切成 6 块每块分别做池化和分类再把局部特征拼接起来。这就是常说的 PCB 结构对遮挡场景特别有效因为遮挡往往只毁了局部部件其他部件的特征仍然可用。注意引入局部特征后Retrieval 时要同时加权全局和局部特征的距离权重比一般取 1:1 或 2:1。我的习惯是每次改完都先在单条遮挡视频上肉眼检查十帧而不是只看 mAP 数字。mAP 高只能说明你的检索排序好不代表你的跟踪轨迹稳。视频 ReID 最终交付的是“连续不跳 ID”的观感这一条一定要守住。希望这些踩坑经验帮你在毕设答辩前少走弯路。本文还有配套的精品资源点击获取
返回列表