196 lines
7.5 KiB
Python
196 lines
7.5 KiB
Python
import os
|
|
import torch
|
|
import torch.nn as nn
|
|
from torchvision import models, transforms
|
|
from PIL import Image
|
|
import numpy as np
|
|
import pandas as pd
|
|
import torch.nn.functional as F
|
|
import matplotlib.pyplot as plt
|
|
|
|
# 1. CONFIGURATION
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai")
|
|
ENCODER_PATH = os.path.join(BASE_DIR, "..", "models", "msi_encoder_trained.pth")
|
|
OUTPUT_DIR = os.path.join(BASE_DIR, "..", "figures", "guided_validation")
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
|
# FIX: Restore the full list of verified vascular landmarks to build a robust structural vector
|
|
ANCHOR_MZS = [155.75, 156.07, 156.40, 273.77, 274.09, 275.07, 286.12, 304.00, 304.33]
|
|
SIMI_THRESHOLD = 0.85 # Calibrated to capture true negative space co-exclusions down to 0.85
|
|
|
|
class MSI_Structural_Encoder(nn.Module):
|
|
"""
|
|
The exact high-fidelity custom architecture used during your successful
|
|
zero-padded 256x256 contrastive training run.
|
|
"""
|
|
def __init__(self):
|
|
super(MSI_Structural_Encoder, self).__init__()
|
|
|
|
self.features = nn.Sequential(
|
|
# Layer 1: Input 256x256x1 (Standardized size)
|
|
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
|
nn.GroupNorm(4, 32),
|
|
nn.ReLU(),
|
|
|
|
# Layer 2
|
|
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
|
nn.GroupNorm(8, 64),
|
|
nn.ReLU(),
|
|
|
|
# Layer 3
|
|
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
|
nn.GroupNorm(16, 128),
|
|
nn.ReLU(),
|
|
|
|
# Layer 4
|
|
nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=False),
|
|
nn.GroupNorm(32, 256),
|
|
nn.ReLU(),
|
|
|
|
nn.AdaptiveAvgPool2d((1, 1))
|
|
)
|
|
|
|
def forward(self, x):
|
|
h = self.features(x).squeeze(-1).squeeze(-1)
|
|
return h
|
|
|
|
def load_trained_encoder(path):
|
|
if not os.path.exists(path):
|
|
raise FileNotFoundError(f"Missing model checkpoint: {path}")
|
|
|
|
model = MSI_Structural_Encoder()
|
|
# Load only the feature extractor weights to match the save format in CNN_proof_1.py
|
|
model.features.load_state_dict(torch.load(path, map_location=DEVICE))
|
|
print(f"Successfully synchronized weights with custom encoder architecture: {path}")
|
|
|
|
model.to(DEVICE)
|
|
model.eval()
|
|
return model
|
|
|
|
if __name__ == "__main__":
|
|
encoder = load_trained_encoder(ENCODER_PATH)
|
|
|
|
transform = transforms.Compose([
|
|
transforms.Resize((256, 256)), # Standardized size matching training
|
|
transforms.ToTensor()
|
|
])
|
|
|
|
# 3. SCAN ALL IMAGE MAPS
|
|
all_files = sorted([f for f in os.listdir(IMG_DIR) if f.endswith('.png')])
|
|
print(f"Discovered {len(all_files)} total ion slices in data pool.")
|
|
|
|
ion_registry = []
|
|
for f in all_files:
|
|
try:
|
|
mz_extracted = float(f.replace("ion_", "").replace(".png", ""))
|
|
ion_registry.append({"filename": f, "mz": mz_extracted})
|
|
except ValueError:
|
|
continue
|
|
|
|
df_ions = pd.DataFrame(ion_registry)
|
|
|
|
# 4. EXTRACT VECTORS
|
|
print("Extracting L2-normalized latent fingerprints...")
|
|
latent_vectors = {}
|
|
|
|
with torch.no_grad():
|
|
for _, row in df_ions.iterrows():
|
|
fpath = os.path.join(IMG_DIR, row['filename'])
|
|
img_tensor = transform(Image.open(fpath).convert("L")).unsqueeze(0).to(DEVICE)
|
|
feat = encoder(img_tensor)
|
|
feat_norm = F.normalize(feat, p=2, dim=1) # Project onto unit hypersphere
|
|
latent_vectors[row['filename']] = feat_norm.squeeze(0).cpu().numpy()
|
|
|
|
# 5. CONSTRUCT SEED
|
|
anchor_features = []
|
|
found_anchors = []
|
|
|
|
for anchor in ANCHOR_MZS:
|
|
match = df_ions[np.isclose(df_ions['mz'], anchor, atol=0.05)]
|
|
if not match.empty:
|
|
fname = match.iloc[0]['filename']
|
|
anchor_features.append(latent_vectors[fname])
|
|
found_anchors.append(fname)
|
|
|
|
if len(anchor_features) == 0:
|
|
print("Error: None of your specified anchor vein images were located.")
|
|
exit()
|
|
|
|
vascular_profile_centroid = np.mean(anchor_features, axis=0)
|
|
vascular_profile_centroid /= np.linalg.norm(vascular_profile_centroid)
|
|
|
|
print(f"Generated unified vascular profile from {len(found_anchors)} anchor ions.")
|
|
|
|
# 6. EVALUATE SIMILARITY
|
|
guided_matches = []
|
|
for fname, vector in latent_vectors.items():
|
|
similarity = np.dot(vector, vascular_profile_centroid)
|
|
if similarity >= SIMI_THRESHOLD:
|
|
mz_val = df_ions[df_ions['filename'] == fname]['mz'].values[0]
|
|
guided_matches.append({"filename": fname, "mz": mz_val, "similarity": similarity})
|
|
|
|
df_guided = pd.DataFrame(guided_matches).sort_values(by="similarity", ascending=False)
|
|
print(f"\nFound {len(df_guided)} ions displaying vascular alignment (Cosine Sim >= {SIMI_THRESHOLD})")
|
|
|
|
csv_out = os.path.join(OUTPUT_DIR, "guided_vascular_ions.csv")
|
|
df_guided.to_csv(csv_out, index=False)
|
|
|
|
# 7. GENERATE GALLERIES
|
|
max_per_page = 12
|
|
chunks = [df_guided[i:i + max_per_page] for i in range(0, len(df_guided), max_per_page)]
|
|
|
|
for page_idx, chunk in enumerate(chunks):
|
|
num_plots = len(chunk)
|
|
rows = int(np.ceil(num_plots / 4))
|
|
cols = min(num_plots, 4)
|
|
|
|
fig, axes = plt.subplots(rows, cols, figsize=(16, rows * 4))
|
|
fig.suptitle(f"Guided Vascular Affinity Grouping - Tier {page_idx + 1}", fontsize=16, y=0.98)
|
|
|
|
flat_axes = axes.flatten() if isinstance(axes, np.ndarray) else [axes]
|
|
|
|
for idx, (_, row) in enumerate(chunk.iterrows()):
|
|
ax = flat_axes[idx]
|
|
img_path = os.path.join(IMG_DIR, row['filename'])
|
|
img = Image.open(img_path)
|
|
# Use 'viridis' for improved structural visibility
|
|
ax.imshow(img, cmap='viridis')
|
|
ax.set_title(f"m/z {row['mz']:.2f}\nSim: {row['similarity']:.4f}", fontsize=10)
|
|
ax.axis('off')
|
|
|
|
for empty_idx in range(num_plots, len(flat_axes)):
|
|
flat_axes[empty_idx].axis('off')
|
|
|
|
plt.tight_layout()
|
|
gallery_out = os.path.join(OUTPUT_DIR, f"Guided_Vascular_Gallery_Page_{page_idx + 1}.png")
|
|
plt.savefig(gallery_out, dpi=200, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
print("\nGuided extraction validation cycle complete.")
|
|
|
|
print("\n--- RUNNING ISOTOPIC FUNCTIONAL VALIDATION ---")
|
|
verified_biological_count = 0
|
|
|
|
for idx, row in df_guided.iterrows():
|
|
target_mz = row['mz']
|
|
isotope_mz = target_mz + 1.0033
|
|
|
|
# Check if the expected isotope exists within your parsed data pool
|
|
isotope_match = df_ions[np.isclose(df_ions['mz'], isotope_mz, atol=0.02)]
|
|
|
|
if not isotope_match.empty:
|
|
iso_fname = isotope_match.iloc[0]['filename']
|
|
iso_vector = latent_vectors[iso_fname]
|
|
|
|
# Calculate similarity of the isotope to your core vascular vector
|
|
iso_sim = np.dot(iso_vector, vascular_profile_centroid)
|
|
|
|
if iso_sim >= 0.80:
|
|
print(f"Verified Structural Pair: Base m/z {target_mz:.2f} (Sim: {row['similarity']:.4f}) -> Isotope m/z {isotope_mz:.2f} (Sim: {iso_sim:.4f})")
|
|
verified_biological_count += 1
|
|
|
|
print(f"Validation complete. Confirmed {verified_biological_count} true isotopic structural pairs.")
|