2c0d51ab31
Refactor the package into common, schema, detection, and tracking namespaces and move dataset-specific ActualTest utilities into tests/support. Add a pluggable detection stack with typed protocols, pydantic-settings config, loguru-based runner logging, cvmmap and headless video sources, NATS and parquet sinks, and a structured coco-wholebody133 payload path. Teach tracking replay loading to consume parquet detection directories directly, preserve empty frames, and keep the video-to-parquet-to-tracking workflow usable for offline E2E runs. Vendor the local mmcv and xtcocotools wheels under Git LFS, update uv sources/lock state, and refresh the mmcv build so mmcv.ops loads successfully with the current torch+cu130 environment.
157 lines
5.2 KiB
Python
157 lines
5.2 KiB
Python
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
pytest.importorskip("rpt")
|
|
|
|
from pose_tracking_exp.common.joints import BODY20_INDEX_BY_NAME
|
|
from pose_tracking_exp.schema import CameraCalibration, CameraFrame, FrameBundle, ProposalCluster, SceneConfig, TrackerConfig
|
|
from pose_tracking_exp.tracking import PoseTracker
|
|
|
|
|
|
def _make_scene() -> SceneConfig:
|
|
cameras = (
|
|
CameraCalibration(
|
|
name="cam0",
|
|
width=640,
|
|
height=480,
|
|
K=np.asarray([[500.0, 0.0, 320.0], [0.0, 500.0, 240.0], [0.0, 0.0, 1.0]], dtype=np.float64),
|
|
DC=np.zeros(5, dtype=np.float64),
|
|
R=np.eye(3, dtype=np.float64),
|
|
T=np.zeros(3, dtype=np.float64),
|
|
),
|
|
CameraCalibration(
|
|
name="cam1",
|
|
width=640,
|
|
height=480,
|
|
K=np.asarray([[500.0, 0.0, 320.0], [0.0, 500.0, 240.0], [0.0, 0.0, 1.0]], dtype=np.float64),
|
|
DC=np.zeros(5, dtype=np.float64),
|
|
R=np.eye(3, dtype=np.float64),
|
|
T=np.asarray([1.0, 0.0, 0.0], dtype=np.float64),
|
|
),
|
|
)
|
|
return SceneConfig(
|
|
room_size=np.asarray([6.0, 4.0, 3.0], dtype=np.float64),
|
|
room_center=np.asarray([0.0, 0.0, 1.0], dtype=np.float64),
|
|
cameras=cameras,
|
|
)
|
|
|
|
|
|
def _make_bundle(bundle_index: int) -> FrameBundle:
|
|
views = tuple(
|
|
CameraFrame(
|
|
camera_name=camera_name,
|
|
frame_index=bundle_index,
|
|
timestamp_unix_ns=1_000_000_000 + bundle_index * 33_000_000,
|
|
detections=(),
|
|
source_size=(640, 480),
|
|
)
|
|
for camera_name in ("cam0", "cam1")
|
|
)
|
|
return FrameBundle(
|
|
bundle_index=bundle_index,
|
|
timestamp_unix_ns=views[0].timestamp_unix_ns,
|
|
views=views,
|
|
)
|
|
|
|
|
|
def _make_proposal(root_x: float, *, score: float = 1.0) -> ProposalCluster:
|
|
pose = np.zeros((20, 4), dtype=np.float64)
|
|
joint_positions = {
|
|
"hip_middle": [root_x, 1.0, 3.0],
|
|
"hip_left": [root_x + 0.12, 1.0, 3.0],
|
|
"hip_right": [root_x - 0.12, 1.0, 3.0],
|
|
"shoulder_middle": [root_x, 1.52, 3.0],
|
|
"shoulder_left": [root_x + 0.18, 1.52, 3.0],
|
|
"shoulder_right": [root_x - 0.18, 1.52, 3.0],
|
|
"elbow_left": [root_x + 0.42, 1.48, 3.02],
|
|
"elbow_right": [root_x - 0.42, 1.48, 3.02],
|
|
"wrist_left": [root_x + 0.64, 1.45, 3.04],
|
|
"wrist_right": [root_x - 0.64, 1.45, 3.04],
|
|
"knee_left": [root_x + 0.1, 0.58, 3.0],
|
|
"knee_right": [root_x - 0.1, 0.58, 3.0],
|
|
"ankle_left": [root_x + 0.1, 0.15, 3.02],
|
|
"ankle_right": [root_x - 0.1, 0.15, 3.02],
|
|
"head": [root_x, 1.82, 3.02],
|
|
"nose": [root_x, 1.8, 3.06],
|
|
"eye_left": [root_x + 0.03, 1.81, 3.05],
|
|
"eye_right": [root_x - 0.03, 1.81, 3.05],
|
|
"ear_left": [root_x + 0.06, 1.81, 3.02],
|
|
"ear_right": [root_x - 0.06, 1.81, 3.02],
|
|
}
|
|
for name, position in joint_positions.items():
|
|
pose[BODY20_INDEX_BY_NAME[name], :3] = position
|
|
pose[BODY20_INDEX_BY_NAME[name], 3] = score
|
|
return ProposalCluster(
|
|
pose3d=pose,
|
|
root=np.asarray([root_x, 1.0, 3.0], dtype=np.float64),
|
|
source_views=frozenset({"cam0", "cam1"}),
|
|
support_size=2,
|
|
mean_score=score,
|
|
)
|
|
|
|
|
|
def test_single_person_mode_caps_active_tracks(monkeypatch) -> None:
|
|
tracker = PoseTracker(
|
|
_make_scene(),
|
|
TrackerConfig(
|
|
max_active_tracks=1,
|
|
tentative_min_age=1,
|
|
tentative_hits_required=1,
|
|
tentative_promote_score=0.0,
|
|
active_miss_to_lost=5,
|
|
proposal_min_score=0.5,
|
|
),
|
|
)
|
|
proposals_by_bundle = {
|
|
0: (_make_proposal(0.0, score=0.95), _make_proposal(0.9, score=0.7)),
|
|
1: (_make_proposal(0.05, score=0.96), _make_proposal(0.85, score=0.75)),
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
tracker,
|
|
"_build_proposals",
|
|
lambda bundle, unmatched: proposals_by_bundle[bundle.bundle_index],
|
|
)
|
|
|
|
results = tracker.run([_make_bundle(0), _make_bundle(1)])
|
|
|
|
assert len(results[0].active_tracks) == 1
|
|
assert len(results[1].active_tracks) == 1
|
|
assert not results[1].tentative_tracks
|
|
assert [track.track_id for track in results[1].active_tracks] == [1]
|
|
|
|
|
|
def test_single_person_mode_reuses_lost_track_id(monkeypatch) -> None:
|
|
tracker = PoseTracker(
|
|
_make_scene(),
|
|
TrackerConfig(
|
|
max_active_tracks=1,
|
|
tentative_min_age=1,
|
|
tentative_hits_required=1,
|
|
tentative_promote_score=0.0,
|
|
active_miss_to_lost=1,
|
|
lost_delete_age=10,
|
|
proposal_min_score=0.5,
|
|
),
|
|
)
|
|
proposals_by_bundle = {
|
|
0: (_make_proposal(0.0, score=0.95),),
|
|
1: (),
|
|
2: (_make_proposal(0.05, score=0.96),),
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
tracker,
|
|
"_build_proposals",
|
|
lambda bundle, unmatched: proposals_by_bundle[bundle.bundle_index],
|
|
)
|
|
|
|
results = tracker.run([_make_bundle(0), _make_bundle(1), _make_bundle(2)])
|
|
|
|
assert [track.track_id for track in results[0].active_tracks] == [1]
|
|
assert [track.track_id for track in results[1].lost_tracks] == [1]
|
|
assert [track.track_id for track in results[2].active_tracks] == [1]
|
|
assert tracker.diagnostics_snapshot().reacquisitions >= 1
|