ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLO26+ByteTrack实战:从检测到跟踪的智慧园区人流统计系统搭建指南

YOLO26+ByteTrack实战:从检测到跟踪的智慧园区人流统计系统搭建指南

1. 先想清楚:多目标跟踪到底解决什么问题

我最早做园区人流统计的时候,以为这活儿很简单:放一个摄像头在门口,检测到人,人数加一,完事。真到现场跑起来才发现,事情完全不是这样。

单帧检测只能回答“这一帧画面里有哪些目标”,回答不了“这个目标上一帧在哪、下一帧要去哪”。而人流统计要的不是“画面里有多少人”,而是“一段时间里有多少人经过”。从“有多少”到“经过了多少”,中间差的正是多目标跟踪(MOT,Multi-Object Tracking)这一层。把YOLO26这类检测器输出的人头框、人体框,串成一条条带ID的轨迹,再依据轨迹与计数区的位置关系做统计,这才是园区场景下真正可用的方案。

这个系列前面讲了很多YOLO26在检测层面的调优,到了第九篇,我们把检测和跟踪接起来,完整走一遍智慧园区人流统计的落地流程。无论你是刚接触跟踪的新手,还是已经在业务里被ID Switch折磨过一阵的开发者,这篇文章的目标只有一个:搞懂原理之后,让你能照着代码把一套可复现的人流统计系统搭出来。

先说清楚这套链路解决了什么:园区管理者想知道某个出入口早高峰的人流量、某个闸机通道是否拥堵、某栋楼一层大堂在不同时段的进出人数。这些需求落到技术上,就是一个视频流进来,经过YOLO26检测出目标框,经过跟踪器给每个目标分配稳定的ID,再经过计数逻辑输出进出人数。适合来做这件事的人包括:正在做智慧园区、智慧楼宇项目的算法工程师,需要快速出demo验证方案的学生,以及想搞明白MOT原理、准备转视觉方向的开发者。

2. 方案选型:YOLO26做检测,跟踪器怎么配

2.1 检测器选型的考量

YOLO26这个系列,核心卖点是把检测精度和推理速度做到了一个很适合边缘部署的平衡点。真实项目里,检测器直接决定了跟踪的上限。道理很简单:跟踪器靠的是检测框作为输入,如果检测器漏检严重、框抖动厉害,后面卡尔曼滤波再强也救不回来。在智慧园区场景里,检测器要面对的往往是广角摄像头下的密集人群、逆光、遮挡、远处的小目标,这些场景对检测器的要求比常规COCO评测指标反映出来的要高得多。

选YOLO26,我个人看中的是这几条:

  • 检测头对小目标的召回比前代版本有明显改善,园区出入口那种顶装摄像头视角下,远处行人只有二三十像素高,小目标能力直接决定漏检率。
  • 推理速度快,TensorRT加速之后可以在Jetson这类设备上跑实时。
  • 训练和部署生态成熟,导出ONNX、量化、TensorRT一条龙都有现成方案。

经验之谈:宁可花一周时间把检测模型在园区真实视角下的漏检率压下来,也不要急着调跟踪参数。跟踪调参只能修补检测的短板,补不了烂地基。

2.2 主流跟踪算法简评

检测框到手之后,下一步是把框“串”成轨迹。常见的跟踪算法主要分两类:一类是SORT家族,基于卡尔曼滤波和匈牙利匹配,主打速度快、逻辑简单;另一类是DeepSORT这种带外观特征的,额外用一个ReID模型提取行人外观特征,用特征相似度辅助匹配,抗遮挡能力强一些。

我做园区人流统计的几次项目里,最开始用的是SORT,后来换DeepSORT,再后来用ByteTrack。简单说下区别:

  • SORT:速度快,但ID Switch严重,一旦目标被遮挡几帧再出现,ID基本就换了。密集场景下统计准确率不高。
  • DeepSORT:加了外观特征,ID稳定不少,但多一个ReID网络,CPU占用高、推理慢,在边缘设备上有点吃力。
  • ByteTrack:思路很有意思,不单用高置信度检测框,低置信度框也拿来匹配,利用目标被遮挡时检测框置信度会降低但框还在这个特点,大幅降低了漏匹配。速度还快,不用额外特征网络。

