Spaces:
Running on Zero
Running on Zero
| """Tests for PoseVisualizer — no GPU, no model downloads.""" | |
| import numpy as np | |
| import pytest | |
| from formscout.types import IngestResult, Pose2DResult | |
| def _make_ingest(n=5, h=480, w=640, fps=30.0): | |
| frames = [np.zeros((h, w, 3), dtype=np.uint8) for _ in range(n)] | |
| return IngestResult(frames=frames, fps=fps, duration=n / fps, n_people=1, width=w, height=h) | |
| def _make_pose(n=5, w=640, h=480): | |
| """Synthetic Pose2DResult: 17 joints at fixed pixel positions, conf=0.9.""" | |
| kps_per_frame = [] | |
| for i in range(n): | |
| frame_kps = {} | |
| for j in range(17): | |
| frame_kps[j] = { | |
| "x": float(50 + j * 30 + i * 2), | |
| "y": float(100 + j * 20), | |
| "conf": 0.9, | |
| } | |
| kps_per_frame.append(frame_kps) | |
| return Pose2DResult(keypoints=kps_per_frame, fps=30.0, confidence=0.9, notes="") | |
| class TestComputeJointVelocity: | |
| def test_returns_17_joints(self): | |
| from formscout.agents.visualizer import compute_joint_velocity | |
| pose = _make_pose(n=5) | |
| result = compute_joint_velocity(pose.keypoints, fps=30.0) | |
| assert len(result) == 17 | |
| def test_each_list_has_n_frames(self): | |
| from formscout.agents.visualizer import compute_joint_velocity | |
| pose = _make_pose(n=5) | |
| result = compute_joint_velocity(pose.keypoints, fps=30.0) | |
| for joint_idx, speeds in result.items(): | |
| assert len(speeds) == 5, f"joint {joint_idx} has {len(speeds)} speeds, expected 5" | |
| def test_speeds_are_non_negative(self): | |
| from formscout.agents.visualizer import compute_joint_velocity | |
| pose = _make_pose(n=5) | |
| result = compute_joint_velocity(pose.keypoints, fps=30.0) | |
| for speeds in result.values(): | |
| assert all(s >= 0.0 for s in speeds) | |
| def test_missing_keypoints_give_zero_speed(self): | |
| from formscout.agents.visualizer import compute_joint_velocity | |
| empty_kps = [{} for _ in range(5)] | |
| result = compute_joint_velocity(empty_kps, fps=30.0) | |
| for speeds in result.values(): | |
| assert all(s == 0.0 for s in speeds) | |
| class TestDrawSkeleton: | |
| def test_skeleton_draws_without_error(self): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |
| kps = {j: {"x": float(50 + j * 30), "y": float(100 + j * 20), "conf": 0.9} | |
| for j in range(17)} | |
| result = vis._draw_skeleton(frame.copy(), kps) | |
| assert result.shape == frame.shape | |
| assert not np.array_equal(result, frame) | |
| def test_low_confidence_keypoints_not_drawn(self): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |
| kps = {j: {"x": float(50 + j * 30), "y": 100.0, "conf": 0.1} for j in range(17)} | |
| result = vis._draw_skeleton(frame.copy(), kps) | |
| assert np.array_equal(result, frame) | |
| class TestDrawTrails: | |
| def test_trails_draw_without_error(self): | |
| from formscout.agents.visualizer import PoseVisualizer, TRAIL_LENGTH | |
| from collections import deque | |
| vis = PoseVisualizer() | |
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |
| trail_history = { | |
| 0: deque([(100 + i * 5, 200 + i * 3) for i in range(5)], maxlen=TRAIL_LENGTH) | |
| } | |
| result = vis._draw_trails(frame.copy(), trail_history) | |
| assert result.shape == frame.shape | |
| assert not np.array_equal(result, frame) | |
| def test_short_trail_no_crash(self): | |
| from formscout.agents.visualizer import PoseVisualizer, TRAIL_LENGTH | |
| from collections import deque | |
| vis = PoseVisualizer() | |
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |
| trail_history = {0: deque([(100, 200)], maxlen=TRAIL_LENGTH)} | |
| result = vis._draw_trails(frame.copy(), trail_history) | |
| assert np.array_equal(result, frame) | |
| class TestDrawVelocityArrows: | |
| def test_arrows_draw_without_error(self): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |
| kps = {j: {"x": float(50 + j * 30), "y": float(100 + j * 20), "conf": 0.9} | |
| for j in range(17)} | |
| prev_kps = {j: {"x": float(48 + j * 30), "y": float(98 + j * 20), "conf": 0.9} | |
| for j in range(17)} | |
| velocities = {j: [0.0] * 5 for j in range(17)} | |
| velocities[5] = [0.0, 10.0, 50.0, 80.0, 120.0] | |
| result = vis._draw_velocity_arrows(frame.copy(), kps, prev_kps, velocities, frame_idx=4) | |
| assert result.shape == frame.shape | |
| def test_no_prev_kps_no_crash(self): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| frame = np.zeros((480, 640, 3), dtype=np.uint8) | |
| kps = {j: {"x": float(50 + j * 30), "y": 100.0, "conf": 0.9} for j in range(17)} | |
| velocities = {j: [50.0] * 5 for j in range(17)} | |
| result = vis._draw_velocity_arrows(frame.copy(), kps, None, velocities, frame_idx=0) | |
| assert result.shape == frame.shape | |
| class TestRenderVideo: | |
| def test_creates_mp4_file(self, tmp_path): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| ingest = _make_ingest(n=5) | |
| pose = _make_pose(n=5) | |
| out = str(tmp_path / "out.mp4") | |
| result = vis.render_video(ingest, pose, {"skeleton"}, out) | |
| assert result is not None | |
| import os | |
| assert os.path.exists(result) | |
| assert os.path.getsize(result) > 0 | |
| def test_empty_layers_returns_none(self, tmp_path): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| out = str(tmp_path / "out.mp4") | |
| result = vis.render_video(_make_ingest(), _make_pose(), set(), out) | |
| assert result is None | |
| def test_no_detections_returns_none(self, tmp_path): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| ingest = _make_ingest(n=5) | |
| empty_pose = Pose2DResult( | |
| keypoints=[{} for _ in range(5)], fps=30.0, confidence=0.0, notes="" | |
| ) | |
| out = str(tmp_path / "out.mp4") | |
| result = vis.render_video(ingest, empty_pose, {"skeleton"}, out) | |
| assert result is None | |
| def test_last_velocities_set_after_render(self, tmp_path): | |
| from formscout.agents.visualizer import PoseVisualizer | |
| vis = PoseVisualizer() | |
| out = str(tmp_path / "out.mp4") | |
| vis.render_video(_make_ingest(n=5), _make_pose(n=5), {"skeleton"}, out) | |
| assert len(vis.last_velocities) == 17 | |
| class TestBuildVelocitySummary: | |
| def test_returns_markdown_table(self): | |
| from formscout.agents.visualizer import build_velocity_summary, compute_joint_velocity | |
| pose = _make_pose(n=10) | |
| vels = compute_joint_velocity(pose.keypoints, fps=30.0) | |
| result = build_velocity_summary(pose.keypoints, vels) | |
| assert "|" in result | |
| assert any(name in result for name in ["knee", "shoulder", "hip", "ankle"]) | |
| def test_empty_keypoints_returns_empty_string(self): | |
| from formscout.agents.visualizer import build_velocity_summary | |
| empty_kps = [{} for _ in range(5)] | |
| vels = {j: [0.0] * 5 for j in range(17)} | |
| result = build_velocity_summary(empty_kps, vels) | |
| assert result == "" | |