| """ |
| 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") |
| |
| for cx in (170, 342): |
| d.ellipse([cx - 90, 120, cx + 90, 400], fill=(60, 60, 60, 255)) |
| |
| d.rectangle([236, 120, 276, 420], fill=(40, 40, 40, 255)) |
| |
| 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") |
|
|
| |
| 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.") |
|
|