园区场景一般不算特别拥挤,但出入口瞬时人流量可能很大,而且存在人和人之间的遮挡。ByteTrack在精度和速度之间的权衡,是目前我在类似场景下的首选。

2.3 智慧园区场景的特殊需求

园区不是公开数据集里的马路街景,它有自己鲜明的场景特点:

  • 机位固定。摄像头常年不动,背景基本不变,这给检测和跟踪都省了很多事,但也意味着一旦光照变化剧烈(比如早晚逆光),模型泛化得靠训练数据里见过类似情况。
  • 目标类别单一。主要就是行人,偶尔有自行车、快递车。类别少,可以专注提升行人的召回。
  • 区域语义明确。可以预先画好进出通道、闸机口、警戒区,这些区域映射到像素坐标后是静止的,计数逻辑可以设计得很简单。
  • 隐私合规要求高。很多园区不希望把原始视频存下来,最好只保留结构化数据(坐标、时间、ID),或者干脆直接在设备端完成推理,只上报统计结果。

这些特点决定了:方案不需要在做通用跟踪上过度投入,反而要在“单场景稳定性”上做文章。依赖场景先验来简化问题,是落地和刷榜最大的区别。

3. 整条链路怎么搭:数据流与技术细节

3.1 数据流向

一套完整的YOLO26人流统计系统,核心数据流如下:

视频流(RTSP摄像头或视频文件)→ 抽帧 → YOLO26推理 → 检测框后处理(NMS、过滤)→ ByteTrack跟踪器更新 → 轨迹与计数区域判断 → 输出结构化统计结果 → 写入数据库或推送业务系统

每一步看起来独立,但设计的时候要当作一个整体流水线来考虑。比如抽帧的间隔直接决定跟踪器能容忍的最大帧间隔;检测框后处理的置信度阈值影响ByteTrack里高、低置信度框的划分;计数区域的判断频率决定了你是逐帧统计还是每N帧统计一次。

实际项目里,我一般把整个流程拆成三个独立模块:采集模块、推理跟踪模块、业务统计模块。模块间用队列或消息解耦,方便后续把推理扔到GPU、把业务统计放到CPU,也方便单独升级某一块。

3.2 跟踪器里的“匹配”到底在做什么

理解ByteTrack这类跟踪器的关键,在于理解匹配过程。每一帧来了之后:

  1. 检测器输出一堆框。跟踪器手里还有上一帧留下来的一堆轨迹(每条轨迹有预测的当前位置、速度等状态信息)。
  2. 用IOU(交并比)或者距离度量,计算每个检测框和每条轨迹预测框之间的相似度,形成一个代价矩阵。
  3. 用匈牙利算法对这个代价矩阵求解,找出全局最优的“检测框-轨迹”配对方式。
  4. 高置信度框先匹配一轮,没匹配上的低置信度框再补匹配一轮,剩下的新框开新轨迹,剩下的轨迹如果没有匹配到则继续保留一小段时间(比如30帧),超过阈值就判为丢失。

这个过程中,卡尔曼滤波的作用是预测轨迹下一帧的位置,让匹配不是在“上一帧的旧位置”和“当前帧新框”之间比,而是在“预测出来的新位置”和“当前帧新框”之间比,这样即使目标移动了,依然能正确对应上。

注意:ByteTrack没有用外观特征,意味着它对“长得像”的目标不做区分。两个人交叉走过,框和轨迹对接的时候主要靠位置和运动趋势,虽然ByteTrack靠分阶段匹配缓解了一点,但极度拥挤场景下依然可能出现ID互换。这是选它的代价,要接受。

3.3 三条轨迹的更新规则

