""" Generate a synthetic demo case set (data/cases.json + placeholder images) so the app runs immediately with ZERO real data. Replace with real P1/P3 outputs for the study: put real images + a real cases.json in a PRIVATE HF dataset and set CASES_DATASET. Case schema (data-driven; the app reads exactly this): { "study": "...", "cases": [ { "case_id": "c001", "question": "Is there pneumonia (lung opacity)? If present, where?", "intro": "optional per-case provenance note (else app default is used)", "reference_image": "img/c001_ref.png", # the original CXR (reference column) "groundtruth_image": "img/c001_gt.png", # reference-region / bbox panel (final row) "items": [ # anonymized systems to score, no names {"item_id": "c001_sysA", "image": "img/c001_a.png", "answer": "Right lower lobe opacity, consistent with pneumonia.", "decision": "answer"}, {"item_id": "c001_sysB", "image": "img/c001_b.png", "answer": "No acute cardiopulmonary abnormality.", "decision": "defer"} ] } ] } NOTE: item_id must be a stable TRUE id (maps to the real system in the backend); the reader never sees it. The app shuffles item order per (annotator, case) and derives rankings later. """ import json import random from pathlib import Path from PIL import Image, ImageDraw HERE = Path(__file__).parent DATA = HERE / "data" IMG = DATA / "img" IMG.mkdir(parents=True, exist_ok=True) random.seed(7) W = H = 512 def base_cxr(seed): rnd = random.Random(seed) im = Image.new("RGB", (W, H), (18, 18, 18)) d = ImageDraw.Draw(im, "RGBA") # two faint lung fields for cx in (170, 342): d.ellipse([cx - 90, 120, cx + 90, 400], fill=(60, 60, 60, 255)) # a mediastinum d.rectangle([236, 120, 276, 420], fill=(40, 40, 40, 255)) # a random faint "opacity" ox, oy = rnd.choice([(150, 320), (330, 300), (200, 200)]) d.ellipse([ox - 40, oy - 30, ox + 40, oy + 30], fill=(150, 150, 150, 120)) return im, (ox, oy) def box(im, center, color, label): d = ImageDraw.Draw(im, "RGBA") cx, cy = center d.rectangle([cx - 55, cy - 45, cx + 55, cy + 45], outline=color, width=4) d.rectangle([cx - 55, cy - 45, cx + 55, cy + 45], fill=color[:3] + (40,)) d.text((cx - 50, cy - 62), label, fill=color) return im cases = [] for k in range(4): cid = f"c{k+1:03d}" ref, opacity = base_cxr(k) ref.save(IMG / f"{cid}_ref.png") gt = ref.copy() box(gt, opacity, (60, 200, 90, 255), "reference") gt.save(IMG / f"{cid}_gt.png") # system A highlights the true region (relevant); system B highlights a wrong region a = ref.copy(); box(a, opacity, (230, 70, 60, 255), "highlight") a.save(IMG / f"{cid}_a.png") wrong = (256, 380) b = ref.copy(); box(b, wrong, (230, 70, 60, 255), "highlight") b.save(IMG / f"{cid}_b.png") cases.append({ "case_id": cid, "question": "Is there a focal lung opacity (e.g. pneumonia)? If present, where?", "reference_image": f"img/{cid}_ref.png", "groundtruth_image": f"img/{cid}_gt.png", "items": [ {"item_id": f"{cid}_sysA", "image": f"img/{cid}_a.png", "answer": "Focal opacity present; likely pneumonia.", "decision": "answer"}, {"item_id": f"{cid}_sysB", "image": f"img/{cid}_b.png", "answer": "Uncertain; recommend radiologist review.", "decision": "defer"}, ], }) with open(DATA / "cases.json", "w", encoding="utf-8") as f: json.dump({"study": "GRACE reader study (demo)", "cases": cases}, f, indent=2) print(f"wrote {DATA/'cases.json'} with {len(cases)} demo cases and placeholder images.")