Object Detection Dataset Scripts
7 scripts to create, convert, 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 box formats without cloning anything:
# Convert COCO-style bboxes to YOLO normalized format
uv run convert-hf-dataset.py merve/coco-dataset merve/coco-yolo \
--from coco_xywh --to yolo --max-samples 100
That's it! The script will:
- Load the dataset from the Hub
- Convert all bounding boxes in-place
- Push the result to a new dataset repo
- View results at:
https://huggingface.co/datasets/merve/coco-yolo
Scripts
| Script | Description |
|---|---|
falcon-perception.py |
Create a detection dataset zero-shot from any image dataset — name a class, get boxes + masks (runs on Apple Silicon too) |
falcon-perception-bucket.py |
Same, reading images from an HF bucket, resumable across restarts |
convert-hf-dataset.py |
Convert between 6 bbox formats and push to Hub |
validate-hf-dataset.py |
Check annotations for errors (invalid bboxes, duplicates, bounds) |
stats-hf-dataset.py |
Compute statistics (counts, label histogram, area, co-occurrence) |
diff-hf-datasets.py |
Compare two datasets semantically (IoU-based annotation matching) |
sample-hf-dataset.py |
Create subsets (random or stratified) and push to Hub |
Supported Bbox Formats
All scripts support these 6 bounding box formats, matching the panlabel Rust CLI:
| Format | Encoding | Coordinate Space |
|---|---|---|
coco_xywh |
[x, y, width, height] |
Pixels |
xyxy |
[xmin, ymin, xmax, ymax] |
Pixels |
voc |
[xmin, ymin, xmax, ymax] |
Pixels (alias for xyxy) |
yolo |
[center_x, center_y, width, height] |
Normalized 0–1 |
tfod |
[xmin, ymin, xmax, ymax] |
Normalized 0–1 |
label_studio |
[x, y, width, height] |
Percentage 0–100 |
Conversions go through XYXY pixel-space as the intermediate representation, so any format can be converted to any other format.
Common Options
All scripts accept flexible column mapping. Datasets can store annotations as flat columns or nested under an objects dict — both layouts are handled automatically.
| Option | Description |
|---|---|
--bbox-column |
Column containing bboxes (default: bbox) |
--category-column |
Column containing category labels (default: category) |
--width-column |
Column for image width (default: width) |
--height-column |
Column for image height (default: height) |
--split |
Dataset split (default: train) |
--max-samples |
Limit number of samples (useful for testing) |
--hf-token |
HF API token (or set HF_TOKEN env var) |
--private |
Make output dataset private |
Every script supports --help to see all available options:
uv run convert-hf-dataset.py --help
Convert (convert-hf-dataset.py)
Convert bounding boxes between any of the 6 supported formats:
# COCO -> XYXY
uv run convert-hf-dataset.py merve/license-plates merve/license-plates-voc \
--from coco_xywh --to voc
# YOLO -> COCO
uv run convert-hf-dataset.py merve/license-plates merve/license-plates-yolo \
--from coco_xywh --to yolo
# TFOD (normalized xyxy) -> COCO
uv run convert-hf-dataset.py merve/license-plates-tfod merve/license-plates-coco \
--from tfod --to coco_xywh
# Label Studio (percentage xywh) -> XYXY
uv run convert-hf-dataset.py merve/ls-dataset merve/ls-xyxy \
--from label_studio --to xyxy
# Test on 10 samples first
uv run convert-hf-dataset.py merve/dataset merve/converted \
--from xyxy --to yolo --max-samples 10
# Shuffle before converting a subset
uv run convert-hf-dataset.py merve/dataset merve/converted \
--from coco_xywh --to tfod --max-samples 500 --shuffle
| Option | Description |
|---|---|
--from |
Source bbox format (required) |
--to |
Target bbox format (required) |
--batch-size |
Batch size for map (default: 1000) |
--create-pr |
Push as PR instead of direct commit |
--shuffle |
Shuffle dataset before processing |
--seed |
Random seed for shuffling (default: 42) |
Validate (validate-hf-dataset.py)
Check annotations for common issues:
# Basic validation
uv run validate-hf-dataset.py merve/coco-dataset
# Validate YOLO-format dataset
uv run validate-hf-dataset.py merve/yolo-dataset --bbox-format yolo
# Validate TFOD-format dataset
uv run validate-hf-dataset.py merve/tfod-dataset --bbox-format tfod
# Strict mode (warnings become errors)
uv run validate-hf-dataset.py merve/dataset --strict
# JSON report
uv run validate-hf-dataset.py merve/dataset --report json
# Stream large datasets without full download
uv run validate-hf-dataset.py merve/huge-dataset --streaming --max-samples 5000
# Push validation report to Hub
uv run validate-hf-dataset.py merve/dataset --output-dataset merve/validation-report
Issue Codes:
| Code | Level | Description |
|---|---|---|
| E001 | Error | Bbox/category count mismatch |
| E002 | Error | Invalid bbox (missing values) |
| E003 | Error | Non-finite coordinates (NaN/Inf) |
| E004 | Error | xmin > xmax |
| E005 | Error | ymin > ymax |
| W001 | Warning | No annotations in example |
| W002 | Warning | Zero or negative area |
| W003 | Warning | Bbox before image origin |
| W004 | Warning | Bbox beyond image bounds |
| W005 | Warning | Empty category label |
| W006 | Warning | Duplicate file name |
Stats (stats-hf-dataset.py)
Compute rich statistics for a dataset:
# Basic stats
uv run stats-hf-dataset.py merve/coco-dataset
# Top 20 label histogram, JSON output
uv run stats-hf-dataset.py merve/dataset --top 20 --report json
# Stats for TFOD-format dataset
uv run stats-hf-dataset.py merve/dataset --bbox-format tfod
# Stream large datasets
uv run stats-hf-dataset.py merve/huge-dataset --streaming --max-samples 10000
# Push stats report to Hub
uv run stats-hf-dataset.py merve/dataset --output-dataset merve/stats-report
Reports include: summary counts, label distribution, annotation density, bbox area/aspect ratio distributions, per-category area stats, category co-occurrence pairs, and image resolution distribution.
Diff (diff-hf-datasets.py)
Compare two datasets semantically using IoU-based annotation matching:
# Basic diff
uv run diff-hf-datasets.py merve/dataset-v1 merve/dataset-v2
# Stricter matching
uv run diff-hf-datasets.py merve/old merve/new --iou-threshold 0.7
# Per-annotation change details
uv run diff-hf-datasets.py merve/old merve/new --detail
# JSON report
uv run diff-hf-datasets.py merve/old merve/new --report json
Reports include: shared/unique images, shared/unique categories, matched/added/removed/modified annotations.
Sample (sample-hf-dataset.py)
Create random or stratified subsets:
# Random 500 samples
uv run sample-hf-dataset.py merve/dataset merve/subset -n 500
# 10% fraction
uv run sample-hf-dataset.py merve/dataset merve/subset --fraction 0.1
# Stratified sampling (preserves class distribution)
uv run sample-hf-dataset.py merve/dataset merve/subset \
-n 200 --strategy stratified
# Filter by categories
uv run sample-hf-dataset.py merve/dataset merve/subset \
-n 100 --categories "cat,dog,bird"
# Reproducible sampling
uv run sample-hf-dataset.py merve/dataset merve/subset \
-n 500 --seed 42
| Option | Description |
|---|---|
-n |
Number of samples to select |
--fraction |
Fraction of dataset (0.0–1.0) |
--strategy |
random (default) or stratified |
--categories |
Comma-separated list of categories to filter by |
--category-mode |
images (default) or annotations |
Run Locally
# Clone and run
git clone https://huggingface.co/datasets/uv-scripts/panlabel
cd panlabel
uv run convert-hf-dataset.py input-dataset output-dataset --from coco_xywh --to yolo
# Or run directly from URL
uv run https://huggingface.co/datasets/uv-scripts/panlabel/raw/main/convert-hf-dataset.py \
input-dataset output-dataset --from coco_xywh --to yolo
Works with any Hugging Face dataset containing object detection annotations — COCO, YOLO, VOC, TFOD, or Label Studio format.
Making a dataset from scratch
The other scripts assume you already have annotations. falcon-perception.py is where they can come from — Falcon-Perception finds every instance of a class you name, with no label set and no training:
# 1. does the model do the thing? (your laptop — no GPU needed)
uv run falcon-perception.py --image page.jpg --query illustration --preview
# 2. does it work on YOUR data? (first rows of the real corpus)
uv run falcon-perception.py --dataset biglam/british-library-book-images \
--config plates --limit 3 --preview
# 3. the whole corpus, on a GPU
hf jobs uv run --flavor a10g-large --secrets HF_TOKEN falcon-perception.py -- \
--dataset biglam/british-library-book-images --config plates \
--id-col fname --query illustration --out you/plates-illustrations
# 4. it is already in `yolo` format — the rest of this directory just works
uv run validate-hf-dataset.py you/plates-illustrations --bbox-format yolo
uv run stats-hf-dataset.py you/plates-illustrations --bbox-format yolo
Falcon emits boxes as normalised centre x,y + w,h, which is the yolo format above, so no conversion step is needed.
The correction loop. A zero-shot first pass is a starting point, not ground truth. Convert it for human review, correct it, then diff the two to find out how good the first pass actually was:
uv run convert-hf-dataset.py you/plates-illustrations you/for-review --from yolo --to label_studio
# ... correct in Label Studio, push as you/corrected ...
uv run diff-hf-datasets.py you/plates-illustrations you/corrected # IoU match = zero-shot accuracy
Runs without a CUDA GPU. Unlike most recipes in this repo, falcon-perception.py selects the MLX backend on Apple Silicon automatically. It is slower there (~6 s/img vs ~0.4 on an A10G), which is the right trade for step 1 and 2 above — checking your class name works before spending GPU hours.
Known limits
Measured, not guessed — see the script docstrings for the failure each one came from.
| Limit | What to do |
|---|---|
--query is a class name, not an instruction |
illustration works; the illustration, excluding captions returns nothing |
| One class per run | A combined query returned 6 instances where three single-class runs found 24. N classes = N runs, then concatenate |
| No confidence scores — the model has no score token | Sort review by the emitted rectangularity (mask area ÷ bbox area, measured 0.34–1.00) and apply an area floor |
a10g-small gets OOMKilled |
The engine's auto-config sizes from the GPU and ignores host RAM — use a10g-large |
Just want the numbers?
--out takes a file path as readily as a repo id — no Hub push, nothing to clean up:
uv run falcon-perception.py --image page.jpg --query illustration --out results.json
uv run falcon-perception.py --image "scans/*.jpg" --query illustration --out results.jsonl
uv run falcon-perception.py --image page.jpg --query illustration --json | jq '.[0].objects.bbox'
Anything ending .json, .jsonl or .parquet is written locally; anything else is treated as a Hub dataset repo id.
Bucket runs
falcon-perception-bucket.py reads images from an HF bucket and writes resumable parquet parts back to a bucket — kill it and re-run the same command, done keys are skipped. Publish once at the end to use the rest of this directory:
from datasets import load_dataset
load_dataset("parquet", data_files=["hf://buckets/you/bl-masks/part-000000.parquet", ...],
split="train").push_to_hub("you/bl-masks")
Output columns
objects.bbox (yolo), objects.category, objects.area, objects.rectangularity, plus image, image_id, width, height, n_instances, and masks_rle (COCO RLE — segmentation rides along; the bbox scripts ignore it).
- Downloads last month
- 45