| """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) |
|
|
|
|
| |
|
|
| 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") |
|
|
|
|
| |
|
|
| 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 = { |
| 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"] |
|
|
|
|
| |
|
|
| 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 |
| |
| 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) |
| res = compute_laban(pose, "deep_squat", fps=30.0) |
| assert res["effort"]["weight"] < 0.2 |
|
|
|
|
| |
|
|
| 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 |
|
|