betty0's picture
Deploy fall-detection-pose Gradio demo
11fab85 verified
Raw
History Blame Contribute Delete
5.01 kB
"""標註影片輸出:骨架 + track id + 狀態標籤 + ALARM 警示。
狀態來源是規則引擎的 per-frame debug 紀錄(單一事實來源:
畫面上看到的狀態就是引擎當下判定的狀態,而非事後由事件區間反推),
ALARM 橫幅出現的時刻即真實告警延遲,demo 不美化。
"""
from __future__ import annotations
from pathlib import Path
import cv2
import numpy as np
import pandas as pd
from ..config import Config
from ..events.schema import FallEvent
from ..io.cache import N_KPTS
from ..io.video import H264VideoWriter, iter_frames, probe
# COCO 17 骨架連線(肢段)
SKELETON = [
(5, 7), (7, 9), # 左臂
(6, 8), (8, 10), # 右臂
(5, 6), # 肩線
(5, 11), (6, 12), # 軀幹
(11, 12), # 髖線
(11, 13), (13, 15), # 左腿
(12, 14), (14, 16), # 右腿
(0, 5), (0, 6), # 頭-肩
]
STATE_COLORS = { # BGR
"UPRIGHT": (80, 200, 80),
"FALLING": (0, 165, 255),
"FALLEN": (0, 60, 255),
"ALARM": (0, 0, 255),
}
def _draw_person(
frame: np.ndarray,
kpts_xy: np.ndarray,
kpts_conf: np.ndarray,
bbox: tuple,
track_id: int,
state: str,
kpt_conf_min: float,
) -> None:
color = STATE_COLORS.get(state, (200, 200, 200))
x1, y1, x2, y2 = (int(v) for v in bbox)
cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
pts = kpts_xy.reshape(N_KPTS, 2)
visible = kpts_conf >= kpt_conf_min
for a, b in SKELETON:
if visible[a] and visible[b]:
pa = tuple(int(v) for v in pts[a])
pb = tuple(int(v) for v in pts[b])
cv2.line(frame, pa, pb, color, 2)
for i in range(N_KPTS):
if visible[i]:
cv2.circle(frame, tuple(int(v) for v in pts[i]), 3, color, -1)
label = f"id {track_id} {state}"
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.55, 2)
ty = max(y1 - 8, th + 4)
cv2.rectangle(frame, (x1, ty - th - 6), (x1 + tw + 6, ty + 4), color, -1)
cv2.putText(
frame, label, (x1 + 3, ty), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 2
)
def _draw_alarm_banner(frame: np.ndarray, track_ids: list[int]) -> None:
h, w = frame.shape[:2]
overlay = frame.copy()
cv2.rectangle(overlay, (0, 0), (w, 46), (0, 0, 220), -1)
cv2.addWeighted(overlay, 0.75, frame, 0.25, 0, frame)
ids = ",".join(str(t) for t in sorted(set(track_ids)))
cv2.putText(
frame,
f"FALL ALARM (track {ids})",
(12, 32),
cv2.FONT_HERSHEY_SIMPLEX,
0.9,
(255, 255, 255),
2,
)
def annotate_video(
video_path: str | Path,
cache_df: pd.DataFrame,
fps: float,
cfg: Config,
events: list[FallEvent],
debug_records: list[dict],
out_path: str | Path,
) -> Path:
"""輸出標註影片(H.264)。
Args:
cache_df: keypoint cache rows(骨架繪製來源)。
events: 引擎輸出的事件(用於畫面右上角事件計數)。
debug_records: ``run_engine(collect_debug=True)`` 的 per-frame 狀態。
"""
out_path = Path(out_path)
info = probe(video_path)
state_by_frame_track: dict[tuple[int, int], str] = {
(d["frame_idx"], d["track_id"]): d["state"] for d in debug_records
}
rows_by_frame: dict[int, list] = {}
for row in cache_df.itertuples(index=False):
rows_by_frame.setdefault(int(row.frame_idx), []).append(row)
with H264VideoWriter(out_path, info.fps, info.width, info.height) as writer:
for frame_idx, frame in iter_frames(video_path):
t_s = frame_idx / fps
alarm_tracks: list[int] = []
for row in rows_by_frame.get(frame_idx, []):
tid = int(row.track_id)
state = state_by_frame_track.get((frame_idx, tid), "UPRIGHT")
if tid < 0:
state = "-" # 未指派 track 的偵測:只畫框不標狀態
_draw_person(
frame,
np.asarray(row.kpts_xy),
np.asarray(row.kpts_conf),
(row.bbox_x1, row.bbox_y1, row.bbox_x2, row.bbox_y2),
tid,
state,
cfg.model.kpt_conf_min,
)
if state == "ALARM":
alarm_tracks.append(tid)
if alarm_tracks:
_draw_alarm_banner(frame, alarm_tracks)
n_done = sum(1 for e in events if e.end_time_s <= t_s)
cv2.putText(
frame,
f"frame {frame_idx} t={t_s:6.2f}s events={n_done}/{len(events)}",
(10, info.height - 12),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(255, 255, 255),
1,
)
writer.write(frame)
return out_path