_displacement (mean per-keypoint delta between the last two track frames, normalized by bbox height) was duplicated verbatim in edge/detectors/fall.py and edge/detectors/immobility.py. Move it to edge/geometry.py as track_displacement(track) and have both detectors call the shared helper instead of their own private copy. Pure refactor — behavior is identical (byte-identical formula), no config or public API changes. Tests: tests/test_geometry.py adds direct coverage for the new public helper (fewer-than-two-frames default, and the normalized-by-height calculation). Existing fall/immobility tests are unchanged and stay green, confirming behavior parity.
66 lines
2.2 KiB
Python
66 lines
2.2 KiB
Python
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
|
|
|
|
|
|
def track_displacement(track) -> float:
|
|
"""Mean per-keypoint displacement between the last two frames, normalized by bbox height. 1.0 if <2 frames."""
|
|
if len(track.frames) < 2:
|
|
return 1.0
|
|
a, b = track.frames[-2], track.frames[-1]
|
|
h = b.bbox.height or 1.0
|
|
d = sum(((p.x - q.x) ** 2 + (p.y - q.y) ** 2) ** 0.5
|
|
for p, q in zip(a.keypoints, b.keypoints)) / len(b.keypoints)
|
|
return normalize(d, h)
|