AbstractPhil's picture
Deploy: 12-task vision extraction + fusion ZeroGPU showcase
fed954e verified
Raw
History Blame Contribute Delete
43.8 kB
"""
datasets.py — Ground-truth providers.
`GTSample` is the uniform shape every loader yields: an image (PIL, or None for
the torch-free smoke set), the per-category ground truth, and the image size used
for coordinate normalization. The packaged smoke set runs on CPU with no network
so tests and `--dataset smoke --runner stub` work offline. Real loaders stream
public datasets via HF `datasets` (imported lazily) for Phase 1+.
GT shapes (per category):
image_classification : {"labels": [acceptable label strings]}
bbox_grounding : {"boxes": [{"label": str, "bbox": [x,y,w,h], "fmt": "xywh"}]}
ocr_text : {"text": "the reference transcription / answer"}
(stub categories) : None (no GT wired yet)
"""
from __future__ import annotations
import itertools
from dataclasses import dataclass, field
from typing import Any, Callable, Optional
@dataclass
class GTSample:
image: Any # PIL.Image or None (smoke / stub)
prompt: str
gt: Any
category: str
image_id: str
size: tuple[int, int] # (W, H) for coordinate normalization
meta: dict = field(default_factory=dict)
# ──────────────────────────────────────────────────────────────────────────────
# Packaged CPU smoke set (no network, no torch). Small but exercises every shape.
# ──────────────────────────────────────────────────────────────────────────────
_SMOKE: dict[str, list[GTSample]] = {
"image_classification": [
GTSample(None, "Classify this image.", {"labels": ["golden retriever", "dog"]},
"image_classification", "smk_cls_0", (640, 480)),
GTSample(None, "Classify this image.", {"labels": ["espresso", "coffee"]},
"image_classification", "smk_cls_1", (512, 512)),
GTSample(None, "Classify this image.", {"labels": ["school bus", "bus"]},
"image_classification", "smk_cls_2", (800, 600)),
],
"bbox_grounding": [
GTSample(None, "Detect all objects.",
{"boxes": [{"label": "dog", "bbox": [64, 48, 128, 96], "fmt": "xywh"}]},
"bbox_grounding", "smk_box_0", (640, 480)),
GTSample(None, "Detect all objects.",
{"boxes": [{"label": "cat", "bbox": [10, 10, 40, 40], "fmt": "xywh"},
{"label": "ball", "bbox": [200, 150, 50, 50], "fmt": "xywh"}]},
"bbox_grounding", "smk_box_1", (640, 480)),
],
"ocr_text": [
GTSample(None, "Read all text.", {"text": "STOP"}, "ocr_text", "smk_ocr_0", (200, 200)),
GTSample(None, "Read all text.", {"text": "no entry"}, "ocr_text", "smk_ocr_1", (300, 200)),
],
}
def smoke_samples(category: str, n: Optional[int] = None) -> list[GTSample]:
"""Smoke samples for a category. Stub categories (no GT) get synthetic blanks."""
if category in _SMOKE:
out = _SMOKE[category]
else:
out = [GTSample(None, "Analyze this image.", None, category, f"smk_{category}_{i}", (64, 64))
for i in range(2)]
return out[:n] if n else list(out)
# ──────────────────────────────────────────────────────────────────────────────
# Real loaders (Phase 1+). HF `datasets` is imported lazily so the smoke path and
# the CPU tests never require it.
# ──────────────────────────────────────────────────────────────────────────────
def _hf_stream(repo: str, split: str, n: int, **kw):
from datasets import load_dataset # lazy
ds = load_dataset(repo, split=split, streaming=True, **kw)
return list(itertools.islice(ds, n))
def load_imagenet_val(n: int = 200, split: str = "validation") -> list[GTSample]:
"""Classification GT. ImageNet-1k is GATED and its label is a bare integer id;
use food101 (ungated parquet) and map the ClassLabel id -> class name."""
from datasets import load_dataset # lazy
last = None
for repo, sp in [("ethz/food101", "validation"), ("food101", "validation")]:
try:
ds = load_dataset(repo, split=sp, streaming=True)
try:
names = ds.features["label"].names
except Exception:
names = None
rows = list(itertools.islice(ds, n))
out = []
for i, r in enumerate(rows):
img = r.get("image")
lbl = r.get("label")
name = (names[lbl].replace("_", " ")
if names and isinstance(lbl, int) and 0 <= lbl < len(names) else str(lbl))
size = (img.width, img.height) if img is not None else (0, 0)
out.append(GTSample(img, "Classify this image.", {"labels": [name]},
"image_classification", f"cls_{i}", size))
if out:
return out
except Exception as e:
last = e
continue
raise RuntimeError(f"no classification dataset streamable (food101): {last}")
# COCO-80 class names in category-id order (confirmed from detection-datasets/coco
# ClassLabel features). objects.bbox is [x1,y1,x2,y2] in absolute pixels (xyxy).
COCO_CLASSES = [
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat",
"traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat",
"dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack",
"umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball",
"kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket",
"bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair",
"couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
"remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator",
"book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush",
]
def _coco_label(cid) -> str:
if isinstance(cid, int) and 0 <= cid < len(COCO_CLASSES):
return COCO_CLASSES[cid]
return str(cid)
def load_coco_detection(n: int = 200, split: str = "val") -> list[GTSample]:
"""detection-datasets/coco: objects.bbox is xyxy pixels; category is a ClassLabel id."""
rows = _hf_stream("detection-datasets/coco", split, n)
out = []
for i, r in enumerate(rows):
img = r["image"]
objs = r.get("objects", {})
boxes = [{"label": _coco_label(c), "bbox": list(map(float, b)), "fmt": "xyxy"}
for c, b in zip(objs.get("category", []), objs.get("bbox", []))]
out.append(GTSample(img, "Detect all objects in this image. Output only the raw JSON object.",
{"boxes": boxes}, "bbox_grounding", f"coco_{i}", (img.width, img.height)))
return out
def load_textvqa(n: int = 200, split: str = "validation") -> list[GTSample]:
"""OCR GT. The script-based 'textvqa' repo is rejected by modern `datasets`; use
parquet repos. GT = {"text": <gold answer>}; the model transcribes the image and
the OCR scorer credits containment of the answer."""
from datasets import load_dataset # lazy
last = None
for repo, sp in [("lmms-lab/textvqa", "validation"), ("howard-hou/OCR-VQA", "test")]:
try:
ds = load_dataset(repo, split=sp, streaming=True)
rows = list(itertools.islice(ds, n))
out = []
for i, r in enumerate(rows):
img = r.get("image")
ans = r.get("answers")
if ans is None:
ans = r.get("answer") or r.get("questions")
if isinstance(ans, (list, tuple)):
ans = next((str(a) for a in ans if str(a).strip()), "")
size = (img.width, img.height) if img is not None else (0, 0)
out.append(GTSample(img, "Read all the text in this image.",
{"text": str(ans or "")}, "ocr_text", f"ocr_{i}", size))
if out:
return out
except Exception as e:
last = e
continue
raise RuntimeError(f"no OCR dataset streamable (textvqa/ocr-vqa): {last}")
# ──────────────────────────────────────────────────────────────────────────────
# Synthetic data-format images (self-contained: no external dataset). Renders a
# small record in several serialization formats to an image, with exact GT for
# both the format (data_type) and the normalized content. Tests whether a VLM can
# recognize a data format from a screenshot and re-serialize it to JSON.
# ──────────────────────────────────────────────────────────────────────────────
_DATATYPE_RECORDS = [
{"name": "Alice", "age": "30", "city": "Paris"},
{"id": "7", "title": "Widget", "price": "9"},
{"user": "bob", "active": "true", "score": "42"},
{"country": "Japan", "capital": "Tokyo", "pop": "14"},
]
def _datatype_font(sz=22):
from PIL import ImageFont
for name in ("DejaVuSansMono.ttf", "DejaVuSans.ttf"):
try:
return ImageFont.truetype(name, sz)
except Exception:
continue
try:
return ImageFont.load_default(size=sz) # Pillow >= 10
except Exception:
return ImageFont.load_default()
def _render_text_image(text: str, size=(640, 360)) -> "object":
from PIL import Image, ImageDraw
img = Image.new("RGB", size, (255, 255, 255))
d = ImageDraw.Draw(img)
d.multiline_text((18, 18), text, fill=(0, 0, 0), font=_datatype_font(22), spacing=8)
return img
def _serialize(rec: dict, fmt: str) -> str:
if fmt == "json":
import json as _j
return _j.dumps(rec, indent=2)
if fmt == "yaml":
return "\n".join(f"{k}: {v}" for k, v in rec.items())
if fmt == "toml":
return "\n".join(f'{k} = "{v}"' for k, v in rec.items())
if fmt == "xml":
inner = "".join(f"<{k}>{v}</{k}>" for k, v in rec.items())
return f"<record>{inner}</record>"
if fmt == "csv":
return ",".join(rec.keys()) + "\n" + ",".join(rec.values())
if fmt == "markdown":
return "# Record\n" + "\n".join(f"- **{k}**: {v}" for k, v in rec.items())
raise ValueError(fmt)
_DATATYPE_FORMATS = ["json", "yaml", "toml", "xml", "csv", "markdown"]
def make_datatype_samples(n: int = 18, split=None) -> list[GTSample]:
"""Self-contained: render records across formats. GT = {data_type, content}."""
out = []
i = 0
while len(out) < n:
rec = _DATATYPE_RECORDS[i % len(_DATATYPE_RECORDS)]
fmt = _DATATYPE_FORMATS[i % len(_DATATYPE_FORMATS)]
text = _serialize(rec, fmt)
# csv normalizes to a one-row list; everything else to the dict
content = [rec] if fmt == "csv" else rec
img = _render_text_image(text)
out.append(GTSample(img, "Identify the data format and contents. Output only raw JSON.",
{"data_type": fmt, "content": content},
"data_type", f"dt_{i}", img.size))
i += 1
return out
# ──────────────────────────────────────────────────────────────────────────────
# Synthetic colored-shapes scenes (self-contained). One scene yields exact GT for
# spatial relations (by x-order), depth ordering (bigger circle = nearer), and
# subject fixation (largest circle = primary subject). Reliable + no download.
# ──────────────────────────────────────────────────────────────────────────────
import itertools as _it
_SHAPE_COLORS = {"red": (220, 30, 30), "green": (30, 170, 30), "blue": (40, 40, 220)}
_SHAPE_NAMES = ["red", "green", "blue"]
_SHAPE_SIZES = [110, 76, 46] # diameters: big / medium / small (depth cue)
def _shape_scene(i: int):
"""Deterministic 3-circle scene. Returns ((W,H), [shape dicts]) sorted left→right."""
W, H = 540, 320
x_centers = [110, 270, 430]
color_perm = list(_it.permutations(range(3)))[i % 6] # which color in which column
size_perm = list(_it.permutations(range(3)))[(i // 6) % 6] # which color gets which size
shapes = []
for ci, color in enumerate(_SHAPE_NAMES):
cx = x_centers[color_perm[ci]]
d = _SHAPE_SIZES[size_perm[ci]]
cy = H // 2
shapes.append({"label": color, "cx": cx, "cy": cy, "d": d, "area": d * d,
"bbox": [cx - d / 2, cy - d / 2, cx + d / 2, cy + d / 2]})
shapes.sort(key=lambda s: s["cx"])
return (W, H), shapes
def _render_scene(size, shapes):
from PIL import Image, ImageDraw
img = Image.new("RGB", size, (245, 245, 245))
d = ImageDraw.Draw(img)
for s in shapes:
d.ellipse(s["bbox"], fill=_SHAPE_COLORS[s["label"]])
return img
def make_shapes_samples(n: int = 12, split=None) -> list[GTSample]:
"""Scenes carrying GT for spatial / depth / subject_fixation simultaneously."""
out = []
for i in range(n):
size, shapes = _shape_scene(i)
# spatial: left_of for every left→right pair
triples = []
for a, b in _it.combinations(shapes, 2): # already x-sorted → a left of b
triples.append([a["label"], "left_of", b["label"]])
# depth: bigger area = nearer
pairs = []
for a, b in _it.combinations(shapes, 2):
pairs.append({"a": a["label"], "b": b["label"],
"a_is": "nearer" if a["area"] >= b["area"] else "farther"})
# subject: largest area
subj = max(shapes, key=lambda s: s["area"])
gt = {"triples": triples, "pairs": pairs,
"label": subj["label"], "box": subj["bbox"], "fmt": "xyxy"}
img = _render_scene(size, shapes)
out.append(GTSample(img, "Analyze the colored shapes. Output only raw JSON.",
gt, "shapes", f"shapes_{i}", size))
return out
def _circle_polygon(cx, cy, d, n=16):
"""Approximate a circle (diameter d, center cx,cy) as a flat pixel-coord
polygon [x1,y1,x2,y2,...] with n vertices."""
import math
r = d / 2.0
flat = []
for k in range(n):
ang = 2.0 * math.pi * k / n
flat.append(cx + r * math.cos(ang))
flat.append(cy + r * math.sin(ang))
return flat
def make_segmentation_samples(n: int = 12, split=None) -> list[GTSample]:
"""Self-contained instance-segmentation GT: reuse the 3-circle shape scenes.
Each colored circle becomes one mask whose polygon is the circle approximated
by 16 vertices (label = color). Polygons are in PIXEL coords; the scorer
converts model polygons from NORM_0_1000 to pixels."""
out = []
for i in range(n):
size, shapes = _shape_scene(i)
masks = [{"label": s["label"],
"polygon_pixels": _circle_polygon(s["cx"], s["cy"], s["d"], n=16)}
for s in shapes]
img = _render_scene(size, shapes)
out.append(GTSample(img, "Segment the colored shapes as labeled polygons. Output only raw JSON.",
{"masks": masks}, "segmentation", f"seg_{i}", size))
return out
def make_outline_samples(n: int = 12, split=None) -> list[GTSample]:
"""Self-contained: reuse the 3-circle synthetic scene. GT outline = the largest
circle approximated as a 16-point polygon (pixels), label = its color."""
out = []
for i in range(n):
size, shapes = _shape_scene(i)
main = max(shapes, key=lambda s: s["area"]) # largest = main object
poly = _circle_polygon(main["cx"], main["cy"], main["d"], 16)
gt = {"outline": poly, "label": main["label"], "bbox": main["bbox"], "fmt": "xyxy"}
img = _render_scene(size, shapes)
out.append(GTSample(img, "Trace the main object's outline. Output only raw JSON.",
gt, "outline_association", f"outline_{i}", size))
return out
_BOX3D_COLORS = {"red": (220, 40, 40), "green": (40, 175, 40), "blue": (50, 50, 225)}
_BOX3D_NAMES = ["red", "green", "blue"]
def _box3d_scene(i: int):
"""Deterministic 2-3 colored boxes at known ground (x,z) positions.
GT convention (normalized 0..1): bbox3d = [x, y, z, w, h, l, yaw] with
x = left-right ground position, z = depth (0 near .. 1 far), y = 0 (on the
floor), (w,h,l) the box footprint width / height / length, yaw = 0. The GT is
exact-by-construction; the render is a simplified ground-plane 3D proxy.
"""
import math
import itertools
W, H = 480, 360
n_boxes = 2 + (i % 2) # 2 or 3 boxes
names = _BOX3D_NAMES[:n_boxes]
perm = list(itertools.permutations(range(n_boxes)))[i % math.factorial(n_boxes)]
x_slots = [0.2, 0.5, 0.8][:n_boxes]
z_slots = [0.25, 0.55, 0.85][:n_boxes]
objects, draw = [], []
for k, color in enumerate(names):
x = x_slots[perm[k] % n_boxes]
z = z_slots[k] # increasing depth per index
w = 0.16 + 0.04 * ((i + k) % 3) # footprint width
l = 0.14
h = 0.22 + 0.03 * (k % 2) # box height
objects.append({"class": color,
"bbox3d": [round(x, 4), 0.0, round(z, 4),
round(w, 4), round(h, 4), round(l, 4), 0.0]})
draw.append((color, x, z, w, h))
return (W, H), objects, draw
def _render_box3d_scene(size, draw):
"""Perspective proxy: nearer (small z) boxes drawn lower in frame and larger."""
from PIL import Image, ImageDraw
W, H = size
img = Image.new("RGB", size, (235, 235, 240))
d = ImageDraw.Draw(img)
d.rectangle([0, int(H * 0.5), W, H], fill=(205, 200, 190)) # ground band
for color, x, z, w, h in sorted(draw, key=lambda t: t[2], reverse=True): # far first
scale = 1.0 - 0.45 * z # nearer = bigger
bw = w * W * scale
bh = h * H * scale
cx = x * W
cy = (0.5 + 0.45 * z) * H # nearer = lower
d.rectangle([cx - bw / 2, cy - bh, cx + bw / 2, cy],
fill=_BOX3D_COLORS[color], outline=(20, 20, 20))
return img
def make_3d_samples(n: int = 12, split=None) -> list[GTSample]:
"""Self-contained synthetic 3D scenes. GT exact-by-construction (proxy)."""
out = []
for i in range(n):
size, objects, draw = _box3d_scene(i)
img = _render_box3d_scene(size, draw)
out.append(GTSample(img, "Identify the 3D boxes. Output only raw JSON.",
{"objects": objects}, "geometric_3d_object_id",
f"box3d_{i}", size))
return out
def make_camera_samples(n: int = 12, split=None) -> list[GTSample]:
"""Self-contained synthetic camera-roll set. A clear orientation cue (an upward
arrow over a horizon line) is drawn upright, then the whole image is rotated by a
KNOWN roll angle that varies by index; yaw=pitch=0 (a single 2D cue cannot
disambiguate yaw/pitch). GT = {"rotation": [0, 0, roll_deg]}.
NOTE: SIMPLIFIED proxy — this tests recovery of ROLL from a 2D cue only; it does
not exercise yaw/pitch (which would need a 3D scene). Reliable, no download.
"""
from PIL import Image, ImageDraw
W, H = 480, 480
cx, cy = W / 2.0, H / 2.0
# deterministic spread of rolls across the wrapped range, indexed by sample
roll_table = [0, 15, 30, 45, 60, 90, -15, -30, -45, -60, -90, 120,
-120, 150, 75, -75, 10, -10]
out = []
for i in range(n):
roll = float(roll_table[i % len(roll_table)])
base = Image.new("RGB", (W, H), (250, 250, 250))
d = ImageDraw.Draw(base)
d.line([(60, cy), (W - 60, cy)], fill=(60, 60, 60), width=6) # horizon line
d.line([(cx, cy), (cx, 90)], fill=(200, 40, 40), width=8) # arrow shaft (points up)
d.polygon([(cx, 60), (cx - 22, 105), (cx + 22, 105)], fill=(200, 40, 40)) # arrow head
# Rotate scene by -roll about the centre (expand=False keeps size + GT stable):
# a positive camera roll (CW) rotates scene content CCW in the image.
img = base.rotate(-roll, resample=Image.BICUBIC, center=(cx, cy),
fillcolor=(250, 250, 250), expand=False)
gt = {"rotation": [0.0, 0.0, roll]}
out.append(GTSample(img,
"Estimate the camera rotation [yaw, pitch, roll]. Output only raw JSON.",
gt, "camera_rotational_offset", f"camrot_{i}", (W, H)))
return out
def make_gqa_samples(n: int = 200, split: str = "validation") -> list[GTSample]:
"""Grounded-VQA GT (REAL, best-effort). Streams a VQA dataset; one sample per
(image, question, answers). The question is per-image and goes in
GTSample.prompt; gt = {"answers": [<gold strings>]}. Image is row["image"]
(a PIL image).
Repo ids are BEST-EFFORT — the maintainer must verify id/config/split:
primary : "lmms-lab/GQA" (testdev_balanced / val splits; row has
"question" + "answer"; image under "image")
fallback: "HuggingFaceM4/VQAv2" (row has "question" + "answers"
list-of-dicts or list-of-strings)
The answer-field probing below tolerates both shapes.
"""
# Script-based repos (HuggingFaceM4/VQAv2, lmms-lab/GQA) are rejected by modern
# `datasets`. Use PARQUET repos (verified format:parquet on the Hub), in order.
rows = None
for repo, sp in [("lmms-lab/VQAv2", split), ("merve/vqav2-small", "validation"),
("merve/vqav2-small", "train"), ("lmms-lab/OK-VQA", "val2014")]:
try:
rows = _hf_stream(repo, sp, n)
if rows:
break
except Exception:
continue
if not rows:
raise RuntimeError("no parquet VQA dataset streamable "
"(tried lmms-lab/VQAv2, merve/vqav2-small, lmms-lab/OK-VQA)")
out = []
for i, r in enumerate(rows):
img = r.get("image")
question = str(r.get("question") or r.get("question_str") or "What is in this image?")
raw_ans = r.get("answers")
if raw_ans is None:
raw_ans = r.get("multiple_choice_answer") or r.get("answer")
if isinstance(raw_ans, dict): # {"answer": "x"} or value-map
raw_ans = raw_ans.get("answer") or list(raw_ans.values())
if isinstance(raw_ans, (list, tuple)):
answers = []
for a in raw_ans:
if isinstance(a, dict): # VQAv2: [{"answer": "x"}, ...]
a = a.get("answer", "")
if str(a).strip():
answers.append(str(a))
elif raw_ans is not None and str(raw_ans).strip():
answers = [str(raw_ans)]
else:
answers = []
size = (img.width, img.height) if img is not None else (0, 0)
out.append(GTSample(img, question, {"answers": answers},
"vit_accuracy_to_prompt", f"vqa_{i}", size))
return out
def make_semantic_samples(n: int = 12, split=None) -> list[GTSample]:
"""Self-contained colored-shapes scenes carrying GT semantic-association triples.
Reuses the deterministic 3-circle scene (`_shape_scene`). Associations are
derived purely from geometry so they are exact and reproducible:
* left->right ordering -> (left, "left_of", right) AND (right, "right_of", left)
* adjacency (consecutive) -> (a, "near", b) for neighbouring shapes
* taxonomy -> (color, "is_a", "circle") for every shape
GT shape: {"triples": [[a, relation, b], ...]} -- read directly by score_triples,
which does tolerant subject/object matching + normalized-exact predicate matching.
Relations are chosen so they round-trip cleanly through metrics._norm_pred
(left_of/right_of/near/is_a stay identical after normalization).
"""
out = []
for i in range(n):
size, shapes = _shape_scene(i) # sorted left->right
triples: list[list] = []
# ordering relations over every left->right pair (both directions)
for a, b in _it.combinations(shapes, 2): # a is left of b
triples.append([a["label"], "left_of", b["label"]])
triples.append([b["label"], "right_of", a["label"]])
# adjacency ("near") for consecutive shapes in the x-ordering
for a, b in zip(shapes, shapes[1:]):
triples.append([a["label"], "near", b["label"]])
# taxonomic: each colored shape is a circle
for s in shapes:
triples.append([s["label"], "is_a", "circle"])
img = _render_scene(size, shapes)
out.append(GTSample(
img,
"List semantic associations between the shapes. Output only raw JSON.",
{"triples": triples}, "semantic_association", f"semassoc_{i}", size,
))
return out
def _style_font(sz=28):
from PIL import ImageFont
for name in ("DejaVuSans.ttf", "DejaVuSansMono.ttf"):
try:
return ImageFont.truetype(name, sz)
except Exception:
continue
try:
return ImageFont.load_default(size=sz) # Pillow >= 10
except Exception:
return ImageFont.load_default()
def _render_style_image(style: str, size=(320, 320)):
"""Render a controllable, visually-distinguishable exemplar for each coarse style.
photo: smooth RGB gradient (photographic continuous tone). painting: soft color blobs
on canvas. sketch: black outlines on white. 3d_render: lit/shaded sphere. anime:
flat-shaded face with big eyes. other: a labelled fallback."""
from PIL import Image, ImageDraw
import math
W, H = size
cx, cy = W // 2, H // 2
if style == "photo":
img = Image.new("RGB", size, (0, 0, 0))
px = img.load()
for y in range(H):
for x in range(W):
px[x, y] = (int(40 + 180 * x / W), int(40 + 180 * y / H),
int(120 + 100 * ((x + y) % 50) / 50))
return img
if style == "painting":
img = Image.new("RGB", size, (235, 225, 205))
d = ImageDraw.Draw(img)
for (bx, by), r, col in [((90, 90), 70, (200, 70, 60)),
((210, 120), 60, (70, 110, 190)),
((140, 220), 80, (90, 170, 90))]:
d.ellipse([bx - r, by - r, bx + r, by + r], fill=col)
return img
if style == "sketch":
img = Image.new("RGB", size, (255, 255, 255))
d = ImageDraw.Draw(img)
d.rectangle([cx - 70, cy - 70, cx + 70, cy + 70], outline=(0, 0, 0), width=3)
d.line([cx - 70, cy - 70, cx + 70, cy + 70], fill=(0, 0, 0), width=2)
d.line([cx + 70, cy - 70, cx - 70, cy + 70], fill=(0, 0, 0), width=2)
d.ellipse([cx - 40, cy - 40, cx + 40, cy + 40], outline=(0, 0, 0), width=2)
return img
if style == "3d_render":
img = Image.new("RGB", size, (245, 245, 250))
d = ImageDraw.Draw(img)
r = 90
for yy in range(cy - r, cy + r):
for xx in range(cx - r, cx + r):
dx, dy = (xx - cx) / r, (yy - cy) / r
if dx * dx + dy * dy <= 1.0:
lx, ly = -0.5, -0.6
nz = math.sqrt(max(0.0, 1.0 - dx * dx - dy * dy))
shade = max(0.12, (-dx * lx - dy * ly + nz) / 1.7)
v = int(60 + 195 * min(1.0, shade))
d.point((xx, yy), fill=(v, int(v * 0.7), int(v * 0.5)))
return img
if style == "anime":
img = Image.new("RGB", size, (250, 240, 230))
d = ImageDraw.Draw(img)
d.ellipse([cx - 80, cy - 90, cx + 80, cy + 70], fill=(255, 224, 196),
outline=(40, 30, 30), width=3)
for ex in (cx - 35, cx + 35):
d.ellipse([ex - 18, cy - 10, ex + 18, cy + 30], fill=(255, 255, 255),
outline=(20, 20, 20), width=2)
d.ellipse([ex - 10, cy + 2, ex + 10, cy + 26], fill=(60, 110, 200))
d.ellipse([ex - 4, cy + 6, ex + 4, cy + 16], fill=(20, 20, 20))
d.polygon([(cx - 90, cy - 90), (cx - 30, cy - 110), (cx, cy - 80)], fill=(90, 60, 40))
return img
# "other" fallback
img = Image.new("RGB", size, (200, 200, 200))
ImageDraw.Draw(img).text((20, H // 2), "other", fill=(0, 0, 0), font=_style_font(28))
return img
# Each rendered style implies a controlled (layout, symmetry) GT pair.
_STYLE_LAYOUTS = {
"photo": ("rule_of_thirds", "none"),
"painting": ("scattered", "none"),
"sketch": ("centered", "radial"),
"3d_render": ("centered", "radial"),
"anime": ("centered", "vertical"),
"other": ("centered", "none"),
}
_STYLE_ORDER = ["photo", "painting", "sketch", "3d_render", "anime"]
def make_style_samples(n: int = 10, split=None) -> list[GTSample]:
"""Self-contained: render distinguishable styles we control. Cycles through
photo/painting/sketch/3d_render/anime. GT = {style, layout, symmetry}."""
out = []
for i in range(n):
style = _STYLE_ORDER[i % len(_STYLE_ORDER)]
layout, symmetry = _STYLE_LAYOUTS[style]
size = (320, 320)
img = _render_style_image(style, size)
gt = {"style": style, "layout": layout, "symmetry": symmetry}
out.append(GTSample(img, "Classify the visual style and structure. Output only raw JSON.",
gt, "style_structural_awareness", f"style_{i}", size))
return out
# ──────────────────────────────────────────────────────────────────────────────
# REAL COCO instance segmentation GT (for segmentation / outline / subject) — parses
# the official COCO annotations JSON directly (no script-dataset, no pycocotools) and
# pulls images by URL. Replaces the synthetic colored-shape GT with real images.
# ──────────────────────────────────────────────────────────────────────────────
_COCO_CACHE: dict = {}
_COCO_PERSON_CAT = 1 # COCO category_id for "person"
def _coco_ann_file(name: str) -> str:
"""Ensure `{cache_dir}/{name}` exists. ONE zip download extracts BOTH
instances_val2017.json and captions_val2017.json (they ship in the same
annotations_trainval2017.zip — extracting only one wastes the 241MB fetch)."""
import io
import os
import urllib.request
import zipfile
cache_dir = os.environ.get("HF_HOME") or os.environ.get("TMPDIR") or "/tmp"
os.makedirs(cache_dir, exist_ok=True)
path = os.path.join(cache_dir, name)
if not os.path.exists(path):
print(f" downloading COCO val2017 annotations (~241MB, one-time) for {name} …")
zurl = "http://images.cocodataset.org/annotations/annotations_trainval2017.zip"
zb = urllib.request.urlopen(zurl, timeout=600).read()
with zipfile.ZipFile(io.BytesIO(zb)) as z:
for member in ("instances_val2017.json", "captions_val2017.json"):
target = os.path.join(cache_dir, member)
if not os.path.exists(target):
with z.open(f"annotations/{member}") as f:
open(target, "wb").write(f.read())
return path
def _coco_ann(kind: str = "instances") -> dict:
"""Parsed-JSON cache for the COCO annotation files. `kind` is "instances" or
"captions". Keeps the existing _COCO_CACHE["ann"] key for instances."""
import json as _json
key = "ann" if kind == "instances" else f"ann_{kind}"
if key not in _COCO_CACHE:
with open(_coco_ann_file(f"{kind}_val2017.json"), encoding="utf-8") as f:
_COCO_CACHE[key] = _json.load(f)
return _COCO_CACHE[key]
def _coco_instances(n: int) -> list:
"""Returns [(image, (W,H), image_id, [{label, polygon_pixels, box_xyxy, area}])].
Downloads + caches instances_val2017.json (~one-time) and the first `n` val images."""
import io
import urllib.request
from collections import defaultdict
from PIL import Image
key = f"inst_{n}"
if key in _COCO_CACHE:
return _COCO_CACHE[key]
data = _coco_ann("instances")
cats = {c["id"]: c["name"] for c in data["categories"]}
imgs = {im["id"]: im for im in data["images"]}
anns = defaultdict(list)
for a in data["annotations"]:
anns[a["image_id"]].append(a)
out = []
for iid in list(imgs):
if len(out) >= n:
break
info = imgs[iid]
try:
raw = urllib.request.urlopen(
f"http://images.cocodataset.org/val2017/{info['file_name']}", timeout=60).read()
img = Image.open(io.BytesIO(raw)).convert("RGB")
except Exception:
continue
objs = []
for a in anns[iid]:
seg = a.get("segmentation")
if a.get("iscrowd") or not isinstance(seg, list) or not seg:
continue # skip RLE / crowd
poly = [float(v) for v in seg[0]]
if len(poly) < 6:
continue
x, y, w, h = a["bbox"]
objs.append({"label": cats.get(a["category_id"], "object"),
"polygon_pixels": poly, "box_xyxy": [x, y, x + w, y + h],
"area": float(a.get("area", w * h))})
if objs:
out.append((img, (img.width, img.height), f"coco_{iid}", objs))
_COCO_CACHE[key] = out
return out
def load_coco_segmentation(n: int = 24, split=None) -> list[GTSample]:
return [GTSample(img, "Segment every object as a labeled polygon.",
{"masks": [{"label": o["label"], "polygon_pixels": o["polygon_pixels"]}
for o in objs]},
"segmentation", iid, size)
for (img, size, iid, objs) in _coco_instances(n)]
def load_coco_outline(n: int = 24, split=None) -> list[GTSample]:
out = []
for (img, size, iid, objs) in _coco_instances(n):
big = max(objs, key=lambda o: o["area"])
out.append(GTSample(img, "Trace the main object's outline.",
{"outline": big["polygon_pixels"], "label": big["label"]},
"outline_association", iid, size))
return out
def load_coco_subject(n: int = 24, split=None) -> list[GTSample]:
out = []
for (img, size, iid, objs) in _coco_instances(n):
big = max(objs, key=lambda o: o["area"])
out.append(GTSample(img, "Identify the primary subject.",
{"label": big["label"], "box": big["box_xyxy"], "fmt": "xyxy"},
"subject_fixation", iid, size))
return out
# ── multi-person slice (fusion-tier validation GT) ────────────────────────────
def _select_multi_person_ids(ann: dict, *, min_persons: int = 2, max_persons: int = 6,
min_person_area_frac: float = 0.005,
require_nonperson: bool = False) -> list:
"""Image ids with TRUSTWORTHY multi-person GT: min..max non-crowd persons, no
crowd-person annotation anywhere in the image (a crowd RLE blob means "many
unlabeled people" — the count GT becomes untrustworthy), and no tiny background
persons (< min_person_area_frac of the image). Deliberately a CLEAN slice; the
bias is stated in every validation report. Pure filter over the parsed
annotations — no network, testable with a fake ann dict."""
from collections import defaultdict
imgs = {im["id"]: im for im in ann["images"]}
per_img = defaultdict(list)
for a in ann["annotations"]:
per_img[a["image_id"]].append(a)
out = []
for iid, image_anns in per_img.items():
info = imgs.get(iid)
if info is None:
continue
wh = float(info["width"] * info["height"]) or 1.0
persons = [a for a in image_anns if a["category_id"] == _COCO_PERSON_CAT]
if any(a.get("iscrowd") for a in persons):
continue
if not (min_persons <= len(persons) <= max_persons):
continue
if any(float(a.get("area", 0.0)) < min_person_area_frac * wh for a in persons):
continue
if require_nonperson and not any(
a["category_id"] != _COCO_PERSON_CAT and not a.get("iscrowd")
for a in image_anns):
continue
out.append(iid)
out.sort() # deterministic selection order
return out
def _multi_person_gt(image_anns: list, cats: dict) -> dict:
"""Shape one image's annotations into the fusion GT. Keeps ALL instances and ALL
polygon parts per annotation — occluded people are routinely split into 2+
polygons; the first-polygon-only rule used by _coco_instances would corrupt
person masks on exactly this slice."""
persons, objects = [], []
for a in image_anns:
if a.get("iscrowd"):
continue
seg = a.get("segmentation")
polys = ([[float(v) for v in part] for part in seg
if isinstance(part, list) and len(part) >= 6]
if isinstance(seg, list) else [])
x, y, w, h = a["bbox"]
rec = {"ann_id": a["id"], "box_xyxy": [x, y, x + w, y + h],
"polygons": polys, "area": float(a.get("area", w * h))}
if a["category_id"] == _COCO_PERSON_CAT:
persons.append(rec)
else:
objects.append(dict(rec, label=cats.get(a["category_id"], "object")))
return {"persons": persons, "objects": objects, "n_persons": len(persons)}
def load_coco_multi_person(n: int = 24, split=None, *, min_persons: int = 2,
max_persons: int = 6, min_person_area_frac: float = 0.005,
require_nonperson: bool = False) -> list[GTSample]:
"""Clean 2-6-person COCO slice for fusion validation. GT retains all instances +
all polygon parts; the 5 human captions ride in meta["captions"]. Filtering runs
over the cached annotations BEFORE any image download."""
import io
import urllib.request
from collections import defaultdict
from PIL import Image
key = (f"multi_{n}_{min_persons}_{max_persons}_{min_person_area_frac}"
f"_{require_nonperson}")
if key in _COCO_CACHE:
return _COCO_CACHE[key]
ann = _coco_ann("instances")
cap_ann = _coco_ann("captions")
cats = {c["id"]: c["name"] for c in ann["categories"]}
imgs = {im["id"]: im for im in ann["images"]}
per_img = defaultdict(list)
for a in ann["annotations"]:
per_img[a["image_id"]].append(a)
caps = defaultdict(list)
for c in cap_ann["annotations"]:
caps[c["image_id"]].append(str(c["caption"]).strip())
out = []
for iid in _select_multi_person_ids(
ann, min_persons=min_persons, max_persons=max_persons,
min_person_area_frac=min_person_area_frac,
require_nonperson=require_nonperson):
if len(out) >= n:
break
info = imgs[iid]
try:
raw = urllib.request.urlopen(
f"http://images.cocodataset.org/val2017/{info['file_name']}",
timeout=60).read()
img = Image.open(io.BytesIO(raw)).convert("RGB")
except Exception:
continue
gt = _multi_person_gt(per_img[iid], cats)
out.append(GTSample(img, "Fuse the scene into entities, relations, and counts.",
gt, "fusion_scene", f"coco_{iid}",
(img.width, img.height), meta={"captions": caps.get(iid, [])}))
_COCO_CACHE[key] = out
return out
def load_coco_multi_person_rich(n: int = 24, split=None) -> list[GTSample]:
"""Multi-person images that ALSO contain a non-person object (relation richness)."""
return load_coco_multi_person(n, split, require_nonperson=True)
DATASET_REGISTRY: dict[str, Callable[..., list[GTSample]]] = {
"imagenet_val": load_imagenet_val,
"coco_detection": load_coco_detection,
"coco_segmentation": load_coco_segmentation,
"coco_outline": load_coco_outline,
"coco_subject": load_coco_subject,
"coco_multi_person": load_coco_multi_person,
"coco_multi_person_rich": load_coco_multi_person_rich,
"textvqa": load_textvqa,
"datatype_synth": make_datatype_samples,
"shapes_synth": make_shapes_samples,
"segmentation_synth": make_segmentation_samples,
"outline_synth": make_outline_samples,
"boxes3d_synth": make_3d_samples,
"camera_rot_synth": make_camera_samples,
"gqa": make_gqa_samples,
"semantic_synth": make_semantic_samples,
"style_synth": make_style_samples,
}
def load_gt(dataset_key: str, n: int = 200, split: str = "validation",
dataset: str = "full") -> list[GTSample]:
"""Top-level GT loader. dataset='smoke' uses the packaged offline set."""
if dataset == "smoke" or dataset_key in ("", "smoke"):
# caller passes the category as dataset_key for smoke
return smoke_samples(dataset_key, n)
loader = DATASET_REGISTRY.get(dataset_key)
if loader is None:
raise KeyError(f"no loader for dataset {dataset_key!r}. known: {list(DATASET_REGISTRY)}")
return loader(n=n, split=split)