File size: 7,470 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""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 == ""