twanghcmut's picture
download
raw
3.79 kB
"""Synthetic, data-free tests for fpgm.geometry.velocity."""
from __future__ import annotations
import numpy as np
from fpgm.config import VelocityConfig
from fpgm.geometry.velocity import VelocityEstimator
from fpgm.types import Track3D
def _linear_track(
velocities: np.ndarray, p0: np.ndarray, n_frames: int, dt: float = 0.1
) -> Track3D:
"""Build a Track3D where point q moves at constant ``velocities[q]`` from ``p0[q]``."""
q = velocities.shape[0]
t = np.arange(n_frames) * dt
xyz = p0[None, :, :] + velocities[None, :, :] * t[:, None, None]
return Track3D(
point_id=np.arange(q, dtype=np.int32),
timestamps=t,
xyz_world=xyz,
valid=np.ones((n_frames, q), dtype=bool),
confidence=np.ones((n_frames, q), dtype=np.float32),
)
class TestConstantVelocity:
def test_recovers_exact_velocity(self):
v_true = np.array([[1.0, -0.5, 2.0]])
p0 = np.array([[0.1, 0.2, 0.3]])
track = _linear_track(v_true, p0, n_frames=20)
estimate = VelocityEstimator().estimate(track, VelocityConfig())
assert np.allclose(estimate.linear_velocity[:, 0, :], v_true[0], atol=1e-6)
assert np.allclose(estimate.object_linear_velocity, v_true[0], atol=1e-6)
assert np.allclose(estimate.object_speed, np.linalg.norm(v_true[0]), atol=1e-6)
assert estimate.frame == "world"
class TestGapHandling:
def test_short_gap_is_bridged(self):
v_true = np.array([[0.5, 0.0, -0.2]])
p0 = np.array([[0.0, 0.0, 1.0]])
n_frames = 15
track = _linear_track(v_true, p0, n_frames=n_frames)
# A 2-frame gap, within max_gap_frames=3 (the default): should be bridged.
track.valid[6:8, 0] = False
track.xyz_world[6:8, 0, :] = np.nan
cfg = VelocityConfig()
estimate = VelocityEstimator().estimate(track, cfg)
assert not np.any(np.isnan(estimate.linear_velocity[:, 0, :]))
assert np.allclose(estimate.linear_velocity[:, 0, :], v_true[0], atol=1e-4)
def test_long_gap_yields_nan_across_gap_and_correct_velocity_on_both_sides(self):
v_true = np.array([[0.5, 0.0, -0.2]])
p0 = np.array([[0.0, 0.0, 1.0]])
n_frames = 15
track = _linear_track(v_true, p0, n_frames=n_frames)
cfg = VelocityConfig() # max_gap_frames=3
# A 5-frame gap: longer than max_gap_frames, must NOT be bridged.
gap = slice(5, 10)
track.valid[gap, 0] = False
track.xyz_world[gap, 0, :] = np.nan
estimate = VelocityEstimator().estimate(track, cfg)
assert np.all(np.isnan(estimate.linear_velocity[gap, 0, :]))
assert np.allclose(estimate.linear_velocity[0:5, 0, :], v_true[0], atol=1e-4)
assert np.allclose(estimate.linear_velocity[10:15, 0, :], v_true[0], atol=1e-4)
class TestRobustAggregation:
def test_single_outlier_does_not_move_median_object_velocity(self):
v0 = np.array([1.0, -0.5, 0.3])
# Four points move at v0 plus tiny, distinct jitter; one point is a gross
# outlier (velocity offset by +10 m/s in every axis).
velocities = np.array(
[
v0 + [0.01, 0.0, 0.0],
v0 + [-0.01, 0.0, 0.0],
v0 + [0.0, 0.01, 0.0],
v0 + [0.0, -0.01, 0.0],
v0 + [10.0, 10.0, 10.0],
]
)
outlier_idx = 4
p0 = np.zeros((5, 3))
track = _linear_track(velocities, p0, n_frames=20)
estimate = VelocityEstimator().estimate(track, VelocityConfig())
assert np.allclose(estimate.object_linear_velocity, v0, atol=0.02)
mid = 10
assert not estimate.inlier_mask[mid, outlier_idx]
assert np.all(estimate.inlier_mask[mid, :outlier_idx])

Xet Storage Details

Size:
3.79 kB
·
Xet hash:
50dd68d7ec3b1752125e66e09146c82d6cd045114125408a2845ea0c0c14088d

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