"""Unified pool of ALL H&E image-text pairs in PathGen-1.6M/ (every text-image source of PAIR_SUMMARY.md; all TCGA patches). Images are de-duplicated by patch key __ (the same patch recurs across sources); one image can carry several texts. Text types: caption : PathGen-1.6M captions, figure_caption_summary / summary_testing / subsample_summary, slide_report desc, sample_data/output txt, output_subsample txt, path_reason_data caption summary : path_summary_train(.json, _5w.json) one-paragraph trainee summaries qa_open : open-ended Q/A -> assistant answers joined (PathGen-Instruct OPEN, output_subsample json, qa_pathinstruct_withreason*.csv answers, path_reason_data question+answer) qa_close : multiple-choice Q/A -> question stem + text of the correct option (PathGen-Instruct CLOSE, qa_instruct_merged_onlyqaforrl, output_reason json, multiple-choice csv rows) Outputs cache/allimg_meta.csv : unique images (idx, key, wsi_id, patient, image_path) -> image features cache/alltxt_meta.csv : unique (image, text) pairs (idx, img_idx, caption, ttype, source) -> text features (the `caption` column holds the text so the extractors' --set option works unchanged) Not used: pathor1_output*.csv (hashed non-TCGA image names, images absent). """ import ast import glob import json import os import re import pandas as pd from common import CACHE, ROOT PG = os.path.join(ROOT, "..", "PathGen-1.6M") img, txt = {}, [] # key -> image path ; list of (key, text, ttype, source) KEY = re.compile(r"(TCGA-[A-Z0-9-]+\.[A-Za-z0-9-]+)_(\d+)_(\d+)") def key_of(s): m = KEY.search(os.path.basename(str(s))) return f"{m.group(1)}_{m.group(2)}_{m.group(3)}" if m else None def add_img(key, path): if key and key not in img and os.path.exists(path): img[key] = path def add_txt(key, text, ttype, source): text = re.sub(r"\s+", " ", str(text).replace("", " ")).strip() if key and text and text.lower() != "nan": txt.append((key, text, ttype, source)) def close_text(question, answer): """multiple-choice -> 'question stem + correct option text'""" q = question.replace("", " ") opts = dict(re.findall(r"(?:^|\n)\s*\(?([A-F])[\).:]\s*([^\n]+)", q)) stem = re.split(r"\n\s*\(?A[\).:]", q)[0].strip() a = answer.strip() letter = re.match(r"^\(?([A-F])\)?(?:[\).:\s]|$)", a) if letter and letter.group(1) in opts: a = opts[letter.group(1)] elif letter and len(a) <= 3: return None else: a = re.sub(r"^\(?[A-F]\)\s*", "", a) return f"{stem} {a.strip()}" def conv_text(conv): """returns (ttype, text) for a LLaVA-style conversation list""" gpt = [c["value"].strip() for c in conv if c["from"] == "gpt"] if len(gpt) == 1 and re.fullmatch(r"\(?[A-F]\)?\.?", gpt[0]): human = [c["value"] for c in conv if c["from"] == "human"][0] t = close_text(human, gpt[0]) return ("qa_close", t) if t else (None, None) return "qa_open", " ".join(gpt) # 1. PathGen-1.6M captions (Data/images, 672 px) for x in json.load(open(os.path.join(PG, "Data", "PathGen-1.6M.json"))): k = f"{x['wsi_id']}_{x['position'][0]}_{x['position'][1]}" add_img(k, os.path.join(PG, "Data", "images", k + ".png")) add_txt(k, x["caption"], "caption", "pathgen1.6m") # 2. PathGen-Instruct (OPEN + CLOSE) idx = pd.read_csv(os.path.join(PG, "PathGen-Instruct.json.image_index.csv")).drop_duplicates("key").set_index("key") for x in json.load(open(os.path.join(PG, "PathGen-Instruct.json"))): k = f"{x['wsi_id']}_{x['position'][0]}_{x['position'][1]}" if k in idx.index: add_img(k, os.path.join(PG, idx.loc[k, "local_image"])) tt, t = conv_text(x["conversations"]) if tt: add_txt(k, t, tt, "pathgen-instruct") # 3. qa_instruct_merged_onlyqaforrl for x in json.load(open(os.path.join(PG, "qa_instruct_merged_onlyqaforrl.json"))): k = x.get("path_list") or f"{x['wsi_id']}_{x['position'][0]}_{x['position'][1]}" for c in glob.glob(os.path.join(PG, "qa_instruct_merged_images", k + ".*")): if c.endswith((".png", ".jpg")): add_img(k, c) tt, t = conv_text(x["conversations"]) if tt: add_txt(k, t, tt, "qa_instruct_merged") # 4. summaries for f in ["path_summary_train.json", "path_summary_train_5w.json"]: for x in json.load(open(os.path.join(PG, f))): k = key_of(x["images"][0]) add_img(k, os.path.join(PG, x["images"][0])) add_txt(k, [m["content"] for m in x["messages"] if m["role"] == "assistant"][0], "summary", f.replace(".json", "")) # 5. output_reason (png + json conversation), output_subsample (png + txt caption / json conversation), sample_data for d, src in [("output_reason", "output_reason"), ("output_subsample", "output_subsample"), ("sample_data/output", "sample_data")]: for pth in glob.glob(os.path.join(PG, d, "*.png")): k = key_of(pth) add_img(k, pth) base = pth[:-4] if os.path.exists(base + ".txt"): add_txt(k, open(base + ".txt").read(), "caption", src) if os.path.exists(base + ".json"): tt, t = conv_text(json.load(open(base + ".json"))) if tt: add_txt(k, t, tt, src) # 6. qa_pathinstruct_withreason*.csv (images in output_reason/) for f in sorted(glob.glob(os.path.join(PG, "qa_pathinstruct_withreason*.csv"))): for _, r in pd.read_csv(f).iterrows(): k = key_of(r.get("path_list", r.get("imagep"))) add_img(k, os.path.join(PG, str(r["imagep"]).lstrip("./"))) t = close_text(r["question"], str(r["answer"])) if re.search(r"\n\s*\(?A[\).:]", str(r["question"])) else r["answer"] if t: add_txt(k, t, "qa_close" if t is not r["answer"] else "qa_open", "qa_pathinstruct_withreason") # 7. path_reason_data (caption + diagnostic Q/A) for f in sorted(glob.glob(os.path.join(PG, "path_reason_data_3600*.csv"))): for _, r in pd.read_csv(f).iterrows(): k = key_of(r["patch_id"]) for c in [os.path.join(PG, str(r["patch_id"]).lstrip("./")), os.path.join(PG, "output_subsample", os.path.basename(str(r["patch_id"])))]: add_img(k, c) add_txt(k, r["caption"], "caption", "path_reason_data") add_txt(k, f"{r['question']} {r['answer']}", "qa_open", "path_reason_data") # 8. figure_caption_summary (summary_testing_data / subsample_summary_sentence), slide_report for f in ["summary_testing_data.csv", "subsample_summary_sentence.csv"]: for _, r in pd.read_csv(os.path.join(PG, f)).iterrows(): x, y = ast.literal_eval(r["position"]) k = f"{r['wsi_id']}_{x}_{y}" add_img(k, os.path.join(PG, "figure_caption_summary", k + ".png")) add_txt(k, r["caption"], "caption", "figure_caption_summary") for f in glob.glob(os.path.join(PG, "slide_report", "*", "*_desc.csv")): k = key_of(f.replace("_desc.csv", "")) add_img(k, f.replace("_desc.csv", ".png")) for _, r in pd.read_csv(f).iterrows(): add_txt(k, r["caption"], "caption", "slide_report") T = pd.DataFrame(txt, columns=["key", "caption", "ttype", "source"]) n_raw = len(T) T = T[T.key.isin(img)] n_noimg = n_raw - len(T) T = T.groupby(["key", "caption"], as_index=False).agg(ttype=("ttype", "first"), source=("source", lambda s: "|".join(sorted(set(s))))) I = pd.DataFrame({"key": sorted(T.key.unique())}) I["image_path"] = I.key.map(img) I["wsi_id"] = I.key.str.rsplit("_", n=2).str[0] I["patient"] = I.wsi_id.str[:12] I["caption"] = "" T["img_idx"] = T.key.map(dict(zip(I.key, I.index))) I.to_csv(os.path.join(CACHE, "allimg_meta.csv"), index_label="idx") T[["img_idx", "key", "caption", "ttype", "source"]].to_csv(os.path.join(CACHE, "alltxt_meta.csv"), index_label="idx") print(f"texts: {n_raw} raw, {n_noimg} without an image on disk, {len(T)} unique (image, text) pairs") print(f"images: {len(I)} unique patches, {I.wsi_id.nunique()} slides, {I.patient.nunique()} patients") print(T.ttype.value_counts().to_string()) print(T.source.str.split("|").explode().value_counts().to_string()) print("texts per image:", T.groupby("img_idx").size().describe().round(2).to_dict()) print("words per text by type:", T.assign(w=T.caption.str.split().str.len()).groupby("ttype").w.median().to_dict())