""" Generate a synthetic two-arm demo case set (data/cases.json + placeholder SRH-like images) so the app runs with ZERO real data. Replace with real cases for the study: put images + cases.json in a PRIVATE HF dataset and set CASES_DATASET. Case schema (data-driven; the app reads exactly this): Arm A (realism): {"case_id": "...", "arm": "A", "image": "img/x.png", "true_source": "real"|"synthetic"|"synthetic_ungated"} Arm B (category): {"case_id": "...", "arm": "B", "cluster_id": "...", "images": ["img/..","img/.."], "is_control": "none"|"positive"|"negative"} - true_source (A) and is_control/cluster_id (B) are hidden from the reader; used only in backend analysis. - The reader is blinded; the app shuffles item order per reader. """ import json import random from pathlib import Path from PIL import Image, ImageDraw, ImageFilter HERE = Path(__file__).parent DATA = HERE / "data" IMG = DATA / "img" IMG.mkdir(parents=True, exist_ok=True) W = H = 300 def srh_patch(seed, family=0): """Pseudo-SRH texture: purple/pink base (CH2/CH3-like) with cellular blobs + noise. family sets a look.""" rnd = random.Random(seed * 131 + family * 7) base = [(60, 30, 70), (40, 55, 60), (70, 40, 55)][family % 3] im = Image.new("RGB", (W, H), base) d = ImageDraw.Draw(im, "RGBA") for _ in range(rnd.randint(40, 90)): x, y = rnd.randint(0, W), rnd.randint(0, H) r = rnd.randint(4, 16) col = (rnd.randint(150, 230), rnd.randint(90, 160), rnd.randint(150, 220), rnd.randint(60, 140)) d.ellipse([x - r, y - r, x + r, y + r], fill=col) im = im.filter(ImageFilter.GaussianBlur(rnd.uniform(0.4, 1.2))) return im cases = [] # --- Arm A: 8 single patches, half labelled real, half synthetic (+ one ungated synthetic) --- labels = ["real", "real", "real", "synthetic", "synthetic", "synthetic", "synthetic_ungated", "real"] for i, src in enumerate(labels): cid = f"A{i+1:03d}" srh_patch(i + 1, family=i % 3).save(IMG / f"{cid}.png") cases.append({"case_id": cid, "arm": "A", "image": f"img/{cid}.png", "true_source": src}) # --- Arm B: 3 clusters (positive control = coherent, negative control = scrambled, one 'discovered') --- def cluster(cid, cluster_id, is_control, imgs): return {"case_id": cid, "arm": "B", "cluster_id": cluster_id, "is_control": is_control, "images": imgs} # positive control: 6 patches, same family (coherent) pos = [] for k in range(6): p = f"img/Bpos_{k}.png"; srh_patch(100 + k, family=1).save(IMG / f"Bpos_{k}.png"); pos.append(p) cases.append(cluster("B001", "pos_ctrl", "positive", pos)) # negative control: 6 patches, mixed families (scrambled / incoherent) neg = [] for k in range(6): p = f"img/Bneg_{k}.png"; srh_patch(200 + k, family=k % 3).save(IMG / f"Bneg_{k}.png"); neg.append(p) cases.append(cluster("B002", "neg_ctrl", "negative", neg)) # a 'discovered' cluster: 6 patches, one family with a couple outliers disc = [] for k in range(6): fam = 2 if k < 4 else (k % 3) p = f"img/Bdisc_{k}.png"; srh_patch(300 + k, family=fam).save(IMG / f"Bdisc_{k}.png"); disc.append(p) cases.append(cluster("B003", "disc_01", "none", disc)) with open(DATA / "cases.json", "w", encoding="utf-8") as f: json.dump({"study": "SRH Pathology Validation Study (demo)", "cases": cases}, f, indent=2) print(f"wrote {DATA/'cases.json'} with {len(cases)} demo cases " f"({sum(1 for c in cases if c['arm']=='A')} Arm-A, {sum(1 for c in cases if c['arm']=='B')} Arm-B).")