YOLOv5+DeepSORT多目标跟踪与车辆行人计数实战指南
简介面向毕业设计、课程设计及目标检测与多目标跟踪入门人群这份资源基于YOLOv5与DeepSORT实现车辆、行人的实时检测、追踪与计数部署门槛低可直接用于项目演示。项目提供完整Python源码、预训练权重及检测、跟踪、工具三大模块代码注释详细新手也能看懂整体功能完善经调试可稳定运行适合作为毕设或期末大作业的高分参考方案。压缩包共78个文件以Python脚本为主另有YAML模型配置、Shell运行脚本、依赖清单、Dockerfile、说明文档及测试视频等覆盖模型推理、跟踪配置、环境还原与效果验证各环节整体大小约82.69MB。资源结构清晰核心逻辑包含检测器、跟踪器与主控脚本并配有独立的目标关联子模块便于按模块学习。目前已有246人学习下载适合需要完整方案模板、快速搭建并展示追踪计数效果的高校学生。结合详细使用说明可系统理解YOLOv5检测、DeepSORT关联及计数思路是一份工程完整、上手友好的高分项目资料。1. 为什么毕设题目里总是不约而同出现 YOLOv5 和 Deepsort车辆和行人的追踪计数几乎是计算机视觉方向毕业设计里最稳的题之一。这个项目标题的核心组合是 YOLOv5 负责检测Deepsort 负责追踪帧间的身份两者串起来后你就能在一段视频上看到每个行人和车辆被框出来、带着一个不变的编号从头走到尾。很多毕设选这个方向是因为它有成熟的公开代码、清晰的两阶段结构、以及能拿数据说话的实验空间。不论是你打算直接跑通一份源码去复现还是想把它改成自己的想法以下内容会按环境搭建、代码拆解、训练替换、排错避坑这条路径讲下来让你少走弯路、能落地。2. 先把环境立住conda 建环境、装依赖、跑通官方 demo一份基于 YOLOv5 和 Deepsort 的毕设源码通常包含两套依赖YOLOv5 官方的 requirements.txt以及 deep_sort_pytorch 的依赖文件。两套依赖直接合并没有想象中顺利最常见的结果是 sklearn 版本冲突、numpy 版本冲突在 import 阶段就报错。所以这一章从选型理由讲起再给出一个实测比较稳的装法最后告诉你怎么验证两套模型都加载正常。2.1 为什么这对组合能在毕设里站稳脚YOLOv5 是单阶段目标检测器DeepSORT 是多目标跟踪算法。两者配合的逻辑很清楚检测器回答“这一帧里有哪些目标、每个目标在哪”追踪器回答“连续帧之间哪个框属于同一个身份”。对车辆行人追踪来说这个 tracking-by-detection 的范式在实时性和精度之间能取到不错的平衡这也是它在高校项目里被反复使用的原因。有人会问ByteTrack 不也做得很好吗为什么毕设题目固定写 Deepsort原因不是哪个更强而是 Deepsort 的模块化更清晰卡尔曼滤波、级联匹配、表观特征 embedding 三个组件都能单独展开写几千字天然适合论文的第二章和第四章。对毕设而言这是更稳妥的选择。从工程复现的角度看YOLOv5 的代码质量在开源检测器里算是很高的文档全、模型文件好找。Deepsort 社区里做得比较完整的实现是 deep_sort_pytorch它提供了一个在行人重识别数据上预训练好的 ReID 权重拿来就能跑。车辆类的数据集要是有也可以自己重新训练 ReID 模型来替换。2.2 conda 环境创建与依赖安装两个 requirements 怎么不打架先说结论不要想着把两份 requirements.txt 直接合并安装。一份是 PyTorch 生态一份是 sklearn/scipy 生态装完很容易把某个包升级到不兼容的版本。更稳的办法是分层装conda create -n traffic python3.8 conda activate traffic # 按你机器 CUDA 版本选择 PyTorch这里以 CUDA 11.3 为例 conda install -c pytorch pytorch1.12.1 torchvision0.13.1 cudatoolkit11.3 # 装 YOLOv5 的核心依赖 pip install opencv-python numpy matplotlib pyyaml tqdm requests pillow # 装 deep_sort_pytorch 需要的追踪依赖 pip install scikit-learn0.23.2 scipy1.7.3 lap参数说明python3.8 兼容性最好。Python 3.10 之后部分旧版 PyTorch 安装失败而 3.7 又太老很多新库开始不支持。把 scikit-learn 固定到 0.23.2 是有意为之。deepsort_pytorch 在旧版本里使用sklearn.utils.linear_assignment_里的 linear_assignment 函数这个函数在 sklearn 0.24 中被移除。如果不锁版本装完以后 import 直接报 “cannot import name linear_assignment”。lap 是一个线性分配库Deepsort 做帧间匹配时会用到匈牙利算法的加速实现CPU 上跑也很快。装完后做一次快速自检确认关键 import 没问题python - EOF import torch, numpy, cv2, sklearn from sklearn.utils import linear_assignment_ print(torch:, torch.__version__) print(sklearn:, sklearn.__version__) print(linear_assignment available) EOF说明如果你的源码用的是较新版本的 deep_sort_pytorch可能已经不再 import linear_assignment而是使用 scipy.optimize.linear_sum_assignment。这种情况下不需要降 sklearn自检命令可以改成只打印版本号。先 grep 一下源码里的 import 语句再决定装哪个版本。2.3 项目目录结构与关键文件说明拿到一份“项目源码详细使用说明”之后先不要急着运行花五分钟把目录结构看清楚。典型布局长这样traffic_counter/ ├─ main.py # 程序入口读视频并逐帧处理 ├─ yolov5/ │ ├─ detect.py │ ├─ models/ # YOLOv5 模型定义 │ ├─ data/ # 默认数据集配置 │ └─ weights/ │ └─ yolov5s.pt # 检测权重 ├─ deep_sort/ │ ├─ deep_sort/ │ │ ├─ deep_sort.py # 追踪器封装 │ │ ├─ track.py # Track 对象定义 │ │ ├─ nn_matching.py # 级联匹配逻辑 │ │ └─ deep/ │ │ └─ checkpoint/ │ │ └─ ckpt.t7 # ReID 特征提取权重 ├─ utils/ # draw、IO、计数的辅助函数 └─ requirements.txt关键点如下yolov5s.pt 和 ckpt.t7 通常不会随源码包一起提供需要手动下载。很多毕设代码里路径写的是相对路径意味着文件必须放在上表指定位置否则 FileNotFoundError 直接拦在开头。main.py 是入口。你之后改输入视频、改检测置信度、改计数线时主要就是改这一个文件。utils 目录下的 draw.py / counter.py 每个项目都不太一样。有些作者会把计数逻辑直接塞进 main.py但从工程上讲拆开更合理。2.4 用官方 demo 验证两套模型都能加载环境建好、文件放好后先跑两个最小验证。第一个是 YOLOv5 自带的图片检测python detect.py --source data/images/bus.jpg --weights yolov5s.pt --conf 0.4 --device 0正常现象是终端打印识别出的类别和置信度并输出一张画了检测框的 bus.jpg。如果 CUDA 不可用把 --device 0 改成 --device cpu先确认逻辑没问题。第二个是验证 Deepsort 能加载python - EOF from deep_sort import DeepSort tracker DeepSort(deep_sort/ckpt.t7, max_dist0.2, min_confidence0.3, max_iou_distance0.7, max_age70, n_init3) print(tracker ok) EOF这段代码做了什么创建了一个 DeepSort 实例指定 ReID 权重路径和五个关键参数。只要最后打印 “tracker ok”说明 Deepsort 模块和依赖都没有问题。这两个验证都通过后面调代码就不会再把时间浪费在环境排查上了。3. 核心代码拆解检测、追踪、计数怎么串成一条流水线跑通 demo 之后真正的目标是读懂主循环在干什么。大部分基于 YOLOv5Deepsort 的毕设源码主流程就是三件事YOLOv5 做目标检测Deepsort 做多目标追踪再根据 track_id 做计数。这里按模块拆开讲最后给出整合后的主循环。3.1 把 YOLOv5 包装成检测函数返回框、置信度和类别项目里一般不会直接调 detect.py而是把 YOLOv5 包装成一个类在视频循环里逐帧调用。常见写法import torch import numpy as np class YOLODetector: def __init__(self, weightsweights/yolov5s.pt, devicecuda, conf0.4, img_size640): self.model torch.hub.load(yolov5, custom, pathweights, sourcelocal) self.model.conf conf self.model.classes [0, 2, 5, 7] # person, car, bus, truck self.model.to(device) self.device device self.img_size img_size def detect(self, frame): # frame 是 OpenCV 读出的 BGR 图 results self.model(frame, sizeself.img_size) dets results.xyxy[0].cpu().numpy() # 每行: [x1, y1, x2, y2, conf, cls] return dets代码说明torch.hub.load 里 sourcelocal 是告诉 hub 直接加载本地仓库不联网拉配置速度快也更稳在离线环境的毕设演示现场尤其重要。self.model.classes [0, 2, 5, 7] 表示只保留 COCO 类别里的 person、car、bus、truck。如果是纯行人数统计改成 [0] 就行。results.xyxy[0] 是一个 n×6 的张量n 是这一帧的检测框数量6 表示 x1、y1、x2、y2、confidence、class id。后面喂给 Deepsort 的正是这个矩阵。参数说明conf0.4 对车辆行人场景是一个比较折中的阈值。太高会把远处的小目标丢掉太低又会引入大量误检造成 ID 频繁切换。img_size640 是模型的输入边长。如果你处理的是 1080p 视频帧不会直接送进去内部会做等比缩放。往大了设会提高小目标召回但推理时间同步上升。3.2 Deepsort 追踪update 流程与五个关键参数Deepsort 的核心机制可以压缩成三句话先用卡尔曼滤波预测每条轨迹在下一帧的位置然后用 IoU 做一次粗匹配最后用 ReID 特征做级联匹配决定哪些检测框该继承旧 ID、哪些该开新 ID。理解这个流程以后你会明白一个关键结论检测器的质量决定 Deepsort 的上限Deepsort 只是在下限上做补偿。实际用起来并不复杂每帧把检测结果交给 tracker.updatefrom deep_sort import DeepSort class TrackProcessor: def __init__(self, model_pathdeep_sort/ckpt.t7): self.deepsort DeepSort( model_path, max_dist0.2, min_confidence0.3, max_iou_distance0.7, max_age70, n_init3, nn_budget100, ) def update(self, detections): # detections: n×6 [x1, y1, x2, y2, conf, cls] tracks self.deepsort.update(detections) return tracks # 每个 track 对象含 bbox 与 track_id这五个参数是调追踪效果的入口值得记一笔参数建议值作用与调整方向max_dist0.2ReID 特征的最大余弦距离。人流密集场景适当降到 0.15避免跨目标匹配车流稀疏时升到 0.3 提高找回率min_confidence0.3进入追踪器的检测置信度下限低于这个值的框直接丢弃max_iou_distance0.7两帧匹配时 IoU 的容差上限。运动速度特别快的场景建议调小到 0.5防止错配max_age70目标连续丢失 70 帧后轨迹才删除。行人遮挡频繁建议加到 100但要警惕低质量轨迹占着 IDn_init3新轨迹要连续匹配 3 帧才被确认输出。太大会漏掉短暂出现的目标太小会让瞬时误检也变成一条轨迹调参经验是优先调 max_age 和 min_confidence因为这两个对计数的影响最直接max_dist 和 nn_budget 多数时候保持默认就行改过头反而会丢掉原本正确的匹配。3.3 计数逻辑从轨迹 ID 到虚拟线过线判定计数最常见的方式是虚拟线计数。在画面里定义一条直线每次目标中心点跨过这条线就认为完成了一次穿越。为了防止同一辆车被重复计数核心技巧是按 track_id 做去重。class LineCounter: def __init__(self, line_start(0, 500), line_end(1280, 500)): self.line_start line_start self.line_end line_end self.line_y line_start[1] # 这里以水平线为例 self.previous_centers {} # track_id - (cx, cy) self.count_up 0 # 从下往上 self.count_down 0 # 从上往下 def update(self, tracks): current_centers {} for trk in tracks: x1, y1, x2, y2 trk.get_bbox() cx (x1 x2) / 2 cy (y1 y2) / 2 current_centers[trk.track_id] (cx, cy) if trk.track_id in self.previous_centers: prev_cy self.previous_centers[trk.track_id][1] if prev_cy self.line_y and cy self.line_y: self.count_down 1 elif prev_cy self.line_y and cy self.line_y: self.count_up 1 self.previous_centers current_centers代码说明用字典记录每个 ID 上一帧的中心点坐标而不是假设相邻两帧的目标一一对应。这个设计在面对漏检和 ID 丢失时依然能保持一定的稳定性。判断穿越用的是“上一帧在线上方、这一帧在下方”的条件因此天然不会重复计数。如果一个目标停在线上抖动除非中心真实跨过线否则计数不会增加。双向计数拆成了 count_up 和 count_down传入方向和传出方向分开统计。如果是商场客流这两个值可以对应进店和出店。3.4 主循环整合与可视化绘制三个模块都准备好之后把它们串进视频循环。主循环的骨架import cv2 def main(video_path): cap cv2.VideoCapture(video_path) det YOLODetector() tracker TrackProcessor() counter LineCounter() while True: ret, frame cap.read() if not ret: break detections det.detect(frame) tracks tracker.update(detections) counter.update(tracks) frame draw_results(frame, tracks, counter) cv2.imshow(traffic_counter, frame) if cv2.waitKey(1) 0xFF 27: # ESC 退出 break cap.release() cv2.destroyAllWindows()画框和画计数的辅助函数建议单独放一个 draw.py 里def draw_results(frame, tracks, counter): for trk in tracks: x1, y1, x2, y2 map(int, trk.get_bbox()) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, fID:{trk.track_id}, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.line(frame, counter.line_start, counter.line_end, (0, 0, 255), 2) cv2.putText(frame, fup: {counter.count_up} down: {counter.count_down}, (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) return frame这段绘制代码还解决了一个常见的显示问题直接读视频流时图片很大窗口放不下。建议在 imshow 前加一句frame cv2.resize(frame, (960, 540))这样演示时窗口更友好也不会卡渲染。主循环跑通后你手上就有一个可以输出计数结果的系统了。接下来要做的是把检测器换成自己训练的模型让论文里的实验部分站得住脚。4. 训练自己的车辆行人数据集从标注、超参数到权重替换如果这个毕设从头到尾都在用别人训练好的权重答辩时很容易被问住。更稳的做法是自己准备一小批车辆行人数据用 YOLOv5 训练一个自定义模型再把权重替换回主流程。这一章从数据标注讲起到训练命令、超参数调整最后回到替代验证。4.1 数据准备与 YOLO 格式标注转换YOLOv5 训练数据的标准格式是每张图片对应一个同名 txttxt 的每一行是“类别 中心点x 中心点y 宽度 高度”所有坐标归一化到 0~1。标注工作最常用的工具是 LabelImg 或 Labelme导出 YOLO 格式。点击图片手动框完后LabelImg 可以直接保存到 YOLO 格式的 txt 文件。Labelme 保存的是 json需要转换成 YOLO 格式。转换逻辑不复杂核心就是把 json 里的多边形包络框转成归一化的 bboximport json import os def labelme_to_yolo(json_path, output_dir, image_width, image_height): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] # 如 person / car points shape[points] # [左上, 右上, 右下, 左下] xs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) cls_id {person: 0, car: 1, bus: 2, truck: 3}[label] x_center ((x_min x_max) / 2) / image_width y_center ((y_min y_max) / 2) / image_height w (x_max - x_min) / image_width h (y_max - y_min) / image_height lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) out_name os.path.splitext(os.path.basename(json_path))[0] .txt with open(os.path.join(output_dir, out_name), w) as f: f.write(\n.join(lines))参数说明这个脚本假设你已经知道图片的原始宽高。如果 json 里存了 imageWidth 和 imageHeight 字段直接读出来更稳妥。类别映射要和你之后数据集 yaml 里的 names 顺序保持一致否则训练时会张冠李戴。转换后一定要抽查几张 txt 的内容并写个回读画框的可视化脚本看看标注框是否贴合目标。坐标归一化错误或者标注框超出图像边界在训练时很难发现但会直接影响 mAP。4.2 数据集配置、训练超参数选择与训练命令数据准备好后先写一个数据集配置文件告诉 YOLOv5 训练集、验证集放在哪有多少类别# data/traffic.yaml train: data/traffic/train/images val: data/traffic/val/images nc: 4 names: [person, car, bus, truck]然后开始训练。毕设级别的数据量几百到一千张图不需要在模型结构上动刀直接让训练脚本跑python train.py \ --data data/traffic.yaml \ --weights yolov5s.pt \ --epochs 200 \ --batch-size 16 \ --imgsz 640 \ --device 0 \ --patience 40 \ --hyp data/hyps/hyp.scratch-low.yaml参数说明使用 yolov5s.pt 做预训练权重开始而不是用空权重从头训练。几百张图的数据量从头训练很难收敛微调是唯一合理策略。epochs200 是折中值。官方默认 300但数据量不大时 150~200 轮就够了配合 patience40 做早停防止后期过拟合。batch-size 16 大约需要 6G 以上显存。显存不够就减到 8或者把 imgsz 降到 416。后一种做法对小目标检测精度影响很大不建议盲目降低。hyp.scratch-low.yaml 是稳定版本超参数里面定了 lr00.01、momentum0.937、weight_decay0.0005 等基础值。数据量不大时不要随意调大 learning rate否则前几个 epoch 损失就崩了。训练日志和权重输出在 runs/train/exp 目录下。训练过程中可以用 TensorBoard 盯着看tensorboard --logdir runs/train关注两个曲线loss 曲线和验证集的 mAP 曲线。如果 mAP 在前 50 轮就基本平坦了说明数据太简单或学习率衰减太快可以考虑调低 lr0 或检查数据集的标注质量。4.3 训练后验证与权重替换训练结束后runs/train/exp/weights/ 下会有 best.pt 和 last.pt。习惯是先跑一组之前的测试图确认检测能力再上视频。命令python detect.py \ --source data/traffic/val/images \ --weights runs/train/exp/weights/best.pt \ --conf 0.4 \ --device 0确认输出图片里车、人、bus、truck 的框都正常后把检测器的权重路径换成这个 best.ptdet YOLODetector(weightsruns/train/exp/weights/best.pt, conf0.4)这里有一个经常踩的坑替换权重后原来的类别 id 映射可能变了。举个例子你用自定义数据集把 person 定义成了类别 0、car 定义成类别 1那 Deepsort 一侧也要相应调整之前代码里按 cls2 判断车辆的逻辑就得同步改掉。检测端和追踪端的类别 id 必须始终对应同一套定义。5. 避坑/常见问题排查5 个让毕设翻车的典型问题在多次接触这类毕设项目后我整理出五个出现频率最高的坑每个都按“现象 → 原因 → 解决”展开。对照你自己的运行结果能省下大量排错时间。5.1 检测框闪断、ID 跳变先调 max_age 而不是先改模型现象视频里同一个行人上一帧是 ID 12下一帧变成 ID 37画面上数字乱跳人在遮阳伞下走一圈 ID 换了七八个。原因目标被短暂遮挡或检测器漏检的瞬间Deepsort 的轨迹匹配失败旧轨迹被判定丢失新检测重新创建了一条轨迹ID 就变了。解决先把 Deepsort 的 max_age 从默认值调到 70 以上。max_age 的含义是“目标连续多少帧没匹配上也可以保留轨迹”取 70 就能覆盖大多数遮挡场景。同时把检测器 conf 提到 0.35~0.4拦截那些置信度 0.2 左右的低级误检。最后看一下帧率是否满足 15FPS如果太低追踪的帧间位移会变得不稳定ID 跳变会更频繁。使用 n_init3 也有帮助它保证一条轨迹要连续确认 3 帧才输出短暂出现的误检框不会被当成正经目标。要注意的是n_init 调得太大比如 5 或 10快速穿过画面的目标反而不会被计数。5.2 重复计数Set 去重是唯一正确的解法吗现象一辆车穿过虚拟线计数器加了三四次一个行人站在线边缘来回移动计数结果像“抽风一样跳”。原因重复计数的根因通常有两个。一是计数逻辑没有按 track_id 去重同一个目标反复穿过判定二是 Deepsort 本身给同一辆车分配了新 ID——目标被遮挡或检测中断后轨迹重建ID 变了新 ID 又一次穿越了同一根线。解决计数逻辑上用集合保存“已经计过数的 ID”一个 ID 只允许计数一次。用 set 收集即可if track_id not in self.counted_ids: self.counted_ids.add(track_id) if cls 2: self.vehicle_count 1如果是 Deepsort 层面丢了 ID上述办法解决不了。你需要先确认自己的 ID 变化到底发生在哪一步。简单做法把每帧的 track_id 和中心点坐标输出到 CSV对同一车辆观察 ID 是否从 5 变成 21。如果是调大 max_age并检查检测器是否在目标越过线时漏检——漏检是 ID 重建的最直接原因。理想情况下ID 应在目标穿过线之前保持足够稳定计数才会可靠。5.3 deep_sort 的 ImportErrorsklearn 版本兼容性现象安装所有依赖后import deep_sort 报错提示 cannot import name linear_assignment或者在 from sklearn.utils import linear_assignment_ 这一行直接 ModuleNotFoundError。原因deepsort_pytorch 老版本依赖 sklearn.utils.linear_assignment_该接口在 sklearn 0.24 被移除。很多毕设 requirements.txt 没有锁定 sklearn 版本pip 默认装上最新版导致运行失败。解决把环境里的 scikit-learn 降到 0.23.2。如果 numpy 版本太新导致冲突把 numpy 固定到 1.21.6。装完后重新执行python - EOF from sklearn.utils.linear_assignment_ import linear_assignment print(sklearn ok) EOF有些较新分支的 deep_sort_pytorch 已经改用 scipy.optimize.linear_sum_assignment这种不需要降版本。先 grep 一下你的源码里实际用的是哪个 import再决定怎么处理不要盲目照抄命令。5.4 权重文件缺失yolov5s.pt 和 ckpt.t7 放哪里现象运行 main.py 时没有提示错误类别直接卡在 torch.hub.load 或者 FileNotFoundError: No such file or directory。原因项目和源码包通常不包含权重文件。yolov5s.pt 是检测器权重ckpt.t7 是 ReID 特征提取权重。作者会在使用说明里提示去下载但很多人直接跳过说明跑代码。解决yolov5s.pt 放到 yolov5/weights/ 或项目根目录的 weights/ 下取决于你的代码里 path 怎么写。ckpt.t7 放到 deep_sort/deep_sort/deep/checkpoint/ 下这是 deep_sort_pytorch 仓库约定的默认位置。放完以后先检查路径大小写是否和源码一致Windows 下路径大小写不敏感Linux 下不一致就是 FileNotFoundError。5.5 推理速度慢与显存不足优化顺序现象视频播放明显卡顿FPS 只有 8~12跑一段时间后报 CUDA out of memory 并中断。原因最常见的情况是直接把 1080p 帧送进 YOLOv5又没有对输入做缩放同时 Deepsort 的 ReID 网络也在 GPU 上占显存。另一个隐藏问题是 nn_budget1000 之类的设置会让匹配阶段计算量暴涨帧率随即下降。解决按性价比从高到低依次做三件事。第一把视频帧缩放后再送进检测器frame cv2.resize(frame, (640, 640))第二把 Deepsort 的 nn_budget 降到 100max_dist 设为 0.2减少特征库规模和匹配时间。第三如果还是不够快用 TensorRT 对 YOLOv5 做加速效果一般是 PyTorch 推理的 2~3 倍。显存不足的应急手段是在主循环里主动清缓存torch.cuda.empty_cache()但这只是缓解别把它当常规手段。真正的解法是控制输入分辨率和 batch size。6. 验证与进阶让答辩老师看到你的实验闭环毕设答辩被问“效果怎么样”时不能只说“跑起来感觉还行”。至少要准备两个东西定量的追踪计数精度以及一组对比实验。定量验证方面自己找一个 3~5 分钟的交通路口视频手动数一遍真实穿线人数和车辆数作为 ground truth。然后跑你的系统算出 precision 和 recall。precision 是系统统计的通过目标里有多少真实目标recall 是真实目标里系统统计出了多少。注意把 Deepsort 的 ID 切换次数也统计出来这是追踪模块最直观的指标。对比实验方面最省事的一组是把 Deepsort 换成纯 IoU 匹配网上有现成实现其他代码不动对比两者的计数准确度。只要数据不是特别理想化Deepsort 通常会有明显的精度优势这张对比表直接能支撑论文里的一个实验小节。如果资源允许再录一段系统运行录屏卡在 ID 稳定穿过虚拟线的时刻答辩时现场放给老师看。进阶方向上如果你还有时间我推荐做以下三件事之一第一双向计数把代码里 count_up 和 count_down 两个方向的统计输出到一个小面板第二热度图把每个目标中心点累积到一副二维直方图可以用 OpenCV 的 applyColorMap 上色直观展示密集区域第三多线计数在画面里画多条虚拟线统计不同区域之间的穿行量这个方向更贴近商业应用场景。我个人的习惯是把训练时的那一套超参数变化曲线和追踪的 ID 切换曲线都截图保存下来论文的实验章节基本就有着落了。最后再提醒一句检测器的 conf 阈值尽量在整个演示过程中保持不变不要为了追求效果在答辩现场临时改数字否则结果和论文对不上反而显得实验不可复现。希望这些方案和排错经验能帮到你也祝你的毕设顺利通过。本文还有配套的精品资源点击获取