---
title: Fall Detection Pose
emoji: 🚨
colorFrom: red
colorTo: gray
sdk: gradio
sdk_version: "6.19.0"
python_version: "3.10"
app_file: app.py
pinned: false
license: mit
short_description: Rule-based fall detection from YOLO26-pose keypoints
---
# fall-detection-pose
[](LICENSE)
[](pyproject.toml)
[](https://github.com/tun0000/fall-detection-pose)
> 這是 [tun0000/fall-detection-pose](https://github.com/tun0000/fall-detection-pose) 的永久 demo 部署。
> 完整原始碼、notebooks、評估/failure-analysis 明細都在 GitHub repo。
以 **YOLO26-pose 預訓練模型 + ByteTrack 多目標追蹤**為基礎的規則式(rule-based)跌倒偵測系統。
本專案重點不在模型創新,而在工程能力:
- **可解釋的規則引擎**:每個 track 一台狀態機(UPRIGHT → FALLING → FALLEN → ALARM),
所有閾值集中於 [config.yaml](config.yaml),每個值附選擇理由與文獻出處。
- **event-level 誠實評估**:在 UR Fall Detection Dataset(30 falls + 40 ADL)上以
明確定義的事件配對協定計算 precision / recall / F1;tune/test 切分防止「在測試集上調參」。
- **失敗分析**:對誤報與漏報案例附特徵時序圖,展示規則「為什麼」觸發或錯過。
- **推論與規則解耦**:GPU 只跑一次姿態抽取落成 keypoint cache,調參/評估為秒級 CPU 工作。
## Demo
| 跌倒 → 正確觸發 ALARM |
 |
| 日常動作(ADL)→ 正確不觸發 |
 |
兩支都是實際跑完整條 pipeline 的畫面(非後製剪輯),用
[notebooks/05_gradio_demo.ipynb](https://github.com/tun0000/fall-detection-pose/blob/main/notebooks/05_gradio_demo.ipynb)
啟動的 Gradio demo 直接螢幕錄影:`fall-06`(tune split)展示跌倒正確觸發紅色 ALARM 橫幅;
`adl-01`(test split)展示日常動作(過程中甚至有蹲下這種容易與跌倒混淆的姿勢)全程正確
保持 UPRIGHT、不誤觸。**上面這個 Space 就是同一個 demo,直接上傳影片試試看。**
## 快速開始
**在瀏覽器裡試(不用裝任何東西)**:點下表任一 notebook 的 Colab 徽章,
`Runtime → Run all`——每一本都是自包含的(clone repo、裝依賴、跑單元測試)。
| Notebook | 內容 | |
|---|---|---|
| `01_smoke_test.ipynb` | 2 支短片(1 fall + 1 ADL)端到端煙測 | [](https://colab.research.google.com/github/tun0000/fall-detection-pose/blob/main/notebooks/01_smoke_test.ipynb) |
| `02_extract_urfd.ipynb` | URFD 全量下載 + 兩模型抽 keypoint cache【唯一需要 GPU 的步驟】 | [](https://colab.research.google.com/github/tun0000/fall-detection-pose/blob/main/notebooks/02_extract_urfd.ipynb) |
| `03_tune_eval.ipynb` | tune split 網格調參 → 凍結 config → test split 定稿 + 失敗分析 | [](https://colab.research.google.com/github/tun0000/fall-detection-pose/blob/main/notebooks/03_tune_eval.ipynb) |
| `04_benchmark.ipynb` | FPS/延遲 benchmark 矩陣 | [](https://colab.research.google.com/github/tun0000/fall-detection-pose/blob/main/notebooks/04_benchmark.ipynb) |
| `05_gradio_demo.ipynb` | 啟動 Gradio demo(上方兩張 GIF 就是這樣錄的) | [](https://colab.research.google.com/github/tun0000/fall-detection-pose/blob/main/notebooks/05_gradio_demo.ipynb) |
**本機 CLI**(`detect` 只吃 keypoint cache,無需 GPU/`[infer]`;`extract`/`annotate`/`bench`
需要 `[infer]`,其中 `extract` 建議上 GPU):
```bash
uv sync # 核心依賴(規則引擎、評估,無 torch)
uv run pytest # 86 個單元測試,秒級,無需 GPU
pip install -e ".[infer,demo]" # 加裝推論(ultralytics/opencv)與 Gradio demo
fdp pipeline --source video.mp4 --out-dir outputs/ # extract → detect → annotate 一條龍
fdp bench --video video.mp4 --model yolo26n-pose.pt --model yolo26s-pose.pt
python -m fall_detection.app.gradio_app --no-share # 本機跑 demo(不建立公開連結)
```
## 架構
**主流程**——只有 `extract`(GPU 姿態推論)是重的一步,其餘全部吃 cache 好的
keypoint parquet、CPU 秒級跑完,這是整個專案「推論與規則解耦」的核心設計。
`app/gradio_app.py`(Gradio demo)不是額外的一條路徑,就是直接依序呼叫
`extract` → `engine` → `annotate` 這三個函式,沒有另外的邏輯。
```mermaid
flowchart TD
V[影片] --> EX
subgraph gpu["GPU(唯一需要 GPU 的步驟)"]
EX["inference/extract.py
YOLO26-pose + ByteTrack"]
end
EX --> CACHE[("keypoint cache
parquet + meta.json")]
subgraph cpu["CPU(秒級,無需 GPU)"]
CACHE --> ENGINE["rules/engine.py
特徵管線 + 每-track 狀態機"]
CFG["config.yaml
全部閾值"] -.-> ENGINE
ENGINE --> EVENTS["events.json"]
ENGINE --> DEBUG["debug JSONL
(--debug)"]
CACHE --> ANNOTATE["viz/annotate.py
骨架 + 狀態 + ALARM 疊加"]
EVENTS --> ANNOTATE
DEBUG --> ANNOTATE
end
ANNOTATE --> OUT["標註影片
(H.264)"]
style EX fill:#f9d5a7
style CACHE fill:#f5f5f5
style ENGINE fill:#a7d5f9
```
**評估流程**(調參/校準用,同樣不需要重跑 GPU——直接吃已經抽好的 cache):
```mermaid
flowchart TD
CACHE[("keypoint cache")] --> REPORT["eval/report.py
grid-search + matching"]
EVENTS["events.json"] --> REPORT
GT["urfall-cam0-*.csv
(ground truth)"] --> REPORT
REPORT --> METRICS["eval/metrics.json"]
```
## 判斷邏輯
**特徵**(像素座標,y 向下;只用 conf ≥ `kpt_conf_min` 的關鍵點,詳見
[features.py](src/fall_detection/rules/features.py)):
- 肩中點 S、髖中點 H(任一側不可見則取可見側;兩側都不可見則整幀 invalid)。
- 軀幹長 `L = ‖H − S‖`,取滑動中位數 `L̃` 當作該 track 的尺度單位——後續量全部
正規化成「軀幹長的倍數」,不受解析度或鏡頭距離影響。
- 軀幹傾角 `θ = atan2(|H.x − S.x|, H.y − S.y)`:直立 ≈ 0°、橫躺 ≈ 90°。
- bbox 寬高比 `r = w / h`。
- 髖-踝相對高度 `h_hip = (踝.y − H.y) / L̃`:站立時髖遠高於踝(數值大),躺地時
髖踝同高(趨近 0);踝不可見時不參與投票,不硬猜。
- 垂直速度 `v_norm`:髖 y 座標先 3 點中位數濾波,再用固定時間窗差分並除以
`L̃` 正規化——單位是「軀幹長/秒」,同一支影片用 30fps 或 15fps 重採樣算出來
的事件起訖時間差 <0.2s(見 `tests/test_engine.py` 的 fps 不變性測試)。
- 關鍵點缺測用 hold-last(TTL = `max_kpt_gap_s`),躺地後 pose 模型常整段掉點,
不能讓缺測直接判定「站起來了」。
**狀態機**(每個 track 各自一台,見 [state_machine.py](src/fall_detection/rules/state_machine.py)):
```
UPRIGHT ──(v_norm > v_fall_enter 或 ω > omega_enter)──▶ FALLING
FALLING ──(躺姿 2-of-3 投票,window_confirm_s 內達 vote_ratio)──▶ FALLEN
FALLING ──(t_falling_timeout_s 內未確認)──▶ UPRIGHT (回退不出事件,擋快速坐下/蹲下)
FALLEN ──(持續躺姿 t_confirm_fallen_s)──▶ ALARM (此刻才「確認」一次跌倒)
FALLEN/ALARM ──(回正持續 t_recover_s,遲滯出口閾值)──▶ UPRIGHT
FALLEN/FALLING 中 track 消失或影片結束 ──▶ 仍收尾成一次事件(見下)
```
躺姿投票取 `[θ > theta_lying_enter, r > r_lying, h_hip < h_hip_lying]` **三取二**:
單一幾何特徵在特定視角都會失效(朝鏡頭跌倒時 r 可能不變、側躺時 θ 不到 90°),
投票機制吸收單一特徵的盲區(GMDCSA 單規則法在 URFD specificity 僅 72.5% 的教訓)。
**收尾規則(`finalize()`)**——track 消失或影片結束時:已在 ALARM 直接收尾;卡在
FALLEN(躺姿已投票確認,但撐不滿 `t_confirm_fallen_s`)也收尾成一次事件;卡在
FALLING 但最後一次平滑觀測已符合躺姿投票同樣收尾。三者背後同一個判斷:與其讓
一次真實跌倒因為 track 提早消失就整個漏判,寧可信任最後一次可信觀測(代價與
邊界情況見下方「失敗分析」`fall-08` 案例、以及「已知限制」)。
**關鍵閾值**(tune split 網格搜尋校準,完整清單與逐項理由見 [config.yaml](config.yaml)):
| 閾值 | 值 | 意義 |
|---|---|---|
| `kpt_conf_min` | 0.25 | 關鍵點可見門檻 |
| `v_fall_enter` | 0.8 軀幹長/s | 進入 FALLING 的垂直速度門檻 |
| `theta_lying_enter` | 40° | 躺姿投票之一(見「已知限制」的類外泛化風險) |
| `window_confirm_s` / `vote_ratio` | 0.4s / 0.8 | 躺姿投票視窗與通過比例 |
| `t_confirm_fallen_s` | 0.3s | FALLEN → ALARM 的持續確認時間 |
## 評估
協定:event-level 配對,預測與 GT ± 0.5s 容忍窗內有任何時間交集即視為候選,
每個 GT 貪婪配對交集最大的一個預測(一對一)。ADL 影片一律視為 0 個跌倒 GT
——即使該影片的姿態標註本身出現「躺姿」區間(URFD 的 ADL 集合刻意包含主動
躺下,如躺床上,用來測試誤報率),任何預測事件都算 FP。閾值全部在 tune split
(10 falls + 13 adls)網格搜尋校準凍結(見 [config.yaml](config.yaml));
以下為 **test split**(20 falls + 27 adls,從未參與調參)的結果,原始數字見
[eval/metrics.json](eval/metrics.json)。
| 模型 | Precision | Recall | F1 | Video-level Specificity |
|---|---|---|---|---|
| yolo26n-pose(預設) | 0.600 | 0.600 | 0.600 | 0.741 |
| yolo26s-pose | 0.611 | 0.550 | 0.579 | 0.778 |
調參經過兩輪:第一輪勝出的 4 個閾值全部卡在候選範圍邊緣(方法論警訊,代表
範圍切太窄);往更敏感方向擴大網格、並修掉一個結構性 bug(track 消失時最後
觀測已符合躺姿卻沒收尾成事件)後,第二輪 F1(yolo26n-pose)從 0.457 提升到
0.600。調參準則:recall 優先、precision ≥ 0.5 才列入候選——跌倒漏判(沒人去
查看)的代價高於一次誤報。
**與文獻數字的對照**(僅供量級參考,見下方注意事項):
| 方法 | Precision | Recall / Sensitivity | F1 |
|---|---|---|---|
| 本專案(yolo26n-pose,test split) | 0.600 | 0.600 | 0.600 |
| GMDCSA(規則法,URFD) | — | 91.67 | —(specificity 72.50) |
| PIFR(2025) | 88.8 | 94.1 | 91.4 |
| Núñez-Marcos et al. 2017(CNN,需訓練) | — | — | acc 98.63 |
**這張表不是拿來宣稱贏過或輸給誰**——各方法的評估協定(事件配對容忍窗、
正類定義、有沒有 tune/test 切分)、資料前處理都不同,數字量級不同不代表方法
優劣。本專案的協定在上面完整揭露、切分名單凍結進版控([eval/splits.yaml](eval/splits.yaml)),
任何人都能重現或挑戰這個數字——這是刻意的設計取捨:比起一個無法重現出處的
高分,一個協定透明、可重現的中等分數對這個作品集的目的更有價值。
## Benchmark
固定一支 URFD 重組影片(`adl-01.mp4`,150 幀,實際可用幀數誠實回報而非湊滿
300)先整支解碼進記憶體,`{yolo26n-pose, yolo26s-pose} x {GPU FP32, GPU FP16,
CPU}` 各跑 3 輪取中位數;GPU 計時前後夾 `torch.cuda.synchronize()`,CPU 為
Colab 標準 2 vCPU(數字偏低,誠實照報)。**不引用官方「CPU 快 43%」的宣傳
數字**——那是 detect 模型的 ONNX 匯出數字,不適用本專案的 pose 模型。原始
數字見 [bench.json](bench.json)。
| 模型 | 裝置 | 端到端 FPS | p50 延遲 | p95 延遲 |
|---|---|---|---|---|
| yolo26n-pose | GPU(T4)FP32 | 59.65 | 13.84ms | 23.52ms |
| yolo26n-pose | GPU(T4)FP16 | 64.64 | 15.63ms | 24.25ms |
| yolo26n-pose | CPU(2 vCPU) | 8.23 | 116.96ms | 178.96ms |
| yolo26s-pose | GPU(T4)FP32 | 72.25 | 13.88ms | 20.40ms |
| yolo26s-pose | GPU(T4)FP16 | 66.18 | 14.69ms | 24.09ms |
| yolo26s-pose | CPU(2 vCPU) | 3.36 | 271.15ms | 408.98ms |
兩個模型在 GPU 上都遠超即時(30fps)所需;CPU 上 `yolo26n-pose` 仍有 8+ FPS
堪用,`yolo26s-pose` 掉到 3.36 FPS 明顯吃緊。GPU 對較大模型的加速幅度也更大
(n:~7.3x、s:~21.5x),符合計算量較重的模型從平行化得利更多的預期。**這個
Space 本身跑在免費 CPU tier 上**,實際處理速度對應上表 CPU 那兩列,一支
5-10 秒的短片預期數十秒內處理完。
**測出來的兩個反直覺數字,誠實報告、不隱藏**:
- `yolo26s-pose` 在 GPU 上量到比 `yolo26n-pose` 更快(73.53 vs 60.65 FPS
純推論)。CPU 上兩者關係符合預期(s 比 n 慢 ~2.45 倍,計算量差異的合理
反映),因此 GPU 這個反轉不太像是程式呼叫錯模型的 bug,較可能是量測順序
效應(n 先跑,GPU/CUDA kernel 尚未完全暖機)或 Colab 共用 T4 的量測雜訊
——樣本數(150 幀 x 3 輪)不足以下更強的結論。
- FP16 對 `yolo26n-pose` 有加速(+8.3%),對 `yolo26s-pose` 反而略慢(-8.6%),
同樣可能是雜訊,也可能反映小模型的 FP16 casting 額外開銷相對計算量比例
較大,抵銷部分理論加速。
- benchmark 腳本(`fdp bench` 或 `src/fall_detection/bench/benchmark.py`)
可攜,任何機器都能補跑一列驗證,不綁定本次 Colab session 的結果。
## 失敗分析
從 test split 挑 1 個漏報(FN)+ 2 個誤報(FP),展示規則引擎「為什麼」錯過或
誤觸發(特徵時序圖見 GitHub repo 的 `notebooks/03_tune_eval.ipynb` 失敗案例分析格):
**FN — `fall-21`**:追蹤器在跌倒的視覺證據真正成形前就整個失去目標。全程軀幹
傾角(θ)只有 0-6°、bbox 寬高比只有 ~0.3,沒有任何躺姿跡象;垂直速度確實
持續爬升,但直到資料結束前才剛好接近門檻,track 就消失了。這是**追蹤持續度**
的限制,不是閾值問題——沒有更多資料,任何規則法都生不出證據。
**FP — `adl-34`**:URFD 的 ADL 集合刻意包含「主動躺下」的日常動作(測試系統
會不會把臥床誤判成跌倒)。這支影片裡 θ 在 6 秒多內反覆於躺姿(~90°)與非躺姿
(~20-30°)之間震盪多次,是「躺下→坐起→躺下…」的主動調整,而非一次性站立→
跌倒→臥地不動。系統從純幾何角度正確判斷「持續符合躺姿」,但協定規定 ADL 影片
零 GT——這是規則法在「主動躺下 vs. 跌倒臥地」上幾何不可分辨的已知極限。
(附帶一提:雖然姿態震盪劇烈,因全程同一個 track id,靠回正需要「持續」的
遲滯設計撐住,並沒有被切成好幾段事件——跟下一個案例形成對比。)
**FP — `fall-08`**:這支**其實是真實跌倒**,但被切成兩個 track id(ByteTrack
在跌倒過程一次短暫的姿態回彈時斷了 id)。兩段預測分別是 `[0.933s,2.233s]`
(track 1,靠 finalize-while-FALLEN 修正正確收尾,對到 GT 算 TP)與
`[2.367s,3.0s]`(track 2,全新 id 重新觸發,判定為「重複預測」算 FP)。根本
原因是**縫合機制與事件合併機制之間的縫隙**:track 縫合靠 bbox IoU + 時間視窗
判斷是否同一人,這次因短暫回彈使 bbox 形狀變化過大沒縫上;事件合併只看
track id 鏈是否有交集,縫合一旦失敗就無法回頭補救,即使兩段事件時間相鄰、
位置相同。
## 已知限制與未來工作
- **`theta_lying_enter=40°` 低於文獻安全下限**:tune split 網格調參結果比
Chen et al. 建議的 45° 更低(該研究指出 45° 會誤判深彎腰)。這 70 支 URFD
影片沒有彎腰類 ADL 可測出此風險,是刻意接受的類外泛化風險——換到有彎腰/
伸展動作的場域需重新校準。
- **track 縫合/事件合併縫隙**(見上方 `fall-08` 分析):跨 track id 鏈的事件
合併目前只認 id 交集,不認時間相鄰 + 空間鄰近。
- **收尾事件不一定會顯示即時 ALARM 畫面**:`track_lost_while_fallen` 這類收尾
規則能正確記錄事件,但如果一次跌倒過程 track id 反覆中斷(如 `fall-01`:
1→3→5 換了三次),「持續躺姿」的計時會跟著中斷重算,狀態機永遠撐不到即時
的 FALLEN→ALARM 轉換,只能靠影片結束時的收尾機制補判——事件本身正確,但
標註影片上看不到紅色 ALARM 橫幅。demo GIF 因此選了 track id 全程穩定的
`fall-06`,而非最早測試、更能體現縫合機制韌性的 `fall-01`。
- **慢速跌倒是規則法已知盲區**:速度/角速度是進入 FALLING 的必要條件,刻意
緩慢的跌倒或躺下不會觸發(見 GitHub repo 的 `tests/test_engine.py`)。
- **`model.conf` 無法事後調參**:偵測信心門檻在 GPU extract 階段就烘進
keypoint cache,調整需要重新抽取,不像規則引擎閾值能在 CPU 上秒級迭代。
- **多人重疊/遮擋未經系統性測試**:URFD 每支影片都是單人。`rules/engine.py`
架構上是「每個 track_id 一台獨立狀態機」,原生支援多人,但多人互相遮擋時
keypoint 品質、ByteTrack 的多人關聯穩定度都沒有實測驗證。
- **鏡頭角度/跨資料集泛化未驗證**:所有閾值都是對 URFD 固定側視角、640x480
鏡頭校準;換成俯視角、魚眼鏡頭或另一個資料集,幾何特徵的合理範圍可能整套
失準,需要重新校準,不是直接遷移這份 config.yaml 的數字。
- **未做邊緣裝置匯出**:Benchmark 只測了 PyTorch 直接推論(GPU/CPU);
ultralytics 原生支援 TensorRT/ONNX 匯出,但匯出後的精度/速度是否一致
本專案沒有驗證,是明確的未來工作方向。
## 資料集聲明
評估使用 [UR Fall Detection Dataset](https://fenix.ur.edu.pl/~mkepski/ds/uf.html)
(CC BY-NC-SA 4.0,**不隨本 repo 散布**,由下載腳本自官方站取得):
> Bogdan Kwolek, Michal Kepski, "Human fall detection on embedded platform using
> depth maps and wireless accelerometer", *Computer Methods and Programs in
> Biomedicine*, Vol. 117, Issue 3, 2014, pp. 489–501.
> DOI: [10.1016/j.cmpb.2014.09.005](https://doi.org/10.1016/j.cmpb.2014.09.005)
## License
程式碼採 [MIT](LICENSE);資料集授權見上。完整原始碼:
[github.com/tun0000/fall-detection-pose](https://github.com/tun0000/fall-detection-pose)。