twanghcmut's picture
download
raw
5.92 kB
"""Synthetic, data-free tests for the clip/trajectory/video time-base mapping.
Covers two things found to be buggy on real data (see fpgm.types.ClipTiming's
docstring): the mp4-fps trap (video_frames_per_step) and the annotation-stride
trap (annotation_stride) -- and the fpgm.data.pointworld.verify_annotation_stride
helper that measures the latter rather than assuming it.
"""
from __future__ import annotations
import numpy as np
import pytest
from fpgm.data.pointworld import verify_annotation_stride
from fpgm.types import ClipTiming, DataError
class TestClipTimingAnnotationStride:
def test_dt_is_stride_over_trajectory_fps(self):
timing = ClipTiming(
clip_start_frame=50, clip_end_frame=61, trajectory_fps=15.0, annotation_stride=2
)
assert timing.dt == pytest.approx(2.0 / 15.0)
def test_dt_defaults_to_one_over_fps_at_stride_one(self):
timing = ClipTiming(
clip_start_frame=0, clip_end_frame=11, trajectory_fps=15.0, annotation_stride=1
)
assert timing.dt == pytest.approx(1.0 / 15.0)
def test_clip_frame_to_trajectory_frame_matches_measured_bug(self):
# The exact case from the bug report: clip "50:61", frame t=0 -> row 100.
timing = ClipTiming(
clip_start_frame=50, clip_end_frame=61, trajectory_fps=15.0, annotation_stride=2
)
t = np.arange(11)
rows = timing.clip_frame_to_trajectory_frame(t)
assert np.array_equal(rows, 2 * (50 + t))
assert rows[0] == 100
assert rows[-1] == 120
def test_clip_frame_to_video_frame_composes_both_traps(self):
# video_frames_per_step != 1 (the mp4-fps trap) must stack multiplicatively
# with annotation_stride (the annotation trap), not replace it.
timing = ClipTiming(
clip_start_frame=10,
clip_end_frame=21,
trajectory_fps=15.0,
video_frames_per_step=0.5,
annotation_stride=2,
)
assert timing.clip_frame_to_video_frame(0) == pytest.approx(10 * 2 * 0.5)
assert timing.clip_frame_to_video_frame(3) == pytest.approx(13 * 2 * 0.5)
def test_clip_frame_to_seconds_spacing_is_dt(self):
timing = ClipTiming(
clip_start_frame=0, clip_end_frame=11, trajectory_fps=15.0, annotation_stride=2
)
seconds = timing.clip_frame_to_seconds(np.arange(11))
diffs = np.diff(seconds)
assert np.allclose(diffs, timing.dt)
assert np.allclose(diffs, 2.0 / 15.0)
def test_video_frame_to_clip_frame_roundtrip(self):
timing = ClipTiming(
clip_start_frame=25,
clip_end_frame=36,
trajectory_fps=15.0,
video_frames_per_step=1.0,
annotation_stride=2,
)
t = np.arange(11)
video_frames = timing.clip_frame_to_video_frame(t)
back = timing.video_frame_to_clip_frame(video_frames)
assert np.allclose(back, t)
def test_from_counts_defaults_and_accepts_explicit_stride(self):
timing = ClipTiming.from_counts(
clip_start=0,
clip_end=11,
trajectory_length=128,
video_frame_count=127,
annotation_stride=2,
)
assert timing.annotation_stride == 2
assert timing.video_frames_per_step == pytest.approx(127 / 128)
class TestVerifyAnnotationStride:
"""Uses tiny synthetic joint arrays -- no real dataset needed."""
def _trajectory(self, n_rows: int = 30, n_joints: int = 7) -> np.ndarray:
rng = np.random.default_rng(0)
return rng.normal(size=(n_rows, n_joints)).astype(np.float32)
def test_detects_stride_two(self):
trajectory = self._trajectory(n_rows=30)
clip_start = 5
n = 8
rows = clip_start * 2 + 2 * np.arange(n)
clip_joint_positions = trajectory[rows]
detected = verify_annotation_stride(clip_joint_positions, trajectory, clip_start)
assert detected == 2
def test_detects_stride_one(self):
trajectory = self._trajectory(n_rows=30)
clip_start = 3
n = 6
rows = clip_start * 1 + 1 * np.arange(n)
clip_joint_positions = trajectory[rows]
detected = verify_annotation_stride(clip_joint_positions, trajectory, clip_start)
assert detected == 1
def test_raises_when_nothing_matches(self):
trajectory = self._trajectory(n_rows=30)
clip_start = 2
n = 6
# Independent random data: no candidate stride should line up with this.
rng = np.random.default_rng(99)
bogus_clip = rng.normal(size=(n, 7)).astype(np.float32)
with pytest.raises(DataError):
verify_annotation_stride(bogus_clip, trajectory, clip_start)
def test_raises_when_every_candidate_runs_past_trajectory_end(self):
trajectory = self._trajectory(n_rows=5)
clip_start = 10 # already past the end of a 5-row trajectory at any stride
clip_joint_positions = self._trajectory(n_rows=4)
with pytest.raises(DataError):
verify_annotation_stride(clip_joint_positions, trajectory, clip_start)
def test_raises_on_empty_clip(self):
trajectory = self._trajectory(n_rows=10)
with pytest.raises(DataError):
verify_annotation_stride(np.zeros((0, 7)), trajectory, 0)
def test_prefers_exact_match_over_atol_noise(self):
# A candidate with tiny floating point noise (float32 round-trip) should
# still be picked over one that doesn't match at all.
trajectory = self._trajectory(n_rows=30)
clip_start = 1
n = 5
rows = clip_start * 2 + 2 * np.arange(n)
clip_joint_positions = (trajectory[rows].astype(np.float64) + 1e-7).astype(np.float32)
detected = verify_annotation_stride(clip_joint_positions, trajectory, clip_start)
assert detected == 2

Xet Storage Details

Size:
5.92 kB
·
Xet hash:
ada507bf8207441e36a0d99b17e4b7c6b84fecf39b85ac4cb154e6ed319a195f

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.