Files
rpotter6298 35cbd9ac3c 2026001
2026-07-01 17:35:58 +02:00

181 lines
7.0 KiB
Python

#!/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'<div class="card">'
f'<img src="{b64}" alt="{sn}">'
f'<div class="label">{sn}</div>'
f'</div>')
if cards:
rows.append(
f'<div class="patient">'
f'<h3>{pid} <span class="count">({len(cards)} images)</span></h3>'
f'<div class="images">{"".join(cards)}</div>'
f'</div>')
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{method}{class_name}</title>
<style>
body {{ font-family: -apple-system, sans-serif; background: #1a1a2e; color: #eee; margin: 20px; }}
h1 {{ color: #e94560; }}
.patient {{ margin-bottom: 30px; border-bottom: 1px solid #333; padding-bottom: 15px; }}
.patient h3 {{ margin: 0 0 8px 0; color: #0f3460; background: #16213e; padding: 6px 12px; border-radius: 4px; display: inline-block; }}
.count {{ font-weight: normal; color: #888; font-size: 0.85em; }}
.images {{ display: flex; flex-wrap: wrap; gap: 8px; }}
.card {{ background: #16213e; border-radius: 4px; overflow: hidden; width: {THUMB_SIZE + 20}px; }}
.card img {{ display: block; width: {THUMB_SIZE}px; height: {THUMB_SIZE}px; object-fit: contain; margin: 0 auto; background: #000; }}
.label {{ font-size: 9px; color: #aaa; text-align: center; padding: 4px; word-break: break-all; }}
a.nav {{ color: #e94560; margin-right: 15px; }}
</style>
</head>
<body>
<h1>{method}{class_name} <small>({len(patients)} patients, {sum(len(v) for v in patients.values())} images)</small></h1>
<p>
<a class="nav" href="{method}_Benign.html">Benign</a>
<a class="nav" href="{method}_Malignant.html">Malignant</a>
<a class="nav" href="{method}_Normal.html">Normal</a>
</p>
{"".join(rows)}
</body>
</html>"""
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()