uv-scripts/object-detection
Object Detection Dataset Scripts 8 scripts to create, convert, review, validate, inspect, diff, and sample object detection datasets on the Hub. Supports 6 bbox formats — no setup required. Start from nothing: falcon-perception.py generates a first-pass detection dataset for any class you can name, zero-shot, with no labelling and no training. The other six then convert, check, and measure it. This repository is inspired by panlabel Quick Start Convert bounding… See the full description on the dataset page: https://huggingface.co/datasets/uv-scripts/object-detection.
10169
1#!/usr/bin/env -S uv run --script2# /// script3# requires-python = ">=3.10"4# dependencies = [5# "datasets>=4.0",6# "huggingface_hub>=1.27", # hf://buckets in HfFileSystem (1.6) + prefix-collision fix (1.27)7# "pillow",8# "numpy",9# "pycocotools>=2.0.11",10# ]11# ///12"""Materialize a COCO directory tree FROM the canonical parquet, in-job, on ephemeral disk.13 14Some trainers (RF-DETR and friends) refuse HF datasets and demand the canonical COCO 201715layout: annotations/instances_train2017.json + train2017/*.jpg. Never hand-assemble or16upload that tree -- generate it from the parquet with this script instead. A generated17tree cannot reference images that are not there, which kills the referenced-vs-uploaded18mismatch class outright (it caused three paid job failures in one measured run).19 20 # inside the training job, before the trainer starts:21 uv run materialize-coco.py --data hf://buckets/<ns>/<training-bucket>/dataset --out /tmp/coco22 23 # or from a dataset repo produced by embed-bucket-images.py:24 uv run materialize-coco.py --data <ns>/<training-dataset> --out /tmp/coco25 26 # training more than once? generate ONCE onto a bucket mount and let later jobs reuse it:27 # hf jobs run ... -v hf://buckets/<ns>/<training-bucket>:/data ...28 uv run materialize-coco.py --data /data/dataset --out /data/coco29 30A split is reused, not regenerated, when its tree is complete AND was built from the same31labels: the annotations file carries a fingerprint of (image ids, boxes), so a corrected32dataset -- the step-6 loop, same images, new labels -- rebuilds automatically. --force rebuilds33regardless. Rows whose image cannot be decoded (teacher error rows, truncated files) are skipped34and counted, never allowed to kill the job.35 36Boxes are converted yolo-normalized -> COCO xywh pixels (pass --bbox-format coco_xywh if your37parquet already stores pixels). masks_rle, when present, is carried through as COCO RLE38segmentation (RF-DETR-class trainers accept RLE natively).39"""40 41import argparse42import hashlib43import io44import json45import shutil46from pathlib import Path47 48import numpy as np49from datasets import Image as HFImage50from datasets import load_dataset51from PIL import Image as PILImage52from pycocotools import mask as mask_utils53 54SPLIT_DIR = {"train": "train2017", "validation": "val2017"}55 56 57def to_xywh(bbox, w, h, fmt):58 if fmt == "coco_xywh":59 return [float(v) for v in bbox]60 cx, cy, bw, bh = bbox # yolo normalised61 return [(cx - bw / 2) * w, (cy - bh / 2) * h, bw * w, bh * h]62 63 64def label_fingerprint(ds):65 """Hash of (image_id, boxes) for every row -- changes when labels change, not when bytes do."""66 labels = ds.select_columns(["image_id", "objects"]).with_format(None)67 items = sorted(68 (69 int(row["image_id"]),70 [[round(float(v), 6) for v in b] for b in row["objects"]["bbox"]],71 )72 for row in labels73 )74 return hashlib.sha1(json.dumps(items).encode()).hexdigest()75 76 77def load_split(data, split):78 if data.startswith("hf://"):79 return load_dataset(80 "parquet", data_files=f"{data.rstrip('/')}/{split}.parquet", split="train"81 )82 return load_dataset(data, split=split)83 84 85def tree_is_reusable(jpath, img_dir, fingerprint):86 if not jpath.exists():87 return False, "no tree yet"88 coco = json.loads(jpath.read_text())89 stamped = coco.get("provenance", {}).get("fingerprint")90 if stamped != fingerprint:91 return False, "labels changed since the tree was built"92 referenced = len(coco["images"])93 present = len(list(img_dir.glob("*.jpg")))94 if not referenced or referenced != present:95 return False, f"tree incomplete ({present} files vs {referenced} referenced)"96 return True, f"complete tree ({present} images), same labels"97 98 99def decode_image(raw):100 """raw is the undecoded {bytes, path} struct (or None for error rows)."""101 if not raw or not raw.get("bytes"):102 return None103 try:104 im = PILImage.open(io.BytesIO(raw["bytes"]))105 im.load()106 return im.convert("RGB")107 except Exception: # noqa: BLE001 -- any decode failure means "skip this row"108 return None109 110 111def main():112 p = argparse.ArgumentParser(description=__doc__.splitlines()[0])113 p.add_argument(114 "--data",115 required=True,116 help="dataset repo id, hf://buckets/... prefix, or local directory holding <split>.parquet",117 )118 p.add_argument(119 "--out",120 required=True,121 help="output dir (ephemeral disk, or a bucket mount to reuse across jobs)",122 )123 p.add_argument("--bbox-format", default="yolo", choices=["yolo", "coco_xywh"])124 p.add_argument("--splits", nargs="+", default=["train", "validation"])125 p.add_argument(126 "--force",127 action="store_true",128 help="rebuild a split even if its tree is complete",129 )130 args = p.parse_args()131 132 out = Path(args.out)133 (out / "annotations").mkdir(parents=True, exist_ok=True)134 135 for split in args.splits:136 ds = load_split(args.data, split)137 assert "image" in ds.column_names, (138 "no image column — run embed-bucket-images.py first"139 )140 img_dir = out / SPLIT_DIR.get(split, split)141 jpath = out / "annotations" / f"instances_{SPLIT_DIR.get(split, split)}.json"142 143 fingerprint = label_fingerprint(ds)144 reusable, why = tree_is_reusable(jpath, img_dir, fingerprint)145 if reusable and not args.force:146 print(f"{split}: reusing {why} at {img_dir} — pass --force to rebuild")147 continue148 print(f"{split}: building ({'--force' if args.force else why})")149 # a rebuild starts from nothing: stale JPEGs from an older tree would fail the150 # files == referenced assert below after all the decode work151 shutil.rmtree(img_dir, ignore_errors=True)152 jpath.unlink(missing_ok=True)153 img_dir.mkdir()154 155 cat_feature = ds.features["objects"]["category"].feature156 names = getattr(cat_feature, "names", None) or ["object"]157 158 # undecoded bytes so a corrupt image is OUR decision to skip, not a crash inside datasets159 rows = ds.cast_column("image", HFImage(decode=False)).with_format(None)160 images, annotations, ann_id, skipped = [], [], 1, []161 for row in rows:162 iid = int(row["image_id"])163 im = None if row.get("error") else decode_image(row["image"])164 if im is None:165 skipped.append(iid)166 continue167 fname = f"{iid}.jpg"168 im.save(img_dir / fname, "JPEG", quality=95)169 # dims from the DECODED image, never metadata columns: error rows carry170 # width/height=None, and the saved JPEG is the frame everything must match171 w, h = im.size172 images.append({"id": iid, "file_name": fname, "width": w, "height": h})173 rles = json.loads(row["masks_rle"]) if row.get("masks_rle") else []174 for i, bbox in enumerate(row["objects"]["bbox"]):175 x, y, bw, bh = to_xywh(bbox, w, h, args.bbox_format)176 ann = {177 "id": ann_id,178 "image_id": iid,179 "category_id": int(row["objects"]["category"][i]) + 1,180 "bbox": [x, y, bw, bh],181 "area": bw * bh,182 "iscrowd": 0,183 }184 if i < len(rles):185 rle = rles[i]186 # masks live in the INFERENCE frame, which diverges from the image187 # frame whenever the teacher thumbnailed -- resize before writing188 if rle["size"] != [h, w]:189 seg = mask_utils.decode(190 {**rle, "counts": rle["counts"].encode()}191 )192 seg = np.asarray(193 PILImage.fromarray(seg).resize((w, h), PILImage.NEAREST)194 )195 enc = mask_utils.encode(np.asfortranarray(seg))196 rle = {"size": [h, w], "counts": enc["counts"].decode("ascii")}197 ann["segmentation"] = rle198 annotations.append(ann)199 ann_id += 1200 201 coco = {202 "images": images,203 "annotations": annotations,204 "categories": [{"id": i + 1, "name": n} for i, n in enumerate(names)],205 "provenance": {206 "source": args.data,207 "fingerprint": fingerprint,208 "skipped_image_ids": skipped,209 },210 }211 jpath.write_text(json.dumps(coco))212 213 n_files = len(list(img_dir.glob("*.jpg")))214 assert n_files == len(images), (215 f"{split}: {n_files} files != {len(images)} referenced"216 )217 note = (218 f" (skipped {len(skipped)} undecodable/error rows, e.g. {skipped[:3]})"219 if skipped220 else ""221 )222 print(223 f"{split}: {len(images)} images / {len(annotations)} annotations -> {img_dir} + {jpath.name}{note}"224 )225 226 227if __name__ == "__main__":228 main()229 