Files
RapidPoseTriangulation/tests/test_interface.py
T

282 lines
9.9 KiB
Python

import json
from pathlib import Path
import numpy as np
import pytest
import rpt
ROOT = Path(__file__).resolve().parents[1]
JOINT_NAMES = [
"nose",
"eye_left",
"eye_right",
"ear_left",
"ear_right",
"shoulder_left",
"shoulder_right",
"elbow_left",
"elbow_right",
"wrist_left",
"wrist_right",
"hip_left",
"hip_right",
"knee_left",
"knee_right",
"ankle_left",
"ankle_right",
"hip_middle",
"shoulder_middle",
"head",
]
def load_case(camera_path: str, pose_path: str):
with (ROOT / camera_path).open("r", encoding="utf-8") as file:
camera_data = json.load(file)
with (ROOT / pose_path).open("r", encoding="utf-8") as file:
pose_data = json.load(file)
poses_2d, person_counts = rpt.pack_poses_2d(pose_data["2D"], joint_count=len(JOINT_NAMES))
return poses_2d, person_counts, camera_data["cameras"]
def make_config(cameras, roomparams) -> rpt.TriangulationConfig:
return rpt.make_triangulation_config(
cameras,
np.asarray(roomparams, dtype=np.float32),
JOINT_NAMES,
)
def test_camera_structure_repr():
camera = rpt.make_camera(
"Camera 1",
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[0, 0, 0, 0, 0],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[1], [2], [3]],
640,
480,
rpt.CameraModel.PINHOLE,
)
rendered = repr(camera)
assert "Camera 1" in rendered
assert "pinhole" in rendered
@pytest.mark.parametrize(
("camera_path", "pose_path", "roomparams"),
[
("data/h1/sample.json", "tests/poses_h1.json", [[4.8, 6.0, 2.0], [0.0, 0.0, 1.0]]),
("data/p1/sample.json", "tests/poses_p1.json", [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]]),
("data/e1/sample.json", "tests/poses_e1.json", [[6.0, 5.0, 2.0], [1.5, 1.0, -0.5]]),
],
)
def test_triangulate_samples(camera_path: str, pose_path: str, roomparams):
poses_2d, person_counts, cameras = load_case(camera_path, pose_path)
config = make_config(cameras, roomparams)
poses_3d = rpt.triangulate_poses(poses_2d, person_counts, config)
assert isinstance(poses_3d, np.ndarray)
assert poses_3d.dtype == np.float32
assert poses_3d.ndim == 3
assert poses_3d.shape[1:] == (len(JOINT_NAMES), 4)
assert poses_3d.shape[0] > 0
assert np.isfinite(poses_3d).all()
def test_triangulate_repeatability():
poses_2d, person_counts, cameras = load_case("data/p1/sample.json", "tests/poses_p1.json")
config = make_config(cameras, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
first = rpt.triangulate_poses(poses_2d, person_counts, config)
second = rpt.triangulate_poses(poses_2d, person_counts, config)
np.testing.assert_allclose(first, second, rtol=1e-5, atol=1e-5)
def test_build_pair_candidates_exposes_cartesian_view_pairs():
poses_2d, person_counts = rpt.pack_poses_2d(
[
np.zeros((2, 2, 3), dtype=np.float32),
np.zeros((1, 2, 3), dtype=np.float32),
np.zeros((3, 2, 3), dtype=np.float32),
],
joint_count=2,
)
pairs = rpt.build_pair_candidates(poses_2d, person_counts)
assert len(pairs) == (2 * 1) + (2 * 3) + (1 * 3)
assert (pairs[0].view1, pairs[0].view2, pairs[0].person1, pairs[0].person2) == (0, 1, 0, 0)
assert pairs[-1].global_person2 == 5
def test_triangulate_accepts_empty_previous_poses():
poses_2d, person_counts, cameras = load_case("data/p1/sample.json", "tests/poses_p1.json")
config = make_config(cameras, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
empty_previous = np.zeros((0, len(JOINT_NAMES), 4), dtype=np.float32)
empty_previous_ids = np.zeros((0,), dtype=np.int64)
baseline = rpt.triangulate_poses(poses_2d, person_counts, config)
result = rpt.triangulate_with_report(
poses_2d,
person_counts,
config,
empty_previous,
empty_previous_ids,
)
np.testing.assert_allclose(result.poses_3d, baseline, rtol=1e-5, atol=1e-5)
assert result.association.unmatched_previous_track_ids == []
def test_triangulate_debug_matches_final_output():
poses_2d, person_counts, cameras = load_case("data/h1/sample.json", "tests/poses_h1.json")
config = make_config(cameras, [[4.8, 6.0, 2.0], [0.0, 0.0, 1.0]])
final_poses = rpt.triangulate_poses(poses_2d, person_counts, config)
trace = rpt.triangulate_debug(poses_2d, person_counts, config)
np.testing.assert_allclose(trace.final_poses, final_poses, rtol=1e-5, atol=1e-5)
assert len(trace.pairs) >= len(trace.core_proposals)
assert trace.association.pose_previous_track_ids == []
for group in trace.grouping.groups:
assert all(0 <= index < len(trace.core_proposals) for index in group.proposal_indices)
for merge_indices in trace.merge.group_proposal_indices:
assert all(0 <= index < len(trace.core_proposals) for index in merge_indices)
def test_filter_pairs_with_previous_poses_returns_debug_matches():
poses_2d, person_counts, cameras = load_case("data/p1/sample.json", "tests/poses_p1.json")
config = make_config(cameras, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
previous_poses = rpt.triangulate_poses(poses_2d, person_counts, config)
previous_track_ids = np.arange(previous_poses.shape[0], dtype=np.int64) + 100
debug = rpt.filter_pairs_with_previous_poses(
poses_2d,
person_counts,
config,
previous_poses,
previous_track_ids,
)
assert debug.used_previous_poses is True
assert len(debug.matches) == len(rpt.build_pair_candidates(poses_2d, person_counts))
assert len(debug.kept_pairs) == len(debug.kept_pair_indices)
assert any(match.matched_view1 or match.matched_view2 for match in debug.matches)
assert any(match.previous_track_id >= 100 for match in debug.matches if match.previous_pose_index >= 0)
def test_triangulate_with_report_resolves_previous_track_ids():
poses_2d, person_counts, cameras = load_case("data/p1/sample.json", "tests/poses_p1.json")
config = make_config(cameras, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
previous_poses = rpt.triangulate_poses(poses_2d, person_counts, config)
previous_track_ids = np.arange(previous_poses.shape[0], dtype=np.int64) + 100
result = rpt.triangulate_with_report(
poses_2d,
person_counts,
config,
previous_poses,
previous_track_ids,
)
assert result.poses_3d.shape == previous_poses.shape
assert len(result.association.pose_previous_track_ids) == result.poses_3d.shape[0]
matched_track_ids = sorted(
track_id for track_id in result.association.pose_previous_track_ids if track_id >= 0
)
unmatched_track_ids = sorted(result.association.unmatched_previous_track_ids)
for pose_index in result.association.new_pose_indices:
assert result.association.pose_previous_track_ids[pose_index] == -1
for pose_index in result.association.ambiguous_pose_indices:
assert result.association.pose_previous_track_ids[pose_index] == -1
assert matched_track_ids == sorted(set(matched_track_ids))
assert sorted(matched_track_ids + unmatched_track_ids) == list(previous_track_ids)
def test_triangulate_with_report_rejects_duplicate_previous_track_ids():
poses_2d, person_counts, cameras = load_case("data/p1/sample.json", "tests/poses_p1.json")
config = make_config(cameras, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
previous_poses = rpt.triangulate_poses(poses_2d, person_counts, config)
duplicate_ids = np.zeros((previous_poses.shape[0],), dtype=np.int64)
with pytest.raises(ValueError, match="unique"):
rpt.triangulate_with_report(
poses_2d,
person_counts,
config,
previous_poses,
duplicate_ids,
)
def test_triangulate_does_not_mutate_inputs():
poses_2d, person_counts, cameras = load_case("data/h1/sample.json", "tests/poses_h1.json")
config = make_config(cameras, [[4.8, 6.0, 2.0], [0.0, 0.0, 1.0]])
poses_before = poses_2d.copy()
counts_before = person_counts.copy()
rpt.triangulate_poses(poses_2d, person_counts, config)
np.testing.assert_array_equal(poses_2d, poses_before)
np.testing.assert_array_equal(person_counts, counts_before)
def test_pack_poses_2d_from_ragged_inputs():
packed, counts = rpt.pack_poses_2d(
[
[[[1, 2, 0.5], [3, 4, 0.6]]],
np.asarray(
[
[[5, 6, 0.7], [7, 8, 0.8]],
[[9, 10, 0.9], [11, 12, 1.0]],
],
dtype=np.float32,
),
[],
],
joint_count=2,
)
assert packed.shape == (3, 2, 2, 3)
assert packed.dtype == np.float32
np.testing.assert_array_equal(counts, np.asarray([1, 2, 0], dtype=np.uint32))
np.testing.assert_allclose(packed[0, 0], np.asarray([[1, 2, 0.5], [3, 4, 0.6]], dtype=np.float32))
np.testing.assert_allclose(packed[1, 1], np.asarray([[9, 10, 0.9], [11, 12, 1.0]], dtype=np.float32))
np.testing.assert_array_equal(packed[2], np.zeros((2, 2, 3), dtype=np.float32))
def test_pack_poses_2d_rejects_inconsistent_joint_count():
with pytest.raises(ValueError, match="joint count"):
rpt.pack_poses_2d(
[
[[[1, 2, 0.5], [3, 4, 0.6]]],
[[[5, 6, 0.7]]],
]
)
def test_pack_poses_2d_rejects_invalid_last_dimension():
with pytest.raises(ValueError, match="shape"):
rpt.pack_poses_2d([np.zeros((1, 2, 2), dtype=np.float32)])
def test_make_triangulation_config_builds_bound_struct():
_, _, cameras = load_case("data/p1/sample.json", "tests/poses_p1.json")
config = make_config(cameras, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
assert isinstance(config, rpt.TriangulationConfig)
assert len(config.cameras) > 0
np.testing.assert_allclose(config.roomparams, [[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]])
assert config.joint_names == JOINT_NAMES
assert config.options.min_match_score == pytest.approx(0.95)
assert config.options.min_group_size == 1