feat(edge): pose geometry utils (torso angle, com, normalization)
This commit is contained in:
parent
33e987d8d9
commit
444c01407a
2 changed files with 94 additions and 0 deletions
55
edge/geometry.py
Normal file
55
edge/geometry.py
Normal 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
39
tests/test_geometry.py
Normal 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
|
||||||
Loading…
Reference in a new issue