2026/9/26 2:03:27

基于YOLOv7-POSE与STGCN的智能监控行为识别实战

基于YOLOv7-POSE与STGCN的智能监控行为识别实战 简介这份资源是一套面向安防监控与计算机视觉方向的智能监控系统完整实现适合具备一定深度学习基础、希望将姿态估计与行为识别落地到实际场景的开发者与研究人员。系统以YOLOv7-POSE完成实时人体关键点检测借助Bytetrack实现复杂场景下的多目标追踪并引入STGCN空间-时间图卷积网络进行跌倒与异常行为识别可用于老人看护、公共安防等需要及时预警的场合。压缩包共107个文件约65.07MB包含41个Python源码、28个pyc编译文件、8个yaml配置、7段mp4演示视频以及pth模型权重、sh运行脚本、Dockerfile与说明文档等覆盖从环境搭建到推理演示的完整链路。目前已有81人学习下载。读者可据此理解多模块协同的工程结构参考关键点检测、目标跟踪与行为分类的衔接方式并借助示例视频与权重快速复现跌倒识别效果为二次开发与场景迁移提供可用的起点。1. 从看得见到看得懂智能监控系统到底在解决什么传统监控摄像头只能做到看得见——录像、存储、回放但真正出事的时候你需要的不是一段视频而是有人摔倒了有人在打架这样的即时判断。基于 YOLOv7-POSE 姿态估计、ByteTrack 多目标跟踪和 STGCN 行为识别的智能监控系统解决的正是从看得见到看得懂这一步跨越。它的核心链路是先用 YOLOv7-POSE 从每一帧画面中提取人体关键点再用 ByteTrack 给每个人分配稳定的 ID 并维持跨帧轨迹最后把每个人的骨架序列送入 STGCN 做行为分类输出跌倒正常行走挥手求助等语义标签。这套方案适合做安防监控、老人看护、工地安全等场景的团队也适合想入门多人姿态估计与行为识别的工程师。整条链路对实时性要求高选型和参数调优直接决定能不能落地。2. 技术选型为什么是 YOLOv7-POSE ByteTrack STGCN2.1 YOLOv7-POSE 在多人姿态估计中的位置人体姿态估计分两大流派自顶向下Top-Down和自底向上Bottom-Up。自顶向下先检测人再逐人估关键点精度高但人数多时耗时线性增长自底向上先检测所有关键点再聚类成人速度稳定但拥挤场景容易串人。YOLOv7-POSE 属于单阶段多人姿态估计把关键点检测直接集成在检测头里一次前向就输出所有人的边界框和 17 个 COCO 关键点。它的优势是推理速度快、部署简单在 1080Ti 上跑 640×640 输入能到 30 FPS满足实时监控的基本要求。和 YOLOv8-POSE 相比YOLOv7-POSE 的生态更成熟TensorRT 部署资料多ONNX 导出踩坑少。25 年姿态估计顶刊里很多新方法精度更高但要么依赖大 backbone要么后处理复杂放到边缘盒子上跑不动。实际项目里我一般优先选 YOLOv7-POSE 做 baseline精度不够再考虑换 RTMPose 或 ViTPose。关键参数上--kpt-label对应 17 个关键点顺序--pose开启姿态分支--img-size建议 640太小关键点抖动大太大帧率掉得厉害。置信度阈值--conf-thres默认 0.25监控场景建议提到 0.4减少误检带来的假轨迹。2.2 ByteTrack 为什么比 SORT/DeepSORT 更适合监控多目标跟踪的核心是数据关联把当前帧的检测框和上一帧的轨迹匹配上。SORT 只用 IoU 做匈牙利匹配遮挡后容易丢 IDDeepSORT 加了 ReID 特征但每帧要跑一次特征提取网络算力翻倍。ByteTrack 的思路很巧妙把检测框按置信度分成高分和低分两组先用高分框匹配轨迹再用低分框去救那些被遮挡导致分数掉下来的轨迹。这样既不增加网络开销又能显著降低 ID Switch。监控场景里人经常被柱子、货架遮挡ByteTrack 的低分框二次匹配正好对症。实测同一段视频SORT 的 ID Switch 有 40 多次ByteTrack 能压到 10 次以内。参数上track_thresh控制高分框阈值默认 0.5track_buffer是轨迹丢失后保留的帧数默认 30监控场景建议调到 60给遮挡留更多恢复时间match_thresh是匹配 IoU 阈值默认 0.8拥挤场景可以降到 0.7。2.3 STGCN 做行为识别的输入构造STGCNSpatial Temporal Graph Convolutional Network把人体骨架看成一张图关节点是节点骨骼连接是边时间维度上同一节点跨帧相连。它同时做空间图卷积和时间卷积能捕捉抬手下蹲这类动作的时空模式。相比 RGB 视频分类骨架输入对光照、衣着、背景干扰更鲁棒模型也小得多。输入构造是关键每个人每个时刻的骨架是 (17, 2) 或 (17, 3) 的坐标一段动作取 T 帧常用 30 或 60组成 (T, 17, 2) 的张量。STGCN 还需要邻接矩阵描述关节连接关系COCO 17 点的骨架边是固定的直接按预定义表构造即可。训练时按动作类别打标签跌倒检测通常是二分类或三分类正常/跌倒/疑似。3. 从零搭起环境、推理与跟踪链路打通3.1 环境准备与依赖安装先确认 CUDA 版本YOLOv7-POSE 官方仓库对 PyTorch 1.10~1.13 兼容最好。我一般用 conda 建独立环境避免和系统里的包打架。conda create -n smart_monitor python3.8 -y conda activate smart_monitor # 按本机 CUDA 版本装 PyTorch这里以 CUDA 11.3 为例 pip install torch1.12.1cu113 torchvision0.13.1cu113 -f https://download.pytorch.org/whl/torch_stable.html # 装 YOLOv7-POSE 依赖 pip install -r requirements.txt # ByteTrack 和 STGCN 的依赖 pip install cython lap scikit-image pip install numpy opencv-python tqdm逻辑说明PyTorch 版本必须和 CUDA 驱动匹配否则torch.cuda.is_available()返回 False后面全部跑 CPU 会慢到没法用。lap是 ByteTrack 做匈牙利匹配的底层库不装会报No module named lap。cython是编译 lap 的前置。参数说明python3.8是兼容性最好的版本3.10 以上有些旧算子会编译失败。如果要用 TensorRT 加速额外装pycuda和tensorrt版本要和 CUDA 对齐。3.2 YOLOv7-POSE 推理与关键点提取下载官方权重yolov7-w6-pose.pt放到weights/目录。下面是最小推理脚本输出每个人的边界框和 17 个关键点。import torch import cv2 import numpy as np from models.experimental import attempt_load from utils.datasets import letterbox from utils.general import non_max_suppression_kpt, scale_coords from utils.plots import output_to_keypoint # 加载模型 device torch.device(cuda:0 if torch.cuda.is_available() else cpu) model attempt_load(weights/yolov7-w6-pose.pt, map_locationdevice) model.eval() def detect_pose(img, img_size640, conf_thres0.4, iou_thres0.65): # 预处理letterbox 保持比例填充到 640x640 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) inp letterbox(img_rgb, img_size, stride64, autoTrue)[0] inp inp.transpose(2, 0, 1) inp torch.from_numpy(inp).float().to(device) / 255.0 inp inp.unsqueeze(0) # 推理 with torch.no_grad(): output, _ model(inp) # NMS 关键点解码 output non_max_suppression_kpt(output, conf_thres, iou_thres, ncmodel.yaml[nc], nkptmodel.yaml[nkpt], kpt_labelTrue) output output_to_keypoint(output) # 坐标映射回原图 if len(output): output[:, 7:] scale_coords(inp.shape[2:], output[:, 7:], img.shape[:2]).round() return output # 每行: [batch_id, x1, y1, x2, y2, conf, cls, kpt_x1, kpt_y1, ...]逻辑说明letterbox保证输入不变形stride64是 YOLOv7-POSE 的下采样倍数。non_max_suppression_kpt是姿态专用的 NMS会同时处理框和关键点。output_to_keypoint把输出整理成每行一个人的格式前 7 列是框信息后面 34 列是 17 个点的 x,y。参数说明conf_thres0.4比默认 0.25 高是为了减少监控画面里的误检。iou_thres0.65控制重叠框合并人多时别调太低否则相邻的人会被吞掉。img_size640是速度和精度的平衡点如果画面里人很小可以提到 960但帧率会掉一半。3.3 ByteTrack 接入与轨迹管理ByteTrack 官方实现是独立的BYTETracker类输入检测框和置信度输出带 track_id 的轨迹。把上一步的关键点按 track_id 挂上去就得到每个人每个时刻的骨架。from yolox.tracker.byte_tracker import BYTETracker from types import SimpleNamespace # ByteTrack 参数 args SimpleNamespace( track_thresh0.5, # 高分检测阈值 track_buffer60, # 轨迹保留帧数监控场景调大 match_thresh0.8, # 匹配 IoU 阈值 mot20False ) tracker BYTETracker(args, frame_rate30) def update_tracks(pose_output, frame): # 把姿态输出转成 ByteTrack 需要的格式 [x1,y1,x2,y2,score] if len(pose_output): dets pose_output[:, 1:6].astype(np.float32) else: dets np.empty((0, 5), dtypenp.float32) # 更新跟踪器 online_targets tracker.update(dets, frame.shape[:2], frame.shape[:2]) # 组装结果track_id - 关键点 results [] for t in online_targets: tid t.track_id tlwh t.tlwh kpts None # 用 IoU 把当前帧的检测框和轨迹关联取对应关键点 for det in pose_output: iou compute_iou(tlwh, det[1:5]) if iou 0.5: kpts det[7:].reshape(17, 2) break results.append({track_id: tid, bbox: tlwh, keypoints: kpts}) return results逻辑说明ByteTrack 的update接收检测框和当前帧尺寸内部完成卡尔曼预测、两次匹配、轨迹生命周期管理。返回的online_targets里每个对象有track_id和tlwh。关键点关联用 IoU 做简单匹配因为同一帧里检测框和轨迹框位置几乎重合。参数说明track_buffer60意味着轨迹丢失后保留 2 秒30FPS遮挡恢复后还能接上原 ID。match_thresh0.8是首次匹配阈值二次匹配用的是 0.5这个在 ByteTrack 源码里写死了。如果画面里人移动很快match_thresh可以降到 0.7。3.4 骨架序列缓存与 STGCN 输入准备STGCN 需要连续 T 帧的骨架所以要给每个 track_id 维护一个滑动窗口。窗口满了就送模型推理然后滑动。from collections import defaultdict, deque SEQ_LEN 30 # 取 30 帧约 1 秒 skeleton_buffer defaultdict(lambda: deque(maxlenSEQ_LEN)) def push_skeleton(track_id, keypoints): if keypoints is None: return None # 归一化以髋关节中心为原点除以肩宽做尺度归一 hip (keypoints[11] keypoints[12]) / 2 shoulder_dist np.linalg.norm(keypoints[5] - keypoints[6]) 1e-6 norm_kpts (keypoints - hip) / shoulder_dist skeleton_buffer[track_id].append(norm_kpts) # 窗口满了才返回供 STGCN 推理 if len(skeleton_buffer[track_id]) SEQ_LEN: return np.stack(skeleton_buffer[track_id]) # (30, 17, 2) return None逻辑说明归一化是必须的否则同一个人走近走远坐标数值变化会被 STGCN 误判成动作变化。以髋关节中心为原点消除平移除以肩宽消除尺度。deque(maxlenSEQ_LEN)自动淘汰旧帧不用手动管理。参数说明SEQ_LEN30对应 1 秒动作跌倒这种快速动作够用如果做缓慢的异常行为如徘徊可以加到 60 或 90。归一化用肩宽而不是身高因为监控里下半身经常被遮挡肩宽更稳定。4. STGCN 行为识别训练、推理与跌倒检测落地4.1 STGCN 网络结构与输入输出定义STGCN 的核心是 ST-GCN 块先做空间图卷积聚合相邻关节特征再做时间卷积聚合相邻帧特征中间加残差连接。输入张量形状是(N, C, T, V, M)N 是 batchC 是通道数坐标 2 或 3T 是帧数V 是关节数 17M 是人数单人为 1。输出是每个样本的类别概率。COCO 17 点的骨架边定义如下构造邻接矩阵时直接用import numpy as np # COCO 17 点骨架连接 edges [(0,1),(0,2),(1,3),(2,4),(0,5),(0,6),(5,7),(7,9), (6,8),(8,10),(5,6),(5,11),(6,12),(11,13),(12,14), (13,15),(14,16)] def build_adjacency(num_nodes17): A np.zeros((num_nodes, num_nodes)) for i, j in edges: A[i, j] 1 A[j, i] 1 # 加自环 A np.eye(num_nodes) # 归一化D^-1/2 A D^-1/2 D np.diag(np.sum(A, axis1)) D_inv_sqrt np.linalg.inv(np.sqrt(D)) return D_inv_sqrt A D_inv_sqrt逻辑说明邻接矩阵描述关节间的物理连接自环让节点保留自身特征。归一化防止度数大的节点如髋关节数值爆炸。STGCN 里通常用多子集邻接矩阵向心、离心、自环这里给的是最简版。参数说明num_nodes17对应 COCO 格式如果用 MediaPipe 的 33 点要改。边表是固定的不要随意增删否则和预训练权重不兼容。4.2 跌倒检测数据集构造与训练脚本公开的跌倒数据集有 UR Fall、Le2i、Fall Detection Dataset但格式各异。我一般统一转成(样本数, 2, 30, 17, 1)的 npy标签 0 正常、1 跌倒。训练用交叉熵Adam 优化器学习率 1e-330 轮足够收敛。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class SkeletonDataset(Dataset): def __init__(self, data_path, label_path): self.data np.load(data_path) # (N, 30, 17, 2) self.labels np.load(label_path) # (N,) def __len__(self): return len(self.labels) def __getitem__(self, idx): # 转成 (C, T, V, M) x self.data[idx].transpose(2, 0, 1)[..., np.newaxis] return torch.FloatTensor(x), torch.LongTensor([self.labels[idx]])[0] # 训练循环 model STGCN(num_class2, in_channels2).cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() for x, y in train_loader: x, y x.cuda(), y.cuda() pred model(x) loss criterion(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() # 每轮验证 model.eval() correct total 0 with torch.no_grad(): for x, y in val_loader: x, y x.cuda(), y.cuda() pred model(x).argmax(dim1) correct (pred y).sum().item() total y.size(0) print(fEpoch {epoch}, Val Acc: {correct/total:.4f})逻辑说明数据转成(C, T, V, M)是 STGCN 的标准输入格式。weight_decay1e-4防过拟合骨架数据量通常不大。每轮验证看准确率跌倒检测更关注召回率可以额外打印混淆矩阵。参数说明num_class2是二分类如果要区分跌倒蹲下正常就改成 3。in_channels2是 2D 坐标用 3D 坐标改 3。学习率 1e-3 是起点loss 不降就降到 1e-4。4.3 实时推理与告警触发逻辑训练好的 STGCN 接进主循环每凑满 30 帧推理一次。跌倒判定不能只看单次输出要加时序平滑否则模型偶尔抽风会误报。from collections import deque alert_buffer defaultdict(lambda: deque(maxlen5)) # 每个 ID 保留最近 5 次预测 def check_fall(track_id, skeleton_seq, model): if skeleton_seq is None: return False x torch.FloatTensor(skeleton_seq.transpose(2, 0, 1)[np.newaxis, ..., np.newaxis]).cuda() with torch.no_grad(): prob torch.softmax(model(x), dim1)[0] pred prob.argmax().item() alert_buffer[track_id].append(pred) # 最近 5 次里至少 3 次判跌倒才告警 if sum(alert_buffer[track_id]) 3: return True return False逻辑说明alert_buffer做多数投票抑制单帧误判。跌倒动作持续 1~2 秒30 帧窗口滑动时会有多次预测投票能稳定结果。告警触发后可以推 RTSP 截图、发 MQTT 消息、写数据库按业务接。参数说明maxlen5和阈值 3 是经验值误报多就提到 4漏报多就降到 2。推理频率取决于 SEQ_LEN 和滑动步长步长 10 帧的话每 0.3 秒推理一次实时性够。5. 避坑与排查那些让我加班到凌晨的细节5.1 关键点抖动导致 STGCN 误判现象同一个人站着不动STGCN 输出在正常和跌倒之间反复跳。原因YOLOv7-POSE 单帧关键点有 2~5 像素抖动归一化后放大成特征噪声。解决对关键点做滑动平均或 One-Euro 滤波窗口 5 帧延迟增加 0.16 秒但稳定性大幅提升。我一般用 One-Euro它对快速动作的跟随比均值滤波好。5.2 ByteTrack ID Switch 在人群密集时飙升现象两个人交叉走过ID 互换骨架序列串了行为识别全乱。原因match_thresh0.8太严交叉时 IoU 掉到 0.6 以下匹配失败。解决把match_thresh降到 0.7同时开mot20True启用更宽松的匹配策略。如果还不行加一个轻量 ReID 特征做二次校验但会牺牲 20% 帧率。5.3 STGCN 训练集和推理输入分布不一致现象训练准确率 95%上线后跌倒全漏。原因训练数据是实验室采集归一化用肩宽现场监控人穿宽大衣服肩宽检测偏大归一化后骨架缩小和训练分布对不上。解决统一归一化方式改用髋到肩的距离做尺度或者干脆不做尺度归一改用固定相机标定。血泪经验归一化方式必须在训练和推理时完全一致差一点都不行。5.4 多进程推理时 CUDA 上下文冲突现象用 multiprocessing 开两个进程分别跑 YOLO 和 STGCN报CUDA error: initialization error。原因CUDA 上下文不能跨进程共享每个进程要独立初始化。解决要么单进程串行跑帧率够就用这个要么每个进程torch.cuda.set_device()指定不同 GPU。单卡的话建议单进程用线程池做 IO 异步。5.5 视频流解码成为瓶颈现象GPU 利用率只有 30%帧率上不去。原因用 OpenCVcv2.VideoCapture读 RTSP 是单线程同步解码CPU 解码 1080P 只能到 25FPS成了瓶颈。解决换 PyAV 或 GStreamer 做硬解码或者用cv2.VideoCapture(url, cv2.CAP_FFMPEG)开硬件加速。更彻底的做法是解码和推理分离解码线程往队列里塞帧推理线程消费。6. 进阶技巧把跌倒检测的误报率压到可接受范围跌倒检测落地最大的敌人不是漏报是误报。老人弯腰捡东西、坐下、蹲下系鞋带骨架形态和跌倒高度相似模型很容易搞混。我踩过的坑里误报率从每小时 10 次降到 0.5 次靠的是三层过滤。第一层是姿态阈值前置过滤。跌倒时人体重心会快速下移髋关节 y 坐标在 0.5 秒内下降超过身高的 30%。在送 STGCN 之前先算这个速度不满足的直接跳过能滤掉 60% 的疑似样本。这个计算几乎不耗算力但效果立竿见影。第二层是 STGCN 输出加时序投票前面 4.3 节已经讲了。这里补充一个细节投票窗口不要固定按动作持续时间自适应。跌倒动作通常 1~2 秒窗口取 5 次预测如果是缓慢的异常行为窗口要拉长到 10 次以上。第三层是空间上下文校验。跌倒的人通常躺在地上边界框的宽高比会从竖长变成横宽。在告警前检查bbox_w / bbox_h 1.2不满足的降级为疑似而不是直接告警。这一层能干掉大部分蹲下和弯腰的误报。三层过滤的代码骨架def fall_alert_pipeline(track_id, kpts_seq, bbox, model): # 第一层重心下降速度 hip_y [(k[11][1] k[12][1]) / 2 for k in kpts_seq] drop_speed (hip_y[0] - hip_y[-1]) / len(hip_y) if drop_speed 0.3 * body_height: return normal # 第二层STGCN 时序投票 pred stgcn_infer(kpts_seq, model) alert_buffer[track_id].append(pred) if sum(alert_buffer[track_id]) 3: return suspicious # 第三层宽高比校验 w, h bbox[2], bbox[3] if w / h 1.2: return suspicious return fall参数说明drop_speed的系数 0.3 是经验值不同相机角度要微调俯拍角度大时系数可以降到 0.2。body_height用边界框高度近似比用关键点算身高稳定。宽高比阈值 1.2 适合站立视角如果是俯拍摄像头这个阈值要重新标定。验证方法上我习惯用回放测试把历史误报的视频片段收集起来跑一遍新逻辑看误报是否被过滤掉同时确认真实跌倒没被误杀。每次调参都跑一遍这个回归集避免按下葫芦浮起瓢。回归集至少要有 50 段正样本和 200 段负样本覆盖不同光照、不同相机角度。还有一个容易忽略的点告警去重。同一个人跌倒后可能躺在地上几分钟STGCN 每 0.3 秒推理一次会连续触发几十次告警。必须加冷却时间同一个 track_id 告警后 60 秒内不再重复告警。这个逻辑简单但必须有否则运维会被告警轰炸到崩溃。最后说个部署习惯所有阈值参数不要硬编码在代码里抽到 YAML 配置文件现场调试时改配置重启就行不用重新打包。我吃过这个亏现场调一个阈值要重新编译 Docker 镜像来回折腾两小时。现在所有可调参数都走配置改完docker restart十秒生效。希望帮到你。本文还有配套的精品资源点击获取