简介:这份资源是一套面向安防监控与计算机视觉方向的智能监控系统完整实现,适合具备一定深度学习基础、希望将姿态估计与行为识别落地到实际场景的开发者与研究人员。系统以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 + STGCN
2.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 做匈牙利匹配,遮挡后容易丢 ID;DeepSORT 加了 ReID 特征,但每帧要跑一次特征提取网络,算力翻倍。ByteTrack 的思路很巧妙:把检测框按置信度分成高分和低分两组,先用高分框匹配轨迹,再用低分框去救那些被遮挡导致分数掉下来的轨迹。这样既不增加网络开销,又能显著降低 ID Switch。
监控场景里人经常被柱子、货架遮挡,ByteTrack 的低分框二次匹配正好对症。实测同一段视频,SORT 的 ID Switch 有 40 多次,ByteTrack 能压到 10 次以内。参数上track_thresh控制高分框阈值,默认 0.5;track_buffer是轨迹丢失后保留的帧数,默认 30,监控场景建议调到 60,给遮挡留更多恢复时间;match_thresh是匹配 IoU 阈值,默认 0.8,拥挤场景可以降到 0.7。
2.3 STGCN 做行为识别的输入构造
STGCN(Spatial 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 python=3.8 -y conda activate smart_monitor # 按本机 CUDA 版本装 PyTorch,这里以 CUDA 11.3 为例 pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 -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 的前置。
参数说明:python=3.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_location=device) model.eval() def detect_pose(img, img_size=640, conf_thres=0.4, iou_thres=0.65): # 预处理:letterbox 保持比例,填充到 640x640 img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) inp = letterbox(img_rgb, img_size, stride=64, auto=True)[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, nc=model.yaml['nc'], nkpt=model.yaml['nkpt'], kpt_label=True) 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保证输入不变形,stride=64是 YOLOv7-POSE 的下采样倍数。non_max_suppression_kpt是姿态专用的 NMS,会同时处理框和关键点。output_to_keypoint把输出整理成每行一个人的格式,前 7 列是框信息,后面 34 列是 17 个点的 x,y。
参数说明:conf_thres=0.4比默认 0.25 高,是为了减少监控画面里的误检。iou_thres=0.65控制重叠框合并,人多时别调太低,否则相邻的人会被吞掉。img_size=640是速度和精度的平衡点,如果画面里人很小,可以提到 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_thresh=0.5, # 高分检测阈值 track_buffer=60, # 轨迹保留帧数,监控场景调大 match_thresh=0.8, # 匹配 IoU 阈值 mot20=False ) tracker = BYTETracker(args, frame_rate=30) 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), dtype=np.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_buffer=60意味着轨迹丢失后保留 2 秒(30FPS),遮挡恢复后还能接上原 ID。match_thresh=0.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(maxlen=SEQ_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(maxlen=SEQ_LEN)自动淘汰旧帧,不用手动管理。
参数说明:SEQ_LEN=30对应 1 秒动作,跌倒这种快速动作够用;如果做缓慢的异常行为(如徘徊),可以加到 60 或 90。归一化用肩宽而不是身高,因为监控里下半身经常被遮挡,肩宽更稳定。
4. STGCN 行为识别:训练、推理与跌倒检测落地
4.1 STGCN 网络结构与输入输出定义
STGCN 的核心是 ST-GCN 块:先做空间图卷积(聚合相邻关节特征),再做时间卷积(聚合相邻帧特征),中间加残差连接。输入张量形状是(N, C, T, V, M),N 是 batch,C 是通道数(坐标 2 或 3),T 是帧数,V 是关节数 17,M 是人数(单人为 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_nodes=17): 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, axis=1)) D_inv_sqrt = np.linalg.inv(np.sqrt(D)) return D_inv_sqrt @ A @ D_inv_sqrt逻辑说明:邻接矩阵描述关节间的物理连接,自环让节点保留自身特征。归一化防止度数大的节点(如髋关节)数值爆炸。STGCN 里通常用多子集邻接矩阵(向心、离心、自环),这里给的是最简版。
参数说明:num_nodes=17对应 COCO 格式,如果用 MediaPipe 的 33 点要改。边表是固定的,不要随意增删,否则和预训练权重不兼容。
4.2 跌倒检测数据集构造与训练脚本
公开的跌倒数据集有 UR Fall、Le2i、Fall Detection Dataset,但格式各异。我一般统一转成(样本数, 2, 30, 17, 1)的 npy,标签 0 正常、1 跌倒。训练用交叉熵,Adam 优化器,学习率 1e-3,30 轮足够收敛。
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_class=2, in_channels=2).cuda() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-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(dim=1) correct += (pred == y).sum().item() total += y.size(0) print(f"Epoch {epoch}, Val Acc: {correct/total:.4f}")逻辑说明:数据转成(C, T, V, M)是 STGCN 的标准输入格式。weight_decay=1e-4防过拟合,骨架数据量通常不大。每轮验证看准确率,跌倒检测更关注召回率,可以额外打印混淆矩阵。
参数说明:num_class=2是二分类,如果要区分"跌倒""蹲下""正常"就改成 3。in_channels=2是 2D 坐标,用 3D 坐标改 3。学习率 1e-3 是起点,loss 不降就降到 1e-4。
4.3 实时推理与告警触发逻辑
训练好的 STGCN 接进主循环,每凑满 30 帧推理一次。跌倒判定不能只看单次输出,要加时序平滑,否则模型偶尔抽风会误报。
from collections import deque alert_buffer = defaultdict(lambda: deque(maxlen=5)) # 每个 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), dim=1)[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 消息、写数据库,按业务接。
参数说明:maxlen=5和阈值 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_thresh=0.8太严,交叉时 IoU 掉到 0.6 以下,匹配失败。解决:把match_thresh降到 0.7,同时开mot20=True启用更宽松的匹配策略。如果还不行,加一个轻量 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十秒生效。
希望帮到你。
本文还有配套的精品资源,点击获取