"""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 == ""