研究ByteTrack源码时,有些人会被它的状态机绕晕。我换个方式总结,把它简化成三条规则:

  • 第一轮匹配:当前帧所有高置信度检测框,和所有轨迹预测框做IOU匹配。匹配上的,更新轨迹位置,重置丢失计数。
  • 第二轮匹配:剩下没匹配上的低置信度检测框,再和“处于丢失状态但还没被移除”的轨迹预测框做IOU匹配。这一步是ByteTrack的精髓,专门找回被遮挡后检测置信度下降但位置还在的目标。
  • 非匹配结果:高置信度框无轨迹可配,生成新轨迹;轨迹连续多帧无检测框匹配,标记丢失并从列表中移除;匹配IOU太低的框轨迹对会被拒绝,拒绝的框继续走新轨迹逻辑。

这套规则跑下来,实际效果是:目标短暂消失在画面里几帧,轨迹能存活;目标重新出现且位置合理,轨迹能续上,ID不变。掌握了这三条规则,后面调参数就有的放矢了。

4. 实操:代码骨架与关键实现

4.1 环境准备与目录结构

实操之前先把环境备好。我常用的组合是:

  • Python 3.8/3.10
  • PyTorch 2.x(训练检测模型用)
  • 推理时用TensorRT或ONNX Runtime
  • 依赖:ultralytics或自定义YOLO26推理脚本、numpy、scipy(匈牙利匹配)、lap(线性分配)
  • opencv-python用于视频流读取和绘制

不要为了图省事把整个视频解码、推理、跟踪都写在一个文件里跑,后面维护会非常痛苦,建议至少拆成三个模块:

project/ ├── detector/ # YOLO26检测器封装 │ ├── __init__.py │ └── yolo_detector.py ├── tracker/ # 跟踪器封装 │ ├── __init__.py │ ├── bytetrack.py │ └── kalman.py ├── counting/ # 计数业务模块 │ ├── zone.py │ └── counter.py ├── main.py # 主流程编排 └── config.yaml # 公共配置

4.2 核心代码:YOLO26推理封装

YOLO26的推理可以用官方仓库的接口,也可以自己封装一个类。推荐封装成独立模块,好处是切TensorRT或者升级版本时只动这一处。简单写一下核心部分:

class YOLO26Detector: def __init__(self, weights_path, conf_threshold=0.35, device="cuda"): # ONNX Runtime / TensorRT / PyTorch 加载模型 self.model = load_model(weights_path, device) self.conf_threshold = conf_threshold def detect(self, frame): # 预处理:resize、归一化、通道转换 blob = preprocess(frame) predictions = self.model(blob) # 后处理:解码、NMS、置信度过滤、类别过滤(只保留person) boxes, scores = postprocess(predictions, self.conf_threshold) return boxes, scores

开发阶段为了调试方便,可以直接返回OpenCV格式的xyxy坐标。到了部署阶段,建议把预处理、NMS都扔进TensorRT引擎里,省掉Python侧重复计算的开销。

4.3 生成轨迹并绑定ID

这一步是跟踪的核心。每帧调用检测器拿到框列表之后,喂给跟踪器。跟踪器内部维护active_tracks列表,每个track包含:

  • track_id:全局唯一的身份编号
  • kf:卡尔曼滤波器实例(维护位置和速度状态)
  • bbox_history:最近若干帧的框位置
  • lost_frames:连续未匹配帧数
  • is_activated:是否已经确认激活(避免单帧误检直接生成轨迹)

伪代码如下:

def update(self, det_boxes): # 1. 用卡尔曼滤波预测所有active轨迹的下一帧位置 for track in self.active_tracks: track.kf.predict() track.pred_bbox = self.kf_to_bbox(track.kf) # 2. 第一轮:高置信度框匹配 matched, unmatched_dets, unmatched_tracks = \ self.match(det_boxes.high, self.active_tracks) # 3. 第二轮:低置信度框补匹配(只找lost状态的轨迹) if det_boxes.low.size: matched_low, _, unmatched_tracks = \ self.match(det_boxes.low, unmatched_tracks) # 更新matched_low对应轨迹的状态 update_tracks(matched_low) # 4. 处理未匹配的高置信度检测框 → 新建轨迹 for det in unmatched_dets: self.active_tracks.append(Track(new_id(), det)) # 5. 清理彻底丢失的轨迹 for track in self.active_tracks: if track.lost_frames > self.max_lost_frames: self.active_tracks.remove(track)

