2026001
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user