rpx_benchmark.dataset_hub.dataset_card

Generate the HuggingFace dataset card (README.md) at the repo root.

The dataset card is what HuggingFace renders on the dataset page and what the Dataset Viewer parses for tags, task categories, and split sizes. HF requires it as README.md at the repo root with YAML frontmatter.

Inputs to the generator:

  • ScanResult — for scene counts, modality file counts, byte totals.
  • Optional PackResult — for shard counts (more accurate than the scan when the packer collapses categories like sam2/* into masks_aux).
  • Optional splits mapping — to populate the per-tier scene counts.
  • Optional label_versions — to render the label versioning section.

Output: <staging>/README.md. Idempotent (overwrites with overwrite=True).

  1"""Generate the HuggingFace dataset card (`README.md`) at the repo root.
  2
  3The dataset card is what HuggingFace renders on the dataset page and what
  4the Dataset Viewer parses for tags, task categories, and split sizes.
  5HF requires it as ``README.md`` at the repo root with YAML frontmatter.
  6
  7Inputs to the generator:
  8
  9* :class:`ScanResult` — for scene counts, modality file counts, byte totals.
 10* Optional :class:`PackResult` — for shard counts (more accurate than the
 11  scan when the packer collapses categories like sam2/* into masks_aux).
 12* Optional ``splits`` mapping — to populate the per-tier scene counts.
 13* Optional ``label_versions`` — to render the label versioning section.
 14
 15Output: ``<staging>/README.md``. Idempotent (overwrites with ``overwrite=True``).
 16"""
 17
 18from __future__ import annotations
 19
 20from dataclasses import dataclass
 21from pathlib import Path
 22from typing import Mapping, Optional
 23
 24from ..exceptions import DatasetError
 25from ..logging_utils import get_logger
 26from .recipes import (
 27    DEFAULT_REPO_ID,
 28    MULTI_OBJECT_TASK_RECIPES,
 29    SINGLE_OBJECT_TASK_RECIPES,
 30    SceneType,
 31)
 32from .scanner import ScanResult
 33
 34log = get_logger(__name__)
 35
 36
 37DEFAULT_LICENSE = "cc-by-4.0"
 38DEFAULT_PRETTY_NAME = "RPX: Robot Perception X"
 39
 40DEFAULT_TASK_CATEGORIES = (
 41    "image-segmentation",
 42    "depth-estimation",
 43    "object-detection",
 44    "visual-question-answering",
 45)
 46DEFAULT_TAGS = (
 47    "robotics",
 48    "embodied-ai",
 49    "rgb-d",
 50    "benchmark",
 51    "perception",
 52    "manipulation",
 53    "stereo",
 54)
 55
 56
 57@dataclass(frozen=True)
 58class CardSpec:
 59    """Knobs for the card generator."""
 60
 61    repo_id: str = DEFAULT_REPO_ID
 62    pretty_name: str = DEFAULT_PRETTY_NAME
 63    license: str = DEFAULT_LICENSE
 64    task_categories: tuple[str, ...] = DEFAULT_TASK_CATEGORIES
 65    tags: tuple[str, ...] = DEFAULT_TAGS
 66    paper_url: Optional[str] = None
 67    code_url: Optional[str] = "https://github.com/IRVLUTD/RPX"
 68
 69
 70def _human_bytes(n: int) -> str:
 71    f = float(n)
 72    for unit in ("B", "KB", "MB", "GB", "TB"):
 73        if f < 1024 or unit == "TB":
 74            return f"{f:.1f} {unit}" if unit != "B" else f"{int(f)} B"
 75        f /= 1024
 76    return f"{f:.1f} TB"
 77
 78
 79def _size_category(total_bytes: int) -> str:
 80    """HF size_categories tag bucket (used for search filters)."""
 81    gb = total_bytes / (1024**3)
 82    if gb < 1:
 83        return "100M<n<1B"
 84    if gb < 10:
 85        return "1B<n<10B"
 86    if gb < 100:
 87        return "10B<n<100B"
 88    if gb < 1024:
 89        return "100B<n<1T"
 90    return "n>1T"
 91
 92
 93def _yaml_list(values) -> str:
 94    return "\n".join(f"- {v}" for v in values)
 95
 96
 97def _frontmatter(spec: CardSpec, scan: ScanResult, splits: Mapping[str, str] | None = None) -> str:
 98    n_multi = len(scan.by_type(SceneType.MULTI_OBJECT))
 99    n_single = len(scan.by_type(SceneType.SINGLE_OBJECT))
100
101    lines = [
102        "---",
103        f"license: {spec.license}",
104        f'pretty_name: "{spec.pretty_name}"',
105        "task_categories:",
106        _yaml_list(spec.task_categories),
107        "language:",
108        "- en",
109        "tags:",
110        _yaml_list(spec.tags),
111        "size_categories:",
112        f"- {_size_category(scan.total_bytes)}",
113    ]
114
115    # If splits are provided, advertise them so HF Datasets viewer
116    # surfaces tier filters in the navigator.
117    if splits and n_multi > 0:
118        tier_counts: dict[str, int] = {"easy": 0, "medium": 0, "hard": 0}
119        for s in scan.by_type(SceneType.MULTI_OBJECT):
120            tier = splits.get(s.scene_id)
121            if tier in tier_counts:
122                tier_counts[tier] += 1
123        lines.append("configs:")
124        lines.append("- config_name: multi_object")
125        lines.append(
126            '  description: "MOS scenes (3 phases each), '
127            'tertile-cut by Effort-Stratified Difficulty."'
128        )
129        lines.append("  data_files:")
130        for tier, n in tier_counts.items():
131            if n:
132                lines.append(f'  - split: "{tier}"')
133                lines.append(f'    path: "splits/{tier}.txt"')
134        lines.append("- config_name: single_object")
135        lines.append(
136            '  description: "SOS scenes (one 360° collection per object); no difficulty split."'
137        )
138    lines.append("---")
139    return "\n".join(lines)
140
141
142def _body(
143    spec: CardSpec,
144    scan: ScanResult,
145    splits: Mapping[str, str] | None = None,
146    label_versions: Mapping[str, str] | None = None,
147) -> str:
148    n_multi = len(scan.by_type(SceneType.MULTI_OBJECT))
149    n_single = len(scan.by_type(SceneType.SINGLE_OBJECT))
150    modalities = scan.modality_totals()
151
152    mod_table_rows = []
153    for name, inv in modalities.items():
154        mod_table_rows.append(
155            f"| `{name}` | {inv.file_count:,} | {_human_bytes(inv.total_bytes)} |"
156        )
157    mod_table = "\n".join(mod_table_rows) if mod_table_rows else "*(none scanned)*"
158
159    multi_recipe_rows = "\n".join(
160        f"| `{name}` | {sorted(r.inputs)} → {sorted(r.labels)} |"
161        for name, r in MULTI_OBJECT_TASK_RECIPES.items()
162    )
163    single_recipe_rows = "\n".join(
164        f"| `{name}` | {sorted(r.inputs)} → {sorted(r.labels)} |"
165        for name, r in SINGLE_OBJECT_TASK_RECIPES.items()
166    )
167    label_v_rows = "\n".join(f"| `{k}` | `{v}` |" for k, v in (label_versions or {}).items())
168    label_v_block = (
169        "| modality | current version |\n|---|---|\n" + label_v_rows
170        if label_v_rows
171        else "*(default versions are in `manifest/current.json`)*"
172    )
173
174    paper_line = f"\n* Paper: {spec.paper_url}\n" if spec.paper_url else ""
175    code_line = f"* Code: {spec.code_url}\n" if spec.code_url else ""
176
177    return f"""
178# {spec.pretty_name}
179
180A real-world RGB-D benchmark for evaluating robot perception under
181embodied deployment conditions.
182
183{paper_line}{code_line}
184
185## Dataset at a glance
186
187| | |
188|---|---|
189| Multi-object scenes (MOS) | **{n_multi}** (3 phases each: clutter / interaction / clean) |
190| Single-object scenes (SOS) | **{n_single}** (one 360° collection per object) |
191| Total files | **{scan.file_count:,}** |
192| Total bytes | **{_human_bytes(scan.total_bytes)}** |
193
194## Modality inventory
195
196Three lossless space-saving steps were applied to this release; each is
197**bit-identical to the source** at the decoded pixel/value level
198(verified per-frame at conversion time):
199
200* `rgb/`, `fisheye/`, `ego/rgb/` are stored as **WebP-lossless**
201  (~45-55% smaller than the source PNGs on real-world photo content).
202* `depth/` and `sam2/masks/` are stored as **PNG re-encoded at
203  `compress_level=9`** (~20% smaller; same PNG file format, palette /
204  16-bit modes preserved exactly).
205* `cam_pose/` per-frame `.npz` files are stored as per-frame `.npy`
206  (~68% smaller; the per-file zip wrapper is removed and the position
207  and orientation are concatenated into one `(7,) float64` vector
208  packing `[x, y, z, qx, qy, qz, qw]`).
209
210Loaders see identical numpy arrays in every modality.
211
212| modality | files | bytes |
213|---|---:|---:|
214{mod_table}
215
216## Quick start
217
218```bash
219pip install "rpx-benchmark[hub]"
220hf auth login
221```
222
223```python
224from rpx_benchmark.dataset_hub import download_for_task
225
226# Pull just RGB + masks for the Easy difficulty tier — never the whole repo.
227res = download_for_task(task="segmentation", split="easy",
228                          repo_id="{spec.repo_id}")
229print(res.local_dir, res.matched_scenes)
230```
231
232```bash
233# Or from the CLI:
234python -m rpx_benchmark.dataset_hub.cli download \\
235    --task segmentation --split easy \\
236    --repo-id {spec.repo_id}
237```
238
239A subsequent call for a different task on the same split (e.g.
240`relative_pose`) reuses the cached RGB tars and only fetches the new
241modality (`cam_pose`) as the delta.
242
243## Repo layout
244
245```
246{spec.repo_id}/
247├── manifest/
248│   ├── frames_v1.parquet     # per-frame metadata (always pulled, ~30 MB)
249│   └── current.json          # default version per label modality
250├── splits/
251│   ├── scene_splits.json
252│   ├── easy.txt  medium.txt  hard.txt
253├── scenes/<scene_id>/<phase>/                     # MOS
254│   ├── rgb.tar  depth.tar  fisheye.tar
255│   └── labels/{{cam_pose,masks,masks_aux,sam2_meta,vqa}}/v1.tar
256├── objects/<object_id>/0/                         # SOS
257│   └── (same modality structure)
258├── objects_meta/                                  # questionnaire dedup
259│   ├── _index.json
260│   └── <object_id>/questionnaire.json
261└── README.md   ←  this file
262```
263
264## Tasks
265
266### Multi-object (use a difficulty split)
267
268| recipe | inputs → labels |
269|---|---|
270{multi_recipe_rows}
271
272### Single-object (no split — these are object templates)
273
274| recipe | inputs → labels |
275|---|---|
276{single_recipe_rows}
277
278## Label versioning
279
280Labels live at `labels/<name>/v<N>.tar`. Newer versions land at new
281paths; old versions stay reachable for reproducibility.
282
283{label_v_block}
284
285To pin to a specific version:
286
287```python
288download_for_task(
289    task="relative_pose", split="easy", repo_id="{spec.repo_id}",
290    label_versions={{"cam_pose": "v1"}},   # don't auto-upgrade to v2
291)
292```
293
294## Citation
295
296```bibtex
297@misc{{rpx2026,
298    title  = {{RPX: Robot Perception X — A real-world RGB-D benchmark for
299              embodied perception}},
300    author = {{IRVL UT Dallas}},
301    year   = 2026,
302    url    = {{https://huggingface.co/datasets/{spec.repo_id}}},
303}}
304```
305
306## License
307
308Released under the **{spec.license}** license.
309"""
310
311
312def write_dataset_card(
313    staging_root: Path,
314    scan: ScanResult,
315    spec: Optional[CardSpec] = None,
316    splits: Optional[Mapping[str, str]] = None,
317    label_versions: Optional[Mapping[str, str]] = None,
318    overwrite: bool = False,
319) -> Path:
320    """Render and write ``<staging_root>/README.md``.
321
322    Returns the destination path.
323    """
324    spec = spec or CardSpec()
325    out_path = Path(staging_root) / "README.md"
326    if out_path.exists() and not overwrite:
327        raise DatasetError(
328            f"refusing to overwrite existing dataset card: {out_path}",
329            hint="Pass overwrite=True or remove the file first.",
330        )
331
332    contents = (
333        _frontmatter(spec, scan, splits)
334        + "\n"
335        + _body(
336            spec,
337            scan,
338            splits,
339            label_versions,
340        )
341    )
342    out_path.parent.mkdir(parents=True, exist_ok=True)
343    out_path.write_text(contents, encoding="utf-8")
344    log.info("wrote dataset card → %s (%d bytes)", out_path, out_path.stat().st_size)
345    return out_path
log = <Logger rpx_benchmark.dataset_hub.dataset_card (WARNING)>
DEFAULT_LICENSE = 'cc-by-4.0'
DEFAULT_PRETTY_NAME = 'RPX: Robot Perception X'
DEFAULT_TASK_CATEGORIES = ('image-segmentation', 'depth-estimation', 'object-detection', 'visual-question-answering')
DEFAULT_TAGS = ('robotics', 'embodied-ai', 'rgb-d', 'benchmark', 'perception', 'manipulation', 'stereo')
@dataclass(frozen=True)
class CardSpec:
58@dataclass(frozen=True)
59class CardSpec:
60    """Knobs for the card generator."""
61
62    repo_id: str = DEFAULT_REPO_ID
63    pretty_name: str = DEFAULT_PRETTY_NAME
64    license: str = DEFAULT_LICENSE
65    task_categories: tuple[str, ...] = DEFAULT_TASK_CATEGORIES
66    tags: tuple[str, ...] = DEFAULT_TAGS
67    paper_url: Optional[str] = None
68    code_url: Optional[str] = "https://github.com/IRVLUTD/RPX"

Knobs for the card generator.

CardSpec( repo_id: str = 'IRVLUTD/RPX', pretty_name: str = 'RPX: Robot Perception X', license: str = 'cc-by-4.0', task_categories: tuple[str, ...] = ('image-segmentation', 'depth-estimation', 'object-detection', 'visual-question-answering'), tags: tuple[str, ...] = ('robotics', 'embodied-ai', 'rgb-d', 'benchmark', 'perception', 'manipulation', 'stereo'), paper_url: Optional[str] = None, code_url: Optional[str] = 'https://github.com/IRVLUTD/RPX')
repo_id: str = $GITHUB_REPOSITORY
pretty_name: str = 'RPX: Robot Perception X'
license: str = 'cc-by-4.0'
task_categories: tuple[str, ...] = ('image-segmentation', 'depth-estimation', 'object-detection', 'visual-question-answering')
tags: tuple[str, ...] = ('robotics', 'embodied-ai', 'rgb-d', 'benchmark', 'perception', 'manipulation', 'stereo')
paper_url: Optional[str] = None
code_url: Optional[str] = 'https://github.com/IRVLUTD/RPX'
def write_dataset_card( staging_root: pathlib.Path, scan: rpx_benchmark.dataset_hub.ScanResult, spec: Optional[CardSpec] = None, splits: Optional[Mapping[str, str]] = None, label_versions: Optional[Mapping[str, str]] = None, overwrite: bool = False) -> pathlib.Path:
313def write_dataset_card(
314    staging_root: Path,
315    scan: ScanResult,
316    spec: Optional[CardSpec] = None,
317    splits: Optional[Mapping[str, str]] = None,
318    label_versions: Optional[Mapping[str, str]] = None,
319    overwrite: bool = False,
320) -> Path:
321    """Render and write ``<staging_root>/README.md``.
322
323    Returns the destination path.
324    """
325    spec = spec or CardSpec()
326    out_path = Path(staging_root) / "README.md"
327    if out_path.exists() and not overwrite:
328        raise DatasetError(
329            f"refusing to overwrite existing dataset card: {out_path}",
330            hint="Pass overwrite=True or remove the file first.",
331        )
332
333    contents = (
334        _frontmatter(spec, scan, splits)
335        + "\n"
336        + _body(
337            spec,
338            scan,
339            splits,
340            label_versions,
341        )
342    )
343    out_path.parent.mkdir(parents=True, exist_ok=True)
344    out_path.write_text(contents, encoding="utf-8")
345    log.info("wrote dataset card → %s (%d bytes)", out_path, out_path.stat().st_size)
346    return out_path

Render and write <staging_root>/README.md.

Returns the destination path.