这套逻辑不需要改动太多,直接套用ByteTrack公开实现的思路就行。唯一要改的是track_id分配时的逻辑——有些业务场景希望ID从1开始连续分配且复用,有些希望每个ID严格唯一,分配前想清楚。

4.4 越线计数逻辑

园区人流统计最常用的计数方式是“虚拟线”法:在画面中画一条线,表示进出口的边界。轨迹跨越这条线的方向决定了是“进”还是“出”。

具体实现上,不推荐直接拿轨迹点和线的位置关系做判断,原因很简单:轨迹点更新频率取决于帧率,帧率不稳定时容易误判。更稳的做法是“累积位移法”——维护轨迹最近N帧的中心点序列,只要发现最新中心点和上一帧中心点分别位于“虚拟线”两侧,就认为发生了一次跨越,方向由跨越前后位置决定。

带方向计数的代码核心:

def is_crossing_line(prev_point, curr_point, line_points): # 用叉积符号判断点在直线的哪一侧 prev_side = cross_product_sign(prev_point, line_points) curr_side = cross_product_sign(curr_point, line_points) return prev_side != curr_side def count_event(track, line_points): if len(track.center_history) < 2: return None prev_p = track.center_history[-2] curr_p = track.center_history[-1] if is_crossing_line(prev_p, curr_p, line_points): # 返回方向:in / out return "in" if curr_p.y > prev_p.y else "out" return None

实践经验:如果摄像头不是正对通道,而是斜45度安装,虚拟线的方向判断要配合实际朝向调整,有时候“y轴向下增加”不一定代表“往画面下方走是出”。建议先在现场截一帧图,标出实际行进方向,再做坐标映射。这一步做反了,统计结果会方向颠倒。

5. 常见问题与排查调优实录

5.1 跟踪列表与“什么是真正完成了一次统计”

统计模块最容易被忽略的一点:不是每次检测到人跨线都要计数,而是“一条轨迹只跨线一次”计数一次。但实际中,人跨线后会站在线附近徘徊,如果每帧都做跨越判断,同一个人会被重复计数好几次。

解决办法有两个:一是给轨迹加一个“已统计”标记,同一个轨迹不管跨越几次线,只取第一次;二是加“冷却时间”,轨迹跨线后T秒内不再接受新计数。推荐两者结合,因为只靠轨迹ID标记,一旦目标在跨线瞬间发生ID切换,新ID会被重复计数;只靠冷却时间,则会漏掉真正连续跨线的人。

我自己常用的是第二种变体:维护一张最近N秒内完成计数的轨迹ID表,新跨线事件先查表,如果ID在表中且时间小于阈值,忽略;否则确认计数并更新时间戳。准确率比单用ID标记高不少。

5.2 ID Switch造成的误差与对策

密集场景下,ID Switch是导致统计失真的头号问题。两个人擦肩而过,ID互换,后面所有基于ID的判断全都歪了。排查经验:

  • 先确认检测框是否稳定。框抖动会直接导致IOU匹配失败,频繁切换ID。解决办法是提升检测的NMS阈值、拉低置信度阈值,或者用EMA平滑检测框。
  • 再确认跟踪器参数。ByteTrack里的lost时间窗口(通常默认30帧)如果太短,一个目标离开画面1秒再回来就会被判为新轨迹,适合园区场景一般设50-100帧,出入口这种目标不会离开太久的地方可以设小一点。
  • 如果确认是并行人流太多、交叉太严重,那就必须引入外观特征辅助,换用DeepSORT类方案。代价是推理时间增加,但密集场景下值得。

5.3 检测频繁漏检的排查技巧

漏检的直接后果是轨迹中断、计数丢失。常见原因和排查步骤:

  • 目标太小。园区顶装摄像头下,远处行人面积可能不足32x32像素,YOLO26虽然小目标能力有提升,但也不能无中生有。对策是提高输入分辨率,把640输入改成1280,同时接受推理时间上升;或者做Tiling,把画面切块分别推理再合并结果。
  • 逆光导致对比度低。对策不是调模型,而是在预处理里做图像增强,或者干脆换摄像头安装角度。
  • 置信度阈值设置过高。建议先降到0.25观察,对比检测效果再逐步调回。阈值太高会爽快漏检,阈值太低又会导致误检框增多,跟踪器多开一堆假轨迹。我的习惯是白天场景0.35,夜间或逆光场景0.25,具体以现场为准。

