Spaces:
Running
Running
File size: 3,575 Bytes
11fab85 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | """單幀幾何特徵(純函式:無狀態、無 I/O)。
座標系為影像像素、y 向下。只有置信度 ≥ ``kpt_conf_min`` 的關鍵點視為可見;
肩或髖完全不可見時整幀標記 invalid(軀幹是所有特徵的基準,不硬猜)。
時間域的平滑、正規化與速度計算不在此處——見 ``engine``。
"""
from __future__ import annotations
import math
from dataclasses import dataclass
import numpy as np
# COCO 17-keypoint 索引(ultralytics pose 模型輸出順序)
NOSE = 0
L_SHOULDER, R_SHOULDER = 5, 6
L_HIP, R_HIP = 11, 12
L_ANKLE, R_ANKLE = 15, 16
_EPS = 1e-6
@dataclass
class FrameGeometry:
"""一幀、一個人的原始幾何量(像素單位,未平滑、未正規化)。"""
valid: bool
theta_deg: float = float("nan") # 軀幹傾角:0=直立、90=橫臥(頭低於髖時 clamp 90)
torso_len: float = float("nan") # 肩中點-髖中點距離(像素);引擎以其滑動中位數 L̃ 作尺度
bbox_aspect: float = float("nan") # bbox 寬/高
hip_y: float = float("nan") # 髖中點 y(像素)
ankle_valid: bool = False
hip_ankle_gap: float = float("nan") # 踝中點 y − 髖中點 y(>0 = 踝在髖下方)
def _visible_mid(
kpts_xy: np.ndarray, kpts_conf: np.ndarray, idxs: tuple[int, int], conf_min: float
) -> np.ndarray | None:
# NaN 座標即使 conf 高也視為不可見:NaN 一旦進入中位數/差分緩衝會汙染整條管線
pts = [
kpts_xy[i]
for i in idxs
if kpts_conf[i] >= conf_min and np.all(np.isfinite(kpts_xy[i]))
]
if not pts:
return None
return np.mean(np.asarray(pts, dtype=np.float64), axis=0)
def compute_frame_geometry(
kpts_xy: np.ndarray,
kpts_conf: np.ndarray,
bbox: np.ndarray,
kpt_conf_min: float,
) -> FrameGeometry:
"""由 17 個關鍵點與 bbox 計算單幀幾何特徵。
Args:
kpts_xy: 形狀 (17, 2) 或展平 (34,) 的像素座標。
kpts_conf: 形狀 (17,) 的置信度;整列 -1.0 為「模型未輸出 conf」哨兵,
必然低於 ``kpt_conf_min``,自動導致 invalid。
bbox: (x1, y1, x2, y2)。
kpt_conf_min: 關鍵點可見門檻。
"""
kpts_xy = np.asarray(kpts_xy, dtype=np.float64).reshape(-1, 2)
kpts_conf = np.asarray(kpts_conf, dtype=np.float64).reshape(-1)
bbox = np.asarray(bbox, dtype=np.float64).reshape(-1)
shoulder = _visible_mid(kpts_xy, kpts_conf, (L_SHOULDER, R_SHOULDER), kpt_conf_min)
hip = _visible_mid(kpts_xy, kpts_conf, (L_HIP, R_HIP), kpt_conf_min)
if shoulder is None or hip is None:
return FrameGeometry(valid=False)
dx = hip[0] - shoulder[0]
dy = hip[1] - shoulder[1]
torso_len = math.hypot(dx, dy)
if not math.isfinite(torso_len) or torso_len < _EPS:
return FrameGeometry(valid=False)
if dy < 0:
# 髖高於肩(倒立/翻滾):傾角語意上已是「非直立」,clamp 為 90°
theta = 90.0
else:
theta = math.degrees(math.atan2(abs(dx), dy))
w = bbox[2] - bbox[0]
h = bbox[3] - bbox[1]
aspect = float(w / h) if (math.isfinite(w) and h > _EPS) else float("nan")
ankle = _visible_mid(kpts_xy, kpts_conf, (L_ANKLE, R_ANKLE), kpt_conf_min)
ankle_valid = ankle is not None
gap = float(ankle[1] - hip[1]) if ankle_valid else float("nan")
return FrameGeometry(
valid=True,
theta_deg=float(theta),
torso_len=float(torso_len),
bbox_aspect=aspect,
hip_y=float(hip[1]),
ankle_valid=ankle_valid,
hip_ankle_gap=gap,
)
|