【视觉检测之人员摔倒检测算法开发思路】

平地摔倒检测算法详解

目录


概述

这是一套基于深度学习的实时平地摔倒检测算法,应用于地铁、车站、学校、工地等公共场所的安防监控场景。系统能够自动检测人员在平地状态下意外摔倒的行为,并及时发出告警,便于工作人员快速响应救助。

核心特性

  • 多模型协同: YOLOv8 检测 + RTMPose 关键点提取 + STGCN 时空图卷积动作识别
  • 按需计算: 仅在检测到 fall 类别时才调用 Pose 和 STGCN,极大节约算力
  • 目标跟踪: 使用 BYTETracker 实现跨帧目标追踪,持续监测倒地状态
  • 时长判定: 基于倒地持续时长过滤误报,避免弯腰、蹲下等短暂动作触发告警
  • 多重过滤: 边缘区域过滤、工作人员识别等多重过滤机制

系统架构

┌─────────────────────────────────────────────────────────────────┐
│                      平地摔倒检测流水线                           │
└─────────────────────────────────────────────────────────────────┘

  输入视频流
       │
       ▼
┌──────────────────┐
│  1. YOLOv8 检测   │  ← 检测五类目标 (fall/sitting/bending/standing/car)
│                   │     ⚡ 仅 fall 类触发后续流程
└──────────────────┘
       │ (仅 fall 类)
       ▼
┌──────────────────┐
│ 2. BYTETracker   │  ← 跨帧跟踪目标,维持 ID 一致性
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ 3. RTMPose 关键点  │  ← 提取人体 17 个关键点 (按需调用)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ 4. STGCN 动作识别  │  ← 基于 25 帧关键点序列识别摔倒动作 (约 1 秒)
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ 5. 倒地时长累计   │  ← 持续倒地超过阈值才报警
└──────────────────┘
       │
       ▼
┌──────────────────┐
│ 6. 多重过滤       │  ← 边缘区域/工作人员识别
└──────────────────┘
       │
       ▼
    报警输出

算力优化设计:

  • YOLOv8 检测每帧必跑 (轻量)
  • RTMPose 关键点: 仅在 fall 类且满足条件时调用
  • STGCN 动作识别: 异步执行,每个目标最多评估 3 次
  • 相比全量调用,算力消耗降低 80%+

模型介绍

1. YOLOv8 检测模型

用途: 检测画面中人员状态,分为五类

类别 ID类别名称说明
0fall摔倒/倒地
1sitting坐姿
2bending弯腰/蹲下
3standing站立/行走
4car扫地车工作人员
模型配置
参数
模型版本YOLOv8m (Medium)
输入尺寸640×384
输出(9, 5040) - 4 坐标 + 5 类别置信度
置信度阈值0.5
IoU 阈值0.5
训练数据
  • 数据集规模: 约 50000 张 标注图片
  • 数据场景:
    • 地铁场景 (主): 站台、站厅、通道、出入口、闸机等全方位覆盖
    • 学校场景: 操场、走廊等补充数据
  • 标注内容: 摔倒、坐姿、弯腰、站立、开小车 五类目标
  • 数据增强: Mosaic、Mixup、随机翻转、色彩抖动、模糊、亮度调整等
类别说明

car 类别特殊说明: 不是指普通汽车,而是指开扫地车的工作人员。这类目标在监控场景中:

  • 通常呈现低矮、移动的形态
  • 容易被误判为摔倒倒地
  • 单独分类后可直接过滤,避免误报警
设计思路

采用多类别检测而非单纯检测"摔倒":

  • 上下文信息: 同时识别坐姿、弯腰等易混淆状态
  • 减少误报: 扫地车等工作人员被识别为 car 类,直接过滤
  • 状态区分: 便于绘制不同颜色的标注框,可视化更清晰
  • 算力优化: 只有 fall 类才触发后续昂贵的 Pose 和 STGCN 计算

2. RTMPose 关键点提取模型

