资讯动态

YOLOv8姿态检测与关键点规则时序平滑的跌倒识别方案

发布时间:2026/10/26 5:07:56 来源:尧图企业网站定制
做跌倒识别这类项目我在几个不同的看护场景里被反复问过同一个问题能不能不接一堆传感器就用一个普通摄像头把“坐着、站着、摔倒”这三种状态分出来。真正落地时你会发现难点从来不是“能不能检测到人”而是“怎么把一个人的姿态稳定地映射成一个语义标签”并且这个标签在光照变化、遮挡、多人交叉、摄像头角度刁钻的情况下还不能乱跳。这套方案我最终定型成YOLOv8 姿态检测 关键点特征工程 规则判决 时序平滑的组合纯视觉、单模型、边缘设备可跑源代码结构也很轻改起来不费劲。这篇文章我打算把整个推理评估链路拆开讲透模型为什么选 YOLOv8-pose 而不是端到端分类、17 个关键点怎么变成能判决的数字、坐站跌三态的具体阈值是怎么定出来的、现场标定时该动哪几个参数、以及我踩过的那些“坐姿被判成跌倒”“蹲下就被报警”的坑。适合已经会跑 YOLO 推理、想把它用姿态任务上的朋友也适合做看护、健身、工位监测这类场景、需要一个能快速出效果的基线方案的人。代码我会给核心片段思路比代码重要思路对了换哪个姿态模型都能复用。1. 方案整体设计与选型思路1.1 为什么不用端到端的行为分类模型一开始我也试过端到端路线把人体框裁出来送进一个动作识别网络直接输出 sit / stand / fall 三个类别。跑 demo 很漂亮一上真实场景就散架。原因很直白——端到端分类模型学的是“图像纹理和整体外观”它对训练集里的拍摄角度、衣着、光照有很强的记忆性换个俯角、换把椅子、换个人穿深色衣服置信度分布就变了。更麻烦的是它不可解释误报了你没法知道是哪里出的问题只能继续加数据、加标注、重新训练迭代成本极高。关键点方案的好处是把“感知”和“判决”彻底解耦。YOLOv8-pose 只负责一件事把人体的 17 个关节坐标找出来。至于这个人是坐着还是摔了交给一层薄薄的几何规则去算。这一层是可以读、可以改、可以针对单个场景微调的。现场发现“蹲下被误报成跌倒”我不需要重新标几千张图只要在规则里加一条膝角约束就能压下去。这种可调试性在真实交付里比多两个点的准确率值钱得多。还有个体量和延迟的问题。姿态模型本身很小yolov8n-pose在普通 CPU 上单帧几十毫秒导出 ONNX 之后边缘盒子上跑 15 到 25 FPS 很轻松。而动作识别网络通常要吃 16 到 32 帧的时序片段显存和算力门槛直接上一个台阶。看护场景往往是一台设备要盯好几个房间这种成本差异会被放大。1.2 YOLOv8-pose 到底吐出了什么很多人卡在这一步模型跑通了results打印出来一堆数字不知道哪个是哪个。我把它说清楚。YOLOv8-pose 的输出张量形状是(1, 56, 8400)其中 8400 是候选框数量三个尺度特征图拼起来的56 这个维度是这样拆的前 4 个是人框的cx, cy, w, h第 5 个是框的置信度剩下 51 个是 17 个关键点每个关键点 3 个值依次是x, y, score。关键点的顺序遵循 COCO 定义这个顺序必须记牢后面所有特征计算都依赖它索引部位索引部位索引部位0鼻子6右肩12右髋1左眼7左肘13左膝2右眼8右肘14右膝3左耳9左腕15左踝4右耳10右腕16右踝5左肩11左髋用 Ultralytics 的封装时取results[0].keypoints.xy拿到的是像素坐标(N, 17, 2)results[0].keypoints.conf拿到的是每个点的置信度(N, 17)。注意一个细节xy里即使某个点置信度接近 0坐标依然会有一个值通常是框内的插值位置所以绝对不能用坐标是否有值来判断点是否可靠必须自己拿conf去过滤。这个坑我见过不止一个人踩直接导致遮挡场景下疯狂误报。另外要注意keypoints.xy的坐标系是原图像素坐标不是归一化的。如果你的输入做了 letterbox 缩放Ultralytics 内部会帮你映射回原图一般不用自己处理但如果你是自己拿 ONNX 做前处理就一定要把坐标反算回去否则算出来的角度全是错的。1.3 整条流水线的分层设计我把整条链路切成五层每层职责单一出问题好定位第一层检测与关键点提取。YOLOv8-pose 出人框和 17 点按conf阈值过滤低质量框。这一层的输出是“原始感知结果”。第二层跟踪绑定。用内置的跟踪器model.track默认 ByteTrack给每个人分配一个稳定 ID。没有 ID 的话时序平滑根本无从谈起——你不知道这一帧的 3 号人和上一帧的 3 号人是不是同一个。第三层特征工程。把 17 个点转换成人体的比例、角度、朝向等几何量。这一层是整套方案的核心转换得好判决就简单转换得糙后面怎么调阈值都别扭。第四层判决。用特征去判断状态输出一个带分数的候选标签。第五层时序平滑与状态机。单帧结论不算数要连续多帧一致才确认状态切换跌倒还要加“不可自恢复”约束。这个分层的好处是你可以单独替换任何一层。比如换更强的姿态模型RTMPose、MediaPipe只要保证输出 17 点格式一致上层完全不用改或者把规则判决换成一个小型分类器GBDT、轻量 MLP输入还是那些特征迁移成本很低。2. 关键点特征工程把 17 个坐标变成可判决的信号2.1 坐标系归一化摆脱像素和分辨率的束缚直接拿像素坐标算角度是可行的但只要涉及“距离”“高度”这类量就必须归一化否则换个分辨率、换个人高矮胖瘦阈值全得重调。我用的归一化基准是人体自身的尺度具体是两个量一是body_h取人体框的高度或者更稳一点取“鼻子到双脚踝的垂直距离”。用框高在人体被截断时比如半个身子出画会严重失真所以我更倾向于用关键点跨度body_span max(y) - min(y)在所有高置信度点上取这个值对截断有一定鲁棒性但截断严重时也会失效所以还要加一个可用性检查。二是torso_len取“肩中点 → 髋中点”的欧氏距离。这个量代表躯干长度是个很稳定的个人尺度基准坐姿站立都不怎么变特别适合用来做距离归一化。有了这两个基准所有距离类特征都除以它们变成无量纲比例。这样一个人离摄像头远近、图像分辨率高低都不影响判决。角度类特征本身无量纲直接算就行。提示如果你要在一个画面里同时监控多个人别用画面高度做归一化基准那样远近的人尺度不一致必须用人体自身尺度。2.2 五个真正有用的核心特征我从十几个候选特征里筛到最后留了五个这五个覆盖了绝大多数判决需求算力也低。特征一躯干倾角torso_angle。肩中点指向髋中点的向量与竖直向下方向的夹角单位度。站立时接近 0 到 20 度坐着可能 0 到 40 度取决于靠背角度摔倒时通常大于 55 度甚至接近 90 度。这是区分“直立类”和“躺倒类”的主特征。特征二头部相对髋部的高度比head_hip_dy。(hip_mid_y - nose_y) / torso_len。站着时这个值大概 1.3 到 1.8因为头在髋上方像素 y 轴向下所以 hip_y 减 nose_y 是正的坐着时大概 0.8 到 1.3摔倒时可能接近 0 甚至变成负数。这个特征比单纯用框高稳因为它只依赖两个点遮挡时还能靠鼻子或者肩膀推算。特征三身体包围盒宽高比body_aspect。(x_max - x_min) / (y_max - y_min)。站姿通常在 0.25 到 0.5坐姿 0.5 到 0.9躺倒时大于 1.0。这是最直观的一个特征但单独用很危险——蹲下、弯腰捡东西、盘腿坐都会让它接近 1所以它只能作为辅助投票项。特征四膝角knee_angle。髋-膝-踝三点构成的夹角。直立时接近 180 度正常坐姿大概 80 到 120 度深蹲大概 40 到 70 度躺平伸展时接近 180 度。注意躺平和站立膝角都可能接近 180所以这个特征必须和躯干角联合使用不能单独判。特征五髋部相对高度hip_height_ratio。(img_h - hip_mid_y) / img_h注意方向让数值越大表示越高。这个特征强依赖摄像头安装高度和角度不能跨设备通用但同一设备下它是判断“这个人是不是降到地面了”最直接的信号。我一般把它做成一个可选的加分项在固定机位的场景里非常好用。这五个特征之间是有冗余的冗余不是坏事。判决时用多特征投票任何单一特征因为遮挡或角度失效其他特征还能兜底。这就是规则方案比端到端稳的根本原因——它有冗余的“证据链”。2.3 特征计算的向量化实现特征计算这块我写得比较克制尽量向量化避免逐人循环多目标场景下性能差别很明显。import numpy as np def safe_mid(a, b, ca, cb, conf_th0.35): 两个点求中点按置信度加权都不可信时返回 None if ca conf_th and cb conf_th: return None if ca conf_th: return b.astype(np.float32) if cb conf_th: return a.astype(np.float32) w np.array([ca, cb], dtypenp.float32) w w / w.sum() return (a * w[0] b * w[1]).astype(np.float32) def angle_between(v1, v2): 两个向量的夹角单位度输入需为非零向量 n1 np.linalg.norm(v1) n2 np.linalg.norm(v2) if n1 1e-6 or n2 1e-6: return None cos float(np.dot(v1, v2) / (n1 * n2)) cos max(-1.0, min(1.0, cos)) return float(np.degrees(np.arccos(cos))) def extract_features(kxy, kconf, img_h, img_w, conf_th0.35): kxy: (17,2) 像素坐标, kconf: (17,) 置信度 L_SH, R_SH 5, 6 L_HIP, R_HIP 11, 12 L_KNEE, R_KNEE 13, 14 L_ANK, R_ANK 15, 16 NOSE 0 sh_mid safe_mid(kxy[L_SH], kxy[R_SH], kconf[L_SH], kconf[R_SH], conf_th) hip_mid safe_mid(kxy[L_HIP], kxy[R_HIP], kconf[L_HIP], kconf[R_HIP], conf_th) if sh_mid is None or hip_mid is None: return None # 肩髋缺失本帧不可判 torso_vec hip_mid - sh_mid torso_len float(np.linalg.norm(torso_vec)) if torso_len 8.0: # 太小说明检测框异常或人极远 return None # 特征一躯干与竖直方向夹角 vertical np.array([0.0, 1.0], dtypenp.float32) torso_angle angle_between(torso_vec, vertical) if torso_angle is None: return None # 特征二头髋高度差以躯干长为单位 head_hip_dy None if kconf[NOSE] conf_th: head_hip_dy float((hip_mid[1] - kxy[NOSE][1]) / torso_len) # 特征四膝角取左右可信侧里角度更大的那个代表支撑腿 knee_cands [] for hip_i, knee_i, ank_i in ((L_HIP, L_KNEE, L_ANK), (R_HIP, R_KNEE, R_ANK)): if min(kconf[hip_i], kconf[knee_i], kconf[ank_i]) conf_th: a angle_between(kxy[hip_i] - kxy[knee_i], kxy[ank_i] - kxy[knee_i]) if a is not None: knee_cands.append(a) knee_angle max(knee_cands) if knee_cands else None # 特征五髋部相对高度 hip_height_ratio float((img_h - hip_mid[1]) / img_h) # 特征三身体宽高比只用可信点 valid kxy[kconf conf_th] body_aspect None if len(valid) 4: w float(valid[:, 0].max() - valid[:, 0].min()) h float(valid[:, 1].max() - valid[:, 1].min()) if h 1e-3: body_aspect w / h return { torso_angle: torso_angle, head_hip_dy: head_hip_dy, knee_angle: knee_angle, body_aspect: body_aspect, hip_height_ratio: hip_height_ratio, torso_len: torso_len, sh_mid: sh_mid, hip_mid: hip_mid, }这段代码有两个设计意图值得说明。第一safe_mid用置信度加权而不是简单平均是因为实际场景里经常一侧肩膀被遮挡侧身、走到画面边缘这时用加权中点能显著降低抖动。第二膝角取左右两侧中较大的那个是因为支撑腿的膝角更接近伸直状态、更能反映站立的意图而被遮挡或弯曲的腿会引入噪声。2.4 置信度处理与不可判帧的代价置信度阈值conf_th 0.35是我实测下来比较通用的值但它在不同模型尺寸上要微调yolov8n-pose精度低一些可以降到 0.3yolov8m-pose或l-pose精度高可以提到 0.45 甚至 0.5这样能过滤掉更多背景误检。这里要强调一个容易被忽略的工程原则允许输出“不可判”比强行输出一个错判要好得多。当肩或髋的关键点缺失、torso_len小到不合理、或者有效点数少于 4 个时我直接返回None让这一帧不参与判决。状态机看到None就保持上一状态不更新而不是把未知当成站立或者跌倒。我在一个项目里吃过反面的教训当初为了“每一帧都有输出”把所有不可判的帧默认填成“站立”结果在夜间低照度下模型经常找不到肩髋系统疯狂地在“跌倒 → 站立”之间抖动值班人员被无效报警折腾得直接关掉了提示。后来改成保留上一有效状态误报率立刻降下来的同时真实报警一个没漏。3. 三态判决逻辑坐、站立、跌倒怎么定阈值3.1 站立判定的组合条件站立是我认为最容易判的一类因为它有强几何约束躯干接近竖直髋在膝上方膝在踝上方。我用的规则是三个条件同时满足torso_angle 32°躯干基本竖直。为什么不是 20°因为真实场景里人站着会前倾看手机、弯腰喝水、伸手拿东西20° 太严会把这些都判成非站立。32° 能覆盖大部分自然站姿的倾斜同时把明确的坐姿靠背椅通常 30° 到 45°挡在边界之外。head_hip_dy 1.0头明显在髋上方。这个条件的作用是拦住“半躺但躯干角不大”的情况比如躺椅调得很陡的时候。knee_angle 150°如果可算支撑腿基本伸直。这个条件是可选加分项蹲下时膝角会掉到 100° 以下直接排除。三个条件里躯干角和头髋高度差是必选膝角是可选的第四票。我给它们不是等权的躯干角权重 0.5头髋高度比 0.3膝角 0.2。加起来过 0.7 就判站立。加权的好处是某个特征处在模糊区间时其他特征能拉一把。有一个容易忽略的细节站姿的判定必须考虑人的朝向。人背对摄像头时鼻子不可见kconf[0]会很低head_hip_dy直接失效。这时候我用耳或眼的中点替代鼻子位置如果这些也都不行就退化成只用躯干角加膝角的两票制权重重新归一化。这个退化逻辑一定要写否则背身站立的人会一直判不出来。3.2 坐姿判定最容易被做成“非站即坐”的一个坐姿是三类里最难判的因为它的外观变化最大。办公椅正坐、懒人沙发半躺、地面盘腿、蹲在凳子上、坐轮椅、骑车——这些在关键点层面呈现的几何形态完全不同。如果只用“躯干角在 30 到 60 度之间”这一个条件漏判率会很高。我的做法是用“髋部高度下降 膝部明显弯曲”这个组合作为主判据而不是靠躯干角。具体来说knee_angle落在70° ~ 135°区间有明显弯膝但不是深蹲权重 0.45hip_height_ratio比该目标的站立基线低 15% 以上权重 0.35torso_angle在5° ~ 55°之间权重 0.2。注意第二个条件用的是“该目标的基线”而不是固定值。这就是带跟踪 ID 的另一个好处我可以给每个 ID 维护一个最近 30 秒的hip_height_ratio中位数作为这个人的站立基线。人坐下来髋部高度相对自己的基线会明显下降这个相对变化比绝对高度可靠得多而且能自动适配不同身高、不同机位。坐姿和蹲姿的区分我用hip_height_ratio的下降幅度来划蹲下时髋部会降得更多通常超过站立基线的 35%而坐着时下降幅度在 15% 到 35% 之间。这个区间不是绝对可靠的蹲姿误判确实会存在所以我在实际系统里通常把“蹲”这个状态单独放一个类让它不触发报警只是不上报为跌倒。宁可多一个中间态也不要让蹲下触发紧急报警。注意如果你的场景里没有“蹲”这个动作比如桌面工位监测可以把蹲的判定直接去掉把坐的阈放宽误报会少很多。规则一定要按场景裁剪别想着写一套通用规则打天下。3.3 跌倒判定单帧判断必然误报必须上时序跌倒这块是整套方案里唯一一个我不敢用单帧结论的地方。原因很简单弯腰捡东西、系鞋带、做俯卧撑、在地上做瑜伽、躺在沙发上——这些动作在某一帧的关键点上和跌倒几乎一模一样。任何基于单帧的“躯干角大于 60 度就报警”的方案上线第一天就会被误报淹没。我的跌倒判决分两部分单帧跌倒候选和时序确认。单帧候选的条件组合torso_angle 55°躺倒倾向权重 0.4body_aspect 0.9身体横向展开权重 0.25head_hip_dy 0.6头部明显下沉到髋部附近或更低权重 0.25hip_height_ratio相对该目标基线下降超过 40%权重 0.1。四项加权和超过 0.65 记为一个“跌倒候选帧”。时序确认我用了两个并行的判据两个都满足才升级为“跌倒事件”判据一持续性连续 8 到 12 帧按 25 FPS 算大约 0.3 到 0.5 秒都是候选帧。这个窗口长度需要按帧率换算我一般写成confirm_frames int(fps * 0.4)。窗口太短扛不住噪声太长会拖慢响应。判据二变化剧烈度跌倒的本质是“从高到低的快速转变”所以我在事件发生前的 1 到 2 秒窗口里检查hip_height_ratio的下降速度和torso_angle的上升速度。如果髋部高度在 1 秒内下降超过 25%同时躯干角增加超过 30 度这个“快速跌落”的信号会大幅提升置信度。反过来如果一个人是慢慢躺到沙发上髋部下降是渐进的这个速度判据就不会触发从而把“躺下”和“摔倒”区分开。这个速度判据是我这套方案里最有价值的一块也是单纯靠单帧规则做不到的。它把“姿态”升级成了“姿态 姿态的变化过程”信息量完全不同。还有一条兜底规则如果某个人在跌倒候选帧之后持续 10 秒以上保持低髋部高度且没有恢复到站立/坐姿即使速度判据没触发也升级为跌倒事件。这条规则覆盖的是“慢慢滑倒”“先坐着然后侧倒”这类低冲击的跌倒在真实场景里占比不低而且往往更危险因为当事人可能已经失去意识。3.4 状态机与滑动窗口投票判决的最后一层是状态机。我用的是一个带迟滞的状态机状态集合是UNKNOWN / STANDING / SITTING / LYING / FALLENFALLEN是事件态不是姿势态触发之后会保持直到人工复位或者检测到重新站立。状态切换用两个机制保证稳定一是滑动窗口多数投票。给每个跟踪 ID 维护一个长度 10 的 deque存最近 10 帧的候选标签取众数作为当前状态。这样单帧的抖动直接被吃掉。窗口长度和 FPS 挂钩我把窗口覆盖的时间固定在 0.4 秒左右。二是进入/退出迟滞。进入某个状态需要连续 N 帧的支持退出该状态需要连续 M 帧的其他状态支持且 N M 或 M N 视情况定。比如进入 FALLEN 需要 10 帧确认退出 FALLEN 需要连续 30 帧的站立证据这个不对称是故意的——宁可晚点解除警报也不要漏掉一个真的摔倒后躺了一会儿又挣扎起来的人。from collections import deque, Counter class PostureStateMachine: def __init__(self, fps25): self.win deque(maxlenmax(5, int(fps * 0.4))) self.state UNKNOWN self.enter_cnt Counter() self.fallen_latched False self.hist_hip deque(maxlenint(fps * 2)) # 用于速度判据 def update(self, cand, feats): cand: STANDING/SITTING/LYING/FALL_CAND 或 None if cand is None: return self.state # 不可判帧不更新 self.win.append(cand) if len(self.win) 3: return self.state vote, cnt Counter(self.win).most_common(1)[0] ratio cnt / len(self.win) if vote FALL_CAND and ratio 0.7: self.enter_cnt[FALL_CAND] 1 if self.enter_cnt[FALL_CAND] 3 and self._fast_drop(): self.fallen_latched True self.state FALLEN else: self.enter_cnt[FALL_CAND] 0 if self.fallen_latched: # 需要长时间站立证据才解除 if vote in (STANDING, SITTING) and ratio 0.85: self.enter_cnt[recover] 1 if self.enter_cnt[recover] 15: self.fallen_latched False self.state vote self.enter_cnt[recover] 0 else: self.enter_cnt[recover] 0 return self.state if vote ! FALL_CAND and ratio 0.6: self.state vote return self.state def _fast_drop(self): if len(self.hist_hip) 8: return False vals list(self.hist_hip) drop max(vals[:len(vals)//2]) - min(vals[len(vals)//2:]) baseline max(vals[:len(vals)//2]) return baseline 1e-6 and (drop / baseline) 0.25这段状态机代码不算优雅但逻辑是清楚的投票负责抗抖计数负责迟滞fallen_latched负责事件的锁存和解除。实际部署时我把FALLEN的触发做成回调一触发就推送通知并保存前后 10 秒的视频片段——保存证据这个功能在事后复盘时太重要了没有它你根本无法判断是误报还是真事。4. 完整实操从环境到跑通一条视频流4.1 环境和依赖环境没什么讲究Python 3.9 到 3.11 都行Ultralytics 版本建议固定别用 latest它迭代快接口偶尔会变。pip install ultralytics8.2.0 opencv-python numpy模型文件第一次运行会自动下载。如果部署环境不能联网提前把yolov8n-pose.pt下载好放到工作目录代码里用本地路径加载。我用n版本做原型验证确认逻辑没问题之后再换s或m版本提精度。这个顺序很重要——先用最轻的模型把流程跑通能立刻知道是逻辑问题还是精度问题不至于一上来就被算力拖住。如果是边缘盒子部署走 ONNX 导出yolo export modelyolov8n-pose.pt formatonnx imgsz640 simplifyTrue导出时imgsz要和推理时一致不一致的话关键点位置会偏移。这个坑很隐蔽因为框看着还挺准但算出来的角度会有几度偏差累加到阈值边界上就变成了误判。我一般会在导出后拿同一张图做一次 PT 和 ONNX 的结果对齐两边的关键点坐标差应该在 1 像素以内。4.2 单帧推理到状态输出的最小闭环先把最小闭环写出来跑通再谈优化。import cv2 import numpy as np from ultralytics import YOLO model YOLO(yolov8n-pose.pt) def judge_single(f, baseline_hipNone): 单帧判决返回候选标签和分数 if f is None: return None, 0.0 ta f[torso_angle] hhd f[head_hip_dy] ka f[knee_angle] asp f[body_aspect] hr f[hip_height_ratio] # 跌倒候选 fall_score 0.0 if ta 55: fall_score 0.40 * min(1.0, (ta - 55) / 30 0.5) if asp is not None and asp 0.9: fall_score 0.25 * min(1.0, (asp - 0.9) / 0.5 0.5) if hhd is not None and hhd 0.6: fall_score 0.25 * min(1.0, (0.6 - hhd) / 0.6 0.3) if baseline_hip and baseline_hip 1e-6: drop (baseline_hip - hr) / baseline_hip if drop 0.40: fall_score 0.10 if fall_score 0.65: return FALL_CAND, fall_score # 站立 stand_score 0.0 if ta 32: stand_score 0.5 if hhd is not None and hhd 1.0: stand_score 0.3 if ka is not None and ka 150: stand_score 0.2 if stand_score 0.7: return STANDING, stand_score # 坐姿 sit_score 0.0 if ka is not None and 70 ka 135: sit_score 0.45 if baseline_hip and baseline_hip 1e-6: drop (baseline_hip - hr) / baseline_hip if 0.12 drop 0.40: sit_score 0.35 if 5 ta 55: sit_score 0.2 if sit_score 0.6: return SITTING, sit_score return None, 0.0这段是核心判决的最简版本。要注意fall_score我写成了分段线性映射而不是硬开关这样分数本身有梯度可以用来做置信度排序也能在阈值附近配合时序平滑。硬开关满足就加固定分的问题是分数没有分辨力0.66 和 0.99 在后续逻辑里完全等价等于浪费了信息。4.3 视频流推理与跟踪绑定跟踪这块直接用 Ultralytics 的model.track配置persistTrue让跟踪器在帧之间保持状态。cap cv2.VideoCapture(input.mp4) fps cap.get(cv2.CAP_PROP_FPS) or 25 machines {} # tid - PostureStateMachine hip_hist {} # tid - deque, 用于基线估计 while True: ok, frame cap.read() if not ok: break h, w frame.shape[:2] res model.track(frame, persistTrue, conf0.4, trackerbytetrack.yaml, verboseFalse)[0] if res.keypoints is None or res.boxes.id is None: continue ids res.boxes.id.int().cpu().numpy() kxy res.keypoints.xy.cpu().numpy() kcf res.keypoints.conf.cpu().numpy() for i, tid in enumerate(ids): tid int(tid) f extract_features(kxy[i], kcf[i], h, w) base None if tid in hip_hist and len(hip_hist[tid]) 20: base float(np.median(hip_hist[tid])) cand, score judge_single(f, base) if tid not in machines: machines[tid] PostureStateMachine(fps) hip_hist[tid] deque(maxlenint(fps * 30)) if f is not None: hip_hist[tid].append(f[hip_height_ratio]) machines[tid].hist_hip.append(f[hip_height_ratio]) state machines[tid].update(cand, f) # 画图略hip_hist我用了 30 秒的窗口来算站立基线。这个长度是个权衡太短刚坐下来基线就被拉低了后面判坐姿会失效太长换了姿势比如从站着换成长期坐着办公基线老化不更新。30 秒在多数场景下够用如果场景里人长时间静坐可以改成用“最近 30 秒内的第 90 百分位”作为基线这样即使大部分时间坐着基线也还是站着的高度。4.4 现场标定的三步流程规则方案必须现场标定这一步省不掉。我总结成三步第一步采集标定片段。让目标用户不是你自己在真实机位下依次做自然站立 30 秒、正坐 30 秒、站起坐下 5 次、快速坐下 3 次、缓慢躺下 3 次、蹲下起立 3 次。这段素材同时用来标参数和算评估指标。第二步看特征分布定阈值。把每个动作片段的所有帧特征画成直方图看站立和坐姿的hip_height_ratio分布有多少重叠看跌倒和躺下的torso_angle和速度分布。阈值就卡在分布的分界处而不是拍脑袋定一个数。我一般会把阈值设在两类分布的第 5/95 百分位宁可多留余量。第三步回放验证只看误报。用标定片段回放把FALLEN的所有触发点记录下来逐个看是谁触发的、为什么触发。这一步我通常会跑三到五轮每轮改一两个参数。经验是跌倒的阈值永远往“更保守”的方向调因为漏报和误报的代价不对称——误报多了系统会被关掉那才是真正的漏报。一个具体的参数调整心得torso_angle的跌倒阈值我一开始设 60 度误报很多改成 70 度之后漏掉了“侧倒”。最后我改成了 55 度阈值但要求body_aspect 0.9同时成立这样侧倒能抓横向展开明显弯腰捡东西不会误报弯腰时身体还是竖直窄条。阈值调不下来的时候往往是缺一个联合条件而不是阈值不够严。5. 常见问题与排查实录5.1 高频问题速查表现象最可能原因排查动作处理方式站立的人被判成坐背身时鼻子不可见head_hip_dy失效站立分数掉到 0.5打印该 ID 各特征的逐帧值看head_hip_dy是否为 None加耳/眼中点替代鼻子的降级逻辑权重归一化蹲下触发跌倒报警单帧body_aspect和torso_angle都接近阈值观察hip_height_ratio下降幅度加蹲姿中间态下降 35% 且膝角 70 时不上报跌倒坐姿和站姿来回跳滑动窗口太短或该 ID 的基线还没建立检查窗口长度是否覆盖 0.4 秒加长窗口基线需要至少 20 帧才启用画面边缘的人一直判不出半个身子出画肩髋缺失看返回特征是否为 None加 ROI 边界检查出画比例太高时输出未知远距离小目标误判严重关键点分辨率不足torso_len只有十几像素打印torso_len分布设下限如 20 像素不足直接跳过多人交叉后状态错乱跟踪 ID 切换历史串到别人身上检查ids是否在相邻帧突变换更稳的跟踪器或状态机绑定id 位置连续性双校验白天准晚上崩低照度下关键点质量下降统计夜间片段的平均conf夜景下调conf_th或主动降级为只报警不判姿态报警延迟明显确认帧数太多算一下confirm_frames / fps缩短确认窗口用速度判据补精度5.2 几个真踩过的坑坑一把“躺着”和“跌倒”混为一谈。我在第一个版本里只要进入LYING就报警。结果一个用户习惯午休时躺在地上午睡系统一天报十几次。后来把LYING和FALLEN彻底分开LYING是姿态FALLEN是事件事件需要“从高到低的快速变化”来触发。一个人慢慢躺下永远不会触发FALLEN摔倒才会。这个区分是整套方案从“能用”到“可交付”的分水岭。坑二跟踪 ID 被背景物体抢占。有一回画面里有把椅子模型偶尔把椅子腿的图案识别成一个极小的人形置信度 0.4 左右分配了一个 ID然后这个 ID 的“人”永远躺着不动稳定触发报警。解决办法是在框级别加一个最小面积和最小torso_len过滤尺寸异常的检测直接丢弃。低置信度 极小尺寸的检测九成是噪声别舍不得扔。坑三摄像头俯角太大导致躯干角失真。顶装摄像头俯视 60 度以上拍出来的人即使是站着肩中点和髋中点在图像上的纵向距离也被严重压缩torso_angle会偏大站坐几乎分不开。这类机位我最后放弃了角度特征只用hip_height_ratio和body_aspect两个特征准确率虽然下降一点但比疯狂误报强。机位决定特征可用性先看机位再定特征别反过来。坑四多人场景下的基线污染。早期我把hip_hist存在一个全局字典里没按 ID 分结果房间里有两个人一个站着办公一个长期坐着基线被拉成中间值两个人都判不准。按 ID 分桶之后问题消失。这个 bug 很傻但很典型多目标场景里任何“历史统计量”都必须按 ID 隔离。5.3 评估方式别只看准确率姿态判决的评估不能只报一个“准确率 92%”那个数字毫无意义因为正负样本极度不平衡——一天里 99% 的时间都是站立或坐着跌倒可能一次都没有。我用的评估方式是这样按事件评估而不是按帧。定义一个跌倒事件从触发到解除为一条记录统计三类指标正确触发数、误触发数每个误触发要归因到具体动作类型比如“蹲下 3 次”“躺下 2 次”、漏触发数。用这三个数算检出率和误报率比帧级准确率有意义得多。分开统计响应延迟。从动作发生到系统触发的时间差我要求在中位数 1 秒以内、P95 在 2 秒以内。响应太慢的话即使检出了也没有实用价值。单独统计“不可判”比例。如果某段素材里有 40% 的帧被判为不可判那说明模型在这个场景下根本没工作准确率的数字再好也是虚的。我一般要求不可判比例低于 15%超过就回头查是光照问题、机位问题还是模型尺寸不够。实际测试时我会刻意构造一批“困难负样本”弯腰系鞋带、蹲下捡东西、坐在低矮的凳子上、躺在地板上看书、在地板上做伸展、快跑后突然停下坐下。这批样本的误报率比正常场景的准确率更能反映系统能不能上线。我自己的经验值是困难负样本上的误报率控制在 5% 以下正常场景的表现就已经足够稳定了。最后分享一个我觉得最值钱的实践经验这套方案的真实精度八成取决于关键点质量两成取决于判决规则。所以当效果不理想时先别急着调阈值去看你的关键点图——把检测出的骨架画在原图上一帧一帧看。如果骨架本身就画歪了、抖了、丢了半边那再怎么调规则都是白费劲该做的是换更大的模型、调输入尺寸、改善光照或者调整机位。规则只能修正“关键点对但判据模糊”的情况救不了“关键点本身就是错的”。

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

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

免费获取报价 →
↑