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
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""manifest_tsne.py — visualize any patient-grouping manifest on the VGG16 t-SNE.
Plots the same VGG16 feature t-SNE used elsewhere, but colored by the groups in
a given manifest (siamese / pca50 / thumbnail). Because every manifest is drawn
on the *identical* layout (same features, PCA, seed, perplexity), the resulting
per-class figures are directly comparable across methods.
For the siamese grouping this is diagnostic: if a siamese "patient" is a coherent
patient it forms a tight island; if it's an over-merged chain, its color is
smeared across feature space (VGG16 sees images the siamese wrongly linked).
Usage:
conda activate fundus_imaging
python scripts/visualizations/manifest_tsne.py \
--manifest results/siamese_manifest.csv --name siamese
python scripts/visualizations/manifest_tsne.py \
--manifest results/simple_patient_manifest.csv --name pca50
"""
import os
import sys
import re
import csv
import argparse
from collections import defaultdict
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
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__))))
FEATURES_DIR = os.path.join(ROOT, "features")
PLOTS_DIR = os.path.join(ROOT, "plots")
RANDOM_STATE = 42
CLASS_OF = {"B": "Benign", "M": "Malignant", "N": "Normal"}
def f2n(fname):
"""VGG16 feature filename -> manifest short name (e.g. 'B_001')."""
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
def load_manifest(path):
"""Return {image_short_name: group_id}."""
mapping = {}
with open(path, newline="") as f:
for row in csv.DictReader(f):
for img in row["images"].split(";"):
if img:
mapping[img] = row["patient_id"]
return mapping
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--manifest", required=True, help="Path to a grouping manifest CSV.")
ap.add_argument("--name", required=True, help="Short method name for titles/filenames.")
ap.add_argument("--features", default="VGG16", help="CNN feature set for the layout.")
ap.add_argument("--no-centroids", dest="centroids", action="store_false",
help="Do not draw per-group centroid diamonds.")
ap.add_argument("--tag", default="")
args = ap.parse_args()
tag = f"_{args.tag}" if args.tag else ""
print(f"Loading {args.features} features ...")
data = np.load(os.path.join(FEATURES_DIR, f"{args.features}_features.npz"),
allow_pickle=True)
X, filenames = data["X"], data["filenames"]
img_ids = np.array([f2n(f) for f in filenames])
img_class = np.array([CLASS_OF.get(i.split("_")[0], "?") for i in img_ids])
print("Computing PCA-50 + t-SNE (shared layout) ...")
n_pca = min(50, X.shape[0] - 1, X.shape[1])
X_pca = PCA(n_components=n_pca, random_state=RANDOM_STATE).fit_transform(X)
X_tsne = TSNE(n_components=2, perplexity=35, learning_rate="auto",
init="pca", random_state=RANDOM_STATE).fit_transform(X_pca)
groups = load_manifest(args.manifest)
img_group = np.array([groups.get(i, "unassigned") for i in img_ids])
for cls in ["Benign", "Malignant", "Normal"]:
fig, ax = plt.subplots(figsize=(14, 10))
ax.scatter(X_tsne[:, 0], X_tsne[:, 1], c="lightgray", s=3, alpha=0.15)
mask = img_class == cls
class_groups = sorted(set(img_group[mask]))
n = len(class_groups)
# Order groups by size so the biggest (most likely over-merged) is obvious.
sizes = {g: int((img_group[mask] == g).sum()) for g in class_groups}
class_groups = sorted(class_groups, key=lambda g: -sizes[g])
cmap = plt.cm.tab20 if n <= 20 else plt.cm.gist_ncar
for gi, g in enumerate(class_groups):
color = cmap(gi % 20) if n <= 20 else cmap(gi / max(n - 1, 1))
gm = mask & (img_group == g)
ax.scatter(X_tsne[gm, 0], X_tsne[gm, 1], c=[color], s=18, alpha=0.8)
if args.centroids:
cx, cy = X_tsne[gm, 0].mean(), X_tsne[gm, 1].mean()
ax.scatter(cx, cy, c=[color], s=60, marker="D", edgecolors="black",
linewidths=0.6, zorder=5)
biggest = class_groups[0]
legend_handles = [
Line2D([0], [0], marker="o", color="w", markerfacecolor="gray",
markersize=8, label="Group images (dots)"),
]
if args.centroids:
legend_handles.append(
Line2D([0], [0], marker="D", color="w", markerfacecolor="gray",
markersize=8, label="Group centroids (diamonds)"))
ax.legend(handles=legend_handles, loc="lower right")
ax.set_title(f"{cls}{args.name} groups on {args.features} t-SNE "
f"({n} groups; largest={sizes[biggest]} imgs)")
ax.set_xlabel("t-SNE dim 1")
ax.set_ylabel("t-SNE dim 2")
plt.tight_layout()
out = os.path.join(PLOTS_DIR, "tsne", f"tsne_{args.name}_{cls}{tag}.png")
os.makedirs(os.path.dirname(out), exist_ok=True)
plt.savefig(out, dpi=150)
plt.close()
print(f" Saved {out}")
print("DONE")
if __name__ == "__main__":
main()