用途: 提取人体 17 个关键点坐标,用于后续 STGCN 动作识别

关键点定义 (COCO 17 点)
0:鼻子,1:左眼,2:右眼,3:左耳,4:右耳
5:左肩,6:右肩,7:左肘,8:右肘,9:左手腕
10:右手腕,11:左髋,12:右髋,13:左膝,14:右膝
15:左脚踝,16:右脚踝
关键点提取接口
def extract_keypoints(image, bboxes):
    """
    对指定检测框提取人体关键点
    
    参数:
        image: 输入图像 (BGR 格式)
        bboxes: 检测框列表 [[x1, y1, x2, y2], ...]
    
    返回:
        {
            "num_person": int,      # 检测到的人数
            "keypoints": np.ndarray, # (N, 17, 3) 关键点坐标
            "scores": np.ndarray     # (N, 17) 关键点置信度
        }
    """
    # 内部使用 RTMPose 模型进行推理
按需调用策略
# 仅在以下条件满足时才调用关键点提取:
need_pose = (
    (clas == 0)                              # YOLO 检测为 fall 类
    and (not target.action_confirmed)        # 尚未被动作识别确认
    and target.action_cooldown <= 0          # 不在冷却期
    and len(target.box_history) >= 15        # 至少有 15 帧历史
    and target.suspect_time >= 5             # 至少被检测为摔倒 5 次
)

算力节约效果:

  • 正常情况下,fall 类检测占比 < 5%
  • 动作识别触发条件更严格,实际调用率 < 1%
  • 相比每帧全量调用,算力消耗降低 80%+

3. STGCN 时空图卷积网络

用途: 基于 25 帧人体关键点序列,识别摔倒动作 (约 1 秒)

STGCN (Spatio-Temporal Graph Convolutional Network) 是一种专门用于人体动作识别的深度学习模型。

核心思想
┌─────────────────────────────────────────────────────────┐
│                    STGCN 架构示意                        │
└─────────────────────────────────────────────────────────┘

  输入:25 帧 × 17 个关键点 × (x, y, confidence)
           │
           ▼
  ┌─────────────────────┐
  │  空间图卷积 (ST-GCN) │  ← 建模人体关节间的空间关系
  │  (如:手 - 肘 - 肩)    │
  └─────────────────────┘
           │
           ▼
  ┌─────────────────────┐
  │  时间卷积 (Temporal) │  ← 建模帧与帧之间的时序变化
  │  (动作的连续演变)    │
  └─────────────────────┘
           │
           ▼
  输出:[摔倒,非摔倒] 的概率分布
模型配置
参数
输入形状(1, 3, 25, 17, 1)
输入含义batch × 通道 (x,y,conf) × 帧数 × 关键点 × 1
输出形状(1, 2)
输出含义[摔倒概率,非摔倒概率]
置信度阈值0.6
训练数据
  • 数据集规模: 约 1200 个摔倒动作视频片段
  • 数据来源: 真实监控录像 + 合成数据
  • 数据清洗: 人工筛选清晰、完整的摔倒过程,并裁剪适合长度片段
  • 数据制作: 利用高精度 POSE 关键点模型,完整导出每一帧的关键点变化
输入序列处理

STGCN 需要固定 25 帧输入,处理策略:

def prepare_action_data(keypoint_sequence, image_width, image_height):
    """
    将懒加载提取的关键点列表转为动作识别模型输入
    
    参数:
        keypoint_sequence: 关键点序列列表,每帧包含 17 个关键点
        image_width: 输入图像宽度
        image_height: 输入图像高度
    
    返回:
        np.ndarray: (1, 3, 25, 17, 1) 格式的 Tensor
    """
    if len(keypoint_sequence) == 0:
        return np.zeros((1, 3, 25, 17, 1), dtype=np.float16)
    
    processed_frames = []
    for frame_data in keypoint_sequence:
        kp_array = np.array(frame_data, dtype=np.float32)
        
        # 归一化坐标到 [0, 1]
        kp_array[:, 0] /= image_width   # x 归一化
        kp_array[:, 1] /= image_height  # y 归一化
        processed_frames.append(kp_array)
    
    # 序列补齐:不足 25 帧复制最后一帧
    while len(processed_frames) < 25:
        processed_frames.append(processed_frames[-1])
    
    # 维度转换:(25, 17, 3) -> (3, 25, 17) -> (1, 3, 25, 17, 1)
    results = np.transpose(np.array(processed_frames[:25]), (2, 0, 1))
    results = np.expand_dims(results, axis=0)
    results = np.expand_dims(results, axis=-1)
    return results

