#!/usr/bin/env python3 """ manifest_html.py — Generate HTML pages for visually inspecting patient manifests. One HTML file per class, each patient's assigned images shown in a row. Usage: python scripts/visualizations/manifest_html.py python scripts/visualizations/manifest_html.py --method siamese python scripts/visualizations/manifest_html.py --manifest results/simple_patient_manifest.csv --method pca50 """ import os, sys, csv, argparse, base64 from io import BytesIO from PIL import Image sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname( os.path.abspath(__file__))))) ROOT = os.path.dirname(os.path.dirname(os.path.dirname( os.path.abspath(__file__)))) DATASET = os.path.join(os.path.dirname(ROOT), "The IQ-OTHNCCD lung cancer dataset") RESULTS_DIR = os.path.join(ROOT, "results") HTML_DIR = os.path.join(ROOT, "plots", "html") MANIFESTS = { "pca50": os.path.join(RESULTS_DIR, "simple_patient_manifest.csv"), "thumbnail": os.path.join(RESULTS_DIR, "thumbnail_patient_manifest.csv"), "siamese": os.path.join(RESULTS_DIR, "siamese_manifest.csv"), } THUMB_SIZE = 150 # px, display width def f2n_back(short_name): """B_009 → ('Bengin cases', 'Bengin case (9).jpg')""" prefix = short_name[0] num = int(short_name.split("_")[1]) cls_map = {"B": ("Bengin cases", "Bengin"), "M": ("Malignant cases", "Malignant"), "N": ("Normal cases", "Normal")} dir_name, file_prefix = cls_map[prefix] return dir_name, f"{file_prefix} case ({num}).jpg" def img_to_b64(path, size=THUMB_SIZE): """Load an image and return a base64 data URI.""" try: img = Image.open(path).convert("L") img.thumbnail((size, size), Image.LANCZOS) buf = BytesIO() img.save(buf, format="PNG") return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}" except Exception: return "" def build_html(manifest_path, method, class_name, class_dir, patients): """Generate an HTML string for one class.""" rows = [] for pid, short_names in patients.items(): # Build image cards cards = [] for sn in short_names: dir_name, fname = f2n_back(sn) img_path = os.path.join(DATASET, dir_name, fname) b64 = img_to_b64(img_path) if b64: cards.append( f'
' f'{sn}' f'
{sn}
' f'
') if cards: rows.append( f'
' f'

{pid} ({len(cards)} images)

' f'
{"".join(cards)}
' f'
') return f""" {method} — {class_name}

{method} — {class_name} ({len(patients)} patients, {sum(len(v) for v in patients.values())} images)

Benign Malignant Normal

{"".join(rows)} """ def main(): ap = argparse.ArgumentParser() ap.add_argument("--method", default=None, help="Which manifest method (pca50, thumbnail, siamese). " "Default: all.") ap.add_argument("--manifest", default=None, help="Path to manifest CSV (overrides --method).") args = ap.parse_args() os.makedirs(HTML_DIR, exist_ok=True) methods_to_run = [args.method] if args.method else list(MANIFESTS.keys()) for method in methods_to_run: manifest_path = args.manifest or MANIFESTS.get(method) if not manifest_path or not os.path.exists(manifest_path): print(f" {method}: manifest not found ({manifest_path})") continue # Load manifest, group by class patients_by_class = {"Benign": {}, "Malignant": {}, "Normal": {}} with open(manifest_path, newline="") as f: reader = csv.DictReader(f) img_col = ("images" if "images" in reader.fieldnames else "confirmed_images") for row in reader: cls = row.get("class", "").strip() # Normalize class names cls_lower = cls.lower() if cls_lower in ("benign", "bengin"): cls = "Benign" elif cls_lower in ("malignant", "malig"): cls = "Malignant" elif cls_lower == "normal": cls = "Normal" elif cls_lower == "unknown": # Infer from patient_id prefix pid = row.get("patient_id", "") if pid.lower().startswith("benign") or pid.lower().startswith("bengin"): cls = "Benign" elif pid.lower().startswith("malignant") or pid.lower().startswith("malig"): cls = "Malignant" elif pid.lower().startswith("normal"): cls = "Normal" if cls not in patients_by_class: continue pid = row["patient_id"] imgs = row[img_col].split(";") if row[img_col] else [] if imgs: patients_by_class[cls][pid] = imgs for cls_name in ["Benign", "Malignant", "Normal"]: patients = patients_by_class[cls_name] if not patients: print(f" {method}/{cls_name}: no patients, skipping") continue html = build_html(manifest_path, method, cls_name, "", patients) out_path = os.path.join(HTML_DIR, f"{method}_{cls_name}.html") with open(out_path, "w") as f: f.write(html) print(f" Saved → plots/html/{method}_{cls_name}.html " f"({len(patients)} patients)") print("DONE") if __name__ == "__main__": main()