feat: add standalone RGBD Python reference project

This commit is contained in:
2026-03-26 13:11:45 +08:00
commit 3025210741
14 changed files with 1865 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
.venv/
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.npy
.DS_Store
+38
View File
@@ -0,0 +1,38 @@
# rapid-pose-rgbd-python-example
Standalone NumPy reference project for the RGBD reconstruction path discussed across `SimpleDepthPose` and `RapidPoseTriangulation`.
It covers:
- padded multi-view 2D pose packing
- aligned depth sampling
- per-joint depth offsets
- camera-space lifting into world coordinates
- stateless multi-view RGBD pose merging
It does not include:
- raw-image 2D detection
- ROS integration
- temporal tracking
- RGB-only triangulation
## Install
```bash
cd /home/crosstyan/Code/rapid-pose-rgbd-python-example
uv sync --dev
```
## Run tests
```bash
uv run basedpyright
uv run pytest
```
## Smoke test
```bash
uv run rapid-pose-rgbd-example
```
+41
View File
@@ -0,0 +1,41 @@
[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"
[project]
name = "rapid-pose-rgbd-python-example"
version = "0.1.0"
description = "Standalone NumPy RGBD pose reconstruction reference project."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"beartype>=0.19",
"click>=8.1",
"jaxtyping>=0.3.2",
"numpy>=2.0",
"scipy>=1.13",
]
[project.scripts]
rapid-pose-rgbd-example = "rapid_pose_rgbd_example.cli:main"
[dependency-groups]
dev = [
"basedpyright>=1.29",
"pytest>=8.3",
]
[tool.hatch.build.targets.wheel]
packages = ["src/rapid_pose_rgbd_example"]
[tool.basedpyright]
include = ["src", "tests"]
pythonVersion = "3.11"
typeCheckingMode = "standard"
[tool.pytest.ini_options]
addopts = "-ra"
testpaths = ["tests"]
[tool.ruff.lint]
ignore = ["F722"]
+35
View File
@@ -0,0 +1,35 @@
from .core import (
DEFAULT_DEPTH_OFFSETS_METERS,
apply_depth_offsets,
lift_depth_poses_to_world,
merge_rgbd_views,
pack_poses_2d,
reconstruct_rgbd,
sample_depth_for_poses,
)
from .models import (
Camera,
CameraModel,
ReconstructionConfig,
RoomBounds,
convert_cameras,
make_camera,
make_reconstruction_config,
)
__all__ = [
"Camera",
"CameraModel",
"DEFAULT_DEPTH_OFFSETS_METERS",
"ReconstructionConfig",
"RoomBounds",
"apply_depth_offsets",
"convert_cameras",
"lift_depth_poses_to_world",
"make_camera",
"make_reconstruction_config",
"merge_rgbd_views",
"pack_poses_2d",
"reconstruct_rgbd",
"sample_depth_for_poses",
]
+4
View File
@@ -0,0 +1,4 @@
from .cli import main
if __name__ == "__main__":
main()
+619
View File
@@ -0,0 +1,619 @@
from dataclasses import dataclass
import numpy as np
from scipy.optimize import linear_sum_assignment
from .models import ReconstructionConfig
Pose3D = np.ndarray
def _clone_pose(pose: Pose3D) -> Pose3D:
return np.array(pose, dtype=np.float32, copy=True)
def _empty_pose_batch(num_joints: int) -> np.ndarray:
return np.zeros((0, num_joints, 4), dtype=np.float32)
def _all_visible_joints_close(
skeleton1: Pose3D,
skeleton2: Pose3D,
max_distance: float,
vis_threshold: float,
) -> bool:
visible = (skeleton1[:, 3] > vis_threshold) & (skeleton2[:, 3] > vis_threshold)
if not np.any(visible):
return False
deltas = skeleton1[visible, :3] - skeleton2[visible, :3]
distances_sq = np.sum(deltas * deltas, axis=1)
return bool(np.all(distances_sq <= max_distance * max_distance))
def add_extra_joints(poses: list[Pose3D], joint_names: tuple[str, ...]) -> None:
try:
head_index = joint_names.index("head")
ear_left_index = joint_names.index("ear_left")
ear_right_index = joint_names.index("ear_right")
nose_index = joint_names.index("nose")
except ValueError:
return
for pose in poses:
head = pose[head_index]
ear_left = pose[ear_left_index]
ear_right = pose[ear_right_index]
if ear_left[3] > 0.1 and ear_right[3] > 0.1:
head[:3] = (ear_left[:3] + ear_right[:3]) * 0.5
head[3] = min(float(ear_left[3]), float(ear_right[3]))
continue
nose = pose[nose_index]
if nose[3] > 0.1:
head[:] = nose
def filter_poses(poses: list[Pose3D], roomparams: np.ndarray) -> None:
room_size = roomparams[0]
room_center = roomparams[1]
room_half_size = room_size / 2.0
wall_distance = 0.1
for pose in poses:
valid = pose[:, 3] > 0.1
if int(np.count_nonzero(valid)) < 5:
pose[:, 3] = 0.001
continue
coords = pose[valid, :3]
mins = np.min(coords, axis=0)
maxs = np.max(coords, axis=0)
mean = np.mean(coords, axis=0)
diff = maxs - mins
if np.any(diff > 2.3) or np.all(diff < 0.3):
pose[:, 3] = 0.001
continue
outside_mean = (mean > room_half_size + room_center) | (mean < -room_half_size + room_center)
outside_extrema = (maxs > room_half_size + room_center + wall_distance) | (
mins < -room_half_size + room_center - wall_distance
)
if bool(np.any(outside_mean) or np.any(outside_extrema)):
pose[:, 3] = 0.001
def add_missing_joints(poses: list[Pose3D], joint_names: tuple[str, ...], vis_threshold: float) -> None:
name_to_index = {name: index for index, name in enumerate(joint_names)}
adjacent_lookup = {
"hip_right": ["hip_middle", "hip_left"],
"hip_left": ["hip_middle", "hip_right"],
"knee_right": ["hip_right", "knee_left"],
"knee_left": ["hip_left", "knee_right"],
"ankle_right": ["knee_right", "ankle_left"],
"ankle_left": ["knee_left", "ankle_right"],
"shoulder_right": ["shoulder_middle", "shoulder_left"],
"shoulder_left": ["shoulder_middle", "shoulder_right"],
"elbow_right": ["shoulder_right", "hip_right"],
"elbow_left": ["shoulder_left", "hip_left"],
"wrist_right": ["elbow_right"],
"wrist_left": ["elbow_left"],
"nose": ["shoulder_middle", "shoulder_right", "shoulder_left"],
"head": ["shoulder_middle", "shoulder_right", "shoulder_left"],
"foot_*_left_*": ["ankle_left"],
"foot_*_right_*": ["ankle_right"],
"face_*": ["nose"],
"hand_*_left_*": ["wrist_left"],
"hand_*_right_*": ["wrist_right"],
}
for pose in poses:
valid = pose[:, 3] > vis_threshold
if not np.any(valid):
continue
body_center = np.mean(pose[valid, :3], axis=0)
for joint_index, joint_name in enumerate(joint_names):
if pose[joint_index, 3] != 0.0:
continue
adjacent_name = ""
if joint_name.startswith("foot_") and "_left" in joint_name:
adjacent_name = "foot_*_left_*"
elif joint_name.startswith("foot_") and "_right" in joint_name:
adjacent_name = "foot_*_right_*"
elif joint_name.startswith("face_"):
adjacent_name = "face_*"
elif joint_name.startswith("hand_") and "_left" in joint_name:
adjacent_name = "hand_*_left_*"
elif joint_name.startswith("hand_") and "_right" in joint_name:
adjacent_name = "hand_*_right_*"
elif joint_name in adjacent_lookup:
adjacent_name = joint_name
if not adjacent_name:
continue
adjacent_position = body_center
adjacent_names = adjacent_lookup.get(adjacent_name, [])
adjacent_positions: list[np.ndarray] = []
for adjacent_joint_name in adjacent_names:
adjacent_joint_index = name_to_index.get(adjacent_joint_name)
if adjacent_joint_index is None:
continue
adjacent_joint = pose[adjacent_joint_index]
if adjacent_joint[3] <= vis_threshold:
continue
adjacent_positions.append(adjacent_joint[:3])
if adjacent_positions:
adjacent_position = np.mean(np.stack(adjacent_positions, axis=0), axis=0)
pose[joint_index, :3] = adjacent_position
pose[joint_index, 3] = 0.1
def replace_far_joints(poses: list[Pose3D], joint_names: tuple[str, ...], min_match_score: float) -> None:
offset = (1.0 - min_match_score) * 2.0
min_score = min_match_score - offset
max_dist_head_sq = 0.20 * 0.20
max_dist_foot_sq = 0.25 * 0.25
max_dist_hand_sq = 0.20 * 0.20
for pose in poses:
center_head = np.zeros((4,), dtype=np.float32)
center_foot_left = np.zeros((4,), dtype=np.float32)
center_foot_right = np.zeros((4,), dtype=np.float32)
center_hand_left = np.zeros((4,), dtype=np.float32)
center_hand_right = np.zeros((4,), dtype=np.float32)
for joint_index, joint_name in enumerate(joint_names):
joint = pose[joint_index]
if joint[3] <= min_score:
continue
if joint_name.startswith("face_") or joint_name in {
"nose",
"eye_left",
"eye_right",
"ear_left",
"ear_right",
}:
center_head[:3] += joint[:3]
center_head[3] += 1.0
elif joint_name.startswith("foot_") or joint_name.startswith("ankle_"):
if "_left" in joint_name:
center_foot_left[:3] += joint[:3]
center_foot_left[3] += 1.0
elif "_right" in joint_name:
center_foot_right[:3] += joint[:3]
center_foot_right[3] += 1.0
elif joint_name.startswith("hand_") or joint_name.startswith("wrist_"):
if "_left" in joint_name:
center_hand_left[:3] += joint[:3]
center_hand_left[3] += 1.0
elif "_right" in joint_name:
center_hand_right[:3] += joint[:3]
center_hand_right[3] += 1.0
for center in (
center_head,
center_foot_left,
center_foot_right,
center_hand_left,
center_hand_right,
):
if center[3] > 0.0:
center[:3] /= center[3]
for joint_index, joint_name in enumerate(joint_names):
center = None
max_dist_sq = 0.0
if joint_name.startswith("face_") or joint_name in {
"nose",
"eye_left",
"eye_right",
"ear_left",
"ear_right",
}:
center = center_head
max_dist_sq = max_dist_head_sq
elif joint_name.startswith("foot_") or joint_name.startswith("ankle_"):
center = center_foot_left if "_left" in joint_name else center_foot_right
max_dist_sq = max_dist_foot_sq
elif joint_name.startswith("hand_") or joint_name.startswith("wrist_"):
center = center_hand_left if "_left" in joint_name else center_hand_right
max_dist_sq = max_dist_hand_sq
if center is None or center[3] <= 0.0:
continue
joint = pose[joint_index]
distance_sq = float(np.sum((joint[:3] - center[:3]) ** 2))
if (joint[3] > 0.0 and distance_sq > max_dist_sq) or joint[3] == 0.0:
joint[:3] = center[:3]
joint[3] = 0.1
@dataclass(slots=True)
class Track:
skeleton: Pose3D
last_detections: list[Pose3D]
class RgbdViewMerger:
def __init__(self, joint_names: tuple[str, ...], num_views: int, max_distance: float) -> None:
self.joint_names = joint_names
self.num_views = float(num_views)
self.max_distance = float(max_distance)
self.min_num_kpts = 7
self.merge_distance = 0.3
self.vis_threshold = 0.1
self.neighbor_joints: dict[str, list[str]] = {
"chest": [
"shoulder_left",
"shoulder_right",
"shoulder_middle",
"hip_left",
"hip_right",
"hip_middle",
],
"head": [
"nose",
"eye_left",
"eye_right",
"ear_left",
"ear_right",
"shoulder_left",
"shoulder_right",
"shoulder_middle",
"neck",
],
"nose": [
"eye_left",
"eye_right",
"ear_left",
"ear_right",
"shoulder_left",
"shoulder_right",
"shoulder_middle",
"neck",
],
"eye_left": [
"nose",
"eye_right",
"ear_left",
"ear_right",
"shoulder_left",
"shoulder_middle",
],
"eye_right": [
"nose",
"eye_left",
"ear_left",
"ear_right",
"shoulder_right",
"shoulder_middle",
],
"ear_left": [
"nose",
"ear_right",
"eye_left",
"eye_right",
"shoulder_left",
"shoulder_middle",
],
"ear_right": [
"nose",
"ear_left",
"eye_left",
"eye_right",
"shoulder_right",
"shoulder_middle",
],
"shoulder_left": [
"nose",
"eye_left",
"ear_left",
"elbow_left",
"hip_left",
"shoulder_middle",
"neck",
],
"shoulder_right": [
"nose",
"eye_right",
"ear_right",
"elbow_right",
"hip_right",
"shoulder_middle",
"neck",
],
"shoulder_middle": [
"nose",
"eye_left",
"eye_right",
"ear_left",
"ear_right",
"elbow_left",
"elbow_right",
"hip_middle",
"shoulder_left",
"shoulder_right",
"neck",
],
"neck": [
"nose",
"eye_left",
"eye_right",
"shoulder_left",
"shoulder_right",
"shoulder_middle",
],
"elbow_left": ["shoulder_left", "wrist_left", "shoulder_middle", "neck"],
"elbow_right": ["shoulder_right", "wrist_right", "shoulder_middle", "neck"],
"wrist_left": ["elbow_left"],
"wrist_right": ["elbow_right"],
"hip_left": ["hip_right", "knee_left", "shoulder_left", "hip_middle"],
"hip_right": ["hip_left", "knee_right", "shoulder_right", "hip_middle"],
"hip_middle": [
"hip_left",
"hip_right",
"knee_right",
"knee_left",
"shoulder_middle",
"neck",
],
"knee_left": ["hip_left", "hip_middle", "hip_right", "ankle_left"],
"knee_right": ["hip_right", "hip_middle", "hip_left", "ankle_right"],
"ankle_left": ["knee_left"],
"ankle_right": ["knee_right"],
}
self.neighbor_joints_ids: dict[str, list[int]] = {}
self.tracks: list[Track] = []
for joint_name, neighbors in self.neighbor_joints.items():
self.neighbor_joints_ids[joint_name] = [
self.joint_names.index(name) for name in neighbors if name in self.joint_names
]
def consume_view(self, view_poses: list[Pose3D]) -> None:
if not view_poses:
return
filtered_detections = self._drop_outlier_joints(view_poses)
matches, new_detection_indices, _outdated_track_indices = self._data_association(filtered_detections)
for track_index, detection_index in matches:
track = self.tracks[track_index]
while len(track.last_detections) >= int(self.num_views):
track.last_detections.pop(0)
track.last_detections.append(_clone_pose(filtered_detections[detection_index]))
for track in self.tracks:
self._update_track_position(track)
for detection_index in new_detection_indices:
pose = _clone_pose(filtered_detections[detection_index])
self.tracks.append(Track(skeleton=pose, last_detections=[_clone_pose(pose)]))
self._merge_tracks(self.merge_distance)
for track in self.tracks:
self._update_track_position(track)
self._merge_tracks(self.merge_distance)
for track in self.tracks:
self._update_track_position(track)
def finalize(self) -> list[Pose3D]:
poses: list[Pose3D] = []
for track in self.tracks:
visible_joints = int(np.count_nonzero(track.skeleton[:, 3] > self.vis_threshold))
if visible_joints >= self.min_num_kpts:
poses.append(_clone_pose(track.skeleton))
return poses
def _neighbor_center(self, skeleton: Pose3D, joint_name: str) -> np.ndarray:
mean = np.zeros((4,), dtype=np.float32)
neighbor_ids = self.neighbor_joints_ids.get(joint_name)
if not neighbor_ids:
return mean
neighbor_joints = skeleton[neighbor_ids]
visible = neighbor_joints[:, 3] > self.vis_threshold
if not np.any(visible):
return mean
mean[:] = np.mean(neighbor_joints[visible], axis=0)
return mean
def _centroid(self, skeleton: Pose3D) -> np.ndarray:
if "chest" in self.joint_names:
chest_index = self.joint_names.index("chest")
if skeleton[chest_index, 3] > 0.0:
return np.array(skeleton[chest_index, :3], dtype=np.float32, copy=True)
center = self._neighbor_center(skeleton, "chest")
return np.array(center[:3], dtype=np.float32, copy=True)
def _drop_outlier_joints(self, detections: list[Pose3D]) -> list[Pose3D]:
filtered: list[Pose3D] = []
for detection in detections:
pose = _clone_pose(detection)
hip_center = self._neighbor_center(pose, "hip_middle")
if hip_center[3] > 0.0:
max_dist_sq = (self.max_distance * 3.5) ** 2
distances_sq = np.sum((pose[:, :3] - hip_center[:3]) ** 2, axis=1)
pose[distances_sq > max_dist_sq, 3] = 0.0
hip_center = self._neighbor_center(pose, "hip_middle")
if hip_center[3] > 0.0:
max_dist_sq = (self.max_distance * 2.5) ** 2
distances_sq = np.sum((pose[:, :3] - hip_center[:3]) ** 2, axis=1)
pose[distances_sq > max_dist_sq, 3] = 0.0
max_dist_sq = self.max_distance * self.max_distance
for joint_index, joint_name in enumerate(self.joint_names):
center = self._neighbor_center(pose, joint_name)
if center[3] <= 0.0:
continue
distance_sq = float(np.sum((pose[joint_index, :3] - center[:3]) ** 2))
if distance_sq > max_dist_sq:
pose[joint_index, 3] = 0.0
if float(np.sum(pose[:, 3])) > 0.0:
filtered.append(pose)
return filtered
def _data_association(self, new_poses: list[Pose3D]) -> tuple[list[tuple[int, int]], list[int], list[int]]:
matches: list[tuple[int, int]] = []
new_detection_indices: list[int] = []
outdated_track_indices: list[int] = []
if not self.tracks:
return matches, list(range(len(new_poses))), outdated_track_indices
track_centroids = np.stack([self._centroid(track.skeleton) for track in self.tracks], axis=0)
detection_centroids = np.stack([self._centroid(pose) for pose in new_poses], axis=0)
deltas = track_centroids[:, None, :] - detection_centroids[None, :, :]
cost_matrix = np.sqrt(np.sum(deltas * deltas, axis=2, dtype=np.float32), dtype=np.float32)
row_indices, col_indices = linear_sum_assignment(cost_matrix)
matched_tracks: set[int] = set()
matched_detections: set[int] = set()
for track_index, detection_index in zip(row_indices.tolist(), col_indices.tolist(), strict=True):
cost = float(cost_matrix[track_index, detection_index])
if cost < self.max_distance:
matches.append((track_index, detection_index))
matched_tracks.add(track_index)
matched_detections.add(detection_index)
for detection_index in range(len(new_poses)):
if detection_index not in matched_detections:
new_detection_indices.append(detection_index)
for track_index in range(len(self.tracks)):
if track_index not in matched_tracks:
outdated_track_indices.append(track_index)
return matches, new_detection_indices, outdated_track_indices
def _update_track_position(self, track: Track) -> None:
current_pose = track.skeleton
num_joints = current_pose.shape[0]
num_detections = len(track.last_detections)
neighbor_centers = np.zeros((num_joints, 3), dtype=np.float32)
for joint_index, joint_name in enumerate(self.joint_names):
neighbor_ids = self.neighbor_joints_ids.get(joint_name)
if not neighbor_ids:
continue
neighbor_joints = current_pose[neighbor_ids]
visible = neighbor_joints[:, 3] > self.vis_threshold
if np.any(visible):
neighbor_centers[joint_index] = np.mean(neighbor_joints[visible, :3], axis=0)
new_pose = np.zeros_like(current_pose, dtype=np.float32)
max_dist_sq = self.max_distance * self.max_distance
for joint_index in range(num_joints):
accum = np.zeros((4,), dtype=np.float32)
count = 0
for detection in track.last_detections:
joint = detection[joint_index]
if joint[3] <= 0.0:
continue
distance_sq = float(np.sum((joint[:3] - neighbor_centers[joint_index]) ** 2))
if distance_sq <= max_dist_sq:
accum += joint
count += 1
if count > 0:
new_pose[joint_index] = accum / float(count)
else:
factor = (self.num_views - 1.0) / self.num_views
new_pose[joint_index] = current_pose[joint_index]
new_pose[joint_index, 3] *= factor
topk = 3
for joint_index in range(num_joints):
valid: list[tuple[float, int]] = []
for detection_index, detection in enumerate(track.last_detections):
joint = detection[joint_index]
if joint[3] <= 0.0:
continue
distance_sq = float(np.sum((joint[:3] - new_pose[joint_index, :3]) ** 2))
if distance_sq <= max_dist_sq:
valid.append((distance_sq, detection_index))
if len(valid) <= topk:
continue
valid.sort(key=lambda item: item[0])
keep = valid[:topk]
accum = np.zeros((4,), dtype=np.float32)
for _distance_sq, detection_index in keep:
accum += track.last_detections[detection_index][joint_index]
new_pose[joint_index] = accum / float(len(keep))
track.skeleton = new_pose
def _merge_tracks(self, distance_threshold: float) -> None:
count = len(self.tracks)
merged_indices: set[int] = set()
merged_tracks: list[Track] = []
skeletons = [_clone_pose(track.skeleton) for track in self.tracks]
for first in range(count):
if first in merged_indices:
continue
merged_track = Track(
skeleton=_clone_pose(self.tracks[first].skeleton),
last_detections=[_clone_pose(detection) for detection in self.tracks[first].last_detections],
)
merged_tracks.append(merged_track)
current_skeleton = skeletons[first]
for second in range(first + 1, count):
if second in merged_indices:
continue
close = _all_visible_joints_close(
current_skeleton,
skeletons[second],
distance_threshold,
self.vis_threshold,
)
if not close:
continue
num_last = len(merged_track.last_detections)
num_current = len(self.tracks[second].last_detections)
for joint_index in range(current_skeleton.shape[0]):
joint1 = current_skeleton[joint_index]
joint2 = skeletons[second][joint_index]
total_score = float(joint1[3] + joint2[3])
if total_score <= 0.0:
continue
inv_score = 1.0 / total_score
inv_count = 1.0 / float(num_last + num_current)
weight1 = (float(joint1[3]) * inv_score) * (float(num_last) * inv_count)
weight2 = (float(joint2[3]) * inv_score) * (float(num_current) * inv_count)
inv_weight = 1.0 / (weight1 + weight2)
weight1 *= inv_weight
weight2 *= inv_weight
merged_track.skeleton[joint_index] = joint1 * weight1 + joint2 * weight2
merged_track.last_detections.extend(
_clone_pose(detection) for detection in self.tracks[second].last_detections
)
merged_indices.add(second)
self.tracks = merged_tracks
def merge_pose_views(
poses_3d_by_view: np.ndarray,
person_counts: np.ndarray,
config: ReconstructionConfig,
*,
max_distance: float = 0.5,
) -> np.ndarray:
merger = RgbdViewMerger(config.joint_names, poses_3d_by_view.shape[0], max_distance)
for view_index in range(poses_3d_by_view.shape[0]):
count = int(person_counts[view_index])
view_poses = [_clone_pose(poses_3d_by_view[view_index, person_index]) for person_index in range(count)]
merger.consume_view(view_poses)
merged = merger.finalize()
add_extra_joints(merged, config.joint_names)
filter_poses(merged, config.roomparams)
add_missing_joints(merged, config.joint_names, 0.1)
replace_far_joints(merged, config.joint_names, config.min_match_score)
if not merged:
return _empty_pose_batch(len(config.joint_names))
return np.stack(merged, axis=0).astype(np.float32, copy=False)
+30
View File
@@ -0,0 +1,30 @@
from collections.abc import Sequence
from typing import Literal, NotRequired, TypeAlias, TypedDict
import numpy as np
import numpy.typing as npt
from jaxtyping import Float, UInt
Matrix3x3Like: TypeAlias = Sequence[Sequence[float]]
VectorLike: TypeAlias = Sequence[float]
RoomParamsLike: TypeAlias = npt.NDArray[np.generic] | 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]]
PoseBatch2D = Float[np.ndarray, "views max_persons joints 3"]
PoseBatchUVD = Float[np.ndarray, "views max_persons joints 4"]
PoseBatch3DByView = Float[np.ndarray, "views max_persons joints 4"]
PoseBatch3D = Float[np.ndarray, "persons joints 4"]
PersonCounts = UInt[np.ndarray, " views"]
class CameraDict(TypedDict):
name: str
K: Matrix3x3Like
DC: VectorLike
R: Matrix3x3Like
T: Sequence[Sequence[float]] | Sequence[float]
width: int
height: int
type: NotRequired[Literal["pinhole", "fisheye"]]
model: NotRequired[Literal["pinhole", "fisheye"]]
+59
View File
@@ -0,0 +1,59 @@
from pathlib import Path
import click
import numpy as np
from .core import reconstruct_rgbd
from .models import make_camera, make_reconstruction_config
def _default_fixture_path() -> Path:
return Path(__file__).resolve().parents[2] / "tests" / "fixtures" / "single_person_two_views.npz"
@click.command()
@click.option(
"--fixture",
"fixture_path",
default=_default_fixture_path,
type=click.Path(exists=True, dir_okay=False, path_type=Path),
show_default=True,
)
@click.option(
"--output",
default=Path("reconstructed_poses.npy"),
type=click.Path(dir_okay=False, path_type=Path),
show_default=True,
)
def main(fixture_path: Path, output: Path) -> None:
"""Run the RGBD reference pipeline on a frozen fixture."""
fixture = np.load(fixture_path, allow_pickle=True)
cameras = [
make_camera(
name=f"Camera {index + 1}",
K=fixture["K"][index],
DC=fixture["DC"][index],
R=fixture["R"][index],
T=fixture["T"][index],
width=int(fixture["widths"][index]),
height=int(fixture["heights"][index]),
model=str(fixture["models"][index]),
)
for index in range(int(fixture["K"].shape[0]))
]
config = make_reconstruction_config(
cameras=cameras,
roomparams=np.stack((fixture["room_size"], fixture["room_center"]), axis=0),
joint_names=fixture["joint_names"].tolist(),
)
result = reconstruct_rgbd(
fixture["poses_2d"],
fixture["person_counts"],
fixture["depth_images"],
config,
)
np.save(output, result)
click.echo(f"Saved reconstructed poses to {output}")
click.echo(f"Output shape: {tuple(result.shape)}")
+258
View File
@@ -0,0 +1,258 @@
from collections.abc import Sequence
import numpy as np
import numpy.typing as npt
from beartype import beartype
from jaxtyping import jaxtyped
from ._merger import merge_pose_views
from ._typing import (
DepthImageLike,
PersonCounts,
PoseBatch2D,
PoseBatch3D,
PoseBatch3DByView,
PoseBatchUVD,
PoseViewLike,
)
from .models import Camera, CameraLike, ReconstructionConfig, convert_cameras
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_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 _normalize_depth_images(
depth_images: npt.NDArray[np.generic] | Sequence[DepthImageLike],
) -> list[npt.NDArray[np.float32]]:
if isinstance(depth_images, np.ndarray):
array = np.asarray(depth_images, dtype=np.float32)
if array.ndim == 2:
return [_coerce_depth_image(array)]
if array.ndim == 3:
return [_coerce_depth_image(array[index]) for index in range(array.shape[0])]
if array.ndim == 4 and array.shape[-1] == 1:
return [_coerce_depth_image(array[index]) for index in range(array.shape[0])]
raise ValueError("depth_images arrays must have shape [views, height, width] or [views, height, width, 1].")
return [_coerce_depth_image(image) for image in depth_images]
@beartype
def pack_poses_2d(
views: Sequence[PoseViewLike],
*,
joint_count: int | None = None,
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.uint32]]:
normalized: list[npt.NDArray[np.float32]] = []
inferred_joint_count = joint_count
for view in views:
array = np.asarray(view, dtype=np.float32)
if array.size == 0:
normalized.append(np.zeros((0, 0, 3), dtype=np.float32))
continue
if array.ndim == 2:
if array.shape[-1] != 3:
raise ValueError("Single-person pose inputs must have shape [joints, 3].")
array = array[np.newaxis, :, :]
elif array.ndim != 3 or array.shape[-1] != 3:
raise ValueError("Each view must have shape [persons, joints, 3] or [joints, 3].")
if inferred_joint_count is None:
inferred_joint_count = int(array.shape[1])
elif int(array.shape[1]) != inferred_joint_count:
raise ValueError("All views must use the same joint count.")
normalized.append(np.ascontiguousarray(array, dtype=np.float32))
if inferred_joint_count is None:
raise ValueError("joint_count is required when all views are empty.")
fixed_views: list[npt.NDArray[np.float32]] = []
max_persons = 0
for array in normalized:
if array.size == 0:
array = np.zeros((0, inferred_joint_count, 3), dtype=np.float32)
elif int(array.shape[1]) != inferred_joint_count:
raise ValueError("All views must use the same joint count.")
max_persons = max(max_persons, int(array.shape[0]))
fixed_views.append(array)
packed = np.zeros((len(fixed_views), max_persons, inferred_joint_count, 3), dtype=np.float32)
counts = np.zeros((len(fixed_views),), dtype=np.uint32)
for view_index, array in enumerate(fixed_views):
person_count = int(array.shape[0])
counts[view_index] = person_count
if person_count:
packed[view_index, :person_count] = array
return packed, counts
@jaxtyped(typechecker=beartype)
def sample_depth_for_poses(
poses_2d: PoseBatch2D,
person_counts: PersonCounts,
depth_images: npt.NDArray[np.generic] | Sequence[DepthImageLike],
*,
window_size: int = 7,
) -> PoseBatchUVD:
poses = np.asarray(poses_2d, dtype=np.float32)
counts = np.asarray(person_counts, dtype=np.uint32)
depth_image_list = _normalize_depth_images(depth_images)
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_image_list) != 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.")
poses_uvd = np.zeros((poses.shape[0], poses.shape[1], poses.shape[2], 4), dtype=np.float32)
for view_index, depth in enumerate(depth_image_list):
poses_uvd[view_index, :, :, :2] = poses[view_index, :, :, :2]
poses_uvd[view_index, :, :, 3] = poses[view_index, :, :, 2]
valid_persons = int(counts[view_index])
if valid_persons == 0:
continue
joints_xy = poses[view_index, :valid_persons, :, :2].astype(np.int32, copy=False).reshape(-1, 2)
scores = poses[view_index, :valid_persons, :, 2:3].reshape(-1, 1)
depth_padded = np.pad(depth, window_size, mode="constant", constant_values=0)
y_indices = np.arange(-window_size // 2, window_size // 2 + 1, dtype=np.int32)
x_indices = np.arange(-window_size, window_size + 1, dtype=np.int32)
vertical_grid = np.add.outer(joints_xy[:, 1], y_indices) + window_size
horizontal_grid = np.add.outer(joints_xy[:, 0], x_indices) + window_size
vertical_depths = depth_padded[vertical_grid, joints_xy[:, 0, None] + window_size]
horizontal_depths = depth_padded[joints_xy[:, 1, None] + window_size, horizontal_grid]
all_depths = np.concatenate((vertical_depths, horizontal_depths), axis=1).astype(np.float32)
all_depths[all_depths <= 0.0] = np.nan
with np.errstate(all="ignore"):
sampled_depths = np.nanmedian(all_depths, axis=1)
sampled_depths = np.where(np.isnan(sampled_depths), 0.0, sampled_depths).astype(np.float32)
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_index, :valid_persons, :, 2:3] = sampled_depths
poses_uvd[view_index, :valid_persons] *= np.concatenate((valid_mask, valid_mask, valid_mask, valid_mask), axis=-1)
return poses_uvd
@jaxtyped(typechecker=beartype)
def apply_depth_offsets(
poses_uvd: PoseBatchUVD,
joint_names: Sequence[str],
) -> PoseBatchUVD:
poses = np.asarray(poses_uvd, dtype=np.float32)
if len(joint_names) != poses.shape[2]:
raise ValueError("joint_names must have the same number of joints as poses_uvd.")
result = np.array(poses, dtype=np.float32, copy=True)
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
@jaxtyped(typechecker=beartype)
def lift_depth_poses_to_world(
poses_uvd: PoseBatchUVD,
cameras: Sequence[CameraLike],
) -> PoseBatch3DByView:
poses = np.asarray(poses_uvd, dtype=np.float32)
converted_cameras = convert_cameras(list(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_index, camera in enumerate(converted_cameras):
uv = poses[view_index, :, :, :2].reshape(-1, 2)
depth_mm = poses[view_index, :, :, 2:3].reshape(-1, 1)
scores = poses[view_index, :, :, 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_index] = pose_world
return result
@jaxtyped(typechecker=beartype)
def merge_rgbd_views(
poses_3d_by_view: PoseBatch3DByView,
person_counts: PersonCounts,
config: ReconstructionConfig,
*,
max_distance: float = 0.5,
) -> PoseBatch3D:
poses = np.asarray(poses_3d_by_view, dtype=np.float32)
counts = np.asarray(person_counts, dtype=np.uint32)
if counts.ndim != 1 or counts.shape[0] != poses.shape[0]:
raise ValueError("person_counts must be a 1D array aligned with poses_3d_by_view.")
if poses.shape[0] != len(config.cameras):
raise ValueError("Number of cameras and 3D views must be the same.")
if poses.shape[2] != len(config.joint_names):
raise ValueError("Number of joint names and 3D poses must be the same.")
if np.any(counts > poses.shape[1]):
raise ValueError("person_counts entries must not exceed the padded person dimension.")
return merge_pose_views(poses, counts, config, max_distance=float(max_distance))
@jaxtyped(typechecker=beartype)
def reconstruct_rgbd(
poses_2d: PoseBatch2D,
person_counts: PersonCounts,
depth_images: npt.NDArray[np.generic] | Sequence[DepthImageLike],
config: ReconstructionConfig,
*,
window_size: int = 7,
max_distance: float = 0.5,
apply_offsets: bool = True,
) -> PoseBatch3D:
poses_uvd = sample_depth_for_poses(poses_2d, person_counts, depth_images, window_size=window_size)
if apply_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)
+197
View File
@@ -0,0 +1,197 @@
from collections.abc import Sequence
from dataclasses import dataclass
from enum import StrEnum
from typing import cast
import numpy as np
from ._typing import CameraDict, Matrix3x3Like, RoomParamsLike, VectorLike
class CameraModel(StrEnum):
PINHOLE = "pinhole"
FISHEYE = "fisheye"
def _freeze_array(array: np.ndarray) -> np.ndarray:
frozen = np.ascontiguousarray(array, dtype=np.float32)
frozen.setflags(write=False)
return frozen
def _normalize_matrix3x3(value: Matrix3x3Like | np.ndarray, *, name: str) -> np.ndarray:
array = np.asarray(value, dtype=np.float32)
if array.shape != (3, 3):
raise ValueError(f"{name} must have shape [3, 3].")
return _freeze_array(array)
def _normalize_translation(value: list[list[float]] | list[float] | np.ndarray | tuple[float, ...]) -> np.ndarray:
array = np.asarray(value, dtype=np.float32)
if array.size != 3:
raise ValueError("T must contain exactly three values.")
return _freeze_array(array.reshape(3, 1))
def _normalize_distortion(value: VectorLike | np.ndarray, *, model: CameraModel) -> np.ndarray:
array = np.asarray(value, dtype=np.float32).reshape(-1)
expected = 4 if model is CameraModel.FISHEYE else 5
if array.size not in {expected, 5}:
raise ValueError(
f"{model.value} cameras require {expected} distortion coefficients"
+ (" (or 5 with a trailing zero)." if model is CameraModel.FISHEYE else ".")
)
if model is CameraModel.FISHEYE and array.size == 4:
array = np.concatenate((array, np.asarray([0.0], dtype=np.float32)))
if array.size != 5:
raise ValueError("Distortion coefficients must normalize to exactly 5 values.")
return _freeze_array(array)
def _coerce_camera_model(model: CameraModel | str) -> CameraModel:
if isinstance(model, CameraModel):
return model
normalized = str(model).strip().lower()
if normalized == "pinhole":
return CameraModel.PINHOLE
if normalized == "fisheye":
return CameraModel.FISHEYE
raise ValueError(f"Unsupported camera model: {model}")
@dataclass(slots=True, frozen=True)
class Camera:
name: str
K: np.ndarray
distortion: np.ndarray
R: np.ndarray
T: np.ndarray
width: int
height: int
model: CameraModel = CameraModel.PINHOLE
def __post_init__(self) -> None:
model = _coerce_camera_model(self.model)
object.__setattr__(self, "model", model)
object.__setattr__(self, "name", str(self.name))
object.__setattr__(self, "K", _normalize_matrix3x3(self.K, name="K"))
object.__setattr__(self, "R", _normalize_matrix3x3(self.R, name="R"))
object.__setattr__(self, "T", _normalize_translation(self.T))
object.__setattr__(self, "distortion", _normalize_distortion(self.distortion, model=model))
object.__setattr__(self, "width", int(self.width))
object.__setattr__(self, "height", int(self.height))
@dataclass(slots=True, frozen=True)
class RoomBounds:
size: np.ndarray
center: np.ndarray
def __post_init__(self) -> None:
size = np.asarray(self.size, dtype=np.float32).reshape(-1)
center = np.asarray(self.center, dtype=np.float32).reshape(-1)
if size.shape != (3,):
raise ValueError("room size must have shape [3].")
if center.shape != (3,):
raise ValueError("room center must have shape [3].")
object.__setattr__(self, "size", _freeze_array(size))
object.__setattr__(self, "center", _freeze_array(center))
@property
def roomparams(self) -> np.ndarray:
return np.stack((self.size, self.center), axis=0, dtype=np.float32)
@classmethod
def from_roomparams(cls, roomparams: RoomParamsLike) -> "RoomBounds":
array = np.asarray(roomparams, dtype=np.float32)
if array.shape != (2, 3):
raise ValueError("roomparams must have shape [2, 3].")
return cls(size=array[0], center=array[1])
@dataclass(slots=True, frozen=True)
class ReconstructionConfig:
cameras: tuple[Camera, ...]
room_bounds: RoomBounds
joint_names: tuple[str, ...]
min_match_score: float = 0.95
min_group_size: int = 1
def __post_init__(self) -> None:
if not self.cameras:
raise ValueError("At least one camera is required.")
if not self.joint_names:
raise ValueError("At least one joint name is required.")
object.__setattr__(self, "cameras", tuple(self.cameras))
object.__setattr__(self, "joint_names", tuple(str(name) for name in self.joint_names))
object.__setattr__(self, "min_match_score", float(self.min_match_score))
object.__setattr__(self, "min_group_size", int(self.min_group_size))
@property
def roomparams(self) -> np.ndarray:
return self.room_bounds.roomparams
CameraLike = Camera | CameraDict
def make_camera(
name: str,
K: Matrix3x3Like,
DC: VectorLike,
R: Matrix3x3Like,
T: list[list[float]] | list[float],
width: int,
height: int,
model: CameraModel | str = CameraModel.PINHOLE,
) -> Camera:
return Camera(
name=str(name),
K=np.asarray(K, dtype=np.float32),
distortion=np.asarray(DC, dtype=np.float32),
R=np.asarray(R, dtype=np.float32),
T=np.asarray(T, dtype=np.float32),
width=int(width),
height=int(height),
model=_coerce_camera_model(model),
)
def convert_cameras(cameras: Sequence[CameraLike]) -> list[Camera]:
converted: list[Camera] = []
for camera in cameras:
if isinstance(camera, Camera):
converted.append(camera)
continue
model = _coerce_camera_model(camera.get("model", camera.get("type", "pinhole")))
converted.append(
make_camera(
name=str(camera["name"]),
K=cast(Matrix3x3Like, camera["K"]),
DC=cast(VectorLike, camera["DC"]),
R=cast(Matrix3x3Like, camera["R"]),
T=cast(list[list[float]] | list[float], camera["T"]),
width=int(camera["width"]),
height=int(camera["height"]),
model=model,
)
)
return converted
def make_reconstruction_config(
cameras: Sequence[CameraLike],
roomparams: RoomParamsLike,
joint_names: Sequence[str],
*,
min_match_score: float = 0.95,
min_group_size: int = 1,
) -> ReconstructionConfig:
return ReconstructionConfig(
cameras=tuple(convert_cameras(cameras)),
room_bounds=RoomBounds.from_roomparams(roomparams),
joint_names=tuple(str(name) for name in joint_names),
min_match_score=float(min_match_score),
min_group_size=int(min_group_size),
)
+1
View File
@@ -0,0 +1 @@
Binary file not shown.
+259
View File
@@ -0,0 +1,259 @@
from pathlib import Path
import numpy as np
from click.testing import CliRunner
from rapid_pose_rgbd_example import (
Camera,
CameraModel,
apply_depth_offsets,
lift_depth_poses_to_world,
make_camera,
make_reconstruction_config,
merge_rgbd_views,
pack_poses_2d,
reconstruct_rgbd,
sample_depth_for_poses,
)
from rapid_pose_rgbd_example.cli import main as cli_main
ROOT = Path(__file__).resolve().parent
FIXTURE_PATH = ROOT / "fixtures" / "single_person_two_views.npz"
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_example(name: str) -> Camera:
return 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,
CameraModel.PINHOLE,
)
def make_config(num_views: int):
return make_reconstruction_config(
[make_camera_example(f"Camera {index + 1}") for index 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 load_frozen_fixture():
fixture = np.load(FIXTURE_PATH, allow_pickle=True)
cameras = [
make_camera(
f"Camera {index + 1}",
fixture["K"][index],
fixture["DC"][index],
fixture["R"][index],
fixture["T"][index],
int(fixture["widths"][index]),
int(fixture["heights"][index]),
str(fixture["models"][index]),
)
for index in range(int(fixture["K"].shape[0]))
]
config = make_reconstruction_config(
cameras,
np.stack((fixture["room_size"], fixture["room_center"]), axis=0),
fixture["joint_names"].tolist(),
)
return fixture, config
def test_pack_poses_2d_pads_and_counts():
poses_2d, person_counts = pack_poses_2d(
[
np.zeros((2, 3, 3), dtype=np.float32),
np.zeros((1, 3, 3), dtype=np.float32),
],
joint_count=3,
)
assert poses_2d.shape == (2, 2, 3, 3)
np.testing.assert_array_equal(person_counts, np.asarray([2, 1], dtype=np.uint32))
np.testing.assert_array_equal(poses_2d[1, 1], np.zeros((3, 3), 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 = 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_apply_depth_offsets_uses_joint_mapping_without_mutating_input():
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 = 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 = lift_depth_poses_to_world(poses_uvd, [make_camera_example("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 = sample_depth_for_poses(poses_2d, person_counts, depth_images)
poses_uvd = apply_depth_offsets(poses_uvd, JOINT_NAMES)
poses_3d_by_view = lift_depth_poses_to_world(poses_uvd, config.cameras)
merged = 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_for_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 = merge_rgbd_views(
lift_depth_poses_to_world(
apply_depth_offsets(sample_depth_for_poses(poses_2d, person_counts, depth_images), JOINT_NAMES),
config.cameras,
),
person_counts,
config,
)
reconstructed = 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 int(np.count_nonzero(reconstructed[0, :, 3] > 0.0)) >= 7
def test_reconstruct_rgbd_matches_frozen_reference_fixture():
fixture, config = load_frozen_fixture()
result = reconstruct_rgbd(
fixture["poses_2d"],
fixture["person_counts"],
fixture["depth_images"],
config,
)
np.testing.assert_allclose(result, fixture["expected_poses_3d"], rtol=1e-4, atol=1e-4)
def test_reconstruct_rgbd_is_repeatable():
fixture, config = load_frozen_fixture()
first = reconstruct_rgbd(
fixture["poses_2d"],
fixture["person_counts"],
fixture["depth_images"],
config,
)
second = reconstruct_rgbd(
fixture["poses_2d"],
fixture["person_counts"],
fixture["depth_images"],
config,
)
np.testing.assert_allclose(first, second, rtol=1e-6, atol=1e-6)
def test_cli_writes_result_file(tmp_path: Path):
output_path = tmp_path / "result.npy"
runner = CliRunner()
result = runner.invoke(
cli_main,
["--fixture", str(FIXTURE_PATH), "--output", str(output_path)],
)
assert result.exit_code == 0, result.output
assert output_path.exists()
saved = np.load(output_path)
assert saved.shape == (1, len(JOINT_NAMES), 4)
Generated
+317
View File
@@ -0,0 +1,317 @@
version = 1
revision = 3
requires-python = ">=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]]
name = "beartype"
version = "0.22.9"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
]
[[package]]
name = "click"
version = "8.3.1"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
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" },
]
[[package]]
name = "jaxtyping"
version = "0.3.9"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "wadler-lindig" },
]
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]]
name = "numpy"
version = "2.4.3"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" },
]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "rapid-pose-rgbd-python-example"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "beartype" },
{ name = "click" },
{ name = "jaxtyping" },
{ name = "numpy" },
{ name = "scipy" },
]
[package.dev-dependencies]
dev = [
{ name = "basedpyright" },
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "beartype", specifier = ">=0.19" },
{ name = "click", specifier = ">=8.1" },
{ name = "jaxtyping", specifier = ">=0.3.2" },
{ name = "numpy", specifier = ">=2.0" },
{ name = "scipy", specifier = ">=1.13" },
]
[package.metadata.requires-dev]
dev = [
{ name = "basedpyright", specifier = ">=1.29" },
{ name = "pytest", specifier = ">=8.3" },
]
[[package]]
name = "scipy"
version = "1.17.1"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
]
[[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" },
]