实际效果展示

在实际逐帧测试中发现 STGCN 具有非常稳定的表现,随着从正常行走到跌倒整个过程的 25 组数据不断更新,置信度评分也同步稳定地变化着直至超出阈值触发告警!
在这里插入图片描述


核心算法流程

第一阶段:YOLOv8 检测

每帧执行 YOLOv8 检测,输出五类目标:

class FallDetector:
    def __init__(self, gpu_id: int = 0):
        self.detector = YOLOv8(
            "detector.engine",
            (1, 3, 384, 640), (1, 9, 5040),
            iou_thres=0.5, conf_thres=0.5, task='det', gpu_id=gpu_id
        )
    
    def __call__(self, image: np.ndarray):
        return self.detector(image)

第二阶段:目标跟踪

使用 BYTETracker 维持目标 ID 连续性:

byte_tracker = ByteTracker(
    frame_rate=fps,
    image_shape=(width, height),
    match_thresh=0.7
)

# 更新跟踪结果
track_results = byte_tracker.update(detections, None)

第三阶段:fall 类筛选与动作识别触发

核心优化: 仅对 fall 类目标进行后续处理:

# 只遍历 fall 类 (clas == 0),避免无意义循环
for detection in detections:
    class_id = int(detection[5])
    if class_id != 0:  # 非 fall 类直接跳过
        continue
    
    detection_box = detection[:4]
    for target in track_results:
        # 检查检测框与跟踪框的 IoU
        if match_by_iou(detection_box, target.xyxy, iou_threshold=0.75):
            target.is_falling_this_frame = True
            target.suspect_time += 1
            
            # 判断是否需要动作识别确认
            if not target.alarm_flag and action_recognition_enabled:
                need_analysis = (
                    (not target.action_confirmed)
                    and target.action_cooldown <= 0
                    and len(target.box_history) >= 15
                    and target.suspect_time >= 5
                    and target.analysis_count < 3
                    and (not target.is_analyzing)
                )
                
                if need_analysis:
                    # 异步提交动作识别任务
                    submit_action_task(target, box_history, frame_buffer)

第四阶段:关键点提取 (懒加载)

动作识别任务中按需提取历史帧关键点:

def submit_action_task(target, box_history, frame_buffer):
    """提交动作识别异步任务"""
    keypoint_sequence = []
    for frame_idx, box_xyxy in box_history:
        # 优先使用缓存
        if frame_idx in target.keypoint_cache:
            keypoint_sequence.append(target.keypoint_cache[frame_idx])
            continue
        
        # 懒加载:从历史帧提取关键点
        if frame_idx in frame_buffer:
            hist_image = frame_buffer[frame_idx].cpu_image
            pose_result = extract_keypoints(
                hist_image, bboxes=[box_xyxy.tolist()]
            )
            
            if pose_result and pose_result.get("num_person", 0) > 0:
                kps = pose_result["keypoints"][0]
                scores = pose_result["scores"][0]
                # 合并坐标和置信度
                merged = [[kp[0], kp[1], sc] for kp, sc in zip(kps, scores)]
                target.keypoint_cache[frame_idx] = merged
                keypoint_sequence.append(merged)
            else:
                # 检测失败,返回空关键点
                empty_kps = [[0, 0, 0] for _ in range(17)]
                target.keypoint_cache[frame_idx] = empty_kps
                keypoint_sequence.append(empty_kps)
    
    # 执行动作识别推理
    perform_action_analysis(target, keypoint_sequence)

