Buckets:
| """PointWorld-DROID dataset access: selective HF download + flows.h5 reading. | |
| PointWorld ships as ``tar.zst`` split into numbered ``.part-*`` files and must | |
| be restored with the dataset's own ``recover_dataset_from_parts.sh | |
| --include-prefix ...`` script -- there is no way to untar a lone | |
| ``.part-0000`` directly, so this module always fetches that recovery script | |
| alongside whatever data prefix it needs and shells out to it rather than | |
| reimplementing zstd+tar restoration. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import json | |
| import os | |
| import re | |
| import stat | |
| import subprocess | |
| from collections.abc import Iterator | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| import h5py | |
| import numpy as np | |
| from huggingface_hub import snapshot_download | |
| from PIL import Image | |
| from fpgm.types import DataError, SceneFlowClip | |
| from fpgm.utils.io import ensure_dir | |
| from fpgm.utils.logging import get_logger | |
| #: Clip groups are keyed ``"{start}:{end}"``; every other root key is episode-level metadata. | |
| _CLIP_KEY_RE = re.compile(r"\d+:\d+") | |
| logger = get_logger(__name__) | |
| HF_REPO_ID = "nvidia/PointWorld-DROID" | |
| RECOVER_SCRIPT_NAME = "recover_dataset_from_parts.sh" | |
| #: 1.23 TB and not selectively restorable -- refused outright, not just "not | |
| #: implemented", so a future caller can't accidentally wire this up either. | |
| FORBIDDEN_PREFIXES = ("droid/depth_320x180",) | |
| def _check_prefix_allowed(prefix: str) -> None: | |
| for forbidden in FORBIDDEN_PREFIXES: | |
| if prefix.startswith(forbidden): | |
| raise ValueError( | |
| f"refusing to download {prefix!r}: {forbidden} is 1.23 TB and not " | |
| "cherry-pickable. This is a hard guardrail, not a suggestion." | |
| ) | |
| class ShardInfo: | |
| """One entry of ``_shards_manifest.json``'s ``"shards"`` list.""" | |
| shard_id: str | |
| member_count: int | |
| members: list[str] | |
| remote_prefix: str | |
| raw_member_bytes: int | |
| def episode_uuids(self) -> list[str]: | |
| """uuid for every ``*_flows.h5`` member, derived from its filename.""" | |
| uuids = [] | |
| for member in self.members: | |
| name = Path(member).name | |
| if name.endswith("_flows.h5"): | |
| uuids.append(name[: -len("_flows.h5")]) | |
| return uuids | |
| class ShardManifest: | |
| """Parsed ``_shards_manifest.json``: which shard holds which episode.""" | |
| def __init__(self, shards: list[ShardInfo]): | |
| self._shards = {s.shard_id: s for s in shards} | |
| self._episode_to_shard = {uuid: s.shard_id for s in shards for uuid in s.episode_uuids} | |
| def from_json(cls, path: Path) -> ShardManifest: | |
| raw = json.loads(Path(path).read_text()) | |
| shards = [ | |
| ShardInfo( | |
| shard_id=s["shard_id"], | |
| member_count=s["member_count"], | |
| members=s["members"], | |
| remote_prefix=s["remote_prefix"], | |
| raw_member_bytes=s["raw_member_bytes"], | |
| ) | |
| for s in raw["shards"] | |
| ] | |
| return cls(shards) | |
| def shard(self, shard_id: str) -> ShardInfo: | |
| try: | |
| return self._shards[shard_id] | |
| except KeyError: | |
| raise DataError(f"unknown shard id {shard_id!r}") from None | |
| def shard_for_episode(self, uuid: str) -> str: | |
| try: | |
| return self._episode_to_shard[uuid] | |
| except KeyError: | |
| raise DataError(f"episode {uuid!r} not found in any shard of the manifest") from None | |
| def episodes_in_shard(self, shard_id: str) -> list[str]: | |
| return self.shard(shard_id).episode_uuids | |
| class PointWorldStore: | |
| """Selective downloader + accessor for the PointWorld-DROID HF dataset.""" | |
| def __init__( | |
| self, | |
| local_dir: Path, | |
| hf_repo: str = HF_REPO_ID, | |
| hf_token: str | None = None, | |
| ): | |
| self.local_dir = ensure_dir(Path(local_dir)) | |
| self.hf_repo = hf_repo | |
| self.hf_token = hf_token or os.environ.get("HF_TOKEN") | |
| self._manifest: ShardManifest | None = None | |
| # -- download ---------------------------------------------------------- | |
| def _snapshot(self, allow_patterns: list[str]) -> Path: | |
| return Path( | |
| snapshot_download( | |
| repo_id=self.hf_repo, | |
| repo_type="dataset", | |
| local_dir=self.local_dir, | |
| allow_patterns=allow_patterns, | |
| token=self.hf_token, | |
| ) | |
| ) | |
| def _recover_script(self) -> Path: | |
| self._snapshot([RECOVER_SCRIPT_NAME]) | |
| script = self.local_dir / RECOVER_SCRIPT_NAME | |
| script.chmod(script.stat().st_mode | stat.S_IEXEC) | |
| return script | |
| def _recover(self, include_prefix: str) -> None: | |
| """Unpack the downloaded ``package.tar.zst.part-*`` for ``include_prefix``. | |
| ``--packages`` and ``--out`` are both mandatory in | |
| ``recover_dataset_from_parts.sh`` (it prints usage and exits non-zero | |
| without them), and passing only ``--include-prefix`` made every | |
| ``download_shard``/``download_cameras`` call fail at the extraction step. | |
| Both point at ``local_dir``: the packaged parts live under it, and the | |
| script's own layout restores members back into the same tree. | |
| Every path is resolved to an absolute one first. ``local_dir`` arrives | |
| relative in the normal case (``download_pointworld.py`` defaults it to | |
| ``data/pointworld``), and ``cwd=self.local_dir`` then re-anchors that same | |
| relative string against the new working directory -- so the interpreter | |
| looked for ``data/pointworld/data/pointworld/recover_dataset_from_parts.sh`` | |
| and raised ``FileNotFoundError`` after the multi-GB download had already | |
| succeeded. | |
| """ | |
| root = self.local_dir.resolve() | |
| script = self._recover_script().resolve() | |
| logger.info("recovering %s via %s", include_prefix, script.name) | |
| subprocess.run( | |
| [ | |
| str(script), | |
| "--packages", str(root), | |
| "--out", str(root), | |
| "--include-prefix", include_prefix, | |
| ], | |
| cwd=root, | |
| check=True, | |
| ) | |
| def download_cameras(self) -> Path: | |
| """Fetch and restore all ~42,935 ``*_cameras.json`` (~19.5 MB packed).""" | |
| prefix = "droid/cameras" | |
| _check_prefix_allowed(prefix) | |
| self._snapshot([f"{prefix}/package.tar.zst.part-*"]) | |
| self._recover(prefix) | |
| return self.local_dir / prefix | |
| def download_manifest(self) -> Path: | |
| """Fetch ``_shards_manifest.json`` (~3.9 MB, not packed -- no restore needed).""" | |
| rel = "droid/flows-fs-optimized/_shards_manifest.json" | |
| self._snapshot([rel]) | |
| return self.local_dir / rel | |
| def download_shard(self, shard_id: str) -> Path: | |
| """Fetch and restore one flows shard (~3.5 GB packed, ~62 episodes).""" | |
| prefix = f"droid/flows-fs-optimized/{shard_id}" | |
| _check_prefix_allowed(prefix) | |
| self._snapshot([f"{prefix}/package.tar.zst.part-*"]) | |
| self._recover(prefix) | |
| return self.local_dir / prefix | |
| # -- manifest ------------------------------------------------------------ | |
| def manifest(self) -> ShardManifest: | |
| """Loads (downloading first if necessary) and caches the shard manifest.""" | |
| if self._manifest is None: | |
| manifest_path = self.local_dir / "droid/flows-fs-optimized/_shards_manifest.json" | |
| if not manifest_path.exists(): | |
| manifest_path = self.download_manifest() | |
| self._manifest = ShardManifest.from_json(manifest_path) | |
| return self._manifest | |
| def shard_for_episode(self, uuid: str) -> str: | |
| return self.manifest.shard_for_episode(uuid) | |
| def episodes_in_shard(self, shard_id: str) -> list[str]: | |
| return self.manifest.episodes_in_shard(shard_id) | |
| # -- flows / cameras access ----------------------------------------------- | |
| def flows_path(self, uuid: str, shard_id: str | None = None) -> Path: | |
| """Locate ``<uuid>_flows.h5`` in the restored tree. | |
| ``recover_dataset_from_parts.sh`` extracts every shard's members into a | |
| single flat ``droid/flows-fs-optimized/`` directory -- the ``shard-NNNNNN`` | |
| level exists only in the *packaged* download, not in the restored layout. | |
| The flat location is therefore tried first, with the sharded path kept as a | |
| fallback for callers who lay the tree out per shard themselves. | |
| """ | |
| root = self.local_dir / "droid/flows-fs-optimized" | |
| flat = root / f"{uuid}_flows.h5" | |
| if flat.exists(): | |
| return flat | |
| shard_id = shard_id or self.shard_for_episode(uuid) | |
| return root / shard_id / f"{uuid}_flows.h5" | |
| def cameras_path(self, uuid: str) -> Path: | |
| return self.local_dir / "droid/cameras" / f"{uuid}_cameras.json" | |
| def load_camera_calibration(self, uuid: str) -> dict[str, Any]: | |
| """Parsed ``<uuid>_cameras.json`` (per-serial ``optimized_extrinsics`` etc).""" | |
| path = self.cameras_path(uuid) | |
| if not path.exists(): | |
| raise DataError(f"cameras json not found for {uuid!r}: {path}") | |
| return json.loads(path.read_text()) | |
| def open_flows(self, uuid: str, shard_id: str | None = None) -> FlowsReader: | |
| """A context-managed :class:`FlowsReader` over ``<uuid>_flows.h5``.""" | |
| return FlowsReader(self.flows_path(uuid, shard_id), episode_uuid=uuid) | |
| class FlowsReader: | |
| """Reads one episode's ``*_flows.h5`` and yields :class:`SceneFlowClip` objects. | |
| Must be used as a context manager (``with store.open_flows(uuid) as | |
| reader:``) so the underlying HDF5 file handle is always closed. | |
| """ | |
| def __init__(self, path: Path, episode_uuid: str | None = None): | |
| if not Path(path).exists(): | |
| raise DataError(f"flows.h5 not found: {path}") | |
| self.path = Path(path) | |
| self.episode_uuid = episode_uuid | |
| self._file: h5py.File | None = None | |
| def __enter__(self) -> FlowsReader: | |
| self._file = h5py.File(self.path, "r") | |
| return self | |
| def __exit__(self, *exc_info: object) -> None: | |
| if self._file is not None: | |
| self._file.close() | |
| self._file = None | |
| def file(self) -> h5py.File: | |
| if self._file is None: | |
| raise DataError("FlowsReader must be used as a context manager (`with ... as reader:`)") | |
| return self._file | |
| def clip_keys(self) -> list[str]: | |
| """``"{start}:{end}"`` clip group keys, ordered by start frame. | |
| The file's root also holds episode-level summaries that are *not* clips | |
| (``ee_pos_motion_magnitudes``, ``ee_rot_motion_magnitudes``, | |
| ``has_gripper_movement``), so keys are filtered on the ``start:end`` shape | |
| rather than taken wholesale. | |
| """ | |
| keys = [k for k in self.file.keys() if _CLIP_KEY_RE.fullmatch(k)] | |
| return sorted(keys, key=lambda k: int(k.split(":")[0])) | |
| def camera_serials(self, clip_key: str) -> list[str]: | |
| """Camera serials present for one clip (group names ``camera_<serial>_ext``).""" | |
| group = self.file[clip_key] | |
| return [ | |
| name[len("camera_") : -len("_ext")] | |
| for name in group.keys() | |
| if name.startswith("camera_") and name.endswith("_ext") | |
| ] | |
| def read_clip(self, clip_key: str, camera_serial: str) -> SceneFlowClip: | |
| """Decode one clip/camera group into a :class:`~fpgm.types.SceneFlowClip`.""" | |
| clip_group = self.file[clip_key] | |
| cam_group = clip_group[f"camera_{camera_serial}_ext"] | |
| start_str, end_str = clip_key.split(":") | |
| def arr(name: str) -> np.ndarray: | |
| return np.asarray(cam_group[name]) | |
| initial_rgb = None | |
| if "initial_rgb" in cam_group: | |
| jpeg_bytes = bytes(cam_group["initial_rgb"][0]) | |
| initial_rgb = np.array(Image.open(io.BytesIO(jpeg_bytes)).convert("RGB")) | |
| # Dense depth exists only for the clip's first frame -- one (H, W) array, | |
| # not a (T, H, W) stream -- so it is read straight through with no T axis. | |
| initial_depth = None | |
| if "initial_depth" in cam_group: | |
| initial_depth = arr("initial_depth").astype(np.uint16) | |
| scene_normals = None | |
| if "scene_normals" in cam_group: | |
| scene_normals = arr("scene_normals").astype(np.int8) | |
| # Proprioception is shared by every camera of a clip, so it is stored one | |
| # level up, on the clip group -- not inside the per-camera group. | |
| gripper_pose = ( | |
| np.asarray(clip_group["gripper_pose"]).astype(np.float32) | |
| if "gripper_pose" in clip_group | |
| else None | |
| ) | |
| gripper_open = ( | |
| np.asarray(clip_group["gripper_open"]).astype(bool) | |
| if "gripper_open" in clip_group | |
| else None | |
| ) | |
| # Also clip-level, shared by every camera -- kept mainly so the clip's | |
| # video/trajectory alignment can be *measured* (verify_annotation_stride) | |
| # rather than assumed. See fpgm.types.ClipTiming's docstring. | |
| joint_positions = ( | |
| np.asarray(clip_group["joint_positions"]).astype(np.float32) | |
| if "joint_positions" in clip_group | |
| else None | |
| ) | |
| return SceneFlowClip( | |
| key=clip_key, | |
| start=int(start_str), | |
| end=int(end_str), | |
| camera_serial=camera_serial, | |
| # scene_flows is stored float16 on disk; upcast for downstream numeric stability. | |
| scene_flows=arr("scene_flows").astype(np.float32), | |
| scene_visibility=arr("scene_visibility").astype(bool), | |
| scene_depth_valid=arr("scene_depth_valid_mask").astype(bool), | |
| scene_colors=arr("scene_colors").astype(np.uint8), | |
| intrinsic=arr("intrinsic").astype(np.float64), | |
| extrinsic=arr("extrinsic").astype(np.float64), | |
| initial_rgb=initial_rgb, | |
| gripper_pose=gripper_pose, | |
| gripper_open=gripper_open, | |
| joint_positions=joint_positions, | |
| initial_depth=initial_depth, | |
| scene_normals=scene_normals, | |
| ) | |
| def read_all(self, camera_serial: str | None = None) -> Iterator[SceneFlowClip]: | |
| """Yield every clip, optionally restricted to one camera serial.""" | |
| for clip_key in self.clip_keys(): | |
| serials = [camera_serial] if camera_serial else self.camera_serials(clip_key) | |
| for serial in serials: | |
| yield self.read_clip(clip_key, serial) | |
| #: Strides tried by :func:`verify_annotation_stride`. 2 is what has been measured | |
| #: on every clip seen so far; 1 and the small neighbours are kept as candidates so | |
| #: the function still tells the truth if a future shard turns out different. | |
| _DEFAULT_CANDIDATE_STRIDES: tuple[int, ...] = (1, 2, 3, 4) | |
| def verify_annotation_stride( | |
| clip_joint_positions: np.ndarray, | |
| trajectory_joint_positions: np.ndarray, | |
| clip_start: int, | |
| candidate_strides: tuple[int, ...] = _DEFAULT_CANDIDATE_STRIDES, | |
| atol: float = 1e-4, | |
| ) -> int: | |
| """Measure the clip-frame -> trajectory-row stride from real joint data. | |
| A PointWorld clip's own frame axis is not guaranteed to be one-to-one with | |
| ``trajectory.h5``'s row axis -- on every clip checked so far, clip frame ``t`` | |
| is trajectory row ``2 * (clip_start + t)``, not ``clip_start + t`` (see | |
| :class:`fpgm.types.ClipTiming`'s docstring). Hardcoding that ``2`` here would | |
| repeat exactly the mistake that produced the original bug: a plausible-looking | |
| constant nobody checks against the data again. So instead, this tries each | |
| ``candidate_strides`` value, reads the corresponding rows out of | |
| ``trajectory_joint_positions``, and returns whichever stride's rows match | |
| ``clip_joint_positions`` almost exactly -- these are two independent readings | |
| of the same physical joint encoders at the same instants, so a correct stride | |
| matches to float32 precision and a wrong one does not match at all. | |
| Args: | |
| clip_joint_positions: ``(T, 7)`` joint positions from the clip's own h5 | |
| group (``SceneFlowClip.joint_positions``). | |
| trajectory_joint_positions: ``(L, 7)`` (or more columns; only the leading 7 | |
| are compared) joint positions from the episode's ``trajectory.h5`` | |
| (``observation/robot_state/joint_positions``). | |
| clip_start: The clip's declared start index (the ``start`` in its | |
| ``"{start}:{end}"`` key). | |
| candidate_strides: Strides to test, in the order tried; the first exact | |
| match wins ties. | |
| atol: Maximum-abs-error tolerance for a candidate to count as a match. | |
| Returns: | |
| The stride (from ``candidate_strides``) whose implied trajectory rows best | |
| match ``clip_joint_positions``. | |
| Raises: | |
| DataError: If no candidate stride keeps every implied row inside | |
| ``trajectory_joint_positions``, or if the best-matching candidate's | |
| error still exceeds ``atol`` -- i.e. nothing measured actually fits, | |
| which should stop the pipeline rather than silently proceed on a guess. | |
| """ | |
| clip_jp = np.asarray(clip_joint_positions, dtype=np.float64) | |
| traj_jp = np.asarray(trajectory_joint_positions, dtype=np.float64) | |
| n = clip_jp.shape[0] | |
| if n == 0: | |
| raise DataError("clip_joint_positions is empty; cannot measure an annotation stride") | |
| n_cols = min(clip_jp.shape[1], traj_jp.shape[1]) | |
| errors: dict[int, float] = {} | |
| for stride in candidate_strides: | |
| rows = clip_start * stride + stride * np.arange(n) | |
| if rows[-1] >= traj_jp.shape[0]: | |
| continue # this stride would read past the end of trajectory.h5 | |
| errors[stride] = float( | |
| np.max(np.abs(clip_jp[:, :n_cols] - traj_jp[rows][:, :n_cols])) | |
| ) | |
| if not errors: | |
| raise DataError( | |
| f"no candidate stride in {candidate_strides} keeps clip_start={clip_start}, " | |
| f"n={n} frames inside a {traj_jp.shape[0]}-row trajectory.h5" | |
| ) | |
| best_stride = min(errors, key=errors.get) | |
| if errors[best_stride] > atol: | |
| raise DataError( | |
| f"no candidate stride in {candidate_strides} matches trajectory_joint_positions " | |
| f"within atol={atol} for clip_start={clip_start}; best was stride={best_stride} " | |
| f"with max abs error {errors[best_stride]:.6g} (all errors: {errors})" | |
| ) | |
| return best_stride | |
Xet Storage Details
- Size:
- 18.6 kB
- Xet hash:
- 06fe5bc18ce22e7b0b5a5f6af9d388efbedcf9624357d32637d549c6e4bd0caf
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.