Download scripts/prepare_cvbench_subset.py from Orangerl/umm: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/Orangerl/umm/resolve/main/scripts/prepare_cvbench_subset.py
- Command line
-
hf download hf://Orangerl/umm/scripts/prepare_cvbench_subset.py
-
curl -L -o prepare_cvbench_subset.py https://huggingface.co/Orangerl/umm/resolve/main/scripts/prepare_cvbench_subset.py
10.9 kB
| #!/usr/bin/env python3 | |
| """Build a deterministic 500-example CV-Bench validation subset. | |
| The source parquet files are fetched without any proxy, but only 250 examples | |
| from each of the 2D and 3D configurations are materialized. Sampling is | |
| stratified by the dataset's task column and is reproducible from ``--seed``. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import io | |
| import json | |
| import os | |
| import random | |
| import shutil | |
| import subprocess | |
| from collections import Counter, defaultdict | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| import pyarrow as pa | |
| import pyarrow.parquet as pq | |
| from PIL import Image | |
| SOURCE_REPO = "nyu-visionx/CV-Bench" | |
| SOURCE_REVISION = "bc284db50d036958861cb60cdd7b77612052ce0d" | |
| SOURCE_FILES = { | |
| "2D": "test_2d.parquet", | |
| "3D": "test_3d.parquet", | |
| } | |
| EXPECTED_ROWS = {"2D": 1438, "3D": 1200} | |
| PROXY_ENV_VARS = ( | |
| "http_proxy", | |
| "https_proxy", | |
| "HTTP_PROXY", | |
| "HTTPS_PROXY", | |
| "all_proxy", | |
| "ALL_PROXY", | |
| ) | |
| IMAGE_SUFFIXES = { | |
| "JPEG": ".jpg", | |
| "PNG": ".png", | |
| "WEBP": ".webp", | |
| "BMP": ".bmp", | |
| "GIF": ".gif", | |
| "TIFF": ".tiff", | |
| } | |
| def direct_environment() -> dict[str, str]: | |
| environment = os.environ.copy() | |
| for name in PROXY_ENV_VARS: | |
| environment.pop(name, None) | |
| return environment | |
| def download_direct(url: str, destination: Path, retries: int = 5) -> None: | |
| if destination.is_file() and destination.stat().st_size > 0: | |
| print(f"[cvbench] reuse {destination} ({destination.stat().st_size} bytes)") | |
| return | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| part = destination.with_suffix(destination.suffix + ".part") | |
| print(f"[cvbench] direct resumable download {url}", flush=True) | |
| subprocess.run( | |
| [ | |
| "curl", | |
| "--noproxy", | |
| "*", | |
| "--fail", | |
| "--location", | |
| "--retry", | |
| str(retries), | |
| "--retry-all-errors", | |
| "--retry-delay", | |
| "2", | |
| "--connect-timeout", | |
| "30", | |
| "--speed-limit", | |
| "1024", | |
| "--speed-time", | |
| "30", | |
| "--continue-at", | |
| "-", | |
| "--output", | |
| str(part), | |
| url, | |
| ], | |
| env=direct_environment(), | |
| check=True, | |
| ) | |
| if not part.is_file() or part.stat().st_size == 0: | |
| raise IOError(f"download did not produce data: {part}") | |
| part.replace(destination) | |
| def proportional_quotas(groups: dict[str, list[int]], total: int) -> dict[str, int]: | |
| population = sum(len(rows) for rows in groups.values()) | |
| exact = {name: total * len(rows) / population for name, rows in groups.items()} | |
| quotas = {name: int(value) for name, value in exact.items()} | |
| remainder = total - sum(quotas.values()) | |
| order = sorted(groups, key=lambda name: (-(exact[name] - quotas[name]), name)) | |
| for name in order[:remainder]: | |
| quotas[name] += 1 | |
| return quotas | |
| def stable_rng(seed: int, config: str, task: str) -> random.Random: | |
| digest = hashlib.sha256(f"{seed}:{config}:{task}".encode()).digest() | |
| return random.Random(int.from_bytes(digest[:8], "big")) | |
| def select_rows( | |
| parquet_path: Path, | |
| config: str, | |
| count: int, | |
| seed: int, | |
| seen_image_hashes: set[bytes], | |
| ) -> list[int]: | |
| metadata = pq.read_table(parquet_path, columns=["type", "task", "image"]) | |
| if metadata.num_rows != EXPECTED_ROWS[config]: | |
| raise ValueError( | |
| f"unexpected {config} row count: {metadata.num_rows} != {EXPECTED_ROWS[config]}" | |
| ) | |
| groups: dict[str, list[int]] = defaultdict(list) | |
| for row_index, task in enumerate(metadata.column("task").to_pylist()): | |
| groups[str(task)].append(row_index) | |
| quotas = proportional_quotas(groups, count) | |
| selected: list[int] = [] | |
| duplicate_images_skipped = 0 | |
| for task in sorted(groups): | |
| candidates = groups[task].copy() | |
| stable_rng(seed, config, task).shuffle(candidates) | |
| task_selected = 0 | |
| for row_index in candidates: | |
| image_value = metadata.column("image")[row_index].as_py() | |
| raw = image_value.get("bytes") | |
| if raw is None: | |
| raise ValueError(f"parquet image has no embedded bytes at {config} row {row_index}") | |
| image_hash = hashlib.sha256(raw).digest() | |
| if image_hash in seen_image_hashes: | |
| duplicate_images_skipped += 1 | |
| continue | |
| seen_image_hashes.add(image_hash) | |
| selected.append(row_index) | |
| task_selected += 1 | |
| if task_selected == quotas[task]: | |
| break | |
| if task_selected != quotas[task]: | |
| raise ValueError( | |
| f"not enough unique {config}/{task} images: {task_selected} != {quotas[task]}" | |
| ) | |
| selected.sort() | |
| if len(selected) != count or len(set(selected)) != count: | |
| raise AssertionError(f"invalid {config} selection size") | |
| print(f"[cvbench] {config} task quotas: {dict(sorted(quotas.items()))}") | |
| print(f"[cvbench] {config} duplicate candidate images skipped: {duplicate_images_skipped}") | |
| return selected | |
| def image_bytes_and_suffix(image_value: dict) -> tuple[bytes, str, int, int]: | |
| raw = image_value.get("bytes") | |
| if raw is None: | |
| raise ValueError(f"parquet image has no embedded bytes: {image_value.get('path')!r}") | |
| with Image.open(io.BytesIO(raw)) as image: | |
| image.verify() | |
| with Image.open(io.BytesIO(raw)) as image: | |
| fmt = str(image.format or "").upper() | |
| width, height = image.size | |
| suffix = IMAGE_SUFFIXES.get(fmt, ".img") | |
| return raw, suffix, width, height | |
| def materialize_config( | |
| parquet_path: Path, | |
| config: str, | |
| selected: list[int], | |
| staging: Path, | |
| ) -> list[dict]: | |
| columns = [ | |
| "idx", | |
| "type", | |
| "task", | |
| "image", | |
| "question", | |
| "choices", | |
| "answer", | |
| "prompt", | |
| "filename", | |
| "source", | |
| "source_dataset", | |
| "source_filename", | |
| "target_class", | |
| "target_size", | |
| "bbox", | |
| ] | |
| table = pq.read_table(parquet_path, columns=columns) | |
| sampled = table.take(pa.array(selected, type=pa.int64())).to_pylist() | |
| images_dir = staging / "images" | |
| images_dir.mkdir(parents=True, exist_ok=True) | |
| records: list[dict] = [] | |
| for ordinal, (row_index, row) in enumerate(zip(selected, sampled)): | |
| raw, suffix, width, height = image_bytes_and_suffix(row.pop("image")) | |
| image_name = f"{config}_{ordinal:03d}_row{row_index:04d}{suffix}" | |
| image_rel = Path("images") / image_name | |
| (staging / image_rel).write_bytes(raw) | |
| record = { | |
| "sample_id": f"{config}-{ordinal:03d}", | |
| "config": config, | |
| "split": "test", | |
| "row_index": row_index, | |
| "image": image_rel.as_posix(), | |
| "image_width": width, | |
| "image_height": height, | |
| **row, | |
| } | |
| records.append(record) | |
| return records | |
| def validate_existing(output: Path, expected: int) -> bool: | |
| manifest = output / "validation.jsonl" | |
| summary = output / "summary.json" | |
| if not manifest.is_file() or not summary.is_file(): | |
| return False | |
| records = [json.loads(line) for line in manifest.read_text().splitlines() if line.strip()] | |
| if len(records) != expected: | |
| return False | |
| return all((output / record["image"]).is_file() for record in records) | |
| def main() -> None: | |
| root = Path(__file__).resolve().parents[1] | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--output", type=Path, default=root / "data" / "cvbench_500") | |
| parser.add_argument("--seed", type=int, default=42) | |
| parser.add_argument("--per-config", type=int, default=250) | |
| parser.add_argument("--keep-parquet", action="store_true") | |
| args = parser.parse_args() | |
| output = args.output.resolve() | |
| expected = 2 * args.per_config | |
| if validate_existing(output, expected): | |
| print(f"[cvbench] validated existing subset at {output}") | |
| return | |
| staging = output.with_name(output.name + ".tmp") | |
| cache = output.with_name(output.name + ".parquet-cache") | |
| if staging.exists(): | |
| shutil.rmtree(staging) | |
| staging.mkdir(parents=True) | |
| cache.mkdir(parents=True, exist_ok=True) | |
| records: list[dict] = [] | |
| selection_items: list[str] = [] | |
| seen_image_hashes: set[bytes] = set() | |
| for config, filename in SOURCE_FILES.items(): | |
| parquet_path = cache / filename | |
| url = ( | |
| f"https://huggingface.co/datasets/{SOURCE_REPO}/resolve/" | |
| f"{SOURCE_REVISION}/{filename}" | |
| ) | |
| download_direct(url, parquet_path) | |
| selected = select_rows( | |
| parquet_path, | |
| config, | |
| args.per_config, | |
| args.seed, | |
| seen_image_hashes, | |
| ) | |
| records.extend(materialize_config(parquet_path, config, selected, staging)) | |
| selection_items.extend(f"{config}:{row_index}" for row_index in selected) | |
| records.sort(key=lambda row: (row["config"], row["row_index"])) | |
| manifest = staging / "validation.jsonl" | |
| with manifest.open("w", encoding="utf-8") as handle: | |
| for record in records: | |
| handle.write(json.dumps(record, ensure_ascii=False) + "\n") | |
| by_type = Counter(record["config"] for record in records) | |
| by_task = Counter(f"{record['config']}/{record['task']}" for record in records) | |
| answer_counts = Counter(record["answer"] for record in records) | |
| summary = { | |
| "source_repo": SOURCE_REPO, | |
| "source_revision": SOURCE_REVISION, | |
| "source_split": "test", | |
| "seed": args.seed, | |
| "sampling": "task-stratified proportional without replacement; unique image bytes", | |
| "rows": len(records), | |
| "unique_images": len(seen_image_hashes), | |
| "counts_by_type": dict(sorted(by_type.items())), | |
| "counts_by_task": dict(sorted(by_task.items())), | |
| "counts_by_answer": dict(sorted(answer_counts.items())), | |
| "selection_sha256": hashlib.sha256("\n".join(selection_items).encode()).hexdigest(), | |
| "created_utc": datetime.now(timezone.utc).isoformat(), | |
| } | |
| (staging / "summary.json").write_text( | |
| json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" | |
| ) | |
| if output.exists(): | |
| raise FileExistsError( | |
| f"incomplete output already exists at {output}; move it aside before rerunning" | |
| ) | |
| staging.replace(output) | |
| if not args.keep_parquet: | |
| shutil.rmtree(cache) | |
| if not validate_existing(output, expected): | |
| raise RuntimeError("materialized CV-Bench subset failed validation") | |
| print(json.dumps(summary, ensure_ascii=False, indent=2)) | |
| print(f"[cvbench] ready at {output}") | |
| if __name__ == "__main__": | |
| main() | |