import math from edge.models import ( Keypoint, BBox, NOSE, L_SHOULDER, R_SHOULDER, L_HIP, R_HIP, CORE_JOINTS, ) def _mean(points: list[tuple[float, float]]) -> tuple[float, float]: n = len(points) return (sum(p[0] for p in points) / n, sum(p[1] for p in points) / n) def com(keypoints: list[Keypoint]) -> tuple[float, float]: pts = [(keypoints[i].x, keypoints[i].y) for i in CORE_JOINTS] return _mean(pts) def _shoulder_center(kps: list[Keypoint]) -> tuple[float, float]: return _mean([(kps[L_SHOULDER].x, kps[L_SHOULDER].y), (kps[R_SHOULDER].x, kps[R_SHOULDER].y)]) def _hip_center(kps: list[Keypoint]) -> tuple[float, float]: return _mean([(kps[L_HIP].x, kps[L_HIP].y), (kps[R_HIP].x, kps[R_HIP].y)]) def torso_angle_deg(keypoints: list[Keypoint]) -> float: """몸통(어깨중심→엉덩이중심) 벡터가 수직축과 이루는 각. 0=수직, 90=수평.""" sx, sy = _shoulder_center(keypoints) hx, hy = _hip_center(keypoints) vx, vy = hx - sx, hy - sy # 수직축(0,1) 대비 편차. 위/아래 무관하게 |성분| 사용. return math.degrees(math.atan2(abs(vx), abs(vy))) if (vx or vy) else 0.0 def head_below_hips(keypoints: list[Keypoint]) -> bool: _, hy = _hip_center(keypoints) return keypoints[NOSE].y > hy def visible_count(keypoints: list[Keypoint], thr: float = 0.2) -> int: return sum(1 for k in keypoints if k.conf >= thr) def core_visible(keypoints: list[Keypoint], thr: float = 0.5) -> bool: return all(keypoints[i].conf >= thr for i in CORE_JOINTS) def height_of(bbox: BBox) -> float: return bbox.height def normalize(value: float, bbox_height: float) -> float: return value / bbox_height if bbox_height else 0.0