资讯动态

基于YOLOv8与ByteTrack的轨迹异常行为检测实战:奔跑与跌倒识别

发布时间:2026/10/18 8:19:40 来源:尧图企业网站定制
视频监控里最常见的尴尬是摄像头对着走廊系统知道“有人”但不知道这个人是正常路过还是在狂奔知道画面里人影晃动但分不清他是蹲下系鞋带还是突然摔倒。要把这一层补上靠单帧检测远远不够得把目标在时间轴上串起来让系统从轨迹里“看懂”行为。基于轨迹的异常行为检测解决的就是这个问题以YOLOv8作为目标检测底座配合视频追踪算法输出稳定的目标轨迹再从轨迹中提取速度、加速度、框形变化等运动特征实现快速奔跑、突然跌倒的自动识别。这篇内容对应我在某智慧安防模拟项目里完整做过的一个行为检测Demo核心是把“检测 → 追踪 → 轨迹管理 → 行为判定”整条链路走通。不只是给现成代码还会把为什么用ByteTrack、跌倒判定的三重信号怎么设计、阈值怎么调才不乱报以及实测中踩过的坑都交代清楚。适合已经会用YOLOv8做目标检测、准备往视频流处理和异常行为分析方向进阶的同学参考。1. 项目定位与技术选型为什么非“轨迹”不可1.1 单帧检测解决不了“时间问题”拿跌倒检测举例。一个人在画面中突然倒地整个事件往往只有十几到几十帧。如果只做单帧目标检测你能拿到的信息是“这一帧中人体框变扁了”但仅凭这一点无法区分他是主动蹲下、侧身捡东西还是真的失去平衡摔倒。再拿奔跑检测举例蹿跑的人在很多帧里可能是运动模糊状态单帧的置信度反而不如正常行走时稳定。单帧方案最大的问题是没有时间上下文而异常行为恰恰是一个时间过程。轨迹数据把时间轴补上了。一条轨迹本质上是一个目标在连续帧中的位置序列携带了三个维度的关键信息目标id、每帧的检测框坐标、对应的时间戳。基于轨迹分析你才能回答这些问题目标是不是突然加速了目标重心是不是在短时间内大幅下降目标倒地后是不是长时间没有恢复站立这些都是单帧检测回答不了的。1.2 追踪器为什么选ByteTrack而不是DeepSORT明确了“需要轨迹”之后下一个问题是追踪器选型。我当时在两套方案之间犹豫DeepSORT和ByteTrack。DeepSORT依赖ReID模型提取外观特征来跨帧关联目标在目标外观稳定的场景表现不错但有两个痛点一是ReID模型在低分辨率、运动模糊、遮挡场景下很容易丢目标二是跌倒场景有个特殊性——人一旦倒地检测框会从高瘦的立框突然变成矮胖的横框框内语义内容变化剧烈ReID特征反而不可靠。ByteTrack是纯几何关联方案主要依据检测框的IoU匹配分配目标不需要额外跑一个外观特征网络。它的核心思想是区分高置信度与低置信度检测框先匹配高置信度框再用低置信度框补漏因此对小目标、遮挡、短暂漏检的容错能力很强而且推理负担更小容易跑实时。实测下来在跌倒这类“框形态剧烈变化”的场景里ByteTrack的ID保持率明显优于DeepSORT。注意ByteTrack依赖检测器质量如果YOLOv8检测框抖动剧烈轨迹会跟着抖。所以实践中建议对检测框做一步轻量的平滑处理比如简单的一阶指数平滑再用平滑后的框更新追踪器。2. 判定原理从轨迹里怎么读出“奔跑”和“跌倒”2.1 一条轨迹里到底有什么在我这个Demo里轨迹数据结构不复杂但字段设计很重要。每条轨迹保留最近60帧的检测框序列和对应时间戳关键字段包括目标id、检测框x, y, w, h、该帧时间戳、瞬时状态正常/奔跑/跌倒、状态持续帧数。你可以把它理解成一个滑动窗口永远只关心目标“最近的动向”因为异常行为判定讲究时效30帧之前的信息对判断此刻是否跌倒没有意义。轨迹的数据来源是追踪器的输出。追踪器每帧返回若干个被跟踪目标每个目标自带一个稳定的track_id我以这个track_id为主键维护轨迹字典。如果track_id已存在就把当前帧的检测框追加到对应轨迹的队列尾部如果track_id是新出现的就新建一条轨迹。目标消失一段时间后队列会自动把过期的帧挤出去。这里有一个容易踩的坑追踪器自身的阈值决定了目标存活时间。ByteTrack中track_buffer参数代表目标丢失后保留轨迹的帧数我设置为30帧也就是目标消失1秒后轨迹才被判定终止。这个参数不能太小否则人在遮挡后回归时ID会变也不能太大否则大量“幽灵轨迹”占用内存还会造成行为判定延迟。2.2 快速奔跑识别的两个关键特征奔跑检测的核心是计算目标在轨迹中的运动速度但在像素坐标系里直接算出的速度没有绝对意义。同样一个成年人在画面近处移动50像素和远处移动50像素对应的实际距离完全不同。理想的方案是做透视变换把图像坐标映射到地面的世界坐标但工程上为了快速落地我用了另一种更实用的标定方式以目标自身的检测框高度为尺子。具体做法是取目标最近5帧的检测框底部中心点即脚部位置计算这一段时间内的平均位移再除以这段时间内检测框的平均高度得到一个“每秒位移多少个自身身高”的无量纲速度。这个指标对摄像头视角、目标距离有不错的鲁棒性。实测参数如下正常行走约每秒位移0.5到1.2个自身身高快速奔跑约每秒位移2.2到3.5个自身身高异常预警阈值建议踩到每秒1.8个自身身高以上保持3帧再触发预警只看速度还不够还要看加速度。奔跑不是匀速的正常人从静止到奔跑会有一个明显的加速过程。我在轨迹里维护一个速度滑动窗口计算最近5帧平均速度与之前5帧平均速度的差值。如果速度瞬间抬升超过0.8个自身身高/秒并且当前速度超过奔跑阈值判定为快速奔跑。加速度特征能有效过滤掉“目标本来就走得快”的误报。2.3 跌倒识别的三重信号框形、重心、静止跌倒识别的特征设计我参考了三点检测框宽高比突变、重心检测框中心y坐标骤降、倒地后持续静止。三个条件都要参与判定任何单一条件都不可靠这是我在实验中确认过的。先看宽高比。站立时人体检测框通常是高大于宽宽高比大约在0.3到0.6之间倒地后人体在图像中变成横向姿态宽高比会迅速超过1.0甚至达到2.0以上。只看这个指标的问题在于目标正常蹲下或躺下休息时宽高比同样会突变。所以必须结合重心变化。再看重心。跌倒通常伴随检测框中心点y坐标在短时间内大幅向下移动。我用“最近3帧内中心y坐标的下降量是否超过目标自身高度的0.4倍”作为条件。这个条件对应的是人体重心失去支撑后快速坠地的物理过程。下面是正常下蹲与跌倒的典型差异下蹲的重心下降是缓慢的、可控的且过程往往会停顿跌倒的重心下降则集中发生在3到8帧之内速度快一个量级。最后看倒地静止。真正的跌倒发生后目标往往不会立刻站起来会有一个低重心、静止或微动的阶段。我设定“宽高比大于1.1持续30帧以上且目标位移小于5像素/帧”作为“倒地确认”条件。这个条件把“跌倒后马上爬起来”和“跌倒后倒地不动”两种场景都覆盖到了。3. 完整实操从追踪到行为判定的代码实现3.1 依赖安装与模型选择环境方面我用的是Python 3.10核心依赖就四个ultralytics、bytetracker、numpy、opencv-python。安装命令如下pip install ultralytics bytetracker numpy opencv-python需要注意bytetracker在不同集成方式下接口略有差异。如果你用的是boxmot库统一管理多种追踪器调用方式会不一样我直接用了独立的bytetracker包接口比较简洁。YOLOv8模型的选择建议追求实时性选yolov8n或yolov8s这两个模型在CPU上能做到15到30帧在GPU上能跑满实时如果检测准确率优先且硬件充裕选yolov8m。我这个Demo里用的yolov8s在720p输入下GPU实测推理时间约8毫秒追踪约2毫秒整体非常宽裕。3.2 轨迹管理类与主循环实现先定义一个轨迹管理类专门负责维护每个目标的轨迹缓冲。这样行为判定逻辑可以复用代码结构也干净。from collections import deque class Track: def __init__(self, track_id, max_len60): self.track_id track_id self.boxes deque(maxlenmax_len) self.timestamps deque(maxlenmax_len) self.state normal # normal / running / fallen self.state_frames 0 # 当前状态持续帧数 self.confirm_frames 0 # 异常状态确认帧数 def update(self, box, timestamp): self.boxes.append(box) self.timestamps.append(timestamp)这里的max_len设定为60对应2秒30fps的历史窗口。这个长度足够覆盖奔跑速度估计和跌倒后静止确认又不会让过期数据干扰判断。接下来是主循环的骨架。核心流程拆成四步读帧、YOLOv8检测人目标、ByteTrack关联生成track_id、按track_id更新轨迹库。import cv2 import numpy as np from ultralytics import YOLO from bytetracker import BYTETracker model YOLO(yolov8s.pt) tracker BYTETracker(track_thresh0.5, track_buffer30, match_thresh0.8, frame_rate30) cap cv2.VideoCapture(demo.mp4) tracks {} while cap.isOpened(): ret, frame cap.read() if not ret: break frame_time cap.get(cv2.CAP_PROP_POS_MSEC) / 1000.0 # 第1步推理只保留人这个类别类别id为0 results model(frame, classes[0], conf0.4, verboseFalse) # 第2步构造ByteTrack要的检测输入 dets [] for box in results[0].boxes: x1, y1, x2, y2 box.xyxy[0].tolist() score float(box.conf[0]) dets.append([x1, y1, x2, y2, score]) if len(dets) 0: dets np.empty((0, 5)) # 第3步更新追踪器 online_targets tracker.update(np.array(dets), frame.shape[:2]) # 第4步更新轨迹库并做行为判定 for target in online_targets: track_id target.track_id x, y, w, h target.tlwh # 将tlwh格式调整为(x, y, w, h) if track_id not in tracks: tracks[track_id] Track(track_id) tracks[track_id].update((x, y, w, h), frame_time) behavior check_behavior(tracks[track_id]) # 在画面上绘制结果 ... # 清理已经消失太久的目标简单策略超过max_len的轨迹如果长时间未更新就不管了这里有一个细节ByteTrack返回的track.tlwh是目标框的左上角坐标和宽高正好是我们要的(x, y, w, h)格式不用再做转换。另外每帧检测之前要清空上一次的检测列表否则会把历史检测框重复喂给追踪器。3.3 行为判定模块实现行为判定是整个Demo的灵魂我在代码里把奔跑和跌倒各拆成一个函数最后汇总到check_behavior里。先写速度计算因为奔跑和跌倒判定都要用到。def foot_center(box): # 脚部中心点取检测框底部中心 x, y, w, h box return np.array([x w / 2, y h]) def estimate_speed(track, window5, fps30): if len(track.boxes) window 1: return 0.0, 0.0 boxes list(track.boxes) ft_now foot_center(boxes[-1]) ft_prev foot_center(boxes[-window]) # 像素位移 dist_pix np.linalg.norm(ft_now - ft_prev) # 窗口内平均身高 avg_h np.mean([b[3] for b in boxes[-window - 1:-1]]) if avg_h 1: return 0.0, 0.0 dt window / fps speed_body (dist_pix / avg_h) / dt # 单位自身身高/秒 return speed_body, speed_body注意这里把速度和加速度合并成了两个返回值的位置占位实际使用时加速度单独计算。建议在建轨迹时每帧都记录速度序列def append_speed(track, speed): if not hasattr(track, speed_deque): track.speed_deque deque(maxlen15) track.speed_deque.append(speed) def estimate_accel(track, window5): if len(track.speed_deque) window * 2: return 0.0 recent np.mean(list(track.speed_deque)[-window:]) older np.mean(list(track.speed_deque)[-window * 2:-window]) return recent - older接下来是奔跑和跌倒的判定函数。为了减少偶发噪声造成的误报这里采用“确认帧数”机制连续3帧满足条件才触发状态切换。RUN_THRESHOLD 1.8 # 奔跑速度阈值自身身高/秒 ACCEL_THRESHOLD 0.8 # 加速度阈值 FALL_ASPECT 1.1 # 宽高比阈值 FALL_DROP_RATIO 0.4 # 重心下降阈值自身身高倍数 FALL_STILL_FRAMES 30 # 倒地静止确认帧数 def check_running(track): speed, _ estimate_speed(track) accel estimate_accel(track) if speed RUN_THRESHOLD and accel ACCEL_THRESHOLD: track.confirm_frames 1 else: track.confirm_frames 0 if track.confirm_frames 3: track.state running return True return False def check_fall(track): if len(track.boxes) 10: return False boxes list(track.boxes) # 条件1框形突变 recent_aspect np.mean([b[2] / b[3] for b in boxes[-3:]]) # 当前宽高比明显变扁 if recent_aspect FALL_ASPECT: track.state normal if track.state fallen else track.state return False # 条件2重心骤降 center_y_prev boxes[-6][1] boxes[-6][3] / 2 center_y_now boxes[-1][1] boxes[-1][3] / 2 avg_h np.mean([b[3] for b in boxes[-6:]]) drop_ratio (center_y_now - center_y_prev) / max(avg_h, 1) if drop_ratio FALL_DROP_RATIO: return False # 条件3倒地后静止 if len(track.boxes) FALL_STILL_FRAMES 2: still_center [foot_center(bx) for bx in boxes[-(FALL_STILL_FRAMES 1):]] max_move np.max([np.linalg.norm(still_center[i] - still_center[i - 1]) for i in range(1, len(still_center))]) if max_move 5: return False track.state fallen return Truecheck_behavior汇总两个函数的结果并把状态绘制到视频帧上。我的优先级设计是跌倒状态优先于奔跑状态。如果一个目标先触发奔跑标记紧接着触发跌倒标记则只上报跌倒避免上报冲突。def check_behavior(track): if check_fall(track): return track.state if check_running(track): return track.state track.state normal return track.state3.4 需要避开的两个实现陷阱第一个陷阱是“跌倒静止判定会误伤奔跑后急停”。实测中一个人快速奔跑后突然停在原地休息同样会触发宽高比变化不大但速度骤降的情况但此时检测框宽高比不会变扁所以不会误判跌倒只会误判为“跑步状态退出”。如果你不希望这种情况触发任何报警可以追加一个“急停缓冲期”状态从running切换到normal时要求目标在3秒内不再出现高加速度启动。第二个陷阱是“检测框高度抖动对跌倒重心下降判定影响很大”。YOLOv8在目标运动模糊或部分遮挡时检测框高度会在连续几帧内跳变10%到20%这会让重心下降ratio失真。我在代码里对重心数据做了一个小技巧用最近3帧的中位数替代单帧值消除偶发抖动。中位数滤波比均值滤波更能抵抗极端离群点实测在跌倒场景里误报率能下降一半以上。4. 参数调优阈值怎么定才不会乱报4.1 核心参数对照表代码里的阈值看起来是一拍脑袋写出来的实际上每个参数背后都对应一类场景。我把最关键的参数整理成了一张表方便你对照自己的应用场景调整参数推荐初始值影响方向调高后效果RUN_THRESHOLD1.8身高/秒奔跑灵敏度更难触发奔跑报警ACCEL_THRESHOLD0.8身高/秒²加速突变灵敏度过滤更多匀速快走FALL_ASPECT1.1跌倒框形条件只识别完全横倒的情况FALL_DROP_RATIO0.4身高重心骤降灵敏度只识别剧烈坠地的情况FALL_STILL_FRAMES30帧倒地确认时长对倒地不动更宽容track_buffer30帧目标消失保留时长轨迹存活更久但幽灵轨迹更多conf检测置信度0.4检测召回率更少漏检但更多低质量框这一组参数是在720p、25帧/秒、摄像头俯视角度约30度的室内走廊场景下标定的。不同场景下尤其是摄像头安装高度和角度变化后速度阈值和宽高比阈值会发生明显漂移一定要重新校准不要拿着这组参数直接上生产。4.2 阈值校准的实操方法我校准阈值的方法是收集一小段带标注的测试视频裁出三类片段正常行走、快速奔跑、跌倒。然后跑一遍检测追踪把生成的轨迹数据导出成日志在日志里看正常片段和异常片段的速度、宽高比分布取两类分布的中间值作为阈值初值。以速度为例。正常行走片段里目标速度一般集中在0.8到1.5身高/秒奔跑片段集中在2.0到3.5身高/秒。两者中间有0.5的间隔初始阈值就打在1.8然后看误报如果正常片段里有快走被误判就往2.0提如果奔跑片段有漏检就往1.6降。多轮迭代后一般能找到一个稳定的工作点。4.3 多条件联合判定的必要性跌倒判定尤其不能迷信单一指标。我做过一组对比实验只用宽高比突变判定误报率约每百帧4次加上重心骤降条件后误报率降到每百帧1次再加上倒地静止确认后误报率降到每千帧接近0次。这说明联合判定不是简单的“三个条件都要满足”而是形成了一条逻辑链框形变化提供线索重心变化确认事件性质静止确认事件结果。三条链路缺一不可。5. 坑点实录我遇到的4个典型问题5.1 追踪ID频繁切换导致轨迹断裂现象是同一人在画面中走动track_id每隔十几帧就变一次轨迹库刚建立起历史窗口就断掉奔跑速度根本来不及计算。排查后发现根因是检测置信度抖动人走到画面边缘或经过遮挡物时置信度从0.7降到0.3ByteTrack把低置信度目标当成新目标处理导致ID切换。解决办法有两个一是把检测置信度从0.4降到0.25让更多低质量检测进入追踪器ByteTrack本身会通过IoU匹配把它们关联到已有轨迹上二是给ByteTrack的track_buffer加大到45帧让目标短暂消失后还能回到原轨迹。这两个改动配合后ID切换频率从每秒接近3次降到每分钟不到1次效果非常明显。5.2 跌倒误报的攻击性场景猫和阴影有一次测试视频里一只猫在走廊快速跑过宽高比和重心下降都满足跌倒条件被误判成“跌倒”。根本原因是追踪器把猫也当成目标关联了行为特征在几何层面和人相似。最有效的过滤方式是加检测类别约束只追踪人。此外阴影变化会产生伪目标检测出的“人形框”其实是地面上的影子追踪轨迹速度忽快忽慢。我的处理方式是检测端额外校验目标框的尺寸宽高比大于2.0或目标高度小于整幅画面高度5%的目标直接丢弃因为正常的人形检测框不会长期保持这两种状态。5.3 实时性不足时的优化策略最初我把推理分辨率设为原始1080p检测耗时接近20毫秒加上追踪和绘制整体30帧勉强跑得上但在边缘设备上就很吃力。后来把输入分辨率压到640像素宽置信度降到0.25检测耗时掉到6毫秒帧率稳稳超过30。另一个优化点是ByteTrack的track_thresh参数这个值决定高置信度框的门槛我调到0.5后追踪器跳过了大量低质量检测框的匹配运算。画面上同时出现五六个目标时整体端到端延迟依然在120毫秒以内。5.4 状态机设计防止同一目标重复触发预警一开始没有状态机跌倒判定成功后只要条件还满足每一帧都会触发一次报警十分钟内同一事件报警几百条。后来我加了“事件已上报”标记目标进入fallen状态并且已经上报后除非轨迹消失或目标重新变为normal比如站了起来否则不再重复触发。这个设计是所有行为告警系统都必须补上的一环否则后端会直接被重复上报淹没。6. 写在最后一点经验之谈这套Demo跑通之后我最大的感受是行为检测类项目的第一难点往往不是算法模型本身而是“特征的工程化落地”。同一个速度特征在固定视角、固定分辨率的场景里效果不错换一个视角就全面失效同一个宽高比特征在白天和夜间灯光下的表现也完全不同。所以如果你要复现这套方案我强烈建议先在自己的场景拍一段真实的测试视频而不是直接用网上的公开视频调参。另一个深有体会的点是轨迹稳定性是整个系统的地基。ID切换频繁时任何行为判定逻辑都无从谈起轨迹平滑了规则再简单也能把事件识别得很准。所以调试顺序建议是先确保检测稳定 → 再确保追踪ID稳定 → 最后才调行为阈值。跳跃调试只会让问题纠缠在一起越调越乱。最后分享一个后续扩展的方向跌倒检测可以再叠加YOLOv8-pose姿态估计做二次校验。在跌倒瞬间人体的肩部与髋部关键点会呈现出明显的纵向堆叠这是基于框的几何特征看不到的信息。把姿态关键点和轨迹运动学特征结合起来识别精度还能再上一个台阶。不过那条链路更重适合在帧率余量充足或专用算力的场景里去尝试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