From 30252107414fc76d0b443582f7af19347a7ca66b Mon Sep 17 00:00:00 2001 From: crosstyan Date: Thu, 26 Mar 2026 13:11:45 +0800 Subject: [PATCH] feat: add standalone RGBD Python reference project --- .gitignore | 7 + README.md | 38 ++ pyproject.toml | 41 ++ src/rapid_pose_rgbd_example/__init__.py | 35 ++ src/rapid_pose_rgbd_example/__main__.py | 4 + src/rapid_pose_rgbd_example/_merger.py | 619 +++++++++++++++++++++ src/rapid_pose_rgbd_example/_typing.py | 30 + src/rapid_pose_rgbd_example/cli.py | 59 ++ src/rapid_pose_rgbd_example/core.py | 258 +++++++++ src/rapid_pose_rgbd_example/models.py | 197 +++++++ src/rapid_pose_rgbd_example/py.typed | 1 + tests/fixtures/single_person_two_views.npz | Bin 0 -> 530080 bytes tests/test_rgbd_pipeline.py | 259 +++++++++ uv.lock | 317 +++++++++++ 14 files changed, 1865 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/rapid_pose_rgbd_example/__init__.py create mode 100644 src/rapid_pose_rgbd_example/__main__.py create mode 100644 src/rapid_pose_rgbd_example/_merger.py create mode 100644 src/rapid_pose_rgbd_example/_typing.py create mode 100644 src/rapid_pose_rgbd_example/cli.py create mode 100644 src/rapid_pose_rgbd_example/core.py create mode 100644 src/rapid_pose_rgbd_example/models.py create mode 100644 src/rapid_pose_rgbd_example/py.typed create mode 100644 tests/fixtures/single_person_two_views.npz create mode 100644 tests/test_rgbd_pipeline.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dad8a07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.venv/ +.pytest_cache/ +__pycache__/ +*.pyc +*.pyo +*.npy +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..84b52a9 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..efa915f --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/src/rapid_pose_rgbd_example/__init__.py b/src/rapid_pose_rgbd_example/__init__.py new file mode 100644 index 0000000..66ca138 --- /dev/null +++ b/src/rapid_pose_rgbd_example/__init__.py @@ -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", +] diff --git a/src/rapid_pose_rgbd_example/__main__.py b/src/rapid_pose_rgbd_example/__main__.py new file mode 100644 index 0000000..9ae637f --- /dev/null +++ b/src/rapid_pose_rgbd_example/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main() diff --git a/src/rapid_pose_rgbd_example/_merger.py b/src/rapid_pose_rgbd_example/_merger.py new file mode 100644 index 0000000..ba1f01a --- /dev/null +++ b/src/rapid_pose_rgbd_example/_merger.py @@ -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) diff --git a/src/rapid_pose_rgbd_example/_typing.py b/src/rapid_pose_rgbd_example/_typing.py new file mode 100644 index 0000000..017be0a --- /dev/null +++ b/src/rapid_pose_rgbd_example/_typing.py @@ -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"]] diff --git a/src/rapid_pose_rgbd_example/cli.py b/src/rapid_pose_rgbd_example/cli.py new file mode 100644 index 0000000..4e650ce --- /dev/null +++ b/src/rapid_pose_rgbd_example/cli.py @@ -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)}") diff --git a/src/rapid_pose_rgbd_example/core.py b/src/rapid_pose_rgbd_example/core.py new file mode 100644 index 0000000..08bc1df --- /dev/null +++ b/src/rapid_pose_rgbd_example/core.py @@ -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) diff --git a/src/rapid_pose_rgbd_example/models.py b/src/rapid_pose_rgbd_example/models.py new file mode 100644 index 0000000..2bb39a4 --- /dev/null +++ b/src/rapid_pose_rgbd_example/models.py @@ -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), + ) diff --git a/src/rapid_pose_rgbd_example/py.typed b/src/rapid_pose_rgbd_example/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/rapid_pose_rgbd_example/py.typed @@ -0,0 +1 @@ + diff --git a/tests/fixtures/single_person_two_views.npz b/tests/fixtures/single_person_two_views.npz new file mode 100644 index 0000000000000000000000000000000000000000..6f6125d01f3c632acf98395366545627280b6b41 GIT binary patch literal 530080 zcmeI$TWlOx835qfBu-qXNfY$~2vyy-s@$ediJhbsX=EqqC6%lurXrw=1jc zU8hY_75M=nBZ81|`wu+iMeJGkI9QeW0)0-&Y>k(O2GiM_>7=^5wUlEW6>pEPLocmdy=i+1AkC*XX~u zso&GI&n->;4Wa*)#{Rc8_5U8Wz17(MzOX&@<1&sH$9jKQrhXiEO=Ei;H}&K8m>>6# z+fzSo|7g?p)Q`tYxv`(lE%sB5&HexXdPfR64|LshA?e&2b>`JtWqR~z<<#_SEfJk> zU{1^SL{D`}^a^DzTJ9-@^NCR`W#Y~Ly{VKfp_K7_W_Dt9a%${&zF5gz*1b?mTDGsG z^P6p8_ub)k+kN*HmeiBY-TQ!W4H#g60R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_ z1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;= zV1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~ z0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz z7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|XgfB^;=V1NMz7+`<_1{h#~0R|Xg zfB^;=V1NMz7+_!}10%z0*KN=0?dI%@&phzZrAF(@uFHn+nw~jR%C=At2Ydu+Z_Ws5xH5RSwb3>i%Q!M|5XJ5NDmA^iefB$`{{udjgq~&Vs z-@S_B!zBy#ch-vaoxb4@hf{rVdLK#UrD;ve)t0yV>5bFZ+}f?T+g8jemVaXEuRm%o z|AVRgXB$~*`5!31e-#tF?CciH`SQs3-cIH8gxP#za(s58mTH=B)RdO(Yntrzj$K7L z3%7VrRthH+V)+_2h8iaF$>S5V|6@fwckt5d%@Z+I8P88H*6?~`BGR(`L>#%N!$;?; zCt@a?&t!<{5ECIPAx?(KL#*_-;>3OE$A>3U&6~o+RV$UL(c0wMJo!5p4i{TmwlBNG zUyPl7t1dgs%KK9kOL^%pUwU^c<(gE=(R_L~um1NIQ@^)*&xDA}(GUspvCy9lu~KWz zt%jZBjd46gyfIeFkGW$ZV$PL}+njqj0ZrW3!AL|_t5zpt}ol7UV%biO+ejMu) zA>v#;5u($&m&;vltZ?YX7RO2Z#q*DI(&@O%<)-7UG|%xGM+@=c#$%*=sME2R%k5exs z&kx?d%ku4t-)J=_MXAFKd^6Kmdy=)eBm3{F0OAm`}@NB z{zraV*niv5JB9T%l{W@oyXW-aXMVeX;dry}Tq>-;a`1r8Fx0&M z%$`>V*WLN6!HZw5pU-zczj>&6{oL1oIr!3te=_*7FC17n{w@3eR9OGipMNoU`uiJ( z()fQn^~b_`{QIG=Z7ck}dHura-xQ8_?>B#4IR9_AT(6Pg(wgmSm%g3nFTQ-^V{5W3 z#?{$Un_HOox`u%T4Xv36x^B8K9};40T}sU6O3#O5&E(ZuWqR~z<FF-MOt+@S*VUAIP09<)xl(y!BX#NJwKX}u9>({+7{%n@h2)l(#n$BbsEbdp7}pm@ c{`l7J_7A?1;jRTCo3qW?Z9!vxZFflgCzh!@3IG5A literal 0 HcmV?d00001 diff --git a/tests/test_rgbd_pipeline.py b/tests/test_rgbd_pipeline.py new file mode 100644 index 0000000..27950fb --- /dev/null +++ b/tests/test_rgbd_pipeline.py @@ -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) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b90661e --- /dev/null +++ b/uv.lock @@ -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" }, +]