简介这份资源面向深度学习入门者、计算机视觉方向的学生以及需要完成毕业设计或课程设计的开发者提供了一套基于YOLO的实时人数统计与追踪完整方案可用于智能视频监控、人流分析等场景。压缩包共5个文件约32.8MB包含2个Python脚本、2个预训练权重文件和1份说明文档脚本分别实现YOLOv5结合DeepSORT、YOLOv8结合ByteTrack的检测与追踪流程权重文件为轻量级模型便于在算力有限的环境中快速推理说明文档则覆盖安装、配置与使用指引。目前已有52人学习。读者可借此理解从目标检测到多目标跟踪再到人数统计的完整链路对比两种跟踪算法的实现差异并在此基础上快速搭建自己的实验环境或二次开发。整体结构紧凑适合作为入门实践与项目参考。1. 从一段监控视频说起为什么人数统计总在“最后一米”翻车商场出入口的客流看板、工地安全帽识别、公交站台候车人数预警这些场景背后都指向同一个技术需求基于 YOLO 的人数统计与追踪。很多人第一次跑通检测模型时信心满满框住人、数出框感觉大功告成可一旦把视频流接进来画面里人一走动计数就开始跳变同一个人被反复统计遮挡一下又凭空消失。问题不在 YOLO 本身而在于“检测”和“统计追踪”是两件事。这个方向适合两类人一类是想把目标检测落地到真实视频流的工程师另一类是手里有 YOLO 权重、却卡在“怎么把逐帧检测变成稳定计数”的开发者。它解决的核心不是“能不能检测到人”而是“怎么让每一帧的检测结果在时间轴上连成一条稳定的轨迹再按规则去重计数”。本文按“检测→追踪→计数→调参→避坑”的顺序拆开讲代码可直接抄参数会说明为什么这么设。先把检测跑稳再谈追踪最后才是计数逻辑顺序反了就是给自己挖坑。2. 检测层怎么搭YOLO 选型、推理封装与最小可跑通代码2.1 为什么人数统计场景优先选 YOLOv8 而不是更重的模型人数统计对检测器的要求有三个硬指标召回率要高漏一个人就少一个、推理要够快视频流是实时的、小目标要稳远处的人往往只有几十像素。常见做法是在 YOLOv8n/s/m 之间选n 最快但小目标召回偏弱m 精度好但帧率吃紧。我的经验是室内固定机位、人距镜头近用 n 或 s 足够室外广场、俯拍远景直接上 m别在 n 上反复调阈值浪费时间。选型还要看部署硬件。CPU 推理时 n 和 s 差距明显GPU 上 s 和 m 的帧率差往往能接受。另一个容易被忽略的点是输入分辨率YOLO 默认 640但远景小目标建议提到 960 或 1280代价是显存和耗时上升。这里没有万能值先固定模型再调分辨率最后调置信度阈值一次只动一个变量否则你根本不知道是哪个参数起了作用。2.2 用 Ultralytics 封装一个只输出“人”的推理函数from ultralytics import YOLO import numpy as np # 加载模型权重文件放在项目根目录 model YOLO(yolov8s.pt) def detect_persons(frame, conf0.35, iou0.5, imgsz960): 只保留 COCO 类别中的人class_id0 conf: 置信度阈值低于此值的框丢弃 iou: NMS 交并比阈值控制重叠框合并 imgsz: 推理输入尺寸远景建议调大 results model.predict( sourceframe, confconf, iouiou, imgszimgsz, classes[0], # 关键只检测人减少后处理负担 verboseFalse ) boxes results[0].boxes # 转成 [x1, y1, x2, y2, conf] 的 numpy 数组 dets np.array([ [*box.xyxy[0].tolist(), float(box.conf[0])] for box in boxes ]) return dets if len(dets) else np.empty((0, 5))这段代码的关键在classes[0]。COCO 数据集里 0 就是 person加上这个参数后模型只输出人的框后处理不用再过滤类别速度会快一点。conf设 0.35 是偏保守的起点宁可多留几个误检也别漏检——追踪阶段还能靠轨迹长度把误检滤掉但漏检是补不回来的。imgsz从 640 提到 960远景召回通常有明显提升代价是单帧耗时增加约一倍需要按硬件实测权衡。2.3 检测结果长什么样先看清数据结构再谈追踪dets每行是[x1, y1, x2, y2, conf]这是追踪算法的输入。很多人直接把这个数组丢给追踪器就完事结果发现追踪器要求的是特定格式比如有的要[x, y, w, h]有的要归一化坐标。转换写错一位框就整体偏移表现出来就是“追踪框乱飞”。建议在检测和追踪之间加一层显式转换函数把格式转换单独测一遍别混在主流程里。另外要注意检测框的稳定性。同一帧里同一个人的框两次推理可能差几个像素这是正常的。追踪器靠的是框之间的 IoU 或外观特征做匹配几个像素的抖动不影响匹配但如果检测框时大时小、时有时无追踪就会频繁断轨。所以检测层的conf不要设太高保证框的连续性比保证框的精确更重要。3. 追踪层怎么接ByteTrack 匹配逻辑与 ID 稳定性的三个关键参数3.1 追踪不是“给框编号”而是“在时间轴上做匹配”追踪的本质是当前帧检测到若干框上一帧有若干已知轨迹要把当前框和已有轨迹配对。配对成功就延续 ID配对失败就新建轨迹或判定轨迹消失。ByteTrack 的思路是“高低分框都利用”高分框先和已有轨迹匹配剩下的低分框再和未匹配轨迹做二次匹配这样能救回一部分被遮挡导致分数下降的目标。这个设计对人数统计很友好因为遮挡是断轨的头号原因。理解这一点后参数就好调了。track_thresh是区分高低分框的阈值match_thresh是匹配时的 IoU 门槛track_buffer是轨迹丢失后保留多少帧再删除。这三个参数直接决定 ID 稳不稳下面逐个说。3.2 用 ByteTrack 把检测框连成轨迹from yolox.tracker.byte_tracker import BYTETracker from types import SimpleNamespace # 追踪器参数按场景调整 args SimpleNamespace( track_thresh0.5, # 高分框阈值高于此值优先匹配 track_buffer30, # 轨迹丢失后保留帧数30帧约等于1秒 match_thresh0.8, # 匹配 IoU 阈值越高越严格 mot20False ) tracker BYTETracker(args, frame_rate30) def update_tracks(dets, frame): dets: [x1,y1,x2,y2,conf] 的数组 返回: 带 track_id 的轨迹列表 # ByteTrack 要求输入为 [x, y, w, h, score] 格式 if len(dets): tlwh dets.copy() tlwh[:, 2] dets[:, 2] - dets[:, 0] # w tlwh[:, 3] dets[:, 3] - dets[:, 1] # h tlwh tlwh[:, [0, 1, 2, 3, 4]] else: tlwh np.empty((0, 5)) online_targets tracker.update(tlwh, [frame.shape[0], frame.shape[1]], [frame.shape[0], frame.shape[1]]) tracks [] for t in online_targets: x1, y1, w, h t.tlwh tracks.append({ id: t.track_id, box: [x1, y1, x1 w, y1 h], score: t.score }) return tracks这里最容易翻车的是坐标格式。ByteTrack 内部用的是tlwh左上角 x、y 加宽高而 YOLO 输出的是xyxy转换时w x2 - x1、h y2 - y1写反了框就完全错位。track_buffer30是按 30fps 估的意思是目标消失后轨迹保留 1 秒超过就删除。如果视频帧率不是 30这个值要按比例换算否则要么轨迹删太快导致 ID 频繁切换要么留太久导致“幽灵轨迹”。3.3 track_thresh、match_thresh、track_buffer 到底怎么设track_thresh设 0.5 是个稳妥起点。设太高被遮挡的人分数掉下来就进不了高分匹配容易断轨设太低误检框也会被当成高分框去匹配可能把轨迹带偏。match_thresh设 0.8 偏严格适合人走动不快、框重叠不严重的场景如果人群密集、框重叠多可以降到 0.6 到 0.7给匹配更多机会代价是可能匹配错人。track_buffer是最需要按场景调的。出入口这种目标快速通过的场景15 到 20 帧就够室内缓慢移动、遮挡频繁的场景30 到 50 帧更稳。判断标准很简单看日志里同一个人的 ID 切换频率如果一个人走完全程换了三次 ID就把track_buffer调大如果画面里出现了已经离开的人的框就调小。4. 计数层怎么做越线计数、区域去重与三种计数策略的取舍4.1 计数不是“数框”而是“数轨迹的穿越事件”最朴素的计数是每帧数框的数量但同一人连续多帧被数结果毫无意义。正确做法是给每条轨迹记一个状态当轨迹的某个参考点通常用框底边中点代表脚的位置从一条线的某一侧移动到另一侧时计数加一。这样一个人无论被检测到多少帧只在他真正穿过线的那一刻计一次。参考点选框底边中点而不是框中心是因为人走动时上半身晃动大脚的位置更稳定。俯拍场景下这个差异尤其明显用中心点会导致计数线附近反复触发。这个细节很多教程不讲但实际调试时能省下大量排查时间。4.2 越线计数的实现与方向判断class LineCounter: def __init__(self, line_start, line_end): self.line (line_start, line_end) self.count 0 self.track_state {} # track_id - 上一次所在侧 def _side(self, point): 判断点在线的哪一侧返回正负号 (x1, y1), (x2, y2) self.line px, py point # 叉积符号决定侧别 return (x2 - x1) * (py - y1) - (y2 - y1) * (px - x1) def update(self, tracks): for t in tracks: x1, y1, x2, y2 t[box] foot ((x1 x2) / 2, y2) # 框底边中点 side self._side(foot) tid t[id] if tid in self.track_state: prev self.track_state[tid] # 符号变化说明穿过了线 if prev * side 0: self.count 1 self.track_state[tid] side return self.count_side用叉积判断点在线的哪一侧符号变化就是穿越。这里有个坑如果参考点正好落在线上叉积为 0prev * side 0不成立这次穿越会被漏掉。实际使用中可以在线上加一个宽度带参考点进入带内就锁定状态离开带再判断方向能减少临界抖动导致的漏计或重复计。4.3 越线计数、区域计数、密度估计三种策略怎么选越线计数适合出入口、闸机这类有明确通行方向的场景精度高、逻辑简单。区域计数适合“当前区域有多少人”的看板需求实现方式是统计框中心落在多边形区域内的轨迹数难点在区域边界附近的人会反复进出需要加滞回逻辑。密度估计适合人群极度密集、检测框大量重叠的场景靠检测已经数不准了得换回归模型但那是另一条技术路线。我的建议是能用越线就别用区域能用区域就别碰密度估计。越线计数的误差来源少调参空间小最容易做稳。区域计数一定要加“进入区域后至少停留 N 帧才计入”的规则否则边界抖动会让数字乱跳。这三种策略没有优劣只有场景匹配度选错了再调参也是事倍功半。5. 避坑与排查人数统计从能跑到能用之间隔着的五个坑5.1 同一个人被反复计数现象是计数只增不减或者一个人走过去数字加了好几次。原因通常是追踪 ID 不稳定同一个人被分配了多个 ID每个新 ID 都被当成一次穿越。解决分两步先把track_buffer调大给轨迹更多恢复时间再检查检测框是否连续如果检测本身时有时无追踪器再强也救不回来这时候要降conf或提imgsz。5.2 计数偏少明显有人走过却没计上现象是画面里人明明穿过了线数字没动。原因可能是参考点选错比如用了框中心人弯腰或遮挡时中心点没越过线也可能是穿越判定太严格参考点擦着线过去没触发符号变化。解决方法是把参考点改成框底边中点并在计数线上加一个容差带参考点进入带内就记录待定状态离开带时按方向判定。5.3 画面边缘的人频繁触发计数现象是计数线靠近画面边缘时人在边缘进出导致数字乱跳。原因是边缘处检测框不完整参考点位置不稳定在线的两侧反复横跳。解决方法是在计数逻辑里加一个“轨迹存活帧数”门槛新出现的轨迹至少存活 5 帧才允许参与计数这样边缘闪现的误检和刚进画面还没稳定的轨迹都会被过滤掉。5.4 帧率变化后参数全部失效现象是换了一个视频源之前调好的参数全不好使了。原因是track_buffer是按帧数计的帧率一变同样的帧数对应的真实时间就变了。解决方法是把track_buffer按“秒”来设计代码里用fps * 期望秒数动态计算而不是写死一个数字。同理任何和“持续多少帧”相关的逻辑都要做帧率归一化。5.5 多人并排走时 ID 互换现象是两个人挨着走追踪 ID 突然对调导致计数错乱。原因是两人框重叠严重时IoU 匹配分不清谁是谁。解决方法是引入外观特征做二次匹配或者接受这个误差——在人数统计场景里ID 互换只要不发生在计数线附近对总数影响很小。如果计数线附近经常并排通行可以考虑把计数线设在通道较窄、人被迫分开的位置。6. 进阶技巧用轨迹平滑和计数校验把误差压到可接受范围检测框抖动是计数误差的隐形来源。一个实用技巧是对每条轨迹的参考点做滑动平均用最近 5 帧的底边中点均值代替当前帧的原始值再去做穿越判定。这样单帧的检测抖动被平滑掉穿越事件只在真实移动时触发。代价是计数会延迟几帧但对看板类应用完全可接受。from collections import deque class SmoothCounter(LineCounter): def __init__(self, line_start, line_end, window5): super().__init__(line_start, line_end) self.history {} # track_id - 最近若干帧的参考点 self.window window def update(self, tracks): for t in tracks: x1, y1, x2, y2 t[box] foot ((x1 x2) / 2, y2) tid t[id] buf self.history.setdefault(tid, deque(maxlenself.window)) buf.append(foot) # 用均值点做穿越判定 avg_x sum(p[0] for p in buf) / len(buf) avg_y sum(p[1] for p in buf) / len(buf) side self._side((avg_x, avg_y)) if tid in self.track_state: if self.track_state[tid] * side 0: self.count 1 self.track_state[tid] side return self.countwindow5是平滑窗口太小起不到平滑作用太大则计数延迟明显。5 帧在 30fps 下约 0.17 秒基本无感。这个技巧对固定机位效果最好如果镜头本身在动参考点会整体漂移平滑反而会引入误差这时候要先做背景对齐或者干脆换用区域计数。另一个进阶做法是加计数校验维护一个“当前画面内轨迹数”和“累计穿越数”正常情况下累计穿越数应该大于等于当前轨迹数如果出现累计数小于当前数说明追踪出了严重问题可以触发告警或自动重置。这个校验逻辑不复杂但能在无人值守时帮你发现参数失效。我自己踩过最深的坑是过早优化计数逻辑而忽略了检测框的连续性。后来养成一个习惯任何计数异常先回放检测结果的可视化视频看框是不是稳定框稳了再查追踪追踪稳了再查计数。这个顺序能省下大量瞎调参数的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?