This commit is contained in:
rpotter6298
2026-07-01 17:35:58 +02:00
parent 9bfcc0243b
commit 35cbd9ac3c
84 changed files with 8500 additions and 423 deletions
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
classification.py — single-seed image-level vs patient-level comparison.
This script OWNS the shared classification-runs cache
(scripts/cache/classification_runs.json): a flat list of per-(model, seed,
split) run dicts. It reads any runs it needs from the cache and writes back any
it has to compute, so the cache is maintained here. figure4_6.py references the
same file to draw the 20-seed boxplots.
Usage:
conda activate fundus_imaging
python scripts/classification.py
"""
import os, sys, json
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
from classes import PatientLeakageClassifier
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(SCRIPT_DIR)
MODELS = ["VGG16", "DenseNet121", "EfficientNetB1", "MobileNetV2", "ResNet50"]
SEED = 20
CACHE_PATH = os.path.join(SCRIPT_DIR, "cache", "classification_runs.json")
# Load the run cache: keep the raw list (to append to) and an index (to look up).
runs = []
if os.path.exists(CACHE_PATH):
with open(CACHE_PATH) as f:
runs = json.load(f)
index = {(r["model"], r["seed"], r["split_type"]): r for r in runs}
clf = None # lazily created only if the cache is missing something
def get_run(name, split):
"""Return the cached run, else compute it and write it back to the cache."""
global clf
hit = index.get((name, SEED, split))
if hit is not None:
return hit
if clf is None:
clf = PatientLeakageClassifier(
os.path.join(ROOT, "results", "simple_patient_manifest.csv"),
os.path.join(ROOT, "features"), n_jobs=8)
print(f" (cache miss for {name}/{split} — computing)")
r = clf.run(name, SEED, split)
runs.append(r)
index[(name, SEED, split)] = r
os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
with open(CACHE_PATH, "w") as f:
json.dump(runs, f, indent=2)
return r
print(f"{'='*60}")
print(f"IMAGE-LEVEL vs PATIENT-LEVEL (seed={SEED})")
print(f"{'='*60}")
results = []
for name in MODELS:
print(f"\n {name} ...")
img = get_run(name, "image")
pat = get_run(name, "patient")
results.append({"model": name,
"image_cv": img["cv"], "image_test": img["test"],
"patient_cv": pat["cv"], "patient_test": pat["test"],
"drop": img["test"] - pat["test"]})
print(f" Image: CV={img['cv']:.4f} Test={img['test']:.4f}")
print(f" Patient: CV={pat['cv']:.4f} Test={pat['test']:.4f}")
print(f" Drop: {img['test'] - pat['test']:.4f}")
print(f"\n {'Model':<18s} {'Img-CV':>8s} {'Img-Test':>9s} "
f"{'Pat-CV':>8s} {'Pat-Test':>9s} {'Drop':>7s}")
print(f" {'-'*54}")
for r in results:
print(f" {r['model']:<18s} {r['image_cv']:>8.4f} {r['image_test']:>9.4f} "
f"{r['patient_cv']:>8.4f} {r['patient_test']:>9.4f} {r['drop']:>7.4f}")
with open(os.path.join(ROOT, "results", "classification_pca50.json"), "w") as f:
json.dump(results, f, indent=2)
print(f"\nDONE")
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""classification_siamese.py — Run classification with siamese patient manifest."""
import os, sys, json
import numpy as np
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
from classes import PatientLeakageClassifier
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
MANIFEST = os.path.join(ROOT, "results", "siamese_manifest.csv")
FEATURES_DIR = os.path.join(ROOT, "features")
RESULTS_DIR = os.path.join(ROOT, "results")
SEED = 20
MODELS = ["VGG16", "DenseNet121", "EfficientNetB1", "MobileNetV2", "ResNet50"]
clf = PatientLeakageClassifier(MANIFEST, FEATURES_DIR, n_jobs=6)
print("=" * 60)
print("Siamese-based patient classification")
print("=" * 60)
results = []
for model_name in MODELS:
print(f"\n {model_name} ...", flush=True)
img = clf.run(model_name, SEED, "image")
pat = clf.run(model_name, SEED, "patient")
drop = img["test"] - pat["test"]
results.append({
"model": model_name, "manifest": "siamese",
"image_cv": img["cv"], "image_test": img["test"],
"patient_cv": pat["cv"], "patient_test": pat["test"],
"drop": drop,
})
print(f" Image: CV={img['cv']:.4f} Test={img['test']:.4f}")
print(f" Patient: CV={pat['cv']:.4f} Test={pat['test']:.4f} "
f"Drop={drop:.4f}")
# Save
out = os.path.join(RESULTS_DIR, "classification_siamese.json")
with open(out, "w") as f:
json.dump(results, f, indent=2)
# Comparison table
print(f"\n{'='*80}")
print("COMPARISON — All three patient-clustering methods (seed=20)")
print(f"{'='*80}")
def safe_load(path):
if os.path.exists(path):
with open(path) as f: return {r["model"]: r for r in json.load(f)}
return None
pca50 = safe_load(os.path.join(RESULTS_DIR, "classification_pca50.json"))
thumbnail = safe_load(os.path.join(RESULTS_DIR, "classification_thumbnail.json"))
siamese = {r["model"]: r for r in results}
print(f"\n{'Model':<18s} {'PCA50 Pat':>10s} {'Thumb Pat':>11s} {'Siam Pat':>10s} "
f"{'PCA50 Drop':>11s} {'Thumb Drop':>11s} {'Siam Drop':>10s}")
print("-" * 82)
for m in MODELS:
f_pat = f"{pca50[m]['patient_test']:>10.4f}" if pca50 else " N/A"
t_pat = f"{thumbnail[m]['patient_test']:>11.4f}" if thumbnail else " N/A"
f_drop = f"{pca50[m]['drop']:>11.4f}" if pca50 else " N/A"
t_drop = f"{thumbnail[m]['drop']:>11.4f}" if thumbnail else " N/A"
print(f"{m:<18s} {f_pat} {t_pat} "
f"{siamese[m]['patient_test']:>10.4f} {f_drop} {t_drop} "
f"{siamese[m]['drop']:>10.4f}")
print(f"\nSaved → {out}")
print("DONE")
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""
classification_thumbnail.py — Run the classification pipeline using the
manuscript's thumbnail-based K-means patient manifest.
Saves results alongside the feature-based results for comparison.
Usage:
conda activate fundus_imaging
python scripts/classification_thumbnail.py
"""
import os, sys, csv, json, re
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
from classes import PatientIdentifier, PatientLeakageClassifier
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))))
DATASET_PATH = os.path.join(os.path.dirname(ROOT),
"The IQ-OTHNCCD lung cancer dataset")
FEATURES_DIR = os.path.join(ROOT, "features")
RESULTS_DIR = os.path.join(ROOT, "results")
SEED = 20
MODELS = ["VGG16", "DenseNet121", "EfficientNetB1", "MobileNetV2", "ResNet50"]
# Known patient counts per class
PATIENT_COUNTS = {
"Bengin cases": 15,
"Malignant cases": 40,
"Normal cases": 55,
}
# ---------------------------------------------------------------------------
# Step 1: Build thumbnail-based patient manifest
# ---------------------------------------------------------------------------
print("=" * 60)
print("Building thumbnail-based patient manifest")
print("=" * 60)
# Collect all image filenames and class labels (same order as feature extraction)
image_paths, all_labels, all_fnames = [], [], []
for class_name in sorted(os.listdir(DATASET_PATH)):
class_path = os.path.join(DATASET_PATH, class_name)
if not os.path.isdir(class_path):
continue
for file in sorted(os.listdir(class_path)):
if file.lower().endswith((".png", ".jpg", ".jpeg")):
image_paths.append(os.path.join(class_path, file))
all_labels.append(class_name)
all_fnames.append(file)
print(f"Found {len(image_paths)} images across "
f"{len(set(all_labels))} classes")
# Run thumbnail K-means
labels_arr = np.array(all_labels)
fnames_arr = np.array(all_fnames)
identifier = PatientIdentifier(
patient_estimates=PATIENT_COUNTS, random_state=42)
groups = identifier.identify_from_thumbnails(
DATASET_PATH, fnames_arr, labels_arr)
# Build manifest
manifest = identifier.build_assignment_dict(groups, fnames_arr, labels_arr)
# Save manifest
manifest_path = os.path.join(RESULTS_DIR, "thumbnail_patient_manifest.csv")
def f2n(fname):
m = re.search(r"\((\d+)\)", fname)
num = int(m.group(1)) if m else None
for cls_key, prefix in [
("Bengin cases", "B"), ("Malignant cases", "M"), ("Normal cases", "N"),
]:
if fname.startswith(cls_key.rstrip("s")):
return f"{prefix}_{num:03d}" if num else fname
return fname
with open(manifest_path, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["patient_id", "class", "n_images", "images"])
for pid in sorted(manifest.keys()):
imgs = manifest[pid]
short_names = [f2n(img) for img in imgs]
cls = pid.split("_")[0] # "benign_0" → "benign"
cls = {"benign": "Benign", "malig": "Malignant",
"normal": "Normal"}.get(cls, cls)
writer.writerow([pid, cls, len(imgs), ";".join(short_names)])
print(f"Manifest saved → {manifest_path}")
print(f" {len(manifest)} estimated patients")
# Per-class stats
for cls in ["Bengin cases", "Malignant cases", "Normal cases"]:
short_cls = {"Bengin cases": "benign", "Malignant cases": "malig",
"Normal cases": "normal"}[cls]
cls_patients = {k: v for k, v in manifest.items()
if k.startswith(short_cls)}
n_pat = len(cls_patients)
n_img = sum(len(v) for v in cls_patients.values())
print(f" {cls}: {n_pat} patients, {n_img} images")
# ---------------------------------------------------------------------------
# Step 2: Run classification with thumbnail manifest
# ---------------------------------------------------------------------------
print(f"\n{'=' * 60}")
print("Running classification (thumbnail manifest, seed=20)")
print("=" * 60)
clf = PatientLeakageClassifier(manifest_path, FEATURES_DIR, n_jobs=6)
results = []
for model_name in MODELS:
print(f"\n {model_name} ...", flush=True)
img = clf.run(model_name, SEED, "image")
pat = clf.run(model_name, SEED, "patient")
results.append({
"model": model_name,
"manifest": "thumbnail",
"image_cv": img["cv"],
"image_test": img["test"],
"patient_cv": pat["cv"],
"patient_test": pat["test"],
"drop": img["test"] - pat["test"],
})
print(f" Image: CV={img['cv']:.4f} Test={img['test']:.4f}")
print(f" Patient: CV={pat['cv']:.4f} Test={pat['test']:.4f} "
f"Drop={img['test'] - pat['test']:.4f}")
# ---------------------------------------------------------------------------
# Save results
# ---------------------------------------------------------------------------
out_path = os.path.join(RESULTS_DIR, "classification_thumbnail.json")
with open(out_path, "w") as f:
json.dump(results, f, indent=2)
# Also load feature-based results for side-by-side comparison
feat_path = os.path.join(RESULTS_DIR, "classification_results.json")
if os.path.exists(feat_path):
with open(feat_path) as f:
feat_results = json.load(f)
print(f"\n{'=' * 80}")
print("COMPARISON: Feature-based (PCA-50) vs Thumbnail K-means")
print(f"{'=' * 80}")
print(f"{'Model':<18s} {'Feat Image':>11s} {'Thumb Image':>12s} "
f"{'Feat Pat':>9s} {'Thumb Pat':>10s} {'Feat Drop':>10s} "
f"{'Thumb Drop':>11s}")
print("-" * 78)
for tr, fr in zip(results, feat_results):
assert tr["model"] == fr["model"]
print(f"{tr['model']:<18s} {fr['image_test']:>11.4f} "
f"{tr['image_test']:>12.4f} {fr['patient_test']:>9.4f} "
f"{tr['patient_test']:>10.4f} {fr['drop']:>10.4f} "
f"{tr['drop']:>11.4f}")
print(f"\nSaved → {out_path}")
print("DONE")