YOLOv5行为识别实战:从检测框到动作语义的完整链路与部署优化

📅 发布时间:2026/10/11 20:31:36
YOLOv5行为识别实战:从检测框到动作语义的完整链路与部署优化
简介这份资源围绕基于YOLOv5的行为识别展开面向计算机视觉入门者、深度学习实践者以及需要视频理解方案的开发者帮助解决从目标检测到行为分类的完整链路搭建问题。包内共6个文件以2个Python脚本、1份doc说明文档、1个md说明、1个license及1个yaml配置为主压缩包约1.6MB脚本与配置可直接用于检测与训练流程文档则补充了YOLOv5网络结构、Anchor机制、Mosaic数据增强等关键知识点。资源已有572人学习下载说明其在同类项目中具备一定参考价值。内容涵盖特征提取、行为建模与分类识别思路并涉及UCF-101、HMDB-51等公开数据集的使用方向读者可据此理解如何用YOLOv5逐帧检测人物与物体再结合LSTM、GRU等模型捕捉时序动态最终完成行为分类与实时视频分析部署适合作为智能监控、安全预警等场景的实践起点。1. 基于 YOLOv5 的行为识别从检测框到动作语义的那一层到底怎么补很多人第一次听到「基于 YOLOv5 的行为识别」脑子里浮现的是把标注好的动作视频丢进 YOLOv5训练完就能输出「打架」「摔倒」「抽烟」这类标签。真跑一遍就会发现YOLOv5 吐出来的永远是person 0.87 [x1,y1,x2,y2]它根本不知道这个人在干什么。这不是模型不行而是 YOLOv5 的定位就是目标检测行为识别要在它的输出之上再补一层时序或姿态逻辑。我做过几个工地安全帽佩戴、加油站打电话、养老院跌倒检测的项目最后落地的方案都不是「一个端到端大模型」而是 YOLOv5 负责人体框后面接一个轻量分类器或规则引擎。这篇文章就把这条链路拆开YOLOv5 训练自己的数据集怎么标、超参数怎么调、行为分类头怎么接、量化到 RK3568 或树莓派 4B 上帧率掉到多少、哪些坑会让你在演示现场翻车。适合已经跑通过 YOLOv5 官方 demo、想把它推到「识别动作」这一步的工程师也适合被产品经理一句「加个行为识别」逼到墙角的人。2. 为什么不能直接拿 YOLOv5 训行为类别检测与识别的边界2.1 YOLOv5 的输出结构决定了它做不了纯时序判断YOLOv5 的检测头输出的是(batch, num_anchors, 5num_classes)的张量每个 anchor 对应一个框的x,y,w,h,obj_conf,cls_conf。它做的是单帧空间上的回归和分类没有循环结构也没有光流或帧间差分。你硬把「打架」当成一个类别去标会出现两个问题第一同一帧里两个人扭打框该画在谁身上第二出拳和收拳两帧视觉上几乎一样但语义相反单帧模型只能学到「两个人靠得近」这种伪特征。我见过有人把「跌倒」标成人体框的一个类结果模型把蹲下系鞋带也判成跌倒因为训练集里蹲下的样本太少模型偷懒学了「人体宽高比变大」这个捷径。正确的分工是YOLOv5 只负责「人在哪」行为识别模块负责「这个人在时间维度上做了什么」。前者是空间问题后者是时空问题。把这两件事混在一个检测头里等于让一个只会看照片的人去判断一段视频里谁先动手。2.2 行为识别的三条主流补法姿态、时序分类、规则引擎补法一姿态估计加规则。用 YOLOv5 检测人体框裁剪后送进轻量姿态模型如 MoveNet、RTMPose 的轻量版拿 17 个关键点再用角度和距离规则判断。比如跌倒可以用「髋部中心点 y 坐标在 0.5 秒内下降超过阈值且头部低于髋部」来触发。优点是可解释、算力低、RK3568 上能跑缺点是规则要针对场景调换一个摄像头角度可能就失效。补法二时序分类网络。把 YOLOv5 框出的人体裁剪序列比如连续 16 帧送进一个 3D CNN 或 CNNLSTM 的小网络输出行为类别。优点是能学到「出拳」这种动态模式缺点是需要标注视频片段数据成本高且裁剪框抖动会让时序特征不稳定。补法三规则引擎兜底。很多工业场景其实不需要深度学习做行为判断比如「进入危险区域」就是框中心点落在多边形内「未戴安全帽」就是头部区域没有检测到安全帽框。这类用 YOLOv5 检测 几何判断就能做到 95% 以上准确率还不用训行为分类器。我一般会先问需求方你要的行为是「状态型」还是「动作型」状态型戴没戴、在不在区域用规则动作型打架、跌倒、攀爬才上时序模型。这个判断能帮你省掉一半的标注成本。2.3 选型对照不同行为类型该走哪条路行为类型典型例子推荐方案单帧可判算力需求状态型安全帽、反光衣、打电话YOLOv5 多类检测是低区域型闯入、越界、停留YOLOv5 多边形判断是极低姿态型跌倒、下蹲、举手YOLOv5 姿态 规则部分中动作型打架、攀爬、抽烟YOLOv5 时序分类否高这张表不是绝对的比如「抽烟」如果只判断手部靠近嘴部单帧也能做但误报率会高到没法用。实际项目里我通常先用状态型和区域型快速上线动作型作为二期迭代这样演示现场至少有东西能跑。3. 用 YOLOv5 训练自己的行为数据集标注、配置与超参数3.1 数据标注人体框和行为标签要分开存做行为识别时标注文件里只标person一个类行为标签单独用一个 CSV 或 JSON 记录「第几帧到第几帧、哪个 track_id、什么行为」。不要试图在 YOLO 的 txt 里写行为类别那样会把检测和分类耦合死。目录结构我一般这样组织dataset/ images/ train/ cam01_0001.jpg cam01_0002.jpg val/ cam01_0500.jpg labels/ train/ cam01_0001.txt cam01_0002.txt val/ cam01_0500.txt behavior_annotations.csvbehavior_annotations.csv的格式frame_path,track_id,behavior,start_frame,end_frame cam01_0001.jpg,3,fall,1,45 cam01_0060.jpg,3,walk,46,120逻辑说明YOLO 的 label 文件每行是class x_center y_center width height归一化到 0-1。行为标注单独维护方便你后面换分类器时不用重新标检测框。参数上track_id需要你先用 ByteTrack 或 DeepSORT 跑一遍生成否则同一帧里多个人你分不清谁是谁。3.2 从官方权重出发--weights和--cfg怎么选训练自己的数据集时不要从零开始训。用yolov5s.pt做预训练权重改--cfg为对应模型结构或者直接用--weights yolov5s.pt让脚本自动匹配。命令示例python train.py \ --img 640 \ --batch 16 \ --epochs 100 \ --data data/behavior_person.yaml \ --weights yolov5s.pt \ --cfg models/yolov5s.yaml \ --hyp data/hyps/hyp.scratch-low.yaml \ --name behavior_person_s \ --cache逻辑说明--data指向你的数据集 yaml里面写train、val、nc: 1、names: [person]。--hyp选hyp.scratch-low.yaml是因为行为场景里人体框通常比 COCO 里小低学习率增强能减少过拟合。--cache把图片缓存到内存小数据集能明显加速但图片超过 2 万张时别开会把内存吃满。参数怎么改--img从 640 提到 960 能改善小目标人体但 RK3568 上推理会慢一倍--batch根据显存调16 是 8G 卡的保守值--epochs100 起步看mAP0.5曲线如果 60 轮就平了后面都是浪费。3.3 超参数里最影响行为场景的三个anchor、mosaic、copy_pasteYOLOv5 默认 anchor 是基于 COCO 的行为场景里人体框往往更窄更高比如站立的人所以训练前最好用kmeans重新聚类 anchorimport numpy as np from utils.autoanchor import kmean_anchors # 传入你的 label 路径和图片尺寸 anchors kmean_anchors( pathdata/behavior_person.yaml, n9, img_size640, thr4.0, gen1000, verboseTrue ) print(anchors)逻辑说明kmean_anchors会读取所有 label 的宽高用遗传算法加 kmeans 生成 9 个 anchor。thr4.0是 anchor 与标注框的宽高比阈值行为场景里人体框长宽比集中在 1:2 到 1:3聚类后能明显提升召回。把输出的 anchor 替换到models/yolov5s.yaml的anchors:字段。mosaic增强在行为识别里要慎用。它会把四张图拼成一张如果四张图里都有行为标注拼接后行为的时间连续性就断了。我一般把mosaic概率从 1.0 降到 0.5或者在行为分类阶段不用 mosaic 增强的图。copy_paste对遮挡场景有用但行为数据里如果两个人重叠copy_paste 可能把一个人的框贴到另一个人身上导致标签错乱建议关闭。3.4 训练完先看混淆矩阵别急着导出 ONNX训练结束后runs/train/behavior_person_s/下会有confusion_matrix.png。行为场景里最怕的是「人体框漏检」因为漏检一个人后面的行为判断直接丢失目标。如果混淆矩阵显示person的漏检率超过 10%先别管行为分类回去补数据增加小目标、遮挡、夜间样本。我见过一个工地项目白天 mAP 0.92晚上掉到 0.61原因是训练集里夜间图片不到 5%。补了 2000 张夜间图后夜间 mAP 回到 0.85。4. 把行为分类头接到 YOLOv5 后面裁剪、时序与推理管线4.1 用 ByteTrack 给人体框加 ID行为才有主体YOLOv5 每帧输出一堆框但行为识别需要知道「这个框上一帧在哪」。直接用 IOU 匹配在遮挡时会断 ID所以用 ByteTrackimport cv2 import torch from yolov5.models.common import DetectMultiBackend from yolov5.utils.general import non_max_suppression from yolov5.utils.torch_utils import select_device device select_device(0) model DetectMultiBackend(runs/train/behavior_person_s/weights/best.pt, devicedevice) model.eval() # ByteTrack 初始化track_thresh 是进入跟踪的置信度阈值 from bytetrack import ByteTrack tracker ByteTrack(track_thresh0.5, track_buffer30, match_thresh0.8) cap cv2.VideoCapture(test.mp4) while cap.isOpened(): ret, frame cap.read() if not ret: break img cv2.resize(frame, (640, 640)) img_tensor torch.from_numpy(img).permute(2, 0, 1).float().unsqueeze(0) / 255.0 pred model(img_tensor) det non_max_suppression(pred, conf_thres0.4, iou_thres0.5)[0] # 把检测框喂给 ByteTrack拿到带 track_id 的轨迹 online_targets tracker.update(det.cpu().numpy(), img.shape[:2]) for t in online_targets: x1, y1, x2, y2, track_id t[:5] cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fID:{int(track_id)}, (int(x1), int(y1)-5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF 27: break逻辑说明track_thresh0.5表示只有置信度超过 0.5 的框才进入跟踪低于这个值的框作为「低分框」参与二次匹配这是 ByteTrack 的核心。track_buffer30表示目标丢失后保留 30 帧的轨迹超过就删除。match_thresh0.8是匹配时的 IOU 阈值行为场景里人体移动慢可以调到 0.7 让匹配更宽松。参数怎么改如果视频里人走动快track_buffer提到 50如果误跟严重一个人被分成两个 ID把match_thresh降到 0.6。RK3568 上跑 ByteTrack 的 CPU 开销大概占 15%如果帧率不够可以把track_buffer降到 15。4.2 裁剪序列送进时序分类器窗口大小和采样策略拿到 track_id 后把每个人体框裁剪出来按 track_id 存成序列。时序分类器的输入窗口我一般用 16 帧步长 8 帧也就是每 8 帧做一次行为判断。窗口太小8 帧学不到完整动作太大32 帧延迟高且算力翻倍。from collections import defaultdict, deque # 每个 track_id 维护一个长度为 16 的队列 track_buffers defaultdict(lambda: deque(maxlen16)) def process_frame(frame, online_targets): for t in online_targets: x1, y1, x2, y2, track_id t[:5] crop frame[int(y1):int(y2), int(x1):int(x2)] crop cv2.resize(crop, (112, 112)) track_buffers[track_id].append(crop) # 队列满了才做行为判断 if len(track_buffers[track_id]) 16: clip np.stack(track_buffers[track_id], axis0) # (16,112,112,3) clip clip.transpose(3, 0, 1, 2) # (3,16,112,112) clip torch.from_numpy(clip).float().unsqueeze(0) / 255.0 with torch.no_grad(): behavior_logits behavior_model(clip) behavior_id behavior_logits.argmax(dim1).item() # 行为标签映射 label behavior_names[behavior_id] cv2.putText(frame, label, (int(x1), int(y2)20), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)逻辑说明deque(maxlen16)自动丢弃旧帧保证窗口滑动。112x112是时序分类器常用的输入尺寸比 224 省算力。行为判断只在队列满时触发避免前几帧误判。参数上如果行为持续时间短比如打架就几秒窗口可以降到 12如果行为是缓慢的比如攀爬窗口提到 24。4.3 推理管线里的帧率分配别让 YOLOv5 拖死时序模型整条管线的耗时是YOLOv5 推理 ByteTrack 裁剪 时序分类。在 RK3568 上YOLOv5s 跑 640 输入大概 80msByteTrack 10ms时序分类器MobileNetV3GRU大概 30ms加起来 120ms也就是 8 FPS。如果摄像头是 25 FPS你需要跳帧每 3 帧做一次检测中间帧用 ByteTrack 预测位置。这样检测耗时摊到每帧是 27ms总耗时降到 70ms 左右能到 14 FPS。跳帧策略用代码控制frame_idx 0 detect_interval 3 # 每 3 帧检测一次 while cap.isOpened(): ret, frame cap.read() if not ret: break if frame_idx % detect_interval 0: det model(frame) online_targets tracker.update(det, frame.shape[:2]) last_targets online_targets else: # 非检测帧用上一帧的轨迹做线性预测 online_targets predict_next(last_targets) process_frame(frame, online_targets) frame_idx 1逻辑说明detect_interval3是精度和帧率的折中。如果行为变化快打架降到 2如果行为慢徘徊提到 5。predict_next用简单的匀速模型x1 vxvx是上一帧到当前帧的位移。这个预测在遮挡时也能维持 ID 不断。5. 部署到 RK3568 和树莓派 4B量化、转换与实测帧率5.1 RK3568 的量化流程从 ONNX 到 RKNNRK3568 的 NPU 只吃 RKNN 模型所以要先导出 ONNX再用rknn-toolkit2转换。导出 ONNX 时注意把--include onnx加上并且用--dynamic让 batch 维度动态python export.py \ --weights runs/train/behavior_person_s/weights/best.pt \ --include onnx \ --img 640 \ --batch 1 \ --dynamic转换脚本from rknn.api import RKNN rknn RKNN(verboseTrue) # 均值方差要和训练时一致YOLOv5 默认是 0-1 归一化 rknn.config( mean_values[[0, 0, 0]], std_values[[255, 255, 255]], target_platformrk3568, quantized_dtypeasymmetric_quantized-8, optimization_level3 ) rknn.load_onnx(modelbest.onnx) # 量化校准集从训练集里抽 200 张有代表性的图 rknn.build(do_quantizationTrue, datasetcalibration.txt) rknn.export_rknn(best.rknn)逻辑说明mean_values和std_values必须和训练时的预处理一致YOLOv5 是img/255所以这里 std 写 255。quantized_dtype选asymmetric_quantized-8是 RK3568 上精度和速度平衡最好的。calibration.txt里每行是一张图的路径校准集要覆盖白天、夜间、遮挡场景否则量化后精度掉得厉害。我一般从训练集里按场景分层抽 200 张不要随机抽随机抽可能全是白天。5.2 树莓派 4B 的部署ONNX Runtime 还是 NCNN树莓派 4B 没有 NPU只能靠 CPU。YOLOv5s 在 4B 上跑 ONNX Runtime 大概 1.5 FPS没法用。实际能跑的是 NCNN 或 TFLite配合 INT8 量化。NCNN 的转换# 先安装 ncnn 工具 git clone https://github.com/Tencent/ncnn.git cd ncnn mkdir build cd build cmake .. make -j4 # 用 onnx2ncnn 转换 ./tools/onnx/onnx2ncnn best.onnx best.param best.bin # 量化 ./tools/quantize/ncnn2table best.param best.bin calibration.txt best.table ./tools/quantize/ncnn2int8 best.param best.bin best.table best_int8.param best_int8.bin逻辑说明onnx2ncnn把 ONNX 转成 NCNN 的 param 和 bin。ncnn2table用校准集生成量化表ncnn2int8生成 INT8 模型。树莓派 4B 上 NCNN INT8 跑 YOLOv5s 640 输入大概 4-5 FPS如果降到 320 输入能到 10 FPS。行为识别部分建议用 MobileNetV3-small 做时序分类INT8 后单次推理 20ms 左右。5.3 实测帧率与精度对照表平台模型输入尺寸精度帧率RK3568YOLOv5s RKNN INT8640mAP0.5 0.8912 FPSRK3568YOLOv5s RKNN INT8320mAP0.5 0.8125 FPS树莓派 4BYOLOv5s NCNN INT8640mAP0.5 0.874.5 FPS树莓派 4BYOLOv5s NCNN INT8320mAP0.5 0.7910 FPSJetson NanoYOLOv5s TensorRT FP16640mAP0.5 0.9118 FPS这张表是我自己项目里测的不是官方数据。RK3568 上如果开双核 NPU640 输入能到 15 FPS但功耗和发热会上去外壳要加散热片。树莓派 4B 跑 640 基本只能做离线分析实时预览会卡。Jetson Nano 是行为识别比较舒服的档位但成本比 RK3568 高。6. 行为识别落地避坑5 个让我返工的血泪教训6.1 现象演示现场行为标签乱跳同一个人一秒内从「走」变「跑」变「跌倒」原因时序分类器的窗口没有做平滑每 8 帧输出一次相邻窗口的预测结果可能完全不同。加上裁剪框抖动输入特征不稳定。解决在输出层加一个滑动投票。维护每个 track_id 最近 5 次的行为预测取众数作为最终标签。如果众数占比低于 60%输出「不确定」。这个后处理能把误报率降一半。代码上用一个defaultdict(deque(maxlen5))存历史预测每次取Counter(history).most_common(1)。6.2 现象夜间红外画面下 YOLOv5 漏检严重行为识别直接失效原因训练集里夜间样本太少且红外画面是灰度图YOLOv5 的 RGB 预训练权重对灰度图不友好。解决训练时把夜间灰度图复制成三通道并在hyp里把hsv_v增强调低灰度图没有饱和度。另外补 2000 张以上夜间标注图标注时注意红外下人体边缘模糊框可以适当放宽 5 个像素。如果实在补不了数据用--img 960提高小目标召回但帧率会掉。6.3 现象RK3568 量化后 mAP 从 0.89 掉到 0.72原因校准集里全是白天图量化参数偏向白天分布夜间和遮挡场景的激活值被截断。解决校准集按场景分层抽样白天、夜间、遮挡各占三分之一总数 200-300 张。另外把optimization_level从 3 降到 2虽然慢一点但精度更稳。如果还不行对检测头部分做混合量化quantized_dtype改成dynamic_fixed_point-16但 RK3568 对 16 位支持有限要查文档确认。6.4 现象ByteTrack 在两个人交叉走过时 ID 互换行为标签跟错人原因IOU 匹配在交叉瞬间两个框重叠匹配矩阵出现歧义。解决ByteTrack 本身有 ReID 特征可选但 RK3568 上跑 ReID 太慢。我的做法是在交叉区域用运动方向预测记录每个 track 最近 5 帧的速度向量交叉时优先匹配方向一致的。另外把match_thresh从 0.8 降到 0.6让匹配更依赖外观而不是位置。如果场景里人流量大直接上 DeepSORT 的轻量 ReID但帧率会掉 20%。6.5 现象训练 loss 正常下降但验证集 mAP 卡在 0.5 上不去原因行为数据集里人体框标注不一致有的人标了全身有的人只标了上半身。YOLOv5 学到的框大小分布混乱。解决标注规范里明确「人体框包含完整可见身体遮挡时标可见部分并加occluded标记」。用脚本检查所有 label 的宽高比如果出现宽高比大于 1:1 的框除非是躺着的人大概率是标错了。另外把anchor重新聚类让 anchor 匹配你的标注习惯。这个坑我踩过两次每次都是标注外包团队换人导致的。7. 行为识别的进阶技巧用「行为置信度时序曲线」做二次判断前面讲的都是单窗口分类但很多行为是有持续时间的。比如「跌倒」不是一帧的事而是「站立→下降→躺地」三段。单窗口分类器可能只在下降段给出高分躺地后反而判成「休息」。我的做法是把每个 track_id 的行为置信度按时间存成曲线用简单的状态机做二次判断。具体实现维护一个behavior_history[track_id]列表每帧存(frame_idx, behavior_id, confidence)。然后定义状态转移规则# 跌倒状态机站立 - 下降 - 躺地 def fall_state_machine(history): # 取最近 30 帧 recent history[-30:] behaviors [h[1] for h in recent] confs [h[2] for h in recent] # 如果出现下降且置信度 0.7且后续出现躺地 if falling in behaviors and max(confs) 0.7: idx behaviors.index(falling) after behaviors[idx:] if lying in after: return fall return normal逻辑说明这个状态机不依赖单帧分类器的绝对准确率而是看行为序列的转移模式。falling和lying是时序分类器输出的两个类单独看都可能误报但「先 falling 后 lying」的组合误报率极低。参数上recent窗口 30 帧对应 1 秒左右30 FPS如果摄像头是 15 FPS窗口调到 15。这个方法的另一个好处是能输出「行为片段」而不是「行为帧」。比如输出fall from frame 120 to frame 180业务系统可以直接拿这个片段去报警或存证不用自己合并帧。我在养老院项目里用这个状态机把跌倒误报从每天 20 次降到 2 次代价是报警延迟增加了 0.5 秒但业务方完全能接受。还有一个技巧是「多尺度窗口投票」同时跑 8 帧、16 帧、24 帧三个窗口的分类器取三者一致的结果。8 帧对快速动作敏感24 帧对慢动作稳三者一致时置信度最高。这个在 RK3568 上会多耗 30% 算力如果帧率有余量可以上。我一般只在 Jetson 或服务器端用边缘设备上还是单窗口加状态机更划算。最后说一个我自己的习惯每次上线新行为类别前先拿一段 10 分钟的真实场景视频跑一遍人工数一下误报和漏报算一个「业务可用率」。如果误报超过 5 次/小时不管 mAP 多高都不上线回去补数据或调规则。这个习惯让我少挨了很多次骂。希望帮到你。本文还有配套的精品资源点击获取