5.4 边缘设备跑不动的性能瓶颈排查

边缘设备上最常见的现象是推理速度和视频流帧率不匹配。排查顺序:

  • 先看检测器耗时,正常TensorRT优化后的YOLO26在Jetson Orin NX级别设备上单帧应该在10-20ms量级。如果超了,优先检查模型是否做了FP16量化、输入分辨率是不是设置得过高。
  • 再看跟踪器耗时,ByteTrack纯CPU计算,一般单帧1ms以内。如果超过5ms,检查是不是每帧都创建了大量临时对象,或者轨迹数量积累到几千条没清理。
  • 最后看视频解码,RTSP拉流+OpenCV imread是最容易拖后腿的环节。建议用硬解码API或者多线程抽帧,把解码和推理解耦。

6. 智慧园区人流统计落地:从demo到业务可用

6.1 定义业务指标

技术demo阶段,输出“进x人、出y人”就够了。落到园区业务里,得把原始计数变成管理者能用的指标:

  • 小时级/天级进出人数曲线,用来评估出入口通行压力。
  • 当前区域滞留人数(累计进-累计出),用来防止楼内过度拥挤。
  • 闸机口排队长度估算,通过统计队伍区域内轨迹数量近似得到。

数据落到数据库后,再通过图表展示,就是一套可交代的智慧园区人流统计系统。从我的经验看,管理者最常看的是“小时级进出人数对比”和“滞留人数预警”,这两项做好了,项目就成功了一半。

6.2 部署架构建议

单摄像头方案最简单,一台边缘设备接一路RTSP,推理结果直接上报MQTT或HTTP接口。多摄像头方案就要考虑集中管理:

  • 每个摄像头的视频流配一个推理实例,各实例独立统计。
  • 上报数据统一带摄像头ID、区域ID、时间戳。
  • 后端服务汇总各设备上报的数据,做跨摄像头的分析。

多摄像头部署时最麻烦的问题是同一人跨多个摄像头,被重复计数。如果业务只关心“园区总人流”,这个误差需要靠摄像头覆盖区域不重叠来规避,或者做跨镜ReID。大多数园区项目会接受按“单点位独立统计”的思路,而不去强行做全园统一的ID体系。

6.3 扩展方向

人流统计只是YOLO26跟踪能力在园区的一个应用。框架跑通之后,加需求基本就是加业务逻辑:

  • 把虚拟线换成多边形区域,做区域入侵检测。
  • 用轨迹速度信息识别异常奔跑行为。
  • 叠加ReID特征,实现跨摄像头目标检索。
  • 结合热力图,分析人群聚集趋势。

每一类扩展的核心,都还是“检测+跟踪+业务规则”这三层结构,检测和跟踪这部分代码可以几乎不动。

7. 一点个人体会

这套系统我前前后后在好几个园区场景里调过。说实话,最难的不是把demo跑起来——YOLO26加现成的ByteTrack代码,一下午就能出效果。真正难的是让统计准确率在真实光线、真实拥挤程度、真实摄像头角度下稳定维持在95%以上。这件事没有捷径,就是反复跑现场、收集数据、针对性调参。

如果你正准备做类似的项目,我建议第一个不上线的版本只做录像分析,不碰实时流。因为录像可以反复回放,排查问题效率高得多。确认指标稳定后再切到实时流,否则现场调试时画面转瞬即逝,你连出错原因都看不清楚。

最后再分享一个小经验:所有跟踪参数,别写死在代码里,全部放进配置文件。YOLO26模型权重、置信度阈值、跟踪器丢失帧数、计数线坐标、摄像头ID,这些在一个yaml文件里管好。项目迭代一个月之后,你会发现这个决定帮你省了至少一周的返工时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进