feat(edge): pose geometry utils (torso angle, com, normalization)

This commit is contained in:
유창욱 2026-07-11 20:18:03 +09:00
parent 33e987d8d9
commit 444c01407a
2 changed files with 94 additions and 0 deletions

55
edge/geometry.py Normal file
View file

@ -0,0 +1,55 @@
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

39
tests/test_geometry.py Normal file
View file

@ -0,0 +1,39 @@
import math
from edge.models import Keypoint, BBox
from edge.geometry import (
com, torso_angle_deg, head_below_hips, visible_count,
core_visible, normalize,
)
def _kps(overrides):
kps = [Keypoint(0, 0, 0.0) for _ in range(17)]
for i, (x, y, c) in overrides.items():
kps[i] = Keypoint(x, y, c)
return kps
def test_com_is_mean_of_shoulders_and_hips():
kps = _kps({5: (0, 0, 1), 6: (10, 0, 1), 11: (0, 10, 1), 12: (10, 10, 1)})
assert com(kps) == (5.0, 5.0)
def test_torso_angle_upright_near_zero():
# shoulders above hips (smaller y), vertical torso
kps = _kps({5: (0, 0, 1), 6: (10, 0, 1), 11: (0, 100, 1), 12: (10, 100, 1)})
assert torso_angle_deg(kps) < 10
def test_torso_angle_lying_near_ninety():
# shoulders and hips at same height, horizontal torso
kps = _kps({5: (0, 50, 1), 6: (0, 55, 1), 11: (100, 50, 1), 12: (100, 55, 1)})
assert torso_angle_deg(kps) > 80
def test_head_below_hips_true_when_nose_lower_on_screen():
kps = _kps({0: (5, 120, 1), 11: (0, 100, 1), 12: (10, 100, 1)})
assert head_below_hips(kps) is True
def test_visible_count_and_core_visible():
kps = _kps({5: (0, 0, 0.9), 6: (1, 0, 0.9), 11: (0, 1, 0.9), 12: (1, 1, 0.9)})
assert visible_count(kps, thr=0.2) == 4
assert core_visible(kps, thr=0.5) is True
def test_normalize_divides_by_bbox_height():
assert normalize(30, 60) == 0.5
assert normalize(30, 0) == 0.0