diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index 0253feb..c428e07 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -17,6 +17,7 @@ find_package(nanobind CONFIG REQUIRED) set(RPT_PYTHON_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/rpt") file(MAKE_DIRECTORY "${RPT_PYTHON_PACKAGE_DIR}") configure_file("${PROJECT_SOURCE_DIR}/src/rpt/__init__.py" "${RPT_PYTHON_PACKAGE_DIR}/__init__.py" COPYONLY) +configure_file("${PROJECT_SOURCE_DIR}/src/rpt/__init__.pyi" "${RPT_PYTHON_PACKAGE_DIR}/__init__.pyi" COPYONLY) configure_file("${PROJECT_SOURCE_DIR}/src/rpt/_helpers.py" "${RPT_PYTHON_PACKAGE_DIR}/_helpers.py" COPYONLY) configure_file("${PROJECT_SOURCE_DIR}/src/rpt/py.typed" "${RPT_PYTHON_PACKAGE_DIR}/py.typed" COPYONLY) @@ -40,4 +41,5 @@ nanobind_add_stub(rpt_core_stub ) install(TARGETS rpt_core_ext LIBRARY DESTINATION rpt) +install(FILES "${RPT_PYTHON_PACKAGE_DIR}/__init__.pyi" DESTINATION rpt) install(FILES "${RPT_PYTHON_PACKAGE_DIR}/_core.pyi" DESTINATION rpt) diff --git a/src/rpt/__init__.py b/src/rpt/__init__.py index 92c4168..82975bb 100644 --- a/src/rpt/__init__.py +++ b/src/rpt/__init__.py @@ -21,30 +21,83 @@ from ._core import ( PreviousPoseMatch, ProposalGroupDebug, TriangulationTrace, - build_pair_candidates, - filter_pairs_with_previous_poses, - make_camera, - triangulate_debug, - triangulate_poses, - triangulate_with_report, + build_pair_candidates as _build_pair_candidates, + filter_pairs_with_previous_poses as _filter_pairs_with_previous_poses, + make_camera as _make_camera, + triangulate_debug as _triangulate_debug, + triangulate_poses as _triangulate_poses, + triangulate_with_report as _triangulate_with_report, ) if TYPE_CHECKING: import numpy as np import numpy.typing as npt - from ._helpers import CameraLike, PoseViewLike + from ._helpers import CameraLike, CameraModelLike, Matrix3x3Like, PoseViewLike, VectorLike + + PoseArray2D = npt.NDArray[np.float32] + PoseArray3D = npt.NDArray[np.float32] + PersonCountArray = npt.NDArray[np.uint32] + TrackIdArray = npt.NDArray[np.int64] + + +Camera.__doc__ = """Immutable camera calibration with precomputed projection cache fields.""" +TriangulationConfig.__doc__ = """Stable scene configuration used for triangulation.""" +TriangulationOptions.__doc__ = """Score and grouping thresholds used by triangulation.""" +TriangulationResult.__doc__ = """Tracked triangulation output containing poses and association metadata.""" +AssociationReport.__doc__ = """Track-association summary for a tracked triangulation call.""" +TriangulationTrace.__doc__ = """Full debug trace for triangulation, including pair, grouping, and association stages.""" def convert_cameras(cameras: "Sequence[CameraLike]") -> list[Camera]: + """Normalize mapping-like camera inputs into immutable bound `Camera` instances.""" + from ._helpers import convert_cameras as _convert_cameras return _convert_cameras(cameras) +def make_camera( + name: str, + K: "Matrix3x3Like", + DC: "VectorLike", + R: "Matrix3x3Like", + T: "Sequence[Sequence[float]]", + width: int, + height: int, + model: "CameraModel | CameraModelLike", +) -> Camera: + """Create an immutable camera and precompute its cached projection fields.""" + + from ._helpers import _coerce_camera_model, _coerce_distortion + + camera_model = _coerce_camera_model(model) + return _make_camera( + name, + K, + _coerce_distortion(DC, camera_model), + R, + T, + width, + height, + camera_model, + ) + + +def build_pair_candidates( + poses_2d: "PoseArray2D", + person_counts: "PersonCountArray", +) -> list[PairCandidate]: + """Enumerate all cross-view person pairs implied by the padded 2D pose batch.""" + + return _build_pair_candidates(poses_2d, person_counts) + + def pack_poses_2d( views: "Sequence[PoseViewLike]", *, joint_count: int | None = None ) -> "tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]": + """Pack ragged per-view pose detections into the padded tensor expected by the core API.""" + from ._helpers import pack_poses_2d as _pack_poses_2d return _pack_poses_2d(views, joint_count=joint_count) @@ -58,6 +111,8 @@ def make_triangulation_config( min_match_score: float = 0.95, min_group_size: int = 1, ) -> TriangulationConfig: + """Build a triangulation config from cameras, room parameters, and joint names.""" + from ._helpers import make_triangulation_config as _make_triangulation_config return _make_triangulation_config( @@ -69,6 +124,77 @@ def make_triangulation_config( ) +def filter_pairs_with_previous_poses( + poses_2d: "PoseArray2D", + person_counts: "PersonCountArray", + config: TriangulationConfig, + previous_poses_3d: "PoseArray3D", + previous_track_ids: "TrackIdArray", +) -> PreviousPoseFilterDebug: + """Filter raw cross-view pairs against caller-owned previous 3D tracks.""" + + return _filter_pairs_with_previous_poses( + poses_2d, + person_counts, + config, + previous_poses_3d, + previous_track_ids, + ) + + +def triangulate_debug( + poses_2d: "PoseArray2D", + person_counts: "PersonCountArray", + config: TriangulationConfig, + previous_poses_3d: "PoseArray3D | None" = None, + previous_track_ids: "TrackIdArray | None" = None, +) -> TriangulationTrace: + """Run triangulation and return the full debug trace. + + If previous-frame 3D tracks are supplied, `previous_track_ids` must be supplied as an + aligned `int64` array of the same length. + """ + + if previous_poses_3d is None: + return _triangulate_debug(poses_2d, person_counts, config) + if previous_track_ids is None: + raise ValueError("previous_track_ids is required when previous_poses_3d is provided.") + return _triangulate_debug(poses_2d, person_counts, config, previous_poses_3d, previous_track_ids) + + +def triangulate_poses( + poses_2d: "PoseArray2D", + person_counts: "PersonCountArray", + config: TriangulationConfig, +) -> "PoseArray3D": + """Triangulate a frame into anonymous 3D poses without track association.""" + + return _triangulate_poses(poses_2d, person_counts, config) + + +def triangulate_with_report( + poses_2d: "PoseArray2D", + person_counts: "PersonCountArray", + config: TriangulationConfig, + previous_poses_3d: "PoseArray3D", + previous_track_ids: "TrackIdArray", +) -> TriangulationResult: + """Triangulate a frame and return caller-owned track association results. + + The previous-frame poses and IDs are treated as immutable caller state. The returned + association report classifies each final pose as matched, new, or ambiguous, and lists + unmatched previous tracks as deletion candidates. + """ + + return _triangulate_with_report( + poses_2d, + person_counts, + config, + previous_poses_3d, + previous_track_ids, + ) + + __all__ = [ "Camera", "CameraModel", diff --git a/src/rpt/__init__.pyi b/src/rpt/__init__.pyi new file mode 100644 index 0000000..16e2968 --- /dev/null +++ b/src/rpt/__init__.pyi @@ -0,0 +1,115 @@ +from collections.abc import Sequence +from typing import TypeAlias, overload + +import numpy as np +import numpy.typing as npt + +from ._core import ( + AssociationReport, + AssociationStatus, + Camera, + CameraModel, + CoreProposalDebug, + FinalPoseAssociationDebug, + FullProposalDebug, + GroupingDebug, + MergeDebug, + PairCandidate, + PreviousPoseFilterDebug, + PreviousPoseMatch, + ProposalGroupDebug, + TriangulationConfig, + TriangulationOptions, + TriangulationResult, + TriangulationTrace, +) +from ._helpers import CameraLike, CameraModelLike, Matrix3x3Like, PoseViewLike, RoomParamsLike, VectorLike + +PoseArray2D: TypeAlias = npt.NDArray[np.float32] +PoseArray3D: TypeAlias = npt.NDArray[np.float32] +PersonCountArray: TypeAlias = npt.NDArray[np.uint32] +TrackIdArray: TypeAlias = npt.NDArray[np.int64] + + +def convert_cameras(cameras: Sequence[CameraLike]) -> list[Camera]: ... + + +def make_camera( + name: str, + K: Matrix3x3Like, + DC: VectorLike, + R: Matrix3x3Like, + T: Sequence[Sequence[float]], + width: int, + height: int, + model: CameraModel | CameraModelLike, +) -> Camera: ... + + +def build_pair_candidates( + poses_2d: PoseArray2D, + person_counts: PersonCountArray, +) -> list[PairCandidate]: ... + + +def pack_poses_2d( + views: Sequence[PoseViewLike], + *, + joint_count: int | None = None, +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]: ... + + +def make_triangulation_config( + cameras: Sequence[CameraLike], + roomparams: RoomParamsLike, + joint_names: Sequence[str], + *, + min_match_score: float = 0.95, + min_group_size: int = 1, +) -> TriangulationConfig: ... + + +def filter_pairs_with_previous_poses( + poses_2d: PoseArray2D, + person_counts: PersonCountArray, + config: TriangulationConfig, + previous_poses_3d: PoseArray3D, + previous_track_ids: TrackIdArray, +) -> PreviousPoseFilterDebug: ... + + +@overload +def triangulate_debug( + poses_2d: PoseArray2D, + person_counts: PersonCountArray, + config: TriangulationConfig, +) -> TriangulationTrace: ... + + +@overload +def triangulate_debug( + poses_2d: PoseArray2D, + person_counts: PersonCountArray, + config: TriangulationConfig, + previous_poses_3d: PoseArray3D, + previous_track_ids: TrackIdArray, +) -> TriangulationTrace: ... + + +def triangulate_poses( + poses_2d: PoseArray2D, + person_counts: PersonCountArray, + config: TriangulationConfig, +) -> PoseArray3D: ... + + +def triangulate_with_report( + poses_2d: PoseArray2D, + person_counts: PersonCountArray, + config: TriangulationConfig, + previous_poses_3d: PoseArray3D, + previous_track_ids: TrackIdArray, +) -> TriangulationResult: ... + + +__all__: list[str]