#!/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'

'
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()