第五阶段:动作识别

# 格式化为动作识别模型输入
action_data = prepare_action_data(keypoint_sequence, width, height)
one_batch = action_data.astype(np.float16, copy=False)

# 执行推理
analysis_result = action_model(one_batch)[0]
probabilities = softmax(analysis_result)
prob_fall = probabilities[0, 0]

# 处理结果
if prob_fall > confidence_threshold:
    target.action_confirmed = True  # 确认摔倒
else:
    # 设置冷却期,避免重复评估
    target.action_cooldown = 40 if prob_fall > 0.1 else 100

第六阶段:倒地时长累计

动作识别确认摔倒后,持续监测倒地状态:

for target in track_results:
    if target.is_falling_this_frame:
        target.fall_time += 1  # 倒地帧数 +1
        target.lost_time = 0
    else:
        target.lost_time += 1  # 未倒地帧数 +1
        if target.lost_time > 5:  # 超过 5 帧未检测到,重置状态
            target.fall_time = 0
            target.action_confirmed = False
            target.suspect_time = 0

# 报警触发
if action_confirmed and target.fall_time >= fall_frame_threshold:
    alarm_flag = True

第七阶段:多重过滤

7.1 边缘区域过滤

摔倒发生在画面边缘通常不是真正的事故:

# 计算画面中心区域范围 (边缘 25% 过滤)
x_min = width // 4
x_max = width // 4 * 3
y_min = height // 4
y_max = height // 4 * 3

# 检查摔倒目标中心是否在中心区域
alarm_flag = (x_min <= cx <= x_max and y_min <= cy <= y_max)
7.2 工作人员识别过滤

检测摔倒者是否为工作人员 (如地铁站维修人员):

if staff_detection_enabled:
    # 检测画面中是否有工作人员障碍
    if get_obstacles_count(segment_obstacles(frame)) > 0:
        alarm_flag = False  # 工作人员不报警

关键代码解析

核心函数:姿态校验

当未启用 STGCN 时,使用简单的姿态规则判断摔倒:

def check_fall_by_pose(target) -> bool:
    """
    基于人体关键点判断是否摔倒
    
    判断逻辑:
    1. 宽高比判断:摔倒时人体框趋于扁平 (w/h > 1.2)
    2. 头髋位置判断:摔倒时头部 Y 坐标接近或高于髋部
    
    参数:
        target: 跟踪目标对象,包含检测框和关键点
    
    返回:
        bool: True 表示检测到摔倒
    """
    keypoints = target.keypoints
    if len(keypoints) == 0:
        return False
    
    x1, y1, x2, y2 = target.xyxy
    w, h = x2 - x1, y2 - y1
    
    # 如果高度为 0,无法判断
    if h == 0:
        return False
    
    # 判断 1: 宽高比 > 1.2 视为横向倒地
    if w / h > 1.2:
        return True
    
    # 判断 2: 头部 Y 坐标接近髋部 Y 坐标 (身体水平)
    head_ys = [keypoints[i][1] for i in [0, 1, 2, 3, 4] if keypoints[i][2] > 0.3]
    hip_ys = [keypoints[i][1] for i in [11, 12] if keypoints[i][2] > 0.3]
    
    if head_ys and hip_ys:
        avg_head_y = sum(head_ys) / len(head_ys)
        avg_hip_y = sum(hip_ys) / len(hip_ys)
        
        # 头部与髋部高度差小于身体高度的 10%,视为倒地
        if avg_hip_y - avg_head_y < h * 0.10:
            return True
    
    return False

判断标准说明:

判断条件阈值说明
宽高比w/h > 1.2摔倒时人体横向倒下,框变宽
头髋高差< h×10%头部与髋部几乎在同一水平线

核心函数:数据格式化

将关键点序列转换为动作识别模型输入:

def prepare_action_data(keypoint_sequence, image_width, image_height):
    """
    懒加载的关键点列表 -> 动作识别 Tensor
    
    参数:
        keypoint_sequence: List[List[[x, y, conf]×17]] - 每帧 17 个关键点
        image_width: 原图宽度
        image_height: 原图高度
    
    返回:
        np.ndarray: (1, 3, 25, 17, 1) 格式的 Tensor
    """
    if len(keypoint_sequence) == 0:
        return np.zeros((1, 3, 25, 17, 1), dtype=np.float16)
    
    processed_frames = []
    for kp in keypoint_sequence:
        kp_array = np.array(kp, dtype=np.float32)
        
        # 全图归一化到 [0, 1]
        kp_array[:, 0] /= float(image_width)   # x 归一化
        kp_array[:, 1] /= float(image_height)  # y 归一化
        processed_frames.append(kp_array)
    
    # 序列补齐:不足 25 帧复制最后一帧
    while len(processed_frames) < 25:
        processed_frames.append(processed_frames[-1])
    
    # 维度转换:(25, 17, 3) -> (3, 25, 17) -> (1, 3, 25, 17, 1)
    results = np.transpose(np.array(processed_frames[:25]), (2, 0, 1))
    results = np.expand_dims(results, axis=0)
    results = np.expand_dims(results, axis=-1)
    
    return results

维度转换详解:

原始数据:keypoint_sequence = [帧 1, 帧 2, ..., 帧 25]
          每帧:    [关键点 1, 关键点 2, ..., 关键点 17]
          每个点:  [x, y, confidence]

Array 形状:(25, 17, 3)

Transpose:  (3, 25, 17) - 将 (x,y,conf) 移到最前

Expand:     (1, 3, 25, 17, 1) - 添加 batch 维和末尾维

STGCN 输入:batch=1, channel=[x,y,conf], frames=25, joints=17, 1

核心类:跟踪目标

class TrackingTarget:
    def __init__(self, tlwh, score, class_id):
        super().__init__(tlwh, score, class_id)
        self.box_history = deque(maxlen=60)   # 记录历史框
        self.alarm_flag = False                # 是否已报警
        self.keypoints = []                    # 姿态关键点 (非 GCN 模式)
        self.fall_time = 0                     # 倒地累计帧数
        self.is_falling_this_frame = False     # 当前帧是否倒地
        self.lost_time = 0                     # 连续未检测到的帧数
        self.action_confirmed = False          # 是否被动作识别确认
        self.action_cooldown = 0               # 动作识别冷却计数
        self.suspect_time = 0                  # 被检测为 fall 的累计次数
        self.keypoint_cache = {}               # 关键点懒加载缓存
        self.analysis_count = 0                # 动作识别已评估次数

状态机流转:

┌─────────────────────────────────────────────────────────────┐
│                    跟踪目标状态机                            │
└─────────────────────────────────────────────────────────────┘

  新目标
    │
    ▼
┌─────────────────┐
│  被 YOLO 检测为 fall│  ← suspect_time += 1
└─────────────────┘
    │
    ▼
┌─────────────────┐
│  满足动作识别条件│  ← box_history≥15, suspect_time≥5
└─────────────────┘
    │
    ▼
┌─────────────────┐
│  动作识别异步评估│  ← 概率>0.6 则确认
└─────────────────┘
    │
    ▼
┌─────────────────┐
│ action_confirmed│  ← 开始累计 fall_time
└─────────────────┘
    │
    ▼
┌─────────────────┐
│ fall_time≥阈值  │  ← 触发报警
└─────────────────┘
    │
    ▼
  alarm_flag = True

算力优化核心:按需调用机制

# 第一层过滤:只处理 fall 类 (clas == 0)
for detection in detections:
    class_id = int(detection[5])
    if class_id != 0:
        continue  # 95%+的目标直接跳过
    
    # 第二层过滤:动作识别触发条件
    need_analysis = (
        (not target.action_confirmed)      # 未确认过
        and target.action_cooldown <= 0    # 不在冷却期
        and len(target.box_history) >= 15  # 有足够历史
        and target.suspect_time >= 5       # 持续可疑
        and target.analysis_count < 3      # 未达上限
        and (not target.is_analyzing)      # 无进行中的任务
    )
    
    if need_analysis:
        # 异步提交,不阻塞主流程
        submit_action_task(...)

算力节约效果:

处理阶段调用比例说明
YOLOv8 检测100%每帧必跑,轻量
RTMPose 关键点< 5%仅 fall 类调用
STGCN 动作识别< 1%严格条件 + 异步
总体节约80%+相比全量调用

配置说明

主要配置参数:

参数默认值说明
gcn_enable1是否启用动作识别 (1=启用,0=仅用姿态校验)
gcn_conf0.6动作识别置信度阈值,超过此值才确认摔倒
fall_time_threshold0倒地时长阈值 (秒),0 表示不限制
iou_same0.75YOLO 框与跟踪框匹配 IoU 阈值
staff_member1是否启用工作人员过滤 (1=启用)

性能相关参数:

参数默认值说明
analysis_max_attempts3每个目标最多动作识别评估次数
action_history_max_samples25动作识别输入最多采样帧数
pending_task_max2同时最多异步评估任务数
pose_retry_cooldown10姿态校验失败后冷却帧数

常见问题

Q1: 为什么需要动作识别?

A: 单纯的 YOLO 检测容易产生误报:

  • 弯腰捡东西可能被误判为摔倒
  • 蹲下系鞋带可能被误判为摔倒
  • 坐在地上可能被误判为摔倒

动作识别通过时序分析可以区分:

  • 摔倒: 快速从站立到倒地的过程
  • 弯腰: 先弯腰再站起,有完整恢复过程
  • 蹲下: 缓慢下蹲,保持低位

Q2: 倒地时长阈值如何设置?

A: 建议根据实际场景调整:

  • 地铁站台: 建议 1-3 秒,快速响应
  • 大厅区域: 建议 3-5 秒,减少误报
  • 特殊场景: 设为 0 表示不限制时长

Q3: 为什么不启用动作识别时误报率高?

A: 仅用姿态校验时:

  • 只能基于单帧判断 (宽高比、头髋位置)
  • 无法区分弯腰、蹲下、坐下等动作
  • 容易受遮挡、视角影响

建议生产环境启用动作识别以获得更高准确率。

Q4: 边缘区域过滤的作用?

A: 画面边缘的摔倒通常是:

  • 正在上下车/上下楼梯
  • 进出监控区域
  • 非事故性倒地

通过过滤边缘区域,可减少大量误报。

Q5: 算力优化是如何实现的?

A: 多层按需调用机制:

  1. 第一层: 仅 fall 类触发后续流程 (95%+目标直接跳过)
  2. 第二层: 动作识别严格触发条件 (历史帧数、可疑次数等)
  3. 第三层: 异步执行 + 冷却机制 + 评估次数限制
  4. 懒加载: 关键点只在需要时提取并缓存

相比每帧全量调用 Pose 和动作识别,算力消耗降低 80%+


总结

本算法采用检测→跟踪→关键点提取→动作识别确认→时长累计→多重过滤的级联方案,核心设计思想:

  1. 分层过滤: 从粗到细逐步筛选,减少无效计算
  2. 按需计算: 仅 fall 类触发关键点提取和动作识别,算力节约 80%+
  3. 时序分析: 基于 25 帧序列识别摔倒动作,准确率高
  4. 状态持续: 跟踪器维持目标状态,支持跨帧累计倒地时长
  5. 异步评估: 动作识别异步执行,不阻塞主流程
  6. 多重保障: 边缘过滤、工作人员识别等多重过滤

本算法适合地铁、学校等公共场所的平地摔倒行为实时监测场景,能够有效平衡响应速度与误报率。

更多技术细节与模型详情欢迎探讨,可以评论或者私信我(爱睡懒觉的焦糖玛奇朵)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值