4 Commits

Author SHA1 Message Date
crosstyan 69e83d8430 Clean up nanobind typing and source layout 2026-03-27 11:19:53 +08:00
crosstyan 9d63177de0 docs(readme): fix upstream repository links
Replace raw upstream repository URLs with proper Markdown links and keep the README focused on repository references instead of paper links or figures.
2026-03-26 13:19:32 +08:00
crosstyan 502a90761b docs(readme): rewrite repo status and fork context
Rewrite the top-level README to describe the current Python-first package, the RGB-D pipeline ported from SimpleDepthPose, and the main differences from upstream RapidPoseTriangulation.
2026-03-26 13:14:06 +08:00
crosstyan ed721729fd feat(rgbd): add RGB-D reconstruction pipeline
Add end-to-end RGB-D reconstruction support across the C++ core and Python API.

- add a native merge_rgbd_views path, view-aware 3D pose containers, and nanobind bindings

- expose Python helpers to sample aligned depth, apply per-joint offsets, lift UVD poses to world space, and run reconstruct_rgbd

- add RGB-D regression tests for merging, manual pipeline parity, symmetric depth sampling windows, and out-of-bounds joints

- bump the project version from 0.1.0 to 0.2.0 for the new feature surface
2026-03-26 13:04:57 +08:00
21 changed files with 2867 additions and 106 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.18) cmake_minimum_required(VERSION 3.18)
project(RapidPoseTriangulation project(RapidPoseTriangulation
VERSION 0.1.0 VERSION 0.2.0
LANGUAGES CXX LANGUAGES CXX
DESCRIPTION "Rapid Pose Triangulation library with Python bindings" DESCRIPTION "Rapid Pose Triangulation library with Python bindings"
) )
@@ -13,5 +13,5 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Add subdirectories # Add subdirectories
add_subdirectory(rpt) add_subdirectory(rpt_cpp)
add_subdirectory(bindings) add_subdirectory(bindings)
+166 -57
View File
@@ -1,76 +1,185 @@
# RapidPoseTriangulation # RapidPoseTriangulation
Fast triangulation of multiple persons from multiple camera views. \ Fast multi-view multi-person pose reconstruction, packaged as a Python-first C++ library.
A general overview can be found in the paper [RapidPoseTriangulation: Multi-view Multi-person Whole-body Human Pose Triangulation in a Millisecond](https://arxiv.org/pdf/2503.21692).
<div align="center"> This repository started from the original upstream RapidPoseTriangulation repository:
<img src="media/2d-k.jpg" alt="2D detections"" width="65%"/>
<b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</b>
<img src="media/3d-p.jpg" alt="3D detections" width="30%"/>
<br>
<br>
<img src="media/2d-p.jpg" alt="3D to 2D projection" width="95%"/>
</div>
<br> - [Percipiote/RapidPoseTriangulation](https://gitlab.com/Percipiote/RapidPoseTriangulation)
## Build The current fork keeps the triangulation core, exposes it through `nanobind`, and adds an RGB-D reconstruction path ported from the original SimpleDepthPose repository:
- Clone this project: - [Percipiote/SimpleDepthPose](https://gitlab.com/Percipiote/SimpleDepthPose)
```bash ## What This Repository Is Now
git clone https://gitlab.com/Percipiote/RapidPoseTriangulation.git
cd RapidPoseTriangulation/
```
- Enable GPU-access for docker building: - A packaged library named `rapid-pose-triangulation` with Python bindings under `rpt`
- A C++ core built with `scikit-build-core` and `nanobind`
- A triangulation library for calibrated multi-view 2D detections
- An RGB-D reconstruction helper layer that samples aligned depth, applies joint offsets, lifts poses into world coordinates, and merges per-view proposals
- Install _nvidia_ container tools: [Link](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) Current package status:
- Run `sudo nano /etc/docker/daemon.json` and add: - Python `>=3.10`
- Runtime dependencies: NumPy, jaxtyping
- Current version: `0.2.0`
```json ## Current Capabilities
{
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
},
"default-runtime": "nvidia"
}
```
- Restart docker: `sudo systemctl restart docker` The public Python API exposed by `rpt` currently includes:
- Build docker container: - Camera/config helpers: `make_camera`, `convert_cameras`, `make_triangulation_config`
- Input preparation: `pack_poses_2d`
- Triangulation: `triangulate_poses`, `triangulate_debug`, `triangulate_with_report`
- Tracking/debug helpers: `filter_pairs_with_previous_poses`
- RGB-D helpers: `sample_depth_for_poses`, `apply_depth_offsets`, `lift_depth_poses_to_world`, `merge_rgbd_views`, `reconstruct_rgbd`
```bash At a high level there are now two supported reconstruction paths:
docker build --progress=plain -t rapidposetriangulation .
./run_container.sh
```
- Build triangulator: 1. Multi-view RGB triangulation from calibrated 2D detections
2. Multi-view RGB-D reconstruction from calibrated 2D detections plus aligned depth images
```bash ## Installation And Development Workflow
cd /RapidPoseTriangulation/
uv sync --group dev
uv run pytest tests/test_interface.py
uv build
```
<br> Clone the repo and use `uv` for local development:
## Citation ```bash
git clone https://git.weihua-iot.cn/crosstyan/RapidPoseTriangulation.git
Please cite [RapidPoseTriangulation](https://arxiv.org/pdf/2503.21692) if you found it helpful for your research or business. cd RapidPoseTriangulation
uv sync --group dev
```bibtex
@article{
rapidtriang,
title={{RapidPoseTriangulation: Multi-view Multi-person Whole-body Human Pose Triangulation in a Millisecond}},
author={Bermuth, Daniel and Poeppel, Alexander and Reif, Wolfgang},
journal={arXiv preprint arXiv:2503.21692},
year={2025}
}
``` ```
Run the test suite:
```bash
uv run pytest -q
```
Run static typing checks against the Python package:
```bash
uv run basedpyright
```
Build source and wheel artifacts:
```bash
uv build
```
`run_container.sh` is still present in the repo, but it is a leftover helper script rather than the primary or best-supported development workflow.
## Typing Workflow
The Python package ships a typed facade in `src/rpt` plus a checked-in stub for the compiled nanobind module at `src/rpt/_core.pyi`.
Refresh the extension stub after changing the bindings:
```bash
cmake --build build --target rpt_core_stub
cp build/bindings/rpt/_core.pyi src/rpt/_core.pyi
uv run basedpyright
```
`tests/test_typing_artifacts.py` checks that the checked-in `_core.pyi` matches the generated nanobind stub whenever the build artifact is available.
## Python API Overview
Typical triangulation flow:
```python
import numpy as np
import rpt
cameras = rpt.convert_cameras(raw_cameras)
config = rpt.make_triangulation_config(
cameras,
roomparams=np.asarray([[5.6, 6.4, 2.4], [0.0, -0.5, 1.2]], dtype=np.float32),
joint_names=joint_names,
)
poses_2d, person_counts = rpt.pack_poses_2d(views, joint_count=len(joint_names))
poses_3d = rpt.triangulate_poses(poses_2d, person_counts, config)
```
Typical RGB-D flow:
```python
poses_2d, person_counts = rpt.pack_poses_2d(views, joint_count=len(joint_names))
poses_3d = rpt.reconstruct_rgbd(
poses_2d,
person_counts,
depth_images,
config,
use_depth_offsets=True,
)
```
The lower-level RGB-D helpers are also available if you want to inspect or customize the intermediate steps:
- `sample_depth_for_poses`: sample aligned depth around visible 2D joints
- `apply_depth_offsets`: add per-joint offsets derived from SimpleDepthPose
- `lift_depth_poses_to_world`: convert `[u, v, d, score]` joints into world-space `[x, y, z, score]`
- `merge_rgbd_views`: merge per-view world-space pose proposals into final poses
## Ported From SimpleDepthPose
This fork ports the RGB-D fusion path from SimpleDepthPose into `rpt`.
Original upstream repository:
- [Percipiote/SimpleDepthPose](https://gitlab.com/Percipiote/SimpleDepthPose)
The ported pieces are:
- Depth sampling around each visible 2D joint, based on the `add_depth` preprocessing flow
- Per-joint depth offsets, matching the SimpleDepthPose body-surface correction idea
- UVD-to-world lifting using the calibrated camera intrinsics/extrinsics
- Multi-view RGB-D pose fusion logic adapted from `PoseFuser`
Compared with the original SimpleDepthPose implementation, the port here has been changed to fit a reusable library:
- The workflow is exposed as stateless functions instead of script-driven pipelines
- The fusion logic lives in the `rpt` core instead of a separate wrapper class
- Camera and scene configuration are routed through `TriangulationConfig`
- The RGB-D path is covered by repo tests and packaged with the same Python API as the triangulation path
This repo does not attempt to port the full SimpleDepthPose project. It only ports the RGB-D reconstruction pieces that fit the current library scope.
## Changed Vs Upstream RapidPoseTriangulation
Compared with the original upstream repository below, this fork has materially changed structure and scope:
- [Percipiote/RapidPoseTriangulation](https://gitlab.com/Percipiote/RapidPoseTriangulation)
- SWIG bindings were replaced with `nanobind`
- The repo was converted into a Python package under `src/rpt`
- The triangulation interface was simplified around immutable cameras and config structs
- The core was reshaped into a more library-oriented, zero-copy style API
- Debug tracing and tracked association reports were added
- Upstream integration layers and extra tooling were removed, including the old `extras/` stack and related deployment/inference wrappers
- An RGB-D reconstruction pipeline was added by porting and adapting parts of SimpleDepthPose
In practice, upstream is closer to a larger project tree with integrations and historical tooling, while this fork is closer to a compact reconstruction library.
## Testing
The repo currently ships Python-facing tests for both triangulation and RGB-D reconstruction:
```bash
uv run pytest tests/test_interface.py
uv run pytest tests/test_rgbd.py
```
Or run everything:
```bash
uv run pytest -q
```
The checked-in sample data under `data/` is used by the triangulation tests.
## Upstream References
Original upstream repositories referenced by this fork:
- RapidPoseTriangulation: [Percipiote/RapidPoseTriangulation](https://gitlab.com/Percipiote/RapidPoseTriangulation)
- SimpleDepthPose: [Percipiote/SimpleDepthPose](https://gitlab.com/Percipiote/SimpleDepthPose)
+2 -1
View File
@@ -30,7 +30,7 @@ set_target_properties(rpt_core_ext PROPERTIES
target_link_libraries(rpt_core_ext PRIVATE rpt_core) target_link_libraries(rpt_core_ext PRIVATE rpt_core)
target_include_directories(rpt_core_ext PRIVATE target_include_directories(rpt_core_ext PRIVATE
"${PROJECT_SOURCE_DIR}/rpt" "${PROJECT_SOURCE_DIR}/rpt_cpp"
) )
nanobind_add_stub(rpt_core_stub nanobind_add_stub(rpt_core_stub
@@ -43,3 +43,4 @@ nanobind_add_stub(rpt_core_stub
install(TARGETS rpt_core_ext LIBRARY DESTINATION rpt) install(TARGETS rpt_core_ext LIBRARY DESTINATION rpt)
install(FILES "${RPT_PYTHON_PACKAGE_DIR}/__init__.pyi" DESTINATION rpt) install(FILES "${RPT_PYTHON_PACKAGE_DIR}/__init__.pyi" DESTINATION rpt)
install(FILES "${RPT_PYTHON_PACKAGE_DIR}/_core.pyi" DESTINATION rpt) install(FILES "${RPT_PYTHON_PACKAGE_DIR}/_core.pyi" DESTINATION rpt)
install(FILES "${RPT_PYTHON_PACKAGE_DIR}/py.typed" DESTINATION rpt)
+46
View File
@@ -23,6 +23,8 @@ using CountArray = nb::ndarray<nb::numpy, const uint32_t, nb::shape<-1>, nb::c_c
using TrackIdArray = nb::ndarray<nb::numpy, const int64_t, nb::shape<-1>, nb::c_contig>; using TrackIdArray = nb::ndarray<nb::numpy, const int64_t, nb::shape<-1>, nb::c_contig>;
using PoseArray3DConst = using PoseArray3DConst =
nb::ndarray<nb::numpy, const float, nb::shape<-1, -1, 4>, nb::c_contig>; nb::ndarray<nb::numpy, const float, nb::shape<-1, -1, 4>, nb::c_contig>;
using PoseArray3DByViewConst =
nb::ndarray<nb::numpy, const float, nb::shape<-1, -1, -1, 4>, nb::c_contig>;
using PoseArray3D = nb::ndarray<nb::numpy, float, nb::shape<-1, -1, 4>, nb::c_contig>; using PoseArray3D = nb::ndarray<nb::numpy, float, nb::shape<-1, -1, 4>, nb::c_contig>;
using PoseArray2DOut = nb::ndarray<nb::numpy, float, nb::shape<-1, 4>, nb::c_contig>; using PoseArray2DOut = nb::ndarray<nb::numpy, float, nb::shape<-1, 4>, nb::c_contig>;
@@ -59,6 +61,32 @@ PoseBatch3DView pose_batch3d_view_from_numpy(const PoseArray3DConst &poses_3d)
}; };
} }
PoseBatch3DByViewView pose_batch3d_by_view_from_numpy(
const PoseArray3DByViewConst &poses_3d,
const CountArray &person_counts)
{
if (poses_3d.shape(0) != person_counts.shape(0))
{
throw std::invalid_argument("poses_3d and person_counts must have the same number of views.");
}
for (size_t i = 0; i < static_cast<size_t>(person_counts.shape(0)); ++i)
{
if (person_counts(i) > poses_3d.shape(1))
{
throw std::invalid_argument("person_counts entries must not exceed the padded person dimension.");
}
}
return PoseBatch3DByViewView {
poses_3d.data(),
person_counts.data(),
static_cast<size_t>(poses_3d.shape(0)),
static_cast<size_t>(poses_3d.shape(1)),
static_cast<size_t>(poses_3d.shape(2)),
};
}
TrackedPoseBatch3DView tracked_pose_batch_view_from_numpy( TrackedPoseBatch3DView tracked_pose_batch_view_from_numpy(
const PoseArray3DConst &poses_3d, const PoseArray3DConst &poses_3d,
const TrackIdArray &track_ids) const TrackIdArray &track_ids)
@@ -432,6 +460,24 @@ NB_MODULE(_core, m)
"person_counts"_a, "person_counts"_a,
"config"_a); "config"_a);
m.def(
"merge_rgbd_views",
[](const PoseArray3DByViewConst &poses_3d,
const CountArray &person_counts,
const TriangulationConfig &config,
float max_distance)
{
const PoseBatch3D merged = merge_rgbd_views(
pose_batch3d_by_view_from_numpy(poses_3d, person_counts),
config,
max_distance);
return pose_batch_to_numpy(merged);
},
"poses_3d"_a,
"person_counts"_a,
"config"_a,
"max_distance"_a = 0.5f);
m.def( m.def(
"triangulate_with_report", "triangulate_with_report",
[](const PoseArray2D &poses_2d, [](const PoseArray2D &poses_2d,
+9 -3
View File
@@ -7,14 +7,20 @@ build-backend = "scikit_build_core.build"
[project] [project]
name = "rapid-pose-triangulation" name = "rapid-pose-triangulation"
version = "0.1.0" version = "0.2.0"
description = "Rapid Pose Triangulation library with nanobind Python bindings" description = "Rapid Pose Triangulation library with nanobind Python bindings"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = ["numpy>=2.0"] dependencies = [
"jaxtyping",
"numpy>=2.0",
]
[dependency-groups] [dependency-groups]
dev = ["pytest>=8.3"] dev = [
"basedpyright>=1.38.3",
"pytest>=8.3",
]
[tool.scikit-build] [tool.scikit-build]
minimum-version = "build-system.requires" minimum-version = "build-system.requires"
+13
View File
@@ -0,0 +1,13 @@
{
"include": ["src"],
"ignore": ["src/rpt/_core.pyi"],
"failOnWarnings": false,
"pythonVersion": "3.10",
"reportMissingModuleSource": "none",
"executionEnvironments": [
{
"root": "tests",
"extraPaths": ["src"]
}
]
}
@@ -3,6 +3,7 @@
set(RPT_SOURCES set(RPT_SOURCES
camera.cpp camera.cpp
interface.cpp interface.cpp
rgbd_merger.cpp
triangulator.cpp triangulator.cpp
) )
@@ -23,6 +23,17 @@ size_t pose3d_offset(size_t person, size_t joint, size_t coord, size_t num_joint
{ {
return (((person * num_joints) + joint) * 4) + coord; return (((person * num_joints) + joint) * 4) + coord;
} }
size_t pose3d_by_view_offset(
size_t view,
size_t person,
size_t joint,
size_t coord,
size_t max_persons,
size_t num_joints)
{
return ((((view * max_persons) + person) * num_joints) + joint) * 4 + coord;
}
} // namespace } // namespace
// ================================================================================================= // =================================================================================================
@@ -53,6 +64,11 @@ const float &TrackedPoseBatch3DView::at(size_t person, size_t joint, size_t coor
return data[pose3d_offset(person, joint, coord, num_joints)]; return data[pose3d_offset(person, joint, coord, num_joints)];
} }
const float &PoseBatch3DByViewView::at(size_t view, size_t person, size_t joint, size_t coord) const
{
return data[pose3d_by_view_offset(view, person, joint, coord, max_persons, num_joints)];
}
const float &PoseBatch2D::at(size_t view, size_t person, size_t joint, size_t coord) const const float &PoseBatch2D::at(size_t view, size_t person, size_t joint, size_t coord) const
{ {
return data[pose2d_offset(view, person, joint, coord, max_persons, num_joints)]; return data[pose2d_offset(view, person, joint, coord, max_persons, num_joints)];
@@ -129,6 +145,27 @@ PoseBatch3DView PoseBatch3D::view() const
return PoseBatch3DView {data.data(), num_persons, num_joints}; return PoseBatch3DView {data.data(), num_persons, num_joints};
} }
float &PoseBatch3DByView::at(size_t view, size_t person, size_t joint, size_t coord)
{
return data[pose3d_by_view_offset(view, person, joint, coord, max_persons, num_joints)];
}
const float &PoseBatch3DByView::at(size_t view, size_t person, size_t joint, size_t coord) const
{
return data[pose3d_by_view_offset(view, person, joint, coord, max_persons, num_joints)];
}
PoseBatch3DByViewView PoseBatch3DByView::view() const
{
return PoseBatch3DByViewView {
data.data(),
person_counts.data(),
num_views,
max_persons,
num_joints,
};
}
NestedPoses3D PoseBatch3D::to_nested() const NestedPoses3D PoseBatch3D::to_nested() const
{ {
NestedPoses3D poses_3d(num_persons); NestedPoses3D poses_3d(num_persons);
@@ -45,6 +45,17 @@ struct TrackedPoseBatch3DView
const float &at(size_t person, size_t joint, size_t coord) const; const float &at(size_t person, size_t joint, size_t coord) const;
}; };
struct PoseBatch3DByViewView
{
const float *data = nullptr;
const uint32_t *person_counts = nullptr;
size_t num_views = 0;
size_t max_persons = 0;
size_t num_joints = 0;
const float &at(size_t view, size_t person, size_t joint, size_t coord) const;
};
struct PoseBatch2D struct PoseBatch2D
{ {
std::vector<float> data; std::vector<float> data;
@@ -74,6 +85,19 @@ struct PoseBatch3D
static PoseBatch3D from_nested(const NestedPoses3D &poses_3d); static PoseBatch3D from_nested(const NestedPoses3D &poses_3d);
}; };
struct PoseBatch3DByView
{
std::vector<float> data;
std::vector<uint32_t> person_counts;
size_t num_views = 0;
size_t max_persons = 0;
size_t num_joints = 0;
float &at(size_t view, size_t person, size_t joint, size_t coord);
const float &at(size_t view, size_t person, size_t joint, size_t coord) const;
PoseBatch3DByViewView view() const;
};
// ================================================================================================= // =================================================================================================
struct PairCandidate struct PairCandidate
@@ -242,6 +266,11 @@ PoseBatch3D triangulate_poses(
const TriangulationConfig &config, const TriangulationConfig &config,
const TriangulationOptions *options_override = nullptr); const TriangulationOptions *options_override = nullptr);
PoseBatch3D merge_rgbd_views(
const PoseBatch3DByViewView &poses_3d,
const TriangulationConfig &config,
float max_distance = 0.5f);
TriangulationResult triangulate_with_report( TriangulationResult triangulate_with_report(
const PoseBatch2DView &poses_2d, const PoseBatch2DView &poses_2d,
const TriangulationConfig &config, const TriangulationConfig &config,
@@ -256,6 +285,14 @@ inline PoseBatch3D triangulate_poses(
return triangulate_poses(poses_2d.view(), config, options_override); return triangulate_poses(poses_2d.view(), config, options_override);
} }
inline PoseBatch3D merge_rgbd_views(
const PoseBatch3DByView &poses_3d,
const TriangulationConfig &config,
float max_distance = 0.5f)
{
return merge_rgbd_views(poses_3d.view(), config, max_distance);
}
inline TriangulationTrace triangulate_debug( inline TriangulationTrace triangulate_debug(
const PoseBatch2D &poses_2d, const PoseBatch2D &poses_2d,
const TriangulationConfig &config, const TriangulationConfig &config,
File diff suppressed because it is too large Load Diff
+91 -7
View File
@@ -24,6 +24,7 @@ from ._core import (
build_pair_candidates as _build_pair_candidates, build_pair_candidates as _build_pair_candidates,
filter_pairs_with_previous_poses as _filter_pairs_with_previous_poses, filter_pairs_with_previous_poses as _filter_pairs_with_previous_poses,
make_camera as _make_camera, make_camera as _make_camera,
merge_rgbd_views as _merge_rgbd_views,
triangulate_debug as _triangulate_debug, triangulate_debug as _triangulate_debug,
triangulate_poses as _triangulate_poses, triangulate_poses as _triangulate_poses,
triangulate_with_report as _triangulate_with_report, triangulate_with_report as _triangulate_with_report,
@@ -33,10 +34,19 @@ if TYPE_CHECKING:
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
from ._helpers import CameraLike, CameraModelLike, Matrix3x3Like, PoseViewLike, VectorLike from ._helpers import (
CameraLike,
CameraModelLike,
DepthImageLike,
Matrix3x3Like,
PoseViewLike,
TranslationVectorLike,
VectorLike,
)
PoseArray2D = npt.NDArray[np.float32] PoseArray2D = npt.NDArray[np.float32]
PoseArray3D = npt.NDArray[np.float32] PoseArray3D = npt.NDArray[np.float32]
PoseArray3DByView = npt.NDArray[np.float32]
PersonCountArray = npt.NDArray[np.uint32] PersonCountArray = npt.NDArray[np.uint32]
TrackIdArray = npt.NDArray[np.int64] TrackIdArray = npt.NDArray[np.int64]
@@ -62,22 +72,25 @@ def make_camera(
K: "Matrix3x3Like", K: "Matrix3x3Like",
DC: "VectorLike", DC: "VectorLike",
R: "Matrix3x3Like", R: "Matrix3x3Like",
T: "Sequence[Sequence[float]]", T: "TranslationVectorLike",
width: int, width: int,
height: int, height: int,
model: "CameraModel | CameraModelLike", model: "CameraModel | CameraModelLike",
) -> Camera: ) -> Camera:
"""Create an immutable camera and precompute its cached projection fields.""" """Create an immutable camera and precompute its cached projection fields.
from ._helpers import _coerce_camera_model, _coerce_distortion `T` may be a flat `[x, y, z]` vector or a nested translation matrix with shape `[1, 3]` or `[3, 1]`.
"""
from ._helpers import _coerce_camera_model, _coerce_distortion, _coerce_matrix3x3, _coerce_translation
camera_model = _coerce_camera_model(model) camera_model = _coerce_camera_model(model)
return _make_camera( return _make_camera(
name, name,
K, _coerce_matrix3x3(K, "K").tolist(),
_coerce_distortion(DC, camera_model), _coerce_distortion(DC, camera_model),
R, _coerce_matrix3x3(R, "R").tolist(),
T, _coerce_translation(T).tolist(),
width, width,
height, height,
camera_model, camera_model,
@@ -103,6 +116,42 @@ def pack_poses_2d(
return _pack_poses_2d(views, joint_count=joint_count) return _pack_poses_2d(views, joint_count=joint_count)
def sample_depth_for_poses(
poses_2d: "PoseArray2D",
person_counts: "PersonCountArray",
depth_images: "Sequence[DepthImageLike]",
*,
window_size: int = 7,
) -> "PoseArray3D":
"""Sample aligned depth for visible 2D joints and return `[u, v, d, score]` rows."""
from ._helpers import sample_depth_for_poses as _sample_depth_for_poses
return _sample_depth_for_poses(poses_2d, person_counts, depth_images, window_size=window_size)
def apply_depth_offsets(
poses_uvd: "PoseArray3D",
joint_names: "Sequence[str]",
) -> "PoseArray3D":
"""Apply the SimpleDepthPose per-joint depth offsets to `[u, v, d, score]` rows."""
from ._helpers import apply_depth_offsets as _apply_depth_offsets
return _apply_depth_offsets(poses_uvd, joint_names)
def lift_depth_poses_to_world(
poses_uvd: "PoseArray3D",
cameras: "Sequence[CameraLike]",
) -> "PoseArray3DByView":
"""Lift `[u, v, d, score]` joints into world-space `[x, y, z, score]` poses."""
from ._helpers import lift_depth_poses_to_world as _lift_depth_poses_to_world
return _lift_depth_poses_to_world(poses_uvd, cameras)
def make_triangulation_config( def make_triangulation_config(
cameras: "Sequence[CameraLike]", cameras: "Sequence[CameraLike]",
roomparams: "npt.NDArray[np.generic] | Sequence[Sequence[float]]", roomparams: "npt.NDArray[np.generic] | Sequence[Sequence[float]]",
@@ -172,6 +221,36 @@ def triangulate_poses(
return _triangulate_poses(poses_2d, person_counts, config) return _triangulate_poses(poses_2d, person_counts, config)
def merge_rgbd_views(
poses_3d: "PoseArray3DByView",
person_counts: "PersonCountArray",
config: TriangulationConfig,
*,
max_distance: float = 0.5,
) -> "PoseArray3D":
"""Merge per-view world-space RGBD pose proposals into final 3D poses."""
return _merge_rgbd_views(poses_3d, person_counts, config, float(max_distance))
def reconstruct_rgbd(
poses_2d: "PoseArray2D",
person_counts: "PersonCountArray",
depth_images: "Sequence[DepthImageLike]",
config: TriangulationConfig,
*,
use_depth_offsets: bool = True,
window_size: int = 7,
max_distance: float = 0.5,
) -> "PoseArray3D":
"""Reconstruct per-frame RGBD poses from calibrated detections and aligned depth images."""
poses_uvd = sample_depth_for_poses(poses_2d, person_counts, depth_images, window_size=window_size)
if use_depth_offsets:
poses_uvd = apply_depth_offsets(poses_uvd, config.joint_names)
poses_3d = lift_depth_poses_to_world(poses_uvd, config.cameras)
return merge_rgbd_views(poses_3d, person_counts, config, max_distance=max_distance)
def triangulate_with_report( def triangulate_with_report(
poses_2d: "PoseArray2D", poses_2d: "PoseArray2D",
person_counts: "PersonCountArray", person_counts: "PersonCountArray",
@@ -200,6 +279,7 @@ __all__ = [
"CameraModel", "CameraModel",
"AssociationReport", "AssociationReport",
"AssociationStatus", "AssociationStatus",
"apply_depth_offsets",
"FinalPoseAssociationDebug", "FinalPoseAssociationDebug",
"TriangulationConfig", "TriangulationConfig",
"TriangulationOptions", "TriangulationOptions",
@@ -216,9 +296,13 @@ __all__ = [
"build_pair_candidates", "build_pair_candidates",
"convert_cameras", "convert_cameras",
"filter_pairs_with_previous_poses", "filter_pairs_with_previous_poses",
"lift_depth_poses_to_world",
"make_camera", "make_camera",
"make_triangulation_config", "make_triangulation_config",
"merge_rgbd_views",
"pack_poses_2d", "pack_poses_2d",
"reconstruct_rgbd",
"sample_depth_for_poses",
"triangulate_debug", "triangulate_debug",
"triangulate_poses", "triangulate_poses",
"triangulate_with_report", "triangulate_with_report",
+104 -20
View File
@@ -5,28 +5,38 @@ import numpy as np
import numpy.typing as npt import numpy.typing as npt
from ._core import ( from ._core import (
AssociationReport, AssociationReport as AssociationReport,
AssociationStatus, AssociationStatus as AssociationStatus,
Camera, Camera as Camera,
CameraModel, CameraModel as CameraModel,
CoreProposalDebug, CoreProposalDebug as CoreProposalDebug,
FinalPoseAssociationDebug, FinalPoseAssociationDebug as FinalPoseAssociationDebug,
FullProposalDebug, FullProposalDebug as FullProposalDebug,
GroupingDebug, GroupingDebug as GroupingDebug,
MergeDebug, MergeDebug as MergeDebug,
PairCandidate, PairCandidate as PairCandidate,
PreviousPoseFilterDebug, PreviousPoseFilterDebug as PreviousPoseFilterDebug,
PreviousPoseMatch, PreviousPoseMatch as PreviousPoseMatch,
ProposalGroupDebug, ProposalGroupDebug as ProposalGroupDebug,
TriangulationConfig, TriangulationConfig as TriangulationConfig,
TriangulationOptions, TriangulationOptions as TriangulationOptions,
TriangulationResult, TriangulationResult as TriangulationResult,
TriangulationTrace, TriangulationTrace as TriangulationTrace,
)
from ._helpers import (
CameraLike,
CameraModelLike,
DepthImageLike,
Matrix3x3Like,
PoseViewLike,
RoomParamsLike,
TranslationVectorLike,
VectorLike,
) )
from ._helpers import CameraLike, CameraModelLike, Matrix3x3Like, PoseViewLike, RoomParamsLike, VectorLike
PoseArray2D: TypeAlias = npt.NDArray[np.float32] PoseArray2D: TypeAlias = npt.NDArray[np.float32]
PoseArray3D: TypeAlias = npt.NDArray[np.float32] PoseArray3D: TypeAlias = npt.NDArray[np.float32]
PoseArray3DByView: TypeAlias = npt.NDArray[np.float32]
PersonCountArray: TypeAlias = npt.NDArray[np.uint32] PersonCountArray: TypeAlias = npt.NDArray[np.uint32]
TrackIdArray: TypeAlias = npt.NDArray[np.int64] TrackIdArray: TypeAlias = npt.NDArray[np.int64]
@@ -39,7 +49,7 @@ def make_camera(
K: Matrix3x3Like, K: Matrix3x3Like,
DC: VectorLike, DC: VectorLike,
R: Matrix3x3Like, R: Matrix3x3Like,
T: Sequence[Sequence[float]], T: TranslationVectorLike,
width: int, width: int,
height: int, height: int,
model: CameraModel | CameraModelLike, model: CameraModel | CameraModelLike,
@@ -59,6 +69,27 @@ def pack_poses_2d(
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]: ... ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]: ...
def sample_depth_for_poses(
poses_2d: PoseArray2D,
person_counts: PersonCountArray,
depth_images: Sequence[DepthImageLike],
*,
window_size: int = 7,
) -> PoseArray3D: ...
def apply_depth_offsets(
poses_uvd: PoseArray3D,
joint_names: Sequence[str],
) -> PoseArray3D: ...
def lift_depth_poses_to_world(
poses_uvd: PoseArray3D,
cameras: Sequence[CameraLike],
) -> PoseArray3DByView: ...
def make_triangulation_config( def make_triangulation_config(
cameras: Sequence[CameraLike], cameras: Sequence[CameraLike],
roomparams: RoomParamsLike, roomparams: RoomParamsLike,
@@ -103,6 +134,27 @@ def triangulate_poses(
) -> PoseArray3D: ... ) -> PoseArray3D: ...
def merge_rgbd_views(
poses_3d: PoseArray3DByView,
person_counts: PersonCountArray,
config: TriangulationConfig,
*,
max_distance: float = 0.5,
) -> PoseArray3D: ...
def reconstruct_rgbd(
poses_2d: PoseArray2D,
person_counts: PersonCountArray,
depth_images: Sequence[DepthImageLike],
config: TriangulationConfig,
*,
use_depth_offsets: bool = True,
window_size: int = 7,
max_distance: float = 0.5,
) -> PoseArray3D: ...
def triangulate_with_report( def triangulate_with_report(
poses_2d: PoseArray2D, poses_2d: PoseArray2D,
person_counts: PersonCountArray, person_counts: PersonCountArray,
@@ -112,4 +164,36 @@ def triangulate_with_report(
) -> TriangulationResult: ... ) -> TriangulationResult: ...
__all__: list[str] __all__ = [
"Camera",
"CameraModel",
"AssociationReport",
"AssociationStatus",
"apply_depth_offsets",
"FinalPoseAssociationDebug",
"TriangulationConfig",
"TriangulationOptions",
"TriangulationResult",
"CoreProposalDebug",
"FullProposalDebug",
"GroupingDebug",
"MergeDebug",
"PairCandidate",
"PreviousPoseFilterDebug",
"PreviousPoseMatch",
"ProposalGroupDebug",
"TriangulationTrace",
"build_pair_candidates",
"convert_cameras",
"filter_pairs_with_previous_poses",
"lift_depth_poses_to_world",
"make_camera",
"make_triangulation_config",
"merge_rgbd_views",
"pack_poses_2d",
"reconstruct_rgbd",
"sample_depth_for_poses",
"triangulate_debug",
"triangulate_poses",
"triangulate_with_report",
]
+530
View File
@@ -0,0 +1,530 @@
from collections.abc import Sequence
import enum
from typing import Annotated, overload
import numpy
from numpy.typing import NDArray
class CameraModel(enum.Enum):
PINHOLE = 0
FISHEYE = 1
class Camera:
"""Immutable camera calibration with precomputed projection cache fields."""
@property
def name(self) -> str: ...
@property
def K(self) -> list[list[float]]: ...
@property
def DC(self) -> list[float]: ...
@property
def R(self) -> list[list[float]]: ...
@property
def T(self) -> list[list[float]]: ...
@property
def width(self) -> int: ...
@property
def height(self) -> int: ...
@property
def model(self) -> CameraModel: ...
@property
def invR(self) -> list[list[float]]: ...
@property
def center(self) -> list[float]: ...
@property
def newK(self) -> list[list[float]]: ...
@property
def invK(self) -> list[list[float]]: ...
def __repr__(self) -> str: ...
class TriangulationOptions:
"""Score and grouping thresholds used by triangulation."""
def __init__(self) -> None: ...
@property
def min_match_score(self) -> float: ...
@min_match_score.setter
def min_match_score(self, arg: float, /) -> None: ...
@property
def min_group_size(self) -> int: ...
@min_group_size.setter
def min_group_size(self, arg: int, /) -> None: ...
class TriangulationConfig:
"""Stable scene configuration used for triangulation."""
def __init__(self) -> None: ...
@property
def cameras(self) -> list[Camera]: ...
@cameras.setter
def cameras(self, arg: Sequence[Camera], /) -> None: ...
@property
def roomparams(self) -> list[list[float]]: ...
@roomparams.setter
def roomparams(self, arg: Sequence[Sequence[float]], /) -> None: ...
@property
def joint_names(self) -> list[str]: ...
@joint_names.setter
def joint_names(self, arg: Sequence[str], /) -> None: ...
@property
def options(self) -> TriangulationOptions: ...
@options.setter
def options(self, arg: TriangulationOptions, /) -> None: ...
class PairCandidate:
def __init__(self) -> None: ...
@property
def view1(self) -> int: ...
@view1.setter
def view1(self, arg: int, /) -> None: ...
@property
def view2(self) -> int: ...
@view2.setter
def view2(self, arg: int, /) -> None: ...
@property
def person1(self) -> int: ...
@person1.setter
def person1(self, arg: int, /) -> None: ...
@property
def person2(self) -> int: ...
@person2.setter
def person2(self, arg: int, /) -> None: ...
@property
def global_person1(self) -> int: ...
@global_person1.setter
def global_person1(self, arg: int, /) -> None: ...
@property
def global_person2(self) -> int: ...
@global_person2.setter
def global_person2(self, arg: int, /) -> None: ...
class PreviousPoseMatch:
def __init__(self) -> None: ...
@property
def previous_pose_index(self) -> int: ...
@previous_pose_index.setter
def previous_pose_index(self, arg: int, /) -> None: ...
@property
def previous_track_id(self) -> int: ...
@previous_track_id.setter
def previous_track_id(self, arg: int, /) -> None: ...
@property
def score_view1(self) -> float: ...
@score_view1.setter
def score_view1(self, arg: float, /) -> None: ...
@property
def score_view2(self) -> float: ...
@score_view2.setter
def score_view2(self, arg: float, /) -> None: ...
@property
def matched_view1(self) -> bool: ...
@matched_view1.setter
def matched_view1(self, arg: bool, /) -> None: ...
@property
def matched_view2(self) -> bool: ...
@matched_view2.setter
def matched_view2(self, arg: bool, /) -> None: ...
@property
def kept(self) -> bool: ...
@kept.setter
def kept(self, arg: bool, /) -> None: ...
@property
def decision(self) -> str: ...
@decision.setter
def decision(self, arg: str, /) -> None: ...
class PreviousPoseFilterDebug:
def __init__(self) -> None: ...
@property
def used_previous_poses(self) -> bool: ...
@used_previous_poses.setter
def used_previous_poses(self, arg: bool, /) -> None: ...
@property
def matches(self) -> list[PreviousPoseMatch]: ...
@matches.setter
def matches(self, arg: Sequence[PreviousPoseMatch], /) -> None: ...
@property
def kept_pair_indices(self) -> list[int]: ...
@kept_pair_indices.setter
def kept_pair_indices(self, arg: Sequence[int], /) -> None: ...
@property
def kept_pairs(self) -> list[PairCandidate]: ...
@kept_pairs.setter
def kept_pairs(self, arg: Sequence[PairCandidate], /) -> None: ...
class CoreProposalDebug:
def __init__(self) -> None: ...
@property
def pair_index(self) -> int: ...
@pair_index.setter
def pair_index(self, arg: int, /) -> None: ...
@property
def pair(self) -> PairCandidate: ...
@pair.setter
def pair(self, arg: PairCandidate, /) -> None: ...
@property
def score(self) -> float: ...
@score.setter
def score(self, arg: float, /) -> None: ...
@property
def kept(self) -> bool: ...
@kept.setter
def kept(self, arg: bool, /) -> None: ...
@property
def drop_reason(self) -> str: ...
@drop_reason.setter
def drop_reason(self, arg: str, /) -> None: ...
@property
def pose_3d(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, 4), order='C')]: ...
class ProposalGroupDebug:
def __init__(self) -> None: ...
@property
def center(self) -> list[float]: ...
@center.setter
def center(self, arg: Sequence[float], /) -> None: ...
@property
def proposal_indices(self) -> list[int]: ...
@proposal_indices.setter
def proposal_indices(self, arg: Sequence[int], /) -> None: ...
@property
def pose_3d(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, 4), order='C')]: ...
class GroupingDebug:
def __init__(self) -> None: ...
@property
def initial_groups(self) -> list[ProposalGroupDebug]: ...
@initial_groups.setter
def initial_groups(self, arg: Sequence[ProposalGroupDebug], /) -> None: ...
@property
def duplicate_pair_drops(self) -> list[int]: ...
@duplicate_pair_drops.setter
def duplicate_pair_drops(self, arg: Sequence[int], /) -> None: ...
@property
def groups(self) -> list[ProposalGroupDebug]: ...
@groups.setter
def groups(self, arg: Sequence[ProposalGroupDebug], /) -> None: ...
class FullProposalDebug:
def __init__(self) -> None: ...
@property
def source_core_proposal_index(self) -> int: ...
@source_core_proposal_index.setter
def source_core_proposal_index(self, arg: int, /) -> None: ...
@property
def pair(self) -> PairCandidate: ...
@pair.setter
def pair(self, arg: PairCandidate, /) -> None: ...
@property
def pose_3d(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, 4), order='C')]: ...
class MergeDebug:
def __init__(self) -> None: ...
@property
def group_proposal_indices(self) -> list[list[int]]: ...
@group_proposal_indices.setter
def group_proposal_indices(self, arg: Sequence[Sequence[int]], /) -> None: ...
@property
def merged_poses(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C')]: ...
class AssociationStatus(enum.Enum):
MATCHED = 0
NEW = 1
AMBIGUOUS = 2
class AssociationReport:
"""Track-association summary for a tracked triangulation call."""
def __init__(self) -> None: ...
@property
def pose_previous_indices(self) -> list[int]: ...
@pose_previous_indices.setter
def pose_previous_indices(self, arg: Sequence[int], /) -> None: ...
@property
def pose_previous_track_ids(self) -> list[int]: ...
@pose_previous_track_ids.setter
def pose_previous_track_ids(self, arg: Sequence[int], /) -> None: ...
@property
def pose_status(self) -> list[AssociationStatus]: ...
@pose_status.setter
def pose_status(self, arg: Sequence[AssociationStatus], /) -> None: ...
@property
def pose_candidate_previous_indices(self) -> list[list[int]]: ...
@pose_candidate_previous_indices.setter
def pose_candidate_previous_indices(self, arg: Sequence[Sequence[int]], /) -> None: ...
@property
def pose_candidate_previous_track_ids(self) -> list[list[int]]: ...
@pose_candidate_previous_track_ids.setter
def pose_candidate_previous_track_ids(self, arg: Sequence[Sequence[int]], /) -> None: ...
@property
def unmatched_previous_indices(self) -> list[int]: ...
@unmatched_previous_indices.setter
def unmatched_previous_indices(self, arg: Sequence[int], /) -> None: ...
@property
def unmatched_previous_track_ids(self) -> list[int]: ...
@unmatched_previous_track_ids.setter
def unmatched_previous_track_ids(self, arg: Sequence[int], /) -> None: ...
@property
def new_pose_indices(self) -> list[int]: ...
@new_pose_indices.setter
def new_pose_indices(self, arg: Sequence[int], /) -> None: ...
@property
def ambiguous_pose_indices(self) -> list[int]: ...
@ambiguous_pose_indices.setter
def ambiguous_pose_indices(self, arg: Sequence[int], /) -> None: ...
class FinalPoseAssociationDebug:
def __init__(self) -> None: ...
@property
def final_pose_index(self) -> int: ...
@final_pose_index.setter
def final_pose_index(self, arg: int, /) -> None: ...
@property
def source_core_proposal_indices(self) -> list[int]: ...
@source_core_proposal_indices.setter
def source_core_proposal_indices(self, arg: Sequence[int], /) -> None: ...
@property
def source_pair_indices(self) -> list[int]: ...
@source_pair_indices.setter
def source_pair_indices(self, arg: Sequence[int], /) -> None: ...
@property
def candidate_previous_indices(self) -> list[int]: ...
@candidate_previous_indices.setter
def candidate_previous_indices(self, arg: Sequence[int], /) -> None: ...
@property
def candidate_previous_track_ids(self) -> list[int]: ...
@candidate_previous_track_ids.setter
def candidate_previous_track_ids(self, arg: Sequence[int], /) -> None: ...
@property
def resolved_previous_index(self) -> int: ...
@resolved_previous_index.setter
def resolved_previous_index(self, arg: int, /) -> None: ...
@property
def resolved_previous_track_id(self) -> int: ...
@resolved_previous_track_id.setter
def resolved_previous_track_id(self, arg: int, /) -> None: ...
@property
def status(self) -> AssociationStatus: ...
@status.setter
def status(self, arg: AssociationStatus, /) -> None: ...
class TriangulationTrace:
"""
Full debug trace for triangulation, including pair, grouping, and association stages.
"""
def __init__(self) -> None: ...
@property
def pairs(self) -> list[PairCandidate]: ...
@pairs.setter
def pairs(self, arg: Sequence[PairCandidate], /) -> None: ...
@property
def previous_filter(self) -> PreviousPoseFilterDebug: ...
@previous_filter.setter
def previous_filter(self, arg: PreviousPoseFilterDebug, /) -> None: ...
@property
def core_proposals(self) -> list[CoreProposalDebug]: ...
@core_proposals.setter
def core_proposals(self, arg: Sequence[CoreProposalDebug], /) -> None: ...
@property
def grouping(self) -> GroupingDebug: ...
@grouping.setter
def grouping(self, arg: GroupingDebug, /) -> None: ...
@property
def full_proposals(self) -> list[FullProposalDebug]: ...
@full_proposals.setter
def full_proposals(self, arg: Sequence[FullProposalDebug], /) -> None: ...
@property
def merge(self) -> MergeDebug: ...
@merge.setter
def merge(self, arg: MergeDebug, /) -> None: ...
@property
def association(self) -> AssociationReport: ...
@association.setter
def association(self, arg: AssociationReport, /) -> None: ...
@property
def final_pose_associations(self) -> list[FinalPoseAssociationDebug]: ...
@final_pose_associations.setter
def final_pose_associations(self, arg: Sequence[FinalPoseAssociationDebug], /) -> None: ...
@property
def final_poses(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C')]: ...
class TriangulationResult:
"""
Tracked triangulation output containing poses and association metadata.
"""
def __init__(self) -> None: ...
@property
def association(self) -> AssociationReport: ...
@association.setter
def association(self, arg: AssociationReport, /) -> None: ...
@property
def poses_3d(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C')]: ...
def make_camera(name: str, K: Sequence[Sequence[float]], DC: Sequence[float], R: Sequence[Sequence[float]], T: Sequence[Sequence[float]], width: int, height: int, model: CameraModel) -> Camera: ...
def build_pair_candidates(poses_2d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 3), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)]) -> list[PairCandidate]: ...
def filter_pairs_with_previous_poses(poses_2d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 3), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)], config: TriangulationConfig, previous_poses_3d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C', writable=False)], previous_track_ids: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> PreviousPoseFilterDebug: ...
@overload
def triangulate_debug(poses_2d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 3), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)], config: TriangulationConfig) -> TriangulationTrace: ...
@overload
def triangulate_debug(poses_2d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 3), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)], config: TriangulationConfig, previous_poses_3d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C', writable=False)], previous_track_ids: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> TriangulationTrace: ...
def triangulate_poses(poses_2d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 3), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)], config: TriangulationConfig) -> Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C')]: ...
def merge_rgbd_views(poses_3d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 4), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)], config: TriangulationConfig, max_distance: float = 0.5) -> Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C')]: ...
def triangulate_with_report(poses_2d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, None, 3), order='C', writable=False)], person_counts: Annotated[NDArray[numpy.uint32], dict(shape=(None,), order='C', writable=False)], config: TriangulationConfig, previous_poses_3d: Annotated[NDArray[numpy.float32], dict(shape=(None, None, 4), order='C', writable=False)], previous_track_ids: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> TriangulationResult: ...
+206 -12
View File
@@ -1,33 +1,67 @@
from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
from typing import Literal, TypeAlias, TypedDict from typing import Literal, TypeAlias, TypedDict
from jaxtyping import Float
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
from ._core import Camera, CameraModel, TriangulationConfig, TriangulationOptions, make_camera from ._core import Camera, CameraModel, TriangulationConfig, TriangulationOptions, make_camera as _make_camera
Matrix3x3Like: TypeAlias = Sequence[Sequence[float]] Matrix3x3: TypeAlias = Float[np.ndarray, "3 3"]
VectorLike: TypeAlias = Sequence[float] DistortionVector: TypeAlias = Float[np.ndarray, "coeffs"]
TranslationVector: TypeAlias = Float[np.ndarray, "3"]
TranslationColumn: TypeAlias = Float[np.ndarray, "3 1"]
TranslationRow: TypeAlias = Float[np.ndarray, "1 3"]
Matrix3x3Like: TypeAlias = Matrix3x3 | Sequence[Sequence[float]]
VectorLike: TypeAlias = DistortionVector | Sequence[float]
TranslationVectorLike: TypeAlias = (
TranslationVector | TranslationColumn | TranslationRow | Sequence[float] | Sequence[Sequence[float]]
)
RoomParamsLike: TypeAlias = npt.NDArray[np.generic] | Sequence[Sequence[float]] RoomParamsLike: TypeAlias = npt.NDArray[np.generic] | Sequence[Sequence[float]]
PoseViewLike: TypeAlias = npt.NDArray[np.generic] | Sequence[Sequence[Sequence[float]]] | Sequence[Sequence[float]] PoseViewLike: TypeAlias = npt.NDArray[np.generic] | Sequence[Sequence[Sequence[float]]] | Sequence[Sequence[float]]
DepthImageLike: TypeAlias = npt.NDArray[np.generic] | Sequence[Sequence[float]]
class CameraDict(TypedDict, total=False): class _CameraDictRequired(TypedDict):
name: str name: str
K: Matrix3x3Like K: Matrix3x3Like
DC: VectorLike DC: VectorLike
R: Matrix3x3Like R: Matrix3x3Like
T: Sequence[Sequence[float]] T: TranslationVectorLike
width: int width: int
height: int height: int
class CameraDict(_CameraDictRequired, total=False):
type: Literal["pinhole", "fisheye"] type: Literal["pinhole", "fisheye"]
model: Literal["pinhole", "fisheye"] | CameraModel model: Literal["pinhole", "fisheye"] | CameraModel
CameraModelLike: TypeAlias = CameraModel | Literal["pinhole", "fisheye"] CameraModelLike: TypeAlias = CameraModel | Literal["pinhole", "fisheye"]
CameraLike = Camera | CameraDict CameraLike: TypeAlias = Camera | CameraDict
DEFAULT_DEPTH_OFFSETS_METERS: dict[str, float] = {
"nose": 0.005,
"eye_left": 0.005,
"eye_right": 0.005,
"ear_left": 0.005,
"ear_right": 0.005,
"shoulder_left": 0.03,
"shoulder_right": 0.03,
"elbow_left": 0.02,
"elbow_right": 0.02,
"wrist_left": 0.01,
"wrist_right": 0.01,
"hip_left": 0.04,
"hip_right": 0.04,
"knee_left": 0.03,
"knee_right": 0.03,
"ankle_left": 0.03,
"ankle_right": 0.03,
"hip_middle": 0.04,
"shoulder_middle": 0.03,
"head": 0.0,
}
def _coerce_camera_model(model: CameraModelLike) -> CameraModel: def _coerce_camera_model(model: CameraModelLike) -> CameraModel:
@@ -55,6 +89,33 @@ def _coerce_distortion(distortion: VectorLike, camera_model: CameraModel) -> tup
return values return values
def _coerce_matrix3x3(matrix: object, field_name: str) -> Matrix3x3:
array = np.asarray(matrix, dtype=np.float32)
if array.shape != (3, 3):
raise ValueError(f"{field_name} must have shape [3, 3].")
return np.ascontiguousarray(array, dtype=np.float32)
def _coerce_translation(translation: object) -> TranslationColumn:
array = np.asarray(translation, dtype=np.float32)
if array.shape == (3,):
array = array[:, np.newaxis]
elif array.shape == (1, 3):
array = array.T
if array.shape != (3, 1):
raise ValueError("T must have shape [3], [1, 3], or [3, 1].")
return np.ascontiguousarray(array, dtype=np.float32)
def _coerce_depth_image(depth_image: DepthImageLike) -> npt.NDArray[np.float32]:
array = np.asarray(depth_image, dtype=np.float32)
if array.ndim == 3 and array.shape[-1] == 1:
array = np.squeeze(array, axis=-1)
if array.ndim != 2:
raise ValueError("Each depth image must have shape [height, width] or [height, width, 1].")
return np.ascontiguousarray(array, dtype=np.float32)
def convert_cameras(cameras: Sequence[CameraLike]) -> list[Camera]: def convert_cameras(cameras: Sequence[CameraLike]) -> list[Camera]:
"""Normalize mappings or existing Camera objects into bound Camera instances.""" """Normalize mappings or existing Camera objects into bound Camera instances."""
@@ -66,12 +127,12 @@ def convert_cameras(cameras: Sequence[CameraLike]) -> list[Camera]:
camera_model = _coerce_camera_model(cam.get("model", cam.get("type", "pinhole"))) camera_model = _coerce_camera_model(cam.get("model", cam.get("type", "pinhole")))
converted.append( converted.append(
make_camera( _make_camera(
str(cam["name"]), str(cam["name"]),
cam["K"], _coerce_matrix3x3(cam["K"], "K").tolist(),
_coerce_distortion(cam["DC"], camera_model), _coerce_distortion(cam["DC"], camera_model),
cam["R"], _coerce_matrix3x3(cam["R"], "R").tolist(),
cam["T"], _coerce_translation(cam["T"]).tolist(),
int(cam["width"]), int(cam["width"]),
int(cam["height"]), int(cam["height"]),
camera_model, camera_model,
@@ -157,3 +218,136 @@ def make_triangulation_config(
options.min_group_size = int(min_group_size) options.min_group_size = int(min_group_size)
config.options = options config.options = options
return config return config
def sample_depth_for_poses(
poses_2d: npt.NDArray[np.generic],
person_counts: npt.NDArray[np.generic],
depth_images: Sequence[DepthImageLike],
*,
window_size: int = 7,
) -> npt.NDArray[np.float32]:
"""Sample aligned depth for each visible 2D joint and return `[u, v, d, score]` rows."""
poses = np.asarray(poses_2d, dtype=np.float32)
counts = np.asarray(person_counts, dtype=np.uint32)
if poses.ndim != 4 or poses.shape[-1] != 3:
raise ValueError("poses_2d must have shape [views, max_persons, joints, 3].")
if counts.ndim != 1 or counts.shape[0] != poses.shape[0]:
raise ValueError("person_counts must be a 1D array aligned with the pose views.")
if len(depth_images) != poses.shape[0]:
raise ValueError("depth_images must have the same number of views as poses_2d.")
if window_size <= 0:
raise ValueError("window_size must be positive.")
radius = window_size // 2
poses_uvd = np.zeros((poses.shape[0], poses.shape[1], poses.shape[2], 4), dtype=np.float32)
for view_idx, depth_image in enumerate(depth_images):
depth = _coerce_depth_image(depth_image)
poses_uvd[view_idx, :, :, :2] = poses[view_idx, :, :, :2]
poses_uvd[view_idx, :, :, 3] = poses[view_idx, :, :, 2]
valid_persons = int(counts[view_idx])
if valid_persons == 0:
continue
joints = poses[view_idx, :valid_persons, :, :2].astype(np.int32, copy=False).reshape(-1, 2)
scores = poses[view_idx, :valid_persons, :, 2:3].reshape(-1, 1)
depth_padded = np.pad(depth, radius, mode="constant", constant_values=0)
offsets = np.arange(-radius, radius + 1, dtype=np.int32)
valid_xy = (
(joints[:, 0] >= 0)
& (joints[:, 0] < depth.shape[1])
& (joints[:, 1] >= 0)
& (joints[:, 1] < depth.shape[0])
)
clamped_x = np.clip(joints[:, 0], 0, depth.shape[1] - 1)
clamped_y = np.clip(joints[:, 1], 0, depth.shape[0] - 1)
center_x = clamped_x[:, None] + radius
center_y = clamped_y[:, None] + radius
vertical_grid = np.clip(np.add.outer(clamped_y, offsets) + radius, 0, depth_padded.shape[0] - 1)
horizontal_grid = np.clip(
np.add.outer(clamped_x, offsets) + radius, 0, depth_padded.shape[1] - 1
)
vertical_depths = depth_padded[vertical_grid, center_x]
horizontal_depths = depth_padded[center_y, horizontal_grid]
all_depths = np.concatenate((vertical_depths, horizontal_depths), axis=1).astype(np.float32)
all_depths[~valid_xy] = np.nan
all_depths[all_depths <= 0] = np.nan
valid_depth_rows = ~np.isnan(all_depths).all(axis=1)
sampled_depths = np.zeros((all_depths.shape[0],), dtype=np.float32)
if np.any(valid_depth_rows):
with np.errstate(all="ignore"):
sampled_depths[valid_depth_rows] = np.nanmedian(all_depths[valid_depth_rows], axis=1)
valid_mask = ((sampled_depths > 0.0).astype(np.float32)[:, None] * (scores > 0.0).astype(np.float32))
sampled_depths = sampled_depths.reshape(valid_persons, poses.shape[2], 1)
valid_mask = valid_mask.reshape(valid_persons, poses.shape[2], 1)
poses_uvd[view_idx, :valid_persons, :, 2:3] = sampled_depths
poses_uvd[view_idx, :valid_persons] *= np.concatenate((valid_mask, valid_mask, valid_mask, valid_mask), axis=-1)
return poses_uvd
def apply_depth_offsets(
poses_uvd: npt.NDArray[np.generic],
joint_names: Sequence[str],
) -> npt.NDArray[np.float32]:
"""Apply the SimpleDepthPose per-joint depth offsets in meters."""
poses = np.asarray(poses_uvd, dtype=np.float32)
if poses.ndim != 4 or poses.shape[-1] != 4:
raise ValueError("poses_uvd must have shape [views, max_persons, joints, 4].")
if len(joint_names) != poses.shape[2]:
raise ValueError("joint_names must have the same number of joints as poses_uvd.")
result = poses.copy()
offsets = np.asarray(
[DEFAULT_DEPTH_OFFSETS_METERS.get(str(joint_name), 0.0) for joint_name in joint_names],
dtype=np.float32,
)
depth_mask = (result[:, :, :, 2:3] > 0.0).astype(np.float32)
result[:, :, :, 2:3] += depth_mask * offsets[np.newaxis, np.newaxis, :, np.newaxis] * 1000.0
return result
def lift_depth_poses_to_world(
poses_uvd: npt.NDArray[np.generic],
cameras: Sequence[CameraLike],
) -> npt.NDArray[np.float32]:
"""Lift `[u, v, d, score]` joints into world-space `[x, y, z, score]` poses."""
poses = np.asarray(poses_uvd, dtype=np.float32)
if poses.ndim != 4 or poses.shape[-1] != 4:
raise ValueError("poses_uvd must have shape [views, max_persons, joints, 4].")
converted_cameras = convert_cameras(cameras)
if len(converted_cameras) != poses.shape[0]:
raise ValueError("cameras must have the same number of views as poses_uvd.")
result = np.zeros_like(poses, dtype=np.float32)
for view_idx, camera in enumerate(converted_cameras):
uv = poses[view_idx, :, :, :2].reshape(-1, 2)
depth_mm = poses[view_idx, :, :, 2:3].reshape(-1, 1)
scores = poses[view_idx, :, :, 3:4].reshape(-1, 1)
depth_m = depth_mm * 0.001
uv_ones = np.concatenate((uv, np.ones((uv.shape[0], 1), dtype=np.float32)), axis=1)
k_inv = np.linalg.inv(np.asarray(camera.K, dtype=np.float32))
xyz_cam = depth_m * (uv_ones @ k_inv.T)
rotation = np.asarray(camera.R, dtype=np.float32)
translation = np.asarray(camera.T, dtype=np.float32).reshape(1, 3)
xyz_world = (rotation @ xyz_cam.T).T + translation
pose_world = np.concatenate((xyz_world, scores), axis=1).reshape(
poses.shape[1], poses.shape[2], 4
)
pose_world *= (pose_world[:, :, 3:4] > 0.0).astype(np.float32)
result[view_idx] = pose_world
return result
+2 -1
View File
@@ -56,7 +56,7 @@ def test_camera_structure_repr():
[[1, 0, 0], [0, 1, 0], [0, 0, 1]], [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[0, 0, 0, 0, 0], [0, 0, 0, 0, 0],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]], [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[1], [2], [3]], [1, 2, 3],
640, 640,
480, 480,
rpt.CameraModel.PINHOLE, rpt.CameraModel.PINHOLE,
@@ -65,6 +65,7 @@ def test_camera_structure_repr():
rendered = repr(camera) rendered = repr(camera)
assert "Camera 1" in rendered assert "Camera 1" in rendered
assert "pinhole" in rendered assert "pinhole" in rendered
np.testing.assert_allclose(np.asarray(camera.T, dtype=np.float32).reshape(3), [1.0, 2.0, 3.0])
@pytest.mark.parametrize( @pytest.mark.parametrize(
+197
View File
@@ -0,0 +1,197 @@
import numpy as np
import rpt
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 make_camera(name: str) -> rpt.Camera:
return rpt.make_camera(
name,
[[1000, 0, 0], [0, 1000, 0], [0, 0, 1]],
[0, 0, 0, 0, 0],
[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
[[0], [0], [0]],
256,
256,
rpt.CameraModel.PINHOLE,
)
def make_config(num_views: int) -> rpt.TriangulationConfig:
return rpt.make_triangulation_config(
[make_camera(f"Camera {idx}") for idx in range(num_views)],
np.asarray([[10.0, 10.0, 10.0], [0.0, 0.0, 0.0]], dtype=np.float32),
JOINT_NAMES,
)
def make_body_2d() -> np.ndarray:
return np.asarray(
[
[150, 50, 1.0],
[145, 48, 1.0],
[155, 48, 1.0],
[138, 50, 1.0],
[162, 50, 1.0],
[135, 80, 1.0],
[165, 80, 1.0],
[125, 115, 1.0],
[175, 115, 1.0],
[115, 150, 1.0],
[185, 150, 1.0],
[145, 130, 1.0],
[155, 130, 1.0],
[145, 175, 1.0],
[155, 175, 1.0],
[145, 220, 1.0],
[155, 220, 1.0],
[150, 130, 1.0],
[150, 80, 1.0],
[150, 50, 1.0],
],
dtype=np.float32,
)
def test_sample_depth_for_poses_respects_person_counts_and_scores():
poses_2d = np.zeros((1, 2, 2, 3), dtype=np.float32)
poses_2d[0, 0, 0] = [5, 6, 0.8]
poses_2d[0, 0, 1] = [7, 8, 0.0]
person_counts = np.asarray([1], dtype=np.uint32)
depth_image = np.full((16, 16), 3000, dtype=np.float32)
depth_image[0, 0] = 1234
poses_uvd = rpt.sample_depth_for_poses(poses_2d, person_counts, [depth_image])
np.testing.assert_allclose(poses_uvd[0, 0, 0], [5.0, 6.0, 3000.0, 0.8], rtol=1e-6, atol=1e-6)
np.testing.assert_array_equal(poses_uvd[0, 0, 1], np.zeros((4,), dtype=np.float32))
np.testing.assert_array_equal(poses_uvd[0, 1], np.zeros((2, 4), dtype=np.float32))
def test_sample_depth_for_poses_uses_symmetric_window():
poses_2d = np.zeros((1, 1, 1, 3), dtype=np.float32)
poses_2d[0, 0, 0] = [5, 5, 1.0]
person_counts = np.asarray([1], dtype=np.uint32)
depth_image = np.zeros((16, 16), dtype=np.float32)
depth_image[5, 5] = 1000.0
depth_image[3, 5] = 5000.0
depth_image[5, 2] = 5000.0
depth_image[5, 3] = 5000.0
depth_image[5, 7] = 5000.0
depth_image[5, 8] = 5000.0
poses_uvd = rpt.sample_depth_for_poses(poses_2d, person_counts, [depth_image], window_size=3)
np.testing.assert_allclose(poses_uvd[0, 0, 0], [5.0, 5.0, 1000.0, 1.0], rtol=1e-6, atol=1e-6)
def test_sample_depth_for_poses_ignores_out_of_bounds_joints():
poses_2d = np.zeros((1, 1, 1, 3), dtype=np.float32)
poses_2d[0, 0, 0] = [99, -4, 0.7]
person_counts = np.asarray([1], dtype=np.uint32)
poses_uvd = rpt.sample_depth_for_poses(
poses_2d,
person_counts,
[np.full((16, 16), 3000, dtype=np.float32)],
)
np.testing.assert_array_equal(poses_uvd[0, 0, 0], np.zeros((4,), dtype=np.float32))
def test_apply_depth_offsets_uses_joint_name_mapping():
poses_uvd = np.zeros((1, 1, 3, 4), dtype=np.float32)
poses_uvd[0, 0, :, 2] = 3000.0
poses_uvd[0, 0, :, 3] = 1.0
adjusted = rpt.apply_depth_offsets(poses_uvd, ["nose", "shoulder_left", "unknown_joint"])
np.testing.assert_allclose(adjusted[0, 0, :, 2], [3005.0, 3030.0, 3000.0], rtol=1e-6, atol=1e-6)
np.testing.assert_allclose(poses_uvd[0, 0, :, 2], [3000.0, 3000.0, 3000.0], rtol=1e-6, atol=1e-6)
def test_lift_depth_poses_to_world_matches_camera_projection():
poses_uvd = np.zeros((1, 1, 2, 4), dtype=np.float32)
poses_uvd[0, 0, 0] = [100.0, 200.0, 3000.0, 0.9]
poses_uvd[0, 0, 1] = [0.0, 0.0, 0.0, 0.0]
lifted = rpt.lift_depth_poses_to_world(poses_uvd, [make_camera("Camera 1")])
np.testing.assert_allclose(lifted[0, 0, 0], [0.3, 0.6, 3.0, 0.9], rtol=1e-6, atol=1e-6)
np.testing.assert_array_equal(lifted[0, 0, 1], np.zeros((4,), dtype=np.float32))
def test_merge_rgbd_views_merges_identical_world_poses():
config = make_config(2)
body_2d = make_body_2d()
poses_2d = np.zeros((2, 1, len(JOINT_NAMES), 3), dtype=np.float32)
poses_2d[0, 0] = body_2d
poses_2d[1, 0] = body_2d
person_counts = np.asarray([1, 1], dtype=np.uint32)
depth_images = [np.full((256, 256), 3000, dtype=np.float32) for _ in range(2)]
poses_uvd = rpt.sample_depth_for_poses(poses_2d, person_counts, depth_images)
poses_uvd = rpt.apply_depth_offsets(poses_uvd, JOINT_NAMES)
poses_3d_by_view = rpt.lift_depth_poses_to_world(poses_uvd, config.cameras)
merged = rpt.merge_rgbd_views(poses_3d_by_view, person_counts, config)
assert merged.shape == (1, len(JOINT_NAMES), 4)
np.testing.assert_allclose(merged[0, :-1], poses_3d_by_view[0, 0, :-1], rtol=1e-5, atol=1e-5)
expected_head = (poses_3d_by_view[0, 0, 3] + poses_3d_by_view[0, 0, 4]) * 0.5
expected_head[3] = min(poses_3d_by_view[0, 0, 3, 3], poses_3d_by_view[0, 0, 4, 3])
np.testing.assert_allclose(merged[0, -1], expected_head, rtol=1e-5, atol=1e-5)
def test_reconstruct_rgbd_matches_manual_pipeline_and_single_view_person():
config = make_config(2)
body_2d = make_body_2d()
poses_2d = np.zeros((2, 1, len(JOINT_NAMES), 3), dtype=np.float32)
poses_2d[0, 0] = body_2d
person_counts = np.asarray([1, 0], dtype=np.uint32)
depth_images = [
np.full((256, 256), 3000, dtype=np.float32),
np.zeros((256, 256), dtype=np.float32),
]
manual = rpt.merge_rgbd_views(
rpt.lift_depth_poses_to_world(
rpt.apply_depth_offsets(
rpt.sample_depth_for_poses(poses_2d, person_counts, depth_images),
JOINT_NAMES,
),
config.cameras,
),
person_counts,
config,
)
reconstructed = rpt.reconstruct_rgbd(poses_2d, person_counts, depth_images, config)
assert reconstructed.shape == (1, len(JOINT_NAMES), 4)
np.testing.assert_allclose(reconstructed, manual, rtol=1e-5, atol=1e-5)
assert np.count_nonzero(reconstructed[0, :, 3] > 0.0) >= 7
+18
View File
@@ -0,0 +1,18 @@
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
def test_checked_in_core_stub_exists():
assert (ROOT / "src" / "rpt" / "_core.pyi").exists()
def test_checked_in_core_stub_matches_generated_stub():
generated_stub = ROOT / "build" / "bindings" / "rpt" / "_core.pyi"
if not generated_stub.exists():
pytest.skip("Build-generated nanobind stub is unavailable.")
checked_in_stub = ROOT / "src" / "rpt" / "_core.pyi"
assert checked_in_stub.read_text(encoding="utf-8") == generated_stub.read_text(encoding="utf-8")
Generated
+79 -3
View File
@@ -6,6 +6,18 @@ resolution-markers = [
"python_full_version < '3.11'", "python_full_version < '3.11'",
] ]
[[package]]
name = "basedpyright"
version = "1.38.4"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/08/b4/26cb812eaf8ab56909c792c005fe1690706aef6f21d61107639e46e9c54c/basedpyright-1.38.4.tar.gz", hash = "sha256:8e7d4f37ffb6106621e06b9355025009cdf5b48f71c592432dd2dd304bf55e70", size = 25354730, upload-time = "2026-03-25T13:50:44.353Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/62/0b/3f95fd47def42479e61077523d3752086d5c12009192a7f1c9fd5507e687/basedpyright-1.38.4-py3-none-any.whl", hash = "sha256:90aa067cf3e8a3c17ad5836a72b9e1f046bc72a4ad57d928473d9368c9cd07a2", size = 12352258, upload-time = "2026-03-25T13:50:41.059Z" },
]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@@ -36,6 +48,52 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
] ]
[[package]]
name = "jaxtyping"
version = "0.3.7"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "wadler-lindig", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/40/a2ea3ce0e3e5f540eb970de7792c90fa58fef1b27d34c83f9fa94fea4729/jaxtyping-0.3.7.tar.gz", hash = "sha256:3bd7d9beb7d3cb01a89f93f90581c6f4fff3e5c5dc3c9307e8f8687a040d10c4", size = 45721, upload-time = "2026-01-30T14:18:47.409Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/78/42/caf65e9a0576a3abadc537e2f831701ba9081f21317fb3be87d64451587a/jaxtyping-0.3.7-py3-none-any.whl", hash = "sha256:303ab8599edf412eeb40bf06c863e3168fa186cf0e7334703fa741ddd7046e66", size = 56101, upload-time = "2026-01-30T14:18:45.954Z" },
]
[[package]]
name = "jaxtyping"
version = "0.3.9"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
resolution-markers = [
"python_full_version >= '3.11'",
]
dependencies = [
{ name = "wadler-lindig", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c2/be/00294e369938937e31b094437d5ea040e4fd1a20b998ebe572c4a1dcfa68/jaxtyping-0.3.9.tar.gz", hash = "sha256:f8c02d1b623d5f1b6665d4f3ddaec675d70004f16a792102c2fc51264190951d", size = 45857, upload-time = "2026-02-16T10:35:13.263Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/94/05/3e39d416fb92b2738a76e8265e6bfc5d10542f90a7c32ad1eb831eea3fa3/jaxtyping-0.3.9-py3-none-any.whl", hash = "sha256:a00557a9d616eff157491f06ed2e21ed94886fad3832399273eb912b345da378", size = 56274, upload-time = "2026-02-16T10:35:11.795Z" },
]
[[package]]
name = "nodejs-wheel-binaries"
version = "24.14.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/05/c75c0940b1ebf82975d14f37176679b6f3229eae8b47b6a70d1e1dae0723/nodejs_wheel_binaries-24.14.0.tar.gz", hash = "sha256:c87b515e44b0e4a523017d8c59f26ccbd05b54fe593338582825d4b51fc91e1c", size = 8057, upload-time = "2026-02-27T02:57:30.931Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/8c/b057c2db3551a6fe04e93dd14e33d810ac8907891534ffcc7a051b253858/nodejs_wheel_binaries-24.14.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:59bb78b8eb08c3e32186da1ef913f1c806b5473d8bd0bb4492702092747b674a", size = 54798488, upload-time = "2026-02-27T02:56:56.831Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/30/88/7e1b29c067b6625c97c81eb8b0ef37cf5ad5b62bb81e23f4bde804910ec9/nodejs_wheel_binaries-24.14.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:348fa061b57625de7250d608e2d9b7c4bc170544da7e328325343860eadd59e5", size = 54972803, upload-time = "2026-02-27T02:57:01.696Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/e0/a83f0ff12faca2a56366462e572e38ac6f5cb361877bb29e289138eb7f24/nodejs_wheel_binaries-24.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:222dbf516ccc877afcad4e4789a81b4ee93daaa9f0ad97c464417d9597f49449", size = 59340859, upload-time = "2026-02-27T02:57:06.125Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/9f/06fad4ae8a723ae7096b5311eba67ad8b4df5f359c0a68e366750b7fef78/nodejs_wheel_binaries-24.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b35d6fcccfe4fb0a409392d237fbc67796bac0d357b996bc12d057a1531a238b", size = 59838751, upload-time = "2026-02-27T02:57:10.449Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8c/72/4916dadc7307c3e9bcfa43b4b6f88237932d502c66f89eb2d90fb07810db/nodejs_wheel_binaries-24.14.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:519507fb74f3f2b296ab1e9f00dcc211f36bbfb93c60229e72dcdee9dafd301a", size = 61340534, upload-time = "2026-02-27T02:57:15.309Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2e/df/a8ba881ee5d04b04e0d93abc8ce501ff7292813583e97f9789eb3fc0472a/nodejs_wheel_binaries-24.14.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:68c93c52ff06d704bcb5ed160b4ba04ab1b291d238aaf996b03a5396e0e9a7ed", size = 61922394, upload-time = "2026-02-27T02:57:20.24Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/60/8c/b8c5f61201c72a0c7dc694b459941f89a6defda85deff258a9940a4e2efc/nodejs_wheel_binaries-24.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:60b83c4e98b0c7d836ac9ccb67dcb36e343691cbe62cd325799ff9ed936286f3", size = 41218783, upload-time = "2026-02-27T02:57:24.175Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/91/23/1f904bc9cbd8eece393e20840c08ba3ac03440090c3a4e95168fa6d2709f/nodejs_wheel_binaries-24.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:78a9bd1d6b11baf1433f9fb84962ff8aa71c87d48b6434f98224bc49a2253a6e", size = 38926103, upload-time = "2026-02-27T02:57:27.458Z" },
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.2.6" version = "2.2.6"
@@ -230,23 +288,32 @@ wheels = [
[[package]] [[package]]
name = "rapid-pose-triangulation" name = "rapid-pose-triangulation"
version = "0.1.0" version = "0.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "jaxtyping", version = "0.3.7", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.11'" },
{ name = "jaxtyping", version = "0.3.9", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.11'" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.3", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.3", source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }, marker = "python_full_version >= '3.11'" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "basedpyright" },
{ name = "pytest" }, { name = "pytest" },
] ]
[package.metadata] [package.metadata]
requires-dist = [{ name = "numpy", specifier = ">=2.0" }] requires-dist = [
{ name = "jaxtyping" },
{ name = "numpy", specifier = ">=2.0" },
]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.3" }] dev = [
{ name = "basedpyright", specifier = ">=1.38.3" },
{ name = "pytest", specifier = ">=8.3" },
]
[[package]] [[package]]
name = "tomli" name = "tomli"
@@ -310,3 +377,12 @@ sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/94/1a
wheels = [ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
] ]
[[package]]
name = "wadler-lindig"
version = "0.1.7"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1e/67/cbae4bf7683a64755c2c1778c418fea96d00e34395bb91743f08bd951571/wadler_lindig-0.1.7.tar.gz", hash = "sha256:81d14d3fe77d441acf3ebd7f4aefac20c74128bf460e84b512806dccf7b2cd55", size = 15842, upload-time = "2025-06-18T07:00:42.843Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8d/96/04e7b441807b26b794da5b11e59ed7f83b2cf8af202bd7eba8ad2fa6046e/wadler_lindig-0.1.7-py3-none-any.whl", hash = "sha256:e3ec83835570fd0a9509f969162aeb9c65618f998b1f42918cfc8d45122fe953", size = 20516, upload-time = "2025-06-18T07:00:41.684Z" },
]