2026001
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user