Spaces:
Running on Zero
Running on Zero
File size: 6,074 Bytes
4948993 | 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """Tests for the movement-analysis engine β no GPU, no model downloads."""
import math
import numpy as np
from formscout.types import Pose2DResult
def _pose_from(traj_by_joint, n, base_conf=0.9):
"""Build a Pose2DResult; traj_by_joint maps joint->callable(i)->(x,y)."""
kps = []
for i in range(n):
frame = {}
for j in range(17):
if j in traj_by_joint:
x, y = traj_by_joint[j](i)
else:
x, y = float(100 + j * 5), float(100 + j * 5)
frame[j] = {"x": float(x), "y": float(y), "conf": base_conf}
kps.append(frame)
return Pose2DResult(keypoints=kps, fps=30.0, confidence=0.9)
# ββ relevant_joints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_relevant_joints_and_primary_consistent():
from formscout.analysis import relevant_joints as RJ
for test in RJ.RELEVANT:
joints = RJ.relevant_joints(test)
angles = RJ.relevant_angles(test)
prim = RJ.primary_angle(test)
assert joints, f"{test} has no relevant joints"
assert prim in angles, f"{test} primary angle not in angles"
def test_openness_label_monotonic():
from formscout.analysis.relevant_joints import openness_label
assert "open" in openness_label(175)
assert openness_label(30).endswith("closed")
# ββ timeseries ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_angle_series_length_matches_frames():
from formscout.analysis.timeseries import angle_series
pose = _pose_from({}, n=8)
series = angle_series(pose, "deep_squat")
assert "left_knee_flexion" in series
for name, vals in series.items():
assert len(vals) == 8
def test_relevant_flexion_reports_degrees_and_openness():
from formscout.analysis.timeseries import relevant_flexion_at
# Straight leg: hip, knee, ankle collinear vertical β ~180Β°
straight = {
11: lambda i: (200, 100), 13: lambda i: (200, 200), 15: lambda i: (200, 300),
12: lambda i: (260, 100), 14: lambda i: (260, 200), 16: lambda i: (260, 300),
5: lambda i: (200, 40), 6: lambda i: (260, 40),
}
pose = _pose_from(straight, n=5)
flex = relevant_flexion_at(pose, "deep_squat", 2)
assert "left_knee_flexion" in flex
assert flex["left_knee_flexion"]["deg"] > 160
assert "open" in flex["left_knee_flexion"]["openness"]
# ββ laban βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_laban_factors_in_unit_range():
from formscout.analysis.laban import compute_laban
pose = _pose_from({13: lambda i: (100 + i * 8, 200)}, n=20)
res = compute_laban(pose, "deep_squat", fps=30.0)
for k, v in res["effort"].items():
assert 0.0 <= v <= 1.0, f"{k}={v} out of range"
assert set(res["labels"]) == {"space", "weight", "time", "flow"}
def test_laban_straight_line_is_direct():
from formscout.analysis.laban import compute_laban
# Knee travels in a straight horizontal line β high directness (Space)
pose = _pose_from({13: lambda i: (100 + i * 10, 200)}, n=20)
res = compute_laban(pose, "deep_squat", fps=30.0)
assert res["effort"]["space"] > 0.8
def test_laban_static_clip_low_energy():
from formscout.analysis.laban import compute_laban
pose = _pose_from({13: lambda i: (200, 200)}, n=20) # no motion
res = compute_laban(pose, "deep_squat", fps=30.0)
assert res["effort"]["weight"] < 0.2
# ββ charts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_angle_over_time_chart(tmp_path):
from formscout.analysis import charts
out = str(tmp_path / "angle.png")
series = {"left_knee_flexion": [90, 100, 110, 95], "right_knee_flexion": [88, 99, 108, 94]}
p = charts.angle_over_time(series, "left_knee_flexion", 2, out)
assert p == out
import os
assert os.path.getsize(out) > 0
def test_velocity_profile_chart(tmp_path):
from formscout.analysis import charts
out = str(tmp_path / "vel.png")
kps = [{j: {"x": 100 + j + i * 3, "y": 100 + j, "conf": 0.9} for j in range(17)} for i in range(10)]
p = charts.velocity_profile(kps, 30.0, [13, 14, 11, 12], out)
import os
assert p == out and os.path.getsize(out) > 0
def test_laban_radar_chart(tmp_path):
from formscout.analysis import charts
out = str(tmp_path / "radar.png")
p = charts.laban_radar({"space": 0.8, "weight": 0.4, "time": 0.6, "flow": 0.3}, out)
import os
assert p == out and os.path.getsize(out) > 0
def test_flexion_bars_chart(tmp_path):
from formscout.analysis import charts
out = str(tmp_path / "flex.png")
flex = {"left_knee_flexion": {"deg": 95.0, "openness": "flexed"},
"left_hip_flexion": {"deg": 120.0, "openness": "mid-range"}}
p = charts.flexion_bars(flex, out)
import os
assert p == out and os.path.getsize(out) > 0
def test_symmetry_bars_chart(tmp_path):
from formscout.analysis import charts
out = str(tmp_path / "sym.png")
asym = [{"test": "hurdle_step", "left_score": 2, "right_score": 3, "delta": 1}]
p = charts.symmetry_bars(asym, out)
import os
assert p == out and os.path.getsize(out) > 0
def test_charts_return_none_on_empty(tmp_path):
from formscout.analysis import charts
assert charts.angle_over_time({}, None, None, str(tmp_path / "a.png")) is None
assert charts.flexion_bars({}, str(tmp_path / "f.png")) is None
assert charts.symmetry_bars([], str(tmp_path / "s.png")) is None
|