Cleaned code repository by separating from manuscript files
27
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "MSI Hybrid Workflow",
|
||||
"build": {
|
||||
"dockerfile": "../Dockerfile",
|
||||
"context": ".."
|
||||
},
|
||||
"workspaceFolder": "/app",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/bin/python3",
|
||||
"julia.executablePath": "/usr/local/bin/julia"
|
||||
},
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"charliermarsh.ruff",
|
||||
"julialang.language-julia"
|
||||
]
|
||||
}
|
||||
},
|
||||
"runArgs": [
|
||||
"--gpus=all",
|
||||
"--shm-size=2gb"
|
||||
],
|
||||
"remoteUser": "root"
|
||||
}
|
||||
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
data/Leaf.imzML
|
||||
data/Leaf.ibd
|
||||
data/Leaf.imzML.cache
|
||||
JuliaMSI/
|
||||
environment/Manifest.toml
|
||||
datos_para_ai/*
|
||||
141
Compare_Scopolamine_304.R
Normal file
@ -0,0 +1,141 @@
|
||||
suppressPackageStartupMessages({
|
||||
library(MALDIquant)
|
||||
library(MALDIquantForeign)
|
||||
library(ggplot2)
|
||||
library(ggrepel)
|
||||
library(dplyr)
|
||||
library(stringr)
|
||||
library(readr)
|
||||
})
|
||||
|
||||
## ===================== Configuration =====================
|
||||
# File paths
|
||||
base_path <- "/home/sierra/Documentos/Analisis de datos/Analisis de datos R"
|
||||
file_std <- file.path(base_path, "Fragmentos/Escopolamina_tuneo_fraq_20ev.mzML")
|
||||
file_smp <- file.path(base_path, "Fragmentos/LD_LTP_MS_cot_fraq_304.mzML")
|
||||
output_dir <- file.path(base_path, "Fragmentos/Resultados_MS2/Comparativos")
|
||||
output_file <- file.path(output_dir, "Mirror_Plot_Scopolamine_304_Academic.png")
|
||||
|
||||
# Plotting parameters
|
||||
mz_range <- c(80, 320)
|
||||
label_threshold <- 0.05
|
||||
topN_labels <- 8
|
||||
dpi_png <- 300
|
||||
bar_width_Da <- 0.2
|
||||
|
||||
# Preprocessing parameters
|
||||
halfWindowSize <- 5
|
||||
snr_peaks <- 2.0
|
||||
tolerance_Da <- 0.02
|
||||
|
||||
## ===================== Utilities =====================
|
||||
massSpectrum_to_df <- function(sp) data.frame(mz = mz(sp), intensity = intensity(sp))
|
||||
|
||||
# Refined centroid (intensity-weighted)
|
||||
refine_centroid <- function(df, m0, w) {
|
||||
sub <- df[df$mz >= (m0 - w) & df$mz <= (m0 + w), ]
|
||||
if (nrow(sub) == 0) return(m0)
|
||||
sum(sub$mz * sub$intensity) / sum(sub$intensity)
|
||||
}
|
||||
|
||||
# Preprocessing pipeline for a single file (averaging scans if multiple exist)
|
||||
process_file <- function(f) {
|
||||
scans <- importMzMl(f)
|
||||
if (length(scans) > 1) {
|
||||
scans <- smoothIntensity(scans, method = "SavitzkyGolay", halfWindowSize = 7)
|
||||
scans <- removeBaseline(scans, method = "SNIP", iterations = 60)
|
||||
scans <- calibrateIntensity(scans, method = "TIC")
|
||||
# Basic alignment if multiple scans
|
||||
pks <- detectPeaks(scans, method = "MAD", halfWindowSize = halfWindowSize, SNR = 1.5)
|
||||
ref <- referencePeaks(pks, minFrequency = 0.5, tolerance = tolerance_Da)
|
||||
scans <- alignSpectra(scans, halfWindowSize = halfWindowSize, SNR = 1.5, tolerance = tolerance_Da, reference = ref)
|
||||
spec_avg <- averageMassSpectra(scans, method = "mean")
|
||||
} else {
|
||||
spec_avg <- scans[[1]]
|
||||
spec_avg <- smoothIntensity(spec_avg, method = "SavitzkyGolay", halfWindowSize = 7)
|
||||
spec_avg <- removeBaseline(spec_avg, method = "SNIP", iterations = 60)
|
||||
spec_avg <- calibrateIntensity(spec_avg, method = "TIC")
|
||||
}
|
||||
return(spec_avg)
|
||||
}
|
||||
|
||||
# Label generation
|
||||
make_labels <- function(spec, df, rel_threshold = 0.05, topN = 5, w = tolerance_Da) {
|
||||
pk <- detectPeaks(spec, method = "MAD", halfWindowSize = halfWindowSize, SNR = snr_peaks)
|
||||
if (length(pk) == 0) return(NULL)
|
||||
|
||||
lab_df <- data.frame(mz = mz(pk), intensity = intensity(pk))
|
||||
lab_df$mz <- vapply(lab_df$mz, function(m) refine_centroid(df, m, w), numeric(1))
|
||||
lab_df$rel_int <- lab_df$intensity / max(lab_df$intensity, na.rm = TRUE)
|
||||
|
||||
# Select top N and those above threshold
|
||||
keep_idx <- unique(c(order(lab_df$rel_int, decreasing = TRUE)[seq_len(min(topN, nrow(lab_df)))],
|
||||
which(lab_df$rel_int >= rel_threshold)))
|
||||
lab_df <- lab_df[sort(keep_idx), ]
|
||||
|
||||
lab_df$label <- sprintf("%.2f", lab_df$mz)
|
||||
return(lab_df)
|
||||
}
|
||||
|
||||
## ===================== Execution =====================
|
||||
message("Processing Scopolamine spectra...")
|
||||
spec_std <- process_file(file_std)
|
||||
spec_smp <- process_file(file_smp)
|
||||
|
||||
df_std <- massSpectrum_to_df(spec_std) %>% mutate(intensity = intensity / max(intensity))
|
||||
df_smp <- massSpectrum_to_df(spec_smp) %>% mutate(intensity = intensity / max(intensity))
|
||||
|
||||
labels_std <- make_labels(spec_std, df_std, label_threshold, topN_labels)
|
||||
labels_smp <- make_labels(spec_smp, df_smp, label_threshold, topN_labels)
|
||||
|
||||
# Prepare mirrored data
|
||||
df_smp$y <- df_smp$intensity
|
||||
df_std$y <- -df_std$intensity
|
||||
df_smp$Group <- "Sample (Leaf)"
|
||||
df_std$Group <- "Reference Standard"
|
||||
|
||||
# Combine for plotting
|
||||
df_all <- bind_rows(df_smp, df_std)
|
||||
|
||||
# Filter by range
|
||||
df_all <- df_all %>% filter(mz >= mz_range[1], mz <= mz_range[2])
|
||||
|
||||
message("Generating mirror plot...")
|
||||
p <- ggplot() +
|
||||
# Mirror spectra (lines)
|
||||
geom_segment(data = df_smp %>% filter(mz >= mz_range[1], mz <= mz_range[2]),
|
||||
aes(x = mz, xend = mz, y = 0, yend = y), color = "#1f77b4", linewidth = 0.3) +
|
||||
geom_segment(data = df_std %>% filter(mz >= mz_range[1], mz <= mz_range[2]),
|
||||
aes(x = mz, xend = mz, y = 0, yend = y), color = "#d62728", linewidth = 0.3) +
|
||||
geom_hline(yintercept = 0, color = "black", linewidth = 0.5) +
|
||||
|
||||
# Labels
|
||||
geom_text_repel(data = labels_smp, aes(x = mz, y = rel_int, label = label),
|
||||
nudge_y = 0.05, size = 3.5, color = "#1f77b4", fontface = "bold",
|
||||
max.overlaps = 20, min.segment.length = 0) +
|
||||
geom_text_repel(data = labels_std, aes(x = mz, y = -rel_int, label = label),
|
||||
nudge_y = -0.05, size = 3.5, color = "#d62728", fontface = "bold",
|
||||
max.overlaps = 20, min.segment.length = 0) +
|
||||
|
||||
# English Academic Annotations
|
||||
scale_y_continuous(labels = function(x) abs(x) * 100, limits = c(-1.15, 1.15),
|
||||
breaks = seq(-1, 1, 0.5)) +
|
||||
scale_x_continuous(limits = mz_range, breaks = seq(mz_range[1], mz_range[2], 20)) +
|
||||
labs(title = expression(bold("MS/MS Spectrum Comparison: Scopolamine (") * bolditalic("m/z") * bold(" 304.17)")),
|
||||
subtitle = "Upper: Scopolamine in Leaf (Sample) | Lower: Scopolamine Standard (Reference)\nCollision Energy: 20 eV",
|
||||
x = expression(italic(m/z)),
|
||||
y = "Relative Abundance (%)") +
|
||||
theme_classic(base_size = 14) +
|
||||
theme(
|
||||
plot.title = element_text(face = "bold", hjust = 0.5),
|
||||
plot.subtitle = element_text(hjust = 0.5, size = 11, lineheight = 1.2),
|
||||
axis.title = element_text(face = "bold"),
|
||||
axis.line = element_line(color = "black"),
|
||||
panel.grid.major.y = element_line(color = "grey90", linetype = "dotted")
|
||||
)
|
||||
|
||||
# Save plot
|
||||
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
|
||||
ggsave(output_file, p, width = 11, height = 7, dpi = dpi_png)
|
||||
|
||||
message("✓ Mirror plot saved to: ", output_file)
|
||||
69
Dockerfile
Normal file
@ -0,0 +1,69 @@
|
||||
# Use NVIDIA CUDA as the base for GPU support
|
||||
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04
|
||||
|
||||
# Set non-interactive for apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 1. Install System Dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
curl \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
git \
|
||||
build-essential \
|
||||
zlib1g-dev \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
xvfb \
|
||||
libgl1-mesa-glx \
|
||||
libgl1-mesa-dri \
|
||||
libglfw3 \
|
||||
libxrandr2 \
|
||||
libxinerama1 \
|
||||
libxcursor1 \
|
||||
libxi6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 2. Install Julia 1.11.3
|
||||
RUN curl -fsSL https://julialang-s3.julialang.org/bin/linux/x64/1.11/julia-1.11.3-linux-x86_64.tar.gz | tar -xz -C /usr/local --strip-components=1
|
||||
|
||||
# 3. Setup Working Directory
|
||||
WORKDIR /app
|
||||
|
||||
# 4. Integrate JuliaMSI Framework
|
||||
COPY JuliaMSI /opt/JuliaMSI
|
||||
WORKDIR /opt/JuliaMSI
|
||||
|
||||
ENV DISPLAY=:99
|
||||
|
||||
# Instantiate framework package
|
||||
RUN xvfb-run -s "-screen 0 1024x768x24" julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()'
|
||||
|
||||
# 5. Setup Project Root and Environments
|
||||
WORKDIR /app
|
||||
COPY environment/ /app/environment/
|
||||
|
||||
# Isolate Python dependencies for cleaner caching layers
|
||||
RUN pip3 install --no-cache-dir -r environment/requirements.txt
|
||||
|
||||
# Force Julia 1.11.3 to resolve environments natively using your fixed UUID entries
|
||||
RUN julia --project=environment -e 'using Pkg; Pkg.resolve(); Pkg.instantiate()'
|
||||
|
||||
# Register JuliaMSI explicitly into the runtime environment project
|
||||
RUN julia --project=environment -e 'using Pkg; Pkg.develop(path="/opt/JuliaMSI")'
|
||||
|
||||
# 6. Copy Application Files
|
||||
COPY . /app
|
||||
|
||||
# 7. Final Configuration
|
||||
ENV JULIA_LOAD_PATH="/opt/JuliaMSI:/opt/JuliaMSI/src:${JULIA_LOAD_PATH}"
|
||||
ENV PYTHONPATH="/app/scripts_python:${PYTHONPATH}"
|
||||
ENV DISPLAY=:99
|
||||
|
||||
LABEL maintainer="MSI Hybrid Workflow Team"
|
||||
LABEL description="CUDA + Julia 1.11.3 + JuliaMSI Framework"
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Carlos Daniel Sierra Álvarez, Juan Francisco Moreno Luna
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
192
MSI_julia-python_mapping.md
Normal file
@ -0,0 +1,192 @@
|
||||
# Deep Learning in Mass Spectrometry Imaging: A Hybrid Julia-Python Workflow for the Automated Anatomical Mapping of *Datura innoxia*
|
||||
|
||||
**Authors:** Carlos Daniel Sierra Álvarez, Juan Francisco Moreno Luna, José Julián Sierra Álvarez, José Jovani Martínez.
|
||||
**Affiliation:** Advanced Genomics Unit, Cinvestav, km 9.6 Libr. Nte. Irapuato-León, Irapuato, 36824, Gto., Mexico.
|
||||
**Keywords:** MSI, Julia Language, Deep Learning, SimCLR, Contrastive Learning, Group Normalization, *Datura innoxia*, Spatial Metabolomics.
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
Mass Spectrometry Imaging (MSI) has emerged as a fundamental tool in spatial metabolomics, enabling the *in situ* visualization of molecular distributions. However, the nature of these data—characterized by massive volumes of multivariate information and high levels of instrumental noise—poses critical computational challenges for traditional analysis methods (Shah et al., 2025). In this work, we present an innovative computational architecture that addresses the "two-language problem" by integrating **Julia** for high-performance data processing and **Python** for representation learning via deep neural networks. We implemented a self-supervised learning workflow based on the **SimCLR** architecture with a **custom Group-Normalized Fully Convolutional** encoder, designed to extract robust morphological features from ion images without prior labeling. This system was applied to the study of *Datura innoxia* (toloache) leaves at a spatial resolution of 150 µm, achieved via Laser Desorption Low-Temperature Plasma (LD-LTP) ionization. By prioritizing anatomical fidelity over mathematical parsimony, we identified an optimal clustering of $k=10$ that accurately distinguishes vascular, paravascular, and parenchymal tissues. Our results demonstrate that the latent space generated by the neural network captures fundamental chemical relationships, such as the co-localization of isotopic patterns, offering a scalable and objective pathway for the interpretation of complex biological systems.
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
The analysis of the spatial distribution of secondary metabolites in medicinal plants is essential for understanding biosynthesis, transport, and chemical defense processes (Ziegler & Facchini, 2008; García-Rojas et al., 2024). *D. innoxia*, known for its rich composition of tropane alkaloids, represents a highly complex biological model due to the biosynthesis of these compounds in the roots and their differential transport to the shoots (Schlesinger et al., 2021). Previous studies have employed laser desorption low-temperature plasma (LD-LTP) techniques to map these alkaloids in fruits and seeds (Moreno-Pedraza et al., 2019).
|
||||
|
||||
Traditionally, MSI data analysis has relied on linear methods and standard imaging protocols (Cole & Clench, 2023). However, a software engineering challenge arises: while Python possesses the most mature ecosystem for Artificial Intelligence, its performance in handling massive binary files in **imzML** format (Schramm et al., 2012) is often limited. Conversely, Julia offers execution speeds comparable to C++ for compute-heavy tasks (Godoy et al., 2023) and outperforms interpreted languages in scientific computing benchmarks (Churavy et al., 2020). The integration of these two ecosystems addresses the "two-language problem"—the historical trade-off between the ease of a high-level language and the performance of a low-level one (Perkel, 2019). In this article, we propose that the synergy between these two languages enables a "next-generation" MSI workflow. The remainder of this paper details the acquisition parameters, the hybrid Julia-Python implementation, the unsupervised segmentation results, and a comparison against traditional linear baselines.
|
||||
|
||||
---
|
||||
|
||||
## 2. Methodology
|
||||
|
||||
### 2.1 MSI Data Acquisition (LD-LTP-MSI)
|
||||
We performed imaging using an **LD-LTP-MSI** system coupled to an **LCQ Fleet Ion Trap** mass spectrometer (Thermo Scientific). The custom ionization source consisted of a Low-Temperature Plasma (LTP) probe (Harper et al., 2008) and a continuous-wave violet laser (400–450 nm, 1.0 W). The LTP probe was operated with helium (0.1 L/min) at 3.3 kV and ~13 kHz, positioned at a 45° angle and 4 mm from the ion transfer capillary (Martínez-Jarquín & Winkler, 2017). The laser was oriented at 90° to the sample surface at a focal distance of 9 cm, producing a spot size of ~22 µm. We acquired data in positive ion mode (*m/z* 50–700) with a spatial resolution of **150 µm** for the leaf sample.
|
||||
|
||||
### 2.2 Computational Environment and Reproducibility
|
||||
All computational analyses, including the high-performance data extraction in Julia and the deep learning-based representation learning in Python, were performed across two mobile workstations. The primary data ingestion and analysis were executed on a system with the following specifications: **Processor:** Intel(R) Core(TM) i5-10210U CPU @ 1.60 GHz (4 cores, 8 threads), **Memory:** 32 GB DDR4 RAM, **Graphics:** Intel UHD Graphics (CometLake-U GT2), and **Operating System:** Ubuntu 24.04.4 LTS (Linux Kernel 6.8.0-110-generic). Hardware-accelerated neural network training (CUDA) was executed on a Dell G15 5525 equipped with an **AMD Ryzen 5 6600H** processor (6 cores, 12 threads @ 4.57 GHz), **8 GB RAM**, and an **NVIDIA GeForce RTX 3050 Mobile** discrete GPU, running **Pop!_OS 24.04 LTS**. To ensure rigorous compliance with MSI reporting standards such as MIAMSIE (Gustafsson et al., 2018) and SMART (Xi et al., 2023), as well as modern machine learning reproducibility guidelines like REFORMS (Kapoor et al., 2024), the complete hybrid workflow was containerized using Docker. This guarantees that both the Julia binary parsing algorithms and the Python machine learning dependencies remain perfectly reproducible across diverse hardware platforms.
|
||||
|
||||
### 2.3 High-Performance Processing and Data Normalization (Julia)
|
||||
We conducted the preprocessing stage using the **JuliaMSI** framework (Sierra-Álvarez et al., 2025), a platform specifically engineered to address the computational bottlenecks of the "two-language problem." By leveraging Julia’s just-in-time (JIT) compilation and efficient memory-mapping capabilities, we facilitated the rapid ingestion and manipulation of massive **imzML** binary files. As demonstrated in previous benchmarks (Sierra-Álvarez et al., 2025), this architecture provides a significant performance advantage over interpreted environments such as Python or R (Rosas-Román et al., 2020), enabling the high-throughput extraction of mass slices without compromising spectral precision.
|
||||
|
||||
The selection of Julia as the primary engine for data ingestion is justified by its superior performance in handling the high-dimensional sparse matrices characteristic of imzML files. Recent benchmarks have demonstrated that Julia-based architectures, such as the JuliaMSI framework, outperform traditional R-based implementations (e.g., RmsiGUI) by significant margins, showing up to 5.2-fold faster processing on Linux environments (Sierra-Álvarez et al., 2025). Furthermore, at the library level, Julia's native parsing of binary spectral data has been shown to be up to 214 times faster than equivalent implementations in R (Rosas-Román et al., 2024). This computational efficiency is critical for resolving the "two-language problem," as it allows the rapid extraction of hundreds of mass slices and their subsequent normalization in a fraction of the time required by interpreted environments, effectively enabling a high-throughput bridge to the Python-based deep learning ecosystem.
|
||||
|
||||
1. **Ion Extraction and Filtering:** We extracted mass slices from the `Leaf.imzML` dataset within the ***m/z* 50.0 to 700.0** range. This upper limit was selected because no significant signals were detected beyond this threshold using the LD-LTP-MSI technique for this specific sample, and its implementation allowed for a more computationally manageable dataset. To ensure the inclusion of only chemically relevant signals, we filtered peaks using an intensity threshold of 2% relative to the base peak of the average spectrum, resulting in a refined subset of ion images for downstream analysis. Owing to the spectral binning inherent to the instrument's data acquisition, a single molecular species may be distributed across adjacent *m/z* channels; this discretization is reflected in the ion image filenames (e.g., *m/z* 304.00 and 304.33 for scopolamine) and must be considered when comparing imaging clusters with high-resolution MS/MS identifications.
|
||||
|
||||
2. **TrIQ Normalization (Threshold Intensity Quantization):** To mitigate the impact of "hotspots"—stochastic pixels with disproportionately high intensities that obscure lower-abundance metabolic distributions—we implemented a dynamic TrIQ algorithm (Rosas-Román & Winkler, 2021). For each raw ion map $I_{raw}$, the algorithm identifies a signal threshold $T$ based on the distribution of non-zero, finite intensity values $V = \{i \in I_{raw}\mid i > 0, i < \infty\}$. The threshold is defined as the 98th percentile of this distribution:
|
||||
|
||||
$$T = P_{98}(V)$$
|
||||
|
||||
In cases where the distribution is extremely sparse and $P_{98} = 0$, the threshold defaults to the maximum observed intensity ($T =\max(V)$). Each pixel's intensity is then normalized and bounded to the unit interval $[0, 1]$ using a clamping function:
|
||||
|
||||
$$I_{norm}(x, y) = \min\left(1, \frac{I_{raw}(x, y)}{T}\right)$$
|
||||
|
||||
This transformation ensures that 98% of the chemical signal is represented within a standardized dynamic range, effectively enhancing the contrast of parenchymal and vascular structures. By suppressing the influence of extreme outliers, TrIQ provides a morphologically consistent input for the subsequent contrastive learning phase, preventing the neural network from over-fitting to instrumental artifacts.
|
||||
|
||||
3. **Spatial Standardization (Pristine Coordinate Mapping):** To ensure compatibility with the deep learning backbone, each normalized ion map was spatially embedded into a standardized **256 $\times$ 256 pixel** matrix using strict **Zero-Padding**, completely avoiding any sub-pixel interpolation. In MSI, the physical laser ablation grid serves as an absolute spatial ground truth. Standard computational interpolation (such as bilinear resizing) forces anisotropic stretching —disproportionately warping the leaf morphology— and introduces artificial floating-point values at the sharp boundaries of the tissue mask. By mapping the raw $161 \times 106$ coordinates directly into the center of a zero-matrix, we preserved the true, undistorted anatomical geometry of the leaf while achieving the uniform dimensionality required by the convolutional layers. As a demonstration of this high-throughput pipeline, the Julia engine parsed the raw `.ibd` binaries, applied TrIQ, and exported 588 padded individual ion slices in less than 16 seconds.
|
||||
|
||||
### 2.4 Self-Supervised Representation Learning (Python)
|
||||
We implemented a Contrastive Learning (SimCLR) scheme in PyTorch (Paszke et al., 2019), adapting the framework to the unique properties of mass spectrometry imaging.
|
||||
|
||||
1. **Model Architecture and VRAM Optimization:** A custom **Fully Convolutional Contrastive Network** was engineered to replace standard heavy computer vision backbones. Crucially, all standard batch normalization blocks were replaced with **Group Normalization (GroupNorm)** layers (8 groups per layer). In datasets characterized by massive empty, zero-padded backgrounds, standard Batch Normalization calculates running statistics that become overwhelmingly biased by the blank space, leading to gradient instability and the artificial clustering of the background mask. Group Normalization computes statistics per individual image slice, mathematically neutralizing the static background and forcing the network to optimize strictly on the high-variance internal biological structures. The network concludes with an Adaptive Average Pooling layer and a 2-layer MLP to project the spatial features into a 128-dimensional latent space (Table 1).
|
||||
|
||||
To enable training on consumer-grade hardware (NVIDIA RTX 3050 Mobile, ~4 GB VRAM), a gradient accumulation strategy was employed. Rather than fitting an effective batch of 32 images simultaneously into GPU memory —which would trigger a CUDA out-of-memory error— the engine processes 8 images physically per step and accumulates gradients over 4 consecutive forward passes before executing a single optimizer update, as described in the Hugging Face documentation (Hugging Face, 2025). This is mathematically equivalent to training with a true batch size of 32 while using only a quarter of the VRAM.
|
||||
|
||||
**Table 1. Training hyperparameters and model configuration for the SimCLR-MSI workflow.**
|
||||
|
||||
| Parameter | Value / Configuration |
|
||||
| :--- | :--- |
|
||||
| **Backbone Architecture** | Custom 4-Layer CNN with Group Normalization |
|
||||
| **Projection Head** | 2-layer MLP (Linear 512-ReLU-Linear 128) |
|
||||
| **Input Image Resolution** | 256 x 256 pixels (Zero-Padded) |
|
||||
| **Optimization Algorithm** | Adam Optimizer (Kingma & Ba, 2014) |
|
||||
| **Learning Rate** | $3 \times 10^{-4}$ |
|
||||
| **Physical Batch Size / Accumulation Steps** | 8 / 4 (Effective Batch Size: 32) |
|
||||
| **Temperature ($\tau$)** | 0.5 (NT-Xent Loss) |
|
||||
| **Training Duration** | 60 Epochs |
|
||||
| **Data Augmentations** | Intensity Scaling (0.8×–1.2×), Poisson Shot Noise, Low-Signal Dropout |
|
||||
|
||||
2. **Dataset Size and the Dynamic Training Pool:** The training corpus comprises 588 ion images that passed the 2% intensity threshold filter from the original spectral binning. This number is mathematically sufficient for this self-supervised task for two compounding reasons. First, the custom 4-layer fully convolutional architecture is a lightweight, low-dimensional feature extractor. Unlike overparameterized ImageNet backbones such as EfficientNet-B0 (Tan & Le, 2019) (which carry over 5 million parameters trained for 1,000 natural image categories), our encoder is purpose-built to extract a compact morphological fingerprint; it requires far fewer samples to converge on a meaningful manifold. Second, the `apply_msi_chemical_noise` augmentation pipeline functions as a stochastic, infinite data generator. Because intensity scaling factors, Poisson noise seeds, and dropout masks are sampled randomly at each epoch, the network never encounters the exact same image representation twice, exponentially expanding the effective training pool and providing robust protection against overfitting.
|
||||
|
||||
3. **The Camp A Paradigm: Prohibition of Spatial Augmentations:** Standard computer vision augmentation libraries (e.g., Torchvision) apply spatial transformations—random rotations, horizontal/vertical flips, and random crops—as a default strategy to promote translation and rotation invariance. This practice is explicitly and deliberately incompatible with our co-localization objective and was therefore completely prohibited. In spatial metabolomics, the absolute $(x, y)$ pixel grid of a single-slice acquisition is the immutable biological ground truth (Alexandrov, 2012). Our framework operates strictly within the paradigm of **Camp A (Single-Experiment Co-localization)**: the goal is not to learn features that are invariant to spatial position, but precisely to learn which ions share the same position. A random 90° flip applied to two different views of the same image would teach the encoder that the top-left quadrant is equivalent to the bottom-right quadrant, destroying the spatial registration matrix and corrupting the co-localization signal. The exclusive use of chemical noise augmentations (which simulate detector artifacts while leaving the coordinate grid untouched) is therefore a deliberate, foundational design choice, consistent with established best practices in MSI deep learning (Hu et al., 2021).
|
||||
|
||||
4. **Spherical K-Means via Latent Re-Normalization:** The SimCLR objective (NT-Xent Loss) trains the encoder to project features onto a unit $L_2$ hypersphere, where semantic proximity is defined by directional cosine angles rather than Euclidean distances. The application of Principal Component Analysis (PCA) prior to clustering is a dimensionality reduction step that, as a side effect, breaks this hypersphere geometry by altering the magnitudes of the latent vectors. To restore geometric integrity, every feature vector was re-normalized to unit length after PCA transformation and before K-Means assignment. This converts standard Euclidean K-Means into a functional **Spherical K-Means** (Hornik et al., 2012), ensuring that cluster centroids are determined by cosine similarity rather than arbitrary vector magnitudes—a critical requirement for correctly grouping ions with identical spatial silhouettes but different absolute intensities.
|
||||
|
||||
5. **Clustering and Optimization:** To determine the optimal number of clusters ($k$), we performed a multi-metric systematic sweep from $k = 2$ to $k = 20$ (Figure 1A). While global mathematical optima were observed at $k = 2$ and $k = 3$ —corresponding to a coarse differentiation between the leaf tissue and the background— these configurations failed to resolve the specialized anatomical compartments of the leaf. We identified $k = 10$ as the functional optimum, supported by a Silhouette Score of 0.6894 (Figure 1B), a Davies-Bouldin Index of 0.6243 (Figure 1C), and a Calinski-Harabasz Index of 2054.20 (Figure 1D). This value represents a stable convergence point where the latent space successfully deconvolutes the hierarchical vascular architecture (midrib and secondary veins) and localized metabolic sequestration sites without incurring over-segmentation. Consequently, this choice prioritizes anatomical fidelity over mathematical parsimony, ensuring that the resulting metabolic map accurately reflects the complex tissue compartmentalization of *D. innoxia*.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 3. Results and Discussion
|
||||
|
||||
We conducted direct surface analysis of *D. innoxia* leaves using an LD-LTP-MS setup in positive ionization mode. To confirm the identity of the tropane alkaloids *in situ*, we subjected the precursor ion at *m/z* 304.17 to tandem mass spectrometry (MS/MS) at a collision energy of 20 eV. Although we acquired raw data in profile mode, we subsequently processed the spectra for baseline correction and centroid refinement to facilitate comparison. The resulting fragmentation pattern from the leaf surface showed an excellent spectral match with the commercial scopolamine reference standard. The presence of diagnostic product ions across the *m/z* 80–320 range confirms the unambiguous structural identification of scopolamine directly from the plant surface without the need for extensive sample preparation (Figure 2; García-Rojas et al., 2024).
|
||||
|
||||

|
||||
|
||||
### 3.2 Latent Space Geometry and Chemical Validation
|
||||
The high-dimensional embeddings generated by the custom GroupNorm backbone were projected into a 2-D manifold using t-SNE (Figure 3) (Van der Maaten & Hinton, 2008). The resulting latent space exhibits a highly structured geometry, where ions are grouped not by their absolute intensity, but by their shared spatial topographies. A cornerstone of the validation for this self-supervised approach is the emergence of **Isotopic Coherence** within the latent manifold (Table 2). In MSI, a molecular species and its naturally occurring isotopes (e.g., $[M+H]^+$ and $[M+H+1]^+$) are fundamentally tied by an identical spatial distribution, providing a form of natural "ground truth" that requires no manual annotation.
|
||||
|
||||
To quantitatively validate this, we calculated the Cosine Similarity between the normalized feature vectors of known isotopic pairs. For example, Atropine (*m/z* 290.02) and its $+1$ isotope (*m/z* 291.02) yielded a Cosine Similarity of **0.9898**. A global statistical analysis across 513 discovered isotopic pairs yielded a mean latent distance of $0.3156$, compared to $0.7076$ for random ion pairs, achieving profound statistical significance (Welch's t-test, $p = 6.7932 \times 10^{-74}$). Although the SimCLR framework is entirely agnostic to the mass-to-charge (*m/z*) values or the chemical identity of the inputs, the encoder consistently projected isotopic pairs into tightly bound coordinates.
|
||||
|
||||
**Disproving Background Mask Exploitation:** A common concern in applying deep learning to zero-padded imaging data is that the network may exploit the outer tissue boundary —an artifact shared identically by every single ion image— as a "shortcut" feature, rather than learning true internal biology (Purushwalkam & Gupta, 2020). Our data provides an elegant empirical refutation of this hypothesis. Atropine (*m/z* 290.02) and its $+1$ isotope (*m/z* 291.02) share a perfectly identical outer mask boundary but have fundamentally different absolute signal intensities due to natural isotopic abundance ratios. If the encoder were exploiting the mask, these two ions would appear identical regardless, yielding a trivially high similarity—providing no discriminative power. Instead, the NT-Xent loss function explicitly penalizes the encoder for relying on zero-variance features shared across all images in the negative pair denominator (Chen et al., 2020). Any feature constant across all ion images —including the outer mask— provides zero contrastive gradient, and is mathematically discarded in favor of internal morphological variations. The observed similarity of $0.9898$ therefore constitutes proof that the network has converged on the true shared spatial silhouette of the ion inside the tissue, not the common boundary around it.
|
||||
|
||||
Furthermore, this structural mapping revealed the phenomenon of **Inverse Mask Correlation** (Negative Space Co-Exclusion). The network successfully grouped ions that concentrate directly within the vascular bundle (positive distributions, e.g., Scopolamine *m/z* 304.00, achieving a similarity of $0.8796$ with Atropine) with ions that are biologically excluded from the vein (negative distributions, appearing as dark shadows). Because the encoder projects all spatial maps onto an $L_2$ unit hypersphere, it recognizes that the sharp geometric boundary of a bright vein and the boundary of its complementary dark shadow are topologically equivalent structures. Such coherence confirms that the learned representations are anchored in global biological reality rather than technical artifacts. Notably, the scopolamine signal identified by MS/MS at *m/z* 304.17 (Figure 2) appears in the imaging data as two adjacent binned channels (*m/z* 304.00 and 304.33), both assigned to Cluster 3, confirming that the spectral binning inherent to MSI acquisition does not compromise the biological coherence of the clustering —the encoder correctly recognizes the shared spatial topography across bins originating from the same compound.
|
||||
|
||||
**Interpreting the Continuous Latent Manifold:** Reviewers from a computer vision background may expect an optimal unsupervised result to yield discrete, well-separated "blobs" in the t-SNE projection. The continuous, connected geometry observed in our manifold—resembling a smooth curve or "S-shape"—is not a sign of clustering failure; it is, in fact, the mathematically correct and expected projection of a continuous biological gradient (Diaconis et al., 2008). A *D. innoxia* leaf is not a collection of discrete, isolated tissue types; it is an anatomical continuum transitioning from the dense, alkaloid-rich midrib through the paravascular tissue into the diffuse parenchyma of the lamina. Because our encoder successfully learned the underlying structural morphology rather than arbitrary cluster membership, the t-SNE algorithm faithfully projects this continuous biological spectrum into a continuous geometric trajectory (Moon et al., 2019). The application of K-Means clustering ($k=10$) onto this manifold is then interpreted as the discretization of this gradient into biologically meaningful bands, not as the discovery of independent, non-overlapping chemical compartments.
|
||||
|
||||
**Table 2. Representative metabolites identified in *D. innoxia* and their latent space assignment.**
|
||||
|
||||
| *m/z* Obs. | Identification | Formula $[M+H]^+$ | Cluster | Anatomical Significance |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| 304.00 | Scopolamine | $C_{17}H_{22}NO_4$ | **3** | Midrib-associated distribution (nervure). Related to chemical defense (biotic and abiotic factors) and pharmaceutical applications. |
|
||||
| 290.02 | Atropine / Hyoscyamine | $C_{17}H_{24}NO_3$ | **9** | Vascular distribution through the midrib with island formation across the lamina. Chemical defense and precursor of scopolamine. |
|
||||
| 225.65 | Cuscohygrine | $C_{13}H_{25}N_2O$ | **0** | Vascular architecture from primary to secondary veins, extending from the petiole to the leaf tip. |
|
||||
| 306.28 | 6-Hydroxyhyoscyamine | $C_{17}H_{24}NO_4$ | **9** | Midrib distribution with evident dispersion across the entire leaf and accumulation in small islands. |
|
||||
| 142.09 | Tropine / Pseudotropine | $C_8H_{16}NO$ | **2** | Distribution mainly through the midrib with island-like accumulation in other leaf regions. Key biosynthetic precursor of tropane alkaloids. |
|
||||
| 140.14 | Tropinone | $C_8H_{14}NO$ | **6** | Present in the midrib and throughout the entire leaf surface. Key biosynthetic precursor of tropane alkaloids, essential for the biosynthesis of downstream tropane alkaloids. |
|
||||
| 291.02 | *Atropine Isotope* | $[M+H+1]^+$ | **9** | Spatial distribution very similar to *m/z* 290.02, with presence in primary and secondary vasculature. |
|
||||
|
||||
(Identified by MS/MS at *m/z* 304.17 (Figure 2). The signal is distributed across adjacent bins (*m/z* 304.00 and 304.33, both in Cluster 3) due to spectral binning of the imaging data, confirming internal consistency between the structural identification and the unsupervised clustering.)
|
||||
|
||||

|
||||
|
||||
### 3.3 Chemical-Anatomical Segmentation in *D. innoxia*
|
||||
The unsupervised decomposition of the latent space yielded ten distinct molecular domains (Clusters 0–9), each representing a reproducible and spatially resolved chemical pattern across the *D. innoxia* leaf. These clusters capture the heterogeneity of ion distributions and reveal a structured organization of metabolites at the tissue level.
|
||||
|
||||
Rather than reflecting random variation, the observed spatial arrangements define consistent chemical domains characterized by differences in ion intensity, co-localization, and spatial continuity. In particular, the distribution of key ions such as atropine ($m/z \approx 290.02$) and scopolamine ($m/z \approx 304.0$) highlights the presence of chemically distinct regions associated with specific anatomical features, including the midrib, lamina, and apical tip (Figure 4, Table 3).
|
||||
|
||||

|
||||
|
||||
**Table 3. Summary of unsupervised segmentation domains in *D. innoxia* leaf.**
|
||||
|
||||
| Domain Group | Clusters | Spatial Pattern | Biological Interpretation |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Apical** | 0, 1 | Large defined islands near apex (0); fully concentrated at distal tip (1) | Terminal accumulation and specialized sequestration at the leaf apex. |
|
||||
| **Vascular** | 3, 5 | Midrib signal with small (3) or medium (5) islands across the lamina | Long-distance transport through the vascular system with parenchymal unloading. |
|
||||
| **Vascular-Apical** | 4, 6, 8 | Combined midrib, tip, and pan-laminar islands of varying size | Metabolites concurrently involved in vascular transport and apical accumulation. |
|
||||
| **Vascular-Parenchymal** | 2, 7, 9 | Uniform entire leaf surface (2); large islands throughout the leaf (7, 9) | Metabolites transported through the vascular system and broadly distributed across the parenchyma via acropetal transport, with varying degrees of tissue penetration and localized accumulation. |
|
||||
|
||||
#### 3.3.1 Vascular and Midrib-Associated Domains (Clusters 3, 4, 5, 6, 8)
|
||||
The clustering analysis reveals a group of domains that exhibit a strong association with the midrib and vascular architecture of the leaf, each with distinct secondary spatial distributions. **Cluster 3** displays a clear midrib signal accompanied by small islands distributed across the lamina and enrichment toward the apical tip, suggesting a vascular component with localized parenchymal deposition and terminal accumulation. **Cluster 4** combines midrib-associated signal with detectable presence at the leaf tip and large metabolic islands across the leaf surface, indicating a broader spatial dispersion of vascular-associated ions with both terminal and parenchymal components. **Cluster 5** presents medium-sized islands throughout the leaf in conjunction with midrib signal, reflecting a predominantly vascular pattern with moderate parenchymal penetration. **Cluster 6** exhibits a complex distribution encompassing the midrib, apical tip, and the entire leaf surface, representing a multi-compartmental pattern consistent with metabolites both transported through the vascular system and widely distributed across the mesophyll. **Cluster 8** shows midrib and tip signal combined with medium-sized islands across the leaf, indicating a mixed vascular-apical distribution with intermediate parenchymal coverage. The convergence of these clusters around the midrib confirms the central role of the vascular network as a primary conduit for metabolite distribution, while the distinct secondary patterns reflect differences in compound-specific transport efficiency, accumulation zones, and tissue affinity.
|
||||
|
||||
#### 3.3.2 Apical Accumulation and Terminal Regions (Clusters 0, 1)
|
||||
Two clusters are defined by a primary spatial association with the apical region of the leaf, characterized by highly localized ion intensity at the distal tip. **Cluster 0** displays large, well-defined islands concentrated near the apex, suggesting specialized sequestration zones proximal to the terminal region. This pattern may reflect the localized accumulation of defense-related compounds at the leaf apex, a common strategy in plant chemical defense against herbivores that first encounter the leaf tip. **Cluster 1** exhibits the most spatially restricted pattern among all clusters, with a highly defined signal concentrated exclusively at the distal tip. This pure apical distribution represents the terminal accumulation of specific metabolites, potentially corresponding to the final deposition of tropane alkaloids following acropetal transport through the vascular system. The clear delineation of these two apical clusters suggests a hierarchical organization within the terminal accumulation zone, with Cluster 1 representing the extreme tip and Cluster 0 capturing the broader peri-apical sequestration domain.
|
||||
|
||||
#### 3.3.3 Vascular-Parenchymal Distributions (Clusters 2, 7, 9)
|
||||
Three clusters are characterized by broad spatial distributions that span the entire leaf surface, reflecting compounds actively transported through the vascular system and distributed across the parenchyma. **Cluster 2** exhibits a uniform pattern across the leaf surface, consistent with the distribution of key biosynthetic precursors such as tropine and pseudotropine that are synthesized in the roots, transported acropetally, and distributed throughout the leaf mesophyll. This pattern reflects the constitutive availability of fundamental building blocks for tropane alkaloid biosynthesis. **Cluster 7** is dominated by large, well-defined islands distributed across the lamina with a minor presence at the apical tip. The island-like pattern within a broad distribution suggests localized zones of metabolic specialization or differential accumulation following vascular transport. **Cluster 9** displays a heterogeneous pattern combining both large and small islands across the leaf surface with moderate enrichment at the tip, representing the most complex distribution among the vascular-parenchymal group and capturing alkaloids such as atropine and 6-hydroxyhyoscyamine that exhibit both vascular and parenchymal characteristics.
|
||||
|
||||
The spatial distribution of key tropane alkaloids reveals distinct but complementary anatomical patterns that reflect their biosynthetic relationships and functional roles. Scopolamine (*m/z* 304.0), assigned to Cluster 3, exhibits a clear midrib-associated distribution, consistent with its role in chemical defense against biotic and abiotic factors and its pharmaceutical significance. Atropine (*m/z* 290.02), assigned to Cluster 9, shows a vascular distribution through the midrib with pronounced island formation across the lamina, functioning as both a defense compound and the biosynthetic precursor of scopolamine. Cuscohygrine (*m/z* 225.65), in Cluster 0, displays a striking vascular architecture from primary to secondary veins extending from the petiole to the leaf tip. 6-Hydroxyhyoscyamine (*m/z* 306.28), also in Cluster 9, follows the midrib with evident dispersion across the entire leaf and accumulation in small islands. Tropine (*m/z* 142.09) and pseudotropine, in Cluster 2, show distribution mainly through the midrib with island-like accumulation, functioning as key biosynthetic precursors of tropane alkaloids. Tropinone (*m/z* 140.14), assigned to Cluster 6, is present in the midrib and throughout the entire leaf surface, serving as the principal biosynthetic precursor essential for the synthesis of downstream tropane alkaloids. The co-localization of atropine, 6-hydroxyhyoscyamine, and the atropine isotopic variant (*m/z* 291.02) within Cluster 9, alongside the distinct vascular assignments of scopolamine (Cluster 3) and cuscohygrine (Cluster 0), demonstrates that the self-supervised encoder successfully discriminates between subtle variations in spatial topography while capturing genuine biosynthetic and metabolic relationships. By resolving these multi-compartmental distributions, the SimCLR-based segmentation effectively maps the spatial continuum from vascular transport to parenchymal storage to terminal accumulation.
|
||||
|
||||
#### 3.4 Advantages of Contrastive Learning over Linear Decomposition
|
||||
Furthermore, the superiority of the self-supervised contrastive learning approach over traditional linear methods, such as Principal Component Analysis (PCA), lies in its fundamental shift from intensity-driven variance to morphological feature extraction (Shah et al., 2025). To establish a fair quantitative baseline, we compared SimCLR and PCA embeddings under identical conditions: both feature sets were projected to the same dimensionality via PCA and clustered using standard Euclidean k-means ($k = 10$) without the Spherical k-means re-normalization described in Section 2.4.4, ensuring a direct apples-to-apples comparison. Under these conditions, the SimCLR pipeline achieved a Silhouette Score of **0.508**, compared to the PCA baseline of 0.163, while the Calinski-Harabasz Index rose from 103.7 to **2147.4**. When the full optimized post-processing pipeline is applied (Spherical k-means with $L_2$ re-normalization, as described in Section 2.4.4–2.4.5), the Silhouette Score further improves to **0.6894** with a Davies-Bouldin Index of **0.6243**, confirming that the combination of contrastive feature learning and geometrically correct clustering produces massive improvements in spatial separation.
|
||||
|
||||
While PCA relies on magnitude-dependent variance—essentially grouping ions based on their absolute 'brightness' and pixel-to-pixel correlation—it is highly sensitive to the dynamic range of the detector and often fails to resolve low-abundance metabolites that do not contribute significantly to the total variance. In contrast, the custom SimCLR architecture prioritizes the underlying spatial topographies (morphological fingerprints) of the ion images. This allows the model to project ions with vastly different absolute intensities but identical anatomical 'silhouettes'—such as a primary alkaloid and its trace-level isotopes or precursors—into adjacent coordinates within the latent space. By decoupling chemical distribution from absolute abundance, this workflow captures the structural integrity of the tissue, offering a more robust and biologically grounded segmentation that traditional linear or intensity-based algorithms cannot easily achieve.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
In this study, we successfully implemented a high-performance computational framework that bridges the gap between raw mass spectrometry data and deep learning-based biological discovery. By resolving the 'two-language problem' through the synergy of Julia and Python, we achieved a scalable workflow capable of processing high-dimensional imzML datasets with unprecedented efficiency. The application of self-supervised contrastive learning (SimCLR) allowed for the objective segmentation of the *D. innoxia* leaf into ten distinct metabolic domains, effectively decoupling biological morphology from instrumental noise and absolute intensity variations.
|
||||
|
||||
Our findings provide a spatial validation of the acropetal transport of tropane alkaloids, demonstrating that the vascular system acts as a specialized conduit for systemic chemical defense. This methodology establishes a new standard for spatial metabolomics, where the prioritization of anatomical fidelity over mathematical parsimony enables the automated interpretation of complex biological systems without the need for manual annotation. Ultimately, the integration of high-performance data ingestion and representation learning offers a robust and objective path forward for the democratization of advanced MSI analysis in plant science and beyond.
|
||||
|
||||
**Data and Code Availability:** The complete hybrid Julia-Python workflow, including the Dockerfile, Julia preprocessing scripts, Python training and evaluation scripts, and pre-trained encoder weights, will be made available at https://git.nube-gran.de/Carlos/MSI_Julia_CNN/src/branch/main upon publication. The Docker container image ensures full reproducibility of all computational results reported in this study.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
* Chen, T., Kornblith, S., Norouzi, M., & Hinton, G. (2020). A simple framework for contrastive learning of visual representations. In Proceedings of the 37th International Conference on Machine Learning (pp. 1597–1607). PMLR. https://doi.org/10.48550/arXiv.2002.05709
|
||||
* Cole, L. M., & Clench, M. R. (Eds.). (2023). Imaging Mass Spectrometry: Methods and Protocols. Springer US. https://doi.org/10.1007/978-1-0716-3319-9
|
||||
* García-Rojas, N. S., Sierra-Álvarez, C. D., Ramos-Aboites, H. E., Moreno-Pedraza, A., & Winkler, R. (2024). Identification of Plant Compounds with Mass Spectrometry Imaging (MSI). Metabolites, 14(8), 419. https://doi.org/10.3390/metabo14080419
|
||||
* Harper, J. D., Charipar, N. A., Mulligan, C. C., Zhang, X., Cooks, R. G., & Ouyang, Z. (2008). Low-temperature plasma probe for ambient desorption ionization. Analytical Chemistry, 80(23), 9097–9104. https://doi.org/10.1021/ac801641a
|
||||
* Hu, H., Bindu, J. P., & Laskin, J. (2022). Self-supervised clustering of mass spectrometry imaging data using contrastive learning. Chemical Science, 13(1), 90–98. https://doi.org/10.1039/d1sc04077d
|
||||
* Hugging Face. (2025). Gradient Accumulation. Hugging Face Documentation. Available at: https://huggingface.co/docs/transformers/en/perf_train_gpu_one#gradient-accumulation
|
||||
* Kingma, D. P., & Ba, J. (2014). Adam: A method for stochastic optimization. arXiv preprint arXiv:1412.6980. https://doi.org/10.48550/arXiv.1412.6980
|
||||
* Martínez-Jarquín, S., & Winkler, R. (2017). Low-temperature plasma (LTP) jets for mass spectrometry (MS): Ion processes, instrumental set-ups, and application examples. Trends in Analytical Chemistry, 89, 133–145. https://doi.org/10.1016/j.trac.2017.01.013
|
||||
* Moreno-Pedraza, A., Rosas-Román, I., García-Rojas, N. S., Guillén-Alonso, H., Ovando-Vázquez, C., Díaz-Ramírez, D., ... & Winkler, R. (2019). Elucidating the Distribution of Plant Metabolites from Native Tissues with Laser Desorption Low-Temperature Plasma Mass Spectrometry Imaging. Analytical Chemistry, 91(4), 2734–2743. https://doi.org/10.1021/acs.analchem.8b04406
|
||||
* Rosas-Román, I., Ovando-Vázquez, C., Moreno-Pedraza, A., Guillén-Alonso, H., & Winkler, R. (2020). Open LabBot and RmsiGUI: Community development kit for sampling automation and ambient imaging. Microchemical Journal, 152, 104343. https://doi.org/10.1016/j.microc.2019.104343
|
||||
* Rosas-Román, I., Guillén-Alonso, H., Moreno-Pedraza, A., & Winkler, R. (2024). Technical Note: mzML and imzML Libraries for Processing Mass Spectrometry Data with the High-Performance Programming Language Julia. Analytical Chemistry, 96(10), 3999–4004. https://doi.org/10.1021/acs.analchem.3c05853
|
||||
* Schlesinger, D., Davidovich Rikanati, R., Faigenboim, A., Vendramin, V., Cattonaro, F., Inbar, M., & Lewinsohn, E. (2021). Tropane alkaloid biosynthesis in *Datura innoxia* Mill. roots and their differential transport to shoots. Phytochemistry Letters, 43, 219–225. https://doi.org/10.1016/j.phytol.2021.04.012
|
||||
* Shah, M., Liu, S., Guo, L., Zhang, Y., Deng, L., Xu, X., ... & Dong, J. (2025). MSInet: A Self-Supervised CNN Framework Integrating Global and Local Context for Robust Mass Spectrometry Imaging Segmentation. Analytical Chemistry, 97(46), 24697–24705. https://doi.org/10.1021/acs.analchem.5c04885
|
||||
* Godoy, W. F., Valero-Lara, P., Dettling, T. E., Trefftz, C., Jorquera, I., Sheehy, T., ... & Churavy, V. (2023). Evaluating performance and portability of high-level programming models: Julia, Python/Numba, and Kokkos on exascale nodes. arXiv preprint arXiv:2303.06195.
|
||||
* Churavy, V., Bezanson, J., & Edelman, A. (2020). Julia Micro-Benchmarks. JuliaLang.org. Available at: https://julialang.github.io/Microbenchmarks/dev/benchmarks/ (Accessed: June 2026).
|
||||
* Perkel, J. M. (2019). Julia: Come for the syntax, stay for the speed. Nature, 572(7767), 141-142.
|
||||
* Gustafsson, O. J. R., Winderbaum, L. J., & Powell, D. R. (2018). Balancing sufficiency and impact in reporting standards for mass spectrometry imaging experiments. GigaScience, 7(10), giy102. https://doi.org/10.1093/gigascience/giy102
|
||||
* Xi, Y., Sohn, A. L., Joignant, A. N., Cologna, S. M., Prentice, B. M., & Muddiman, D. C. (2023). SMART: A data reporting standard for mass spectrometry imaging. Journal of Mass Spectrometry, 58(2), e4904. https://doi.org/10.1002/jms.4904
|
||||
* Kapoor, S., Cantrell, E. M., Peng, K., Pham, T. H., Bail, C. A., Gundersen, O. E., Hofman, J. M., Hullman, J., Lones, M. A., Malik, M. M., Nanayakkara, P., Poldrack, R. A., Raji, I. D., Roberts, M., Salganik, M. J., Serra-Garcia, M., Stewart, B. M., Vandewiele, G., & Narayanan, A. (2024). REFORMS: Consensus-based Recommendations for Machine-learning-based Science. Science Advances, 10(18), eadk3452. https://doi.org/10.1126/sciadv.adk3452
|
||||
* Zhang, Y., Li, Z., Bhatt, P., & Bhattacharya, D. (2024). DeepION: A Deep Learning-Based Low-Dimensional Representation Model of Ion Images for Mass Spectrometry Imaging. Analytical Chemistry, 96(9), 3829–3836. https://doi.org/10.1021/acs.analchem.3c05002
|
||||
* Rosas-Román, I., & Winkler, R. (2021). Contrast optimization of mass spectrometry imaging (MSI) data visualization by threshold intensity quantization (TrIQ). PeerJ Computer Science, 7, e585. https://doi.org/10.7717/peerj-cs.585
|
||||
* Paszke, A., et al. (2019). PyTorch: An imperative style, high-performance deep learning library. Advances in Neural Information Processing Systems, 32.
|
||||
* Schramm, T., et al. (2012). imzML — A common data format for the flexible exchange and processing of mass spectrometry imaging data. Journal of Proteomics, 75(16), 5106–5110. https://doi.org/10.1016/j.jprot.2012.07.026
|
||||
* Sierra-Álvarez, J. J., Rosas-Román, I., García-Rojas, N. S., Moreno-Pedraza, A., & Sierra-Álvarez, C. D. (2025). JuliaMSI: A high-performance graphical platform for mass spectrometry imaging data analysis. Analytica Chimica Acta, 1377, 344613. https://doi.org/10.1016/j.aca.2025.344613
|
||||
* Tan, M., & Le, Q. (2019). EfficientNet: Rethinking model scaling for convolutional neural networks. In Proceedings of the 36th International Conference on Machine Learning (pp. 6105–6114). PMLR. https://doi.org/10.48550/arXiv.1905.11946
|
||||
* Van der Maaten, L., & Hinton, G. (2008). Visualizing data using t-SNE. Journal of Machine Learning Research, 9(11), 2579–2605.
|
||||
* Ziegler, J., & Facchini, P. J. (2008). Alkaloid Biosynthesis: Metabolism and Trafficking. Annual Review of Plant Biology, 59(1), 735–769. https://doi.org/10.1146/annurev.arplant.59.032607.092730
|
||||
* Alexandrov, T. (2012). MALDI imaging mass spectrometry: statistical data analysis and current computational challenges. BMC Bioinformatics, 13(Suppl 16), S11. https://doi.org/10.1186/1471-2105-13-S16-S11
|
||||
* Hu, B., Huang, Z., Wu, X., Shen, X., & Laskin, J. (2021). DeepION: Deep Learning-Based Ion Image Reconstruction for Mass Spectrometry Imaging. Analytical Chemistry, 93(27), 9945–9954. https://doi.org/10.1021/acs.analchem.1c00223
|
||||
* Purushwalkam, S., & Gupta, A. (2020). Demystifying Contrastive Self-Supervised Learning: Invariances, Shortcuts and Geometries. Advances in Neural Information Processing Systems (NeurIPS), 33, 16903–16916.
|
||||
* Diaconis, P., Goel, S., & Holmes, S. (2008). Horseshoes in multidimensional scaling and local kernel methods. The Annals of Applied Statistics, 2(3), 777–807. https://doi.org/10.1214/08-AOAS165
|
||||
* Moon, K. R., van Dijk, D., Wang, Z., Gigante, S., Burkhardt, D. B., Chen, W. S., ... & Krishnaswamy, S. (2019). Visualizing structure and transitions in high-dimensional biological data. Nature Biotechnology, 37(12), 1482–1492. https://doi.org/10.1038/s41587-019-0336-3
|
||||
* Hornik, K., Feinerer, I., Kober, M., & Buchta, C. (2012). Spherical k-Means Clustering. Journal of Statistical Software, 50(10), 1–22. https://doi.org/10.18637/jss.v050.i10
|
||||
|
||||
119
README.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Deep Learning in Mass Spectrometry Imaging: A Hybrid Julia-Python Workflow
|
||||
|
||||
This repository contains the computational pipeline for the automated anatomical mapping of *Datura innoxia* (toloache) using Mass Spectrometry Imaging (MSI). The workflow addresses the "two-language problem" by leveraging **Julia** for high-performance data preprocessing and **Python** for self-supervised representation learning.
|
||||
|
||||
## Key Features
|
||||
- **High-Performance Preprocessing**: Rapid `.imzML` ingestion and TrIQ normalization using Julia.
|
||||
- **Self-Supervised Learning**: Contrastive learning (SimCLR) with an EfficientNet-B0 backbone implemented in PyTorch.
|
||||
- **Reproducibility**: Integrated Docker environment and fixed random seeds for consistent results.
|
||||
- **Performance Benchmarking**: Real-time monitoring of execution time and RAM usage across the hybrid pipeline.
|
||||
- **CUDA/GPU Support**: Optimized for NVIDIA hardware for accelerated training and inference.
|
||||
- **Hybrid Bridge**: Automated data transfer between Julia and Python ecosystems via standardized intermediate files.
|
||||
|
||||
## Project Structure
|
||||
- `scripts_julia/`: Julia scripts for mass slice extraction and TrIQ normalization.
|
||||
- `scripts_python/`: Python scripts for SimCLR training, feature extraction, and clustering.
|
||||
- `models/`: Contains the pre-trained weights (`msi_encoder_trained.pth`).
|
||||
- `environment/`: Dependency files (`requirements.txt`, `Project.toml`).
|
||||
- `figures/`: Generated plots and cluster galleries.
|
||||
- `data/`: (User-provided) Location for `.imzML` and mask files.
|
||||
|
||||
## Reproducibility & Docker (Recommended)
|
||||
To ensure identical environments for both Julia and Python, we provide a unified Docker setup. This setup includes the **JuliaMSI** framework.
|
||||
|
||||
### Building the Container
|
||||
Before building, ensure the `JuliaMSI` repository is present in the root directory (it was cloned from `https://git.nube-gran.de/Julian/JuliaMSI`).
|
||||
```bash
|
||||
docker build -t msi-hybrid-workflow .
|
||||
```
|
||||
|
||||
### Running the Container (CPU Mode)
|
||||
```bash
|
||||
docker run -it --rm -v $(pwd):/app msi-hybrid-workflow
|
||||
```
|
||||
|
||||
### Running the Container (GPU/CUDA Mode)
|
||||
```bash
|
||||
docker run -it --rm --shm-size=2gb --gpus all -v $(pwd):/app msi-hybrid-workflow
|
||||
```
|
||||
|
||||
## Usage Workflow
|
||||
|
||||
### 1. Preprocessing (Julia)
|
||||
Leverage Julia's performance and the **JuliaMSI** framework.
|
||||
|
||||
**Run this clean runtime instantiation command one time to sync everything down to your local directory:**
|
||||
```bash
|
||||
docker run -it --rm -v $(pwd):/app msi-hybrid-workflow \
|
||||
julia --project=/app/environment -e 'using Pkg; Pkg.resolve(); Pkg.instantiate()'
|
||||
```
|
||||
|
||||
**Run the MSI Bridge (Normalization & PNG Export):**
|
||||
```bash
|
||||
docker run -it --rm --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
julia --threads auto --project=/app/environment /app/scripts_julia/julia_msi_bridge.jl
|
||||
```
|
||||
|
||||
**Optional: Plot Cluster Spectra:**
|
||||
```bash
|
||||
docker run -it --rm --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
julia --threads auto --project=/app/environment scripts_julia/graph_cluster_spectra.jl
|
||||
```
|
||||
|
||||
### 2. Representation Learning (Python)
|
||||
Train the SimCLR encoder to extract morphological features using CUDA.
|
||||
|
||||
**Run CNN Training (SimCLR):**
|
||||
```bash
|
||||
docker run -it --rm \
|
||||
--shm-size=2gb \
|
||||
--gpus all \
|
||||
-v $(pwd):/app \
|
||||
-v msi_torch_cache:/root/.cache/torch \
|
||||
msi-hybrid-workflow \
|
||||
python3 scripts_python/CNN_proof_1.py
|
||||
```
|
||||
*Note: The script automatically detects CUDA and logs per-epoch RAM usage.*
|
||||
|
||||
### 3. Clustering & Validation
|
||||
Determine optimal clusters and perform quantitative validation.
|
||||
|
||||
**Generate Clusters:**
|
||||
```bash
|
||||
docker run -it --rm --shm-size=2gb --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
python3 scripts_python/cluster_msi_direct.py
|
||||
```
|
||||
|
||||
**Evaluate Clustering:**
|
||||
```bash
|
||||
docker run -it --rm --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
python3 scripts_python/evaluate_clustering.py
|
||||
```
|
||||
|
||||
**Quantitative Validation:**
|
||||
```bash
|
||||
docker run -it --rm --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
python3 scripts_python/quantitative_validation.py
|
||||
```
|
||||
|
||||
### 4. Visualization
|
||||
Generate the final galleries and t-SNE maps.
|
||||
|
||||
**Generate t-SNE Maps:**
|
||||
```bash
|
||||
docker run -it --rm --shm-size=2gb --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
python3 scripts_python/graficar_tsne_msi.py
|
||||
```
|
||||
|
||||
**Create Cluster Galleries:**
|
||||
```bash
|
||||
docker run -it --rm --shm-size=2gb --gpus all -v $(pwd):/app msi-hybrid-workflow \
|
||||
python3 scripts_python/visualize_clusters.py
|
||||
```
|
||||
|
||||
## Citation & Acknowledgments
|
||||
This implementation is based on the methodology proposed by:
|
||||
> Hu, H., Bindu, J. P., & Laskin, J. (2022). Self-supervised clustering of mass spectrometry imaging data using contrastive learning. *Chemical Science*, 13(1), 90-98. DOI: [10.1039/d1sc04077d](https://doi.org/10.1039/d1sc04077d)
|
||||
|
||||
## License
|
||||
This project is licensed under the **MIT License**.
|
||||
16
References_DOI.md
Normal file
@ -0,0 +1,16 @@
|
||||
# DOIs References
|
||||
|
||||
* Ziegler & Facchini (2008): https://doi.org/10.1146/annurev.arplant.59.032607.092730
|
||||
* Schlesinger et al. (2021): https://doi.org/10.1016/j.phytol.2021.04.012
|
||||
* Moreno-Pedraza et al. (2019): https://doi.org/10.1021/acs.analchem.8b04406
|
||||
* Cole & Clench (2023): https://doi.org/10.1007/978-1-0716-3319-9
|
||||
* Schramm et al. (2012): https://doi.org/10.1016/j.jprot.2012.07.026
|
||||
* Rosas-Román et al. (2024): https://doi.org/10.1021/acs.analchem.3c05853
|
||||
* Sierra-Álvarez et al. (2025): https://doi.org/10.1016/j.aca.2025.344613
|
||||
* Martínez-Jarquín & Winkler (2017): https://doi.org/10.1016/j.trac.2017.01.013
|
||||
* Rosas-Román et al. (2020): https://doi.org/10.1016/j.microc.2019.104343
|
||||
* Hu et al. (2022): https://doi.org/10.1039/d1sc04077d
|
||||
* Chen et al. (2020): https://doi.org/10.48550/arXiv.2002.05709
|
||||
* Tan & Le (2019): https://doi.org/10.48550/arXiv.1905.11946
|
||||
* Kingma & Ba (2014): https://doi.org/10.48550/arXiv.1412.6980
|
||||
* García-Rojas et al. (2024): https://doi.org/10.3390/metabo14080419
|
||||
BIN
data/Leaf.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
data/region_of_interest.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
73
documentation.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Technical Documentation & Experimental Rationale
|
||||
|
||||
This document provides a detailed technical overview of the computational choices, hyperparameter tuning, and validation strategies implemented in the hybrid Julia-Python MSI workflow.
|
||||
|
||||
## Docker Maintenance & Disk Space
|
||||
If you encounter disk space issues due to failed builds or unused images, use these commands:
|
||||
|
||||
- **Safe Clean (Dangling layers only):** `docker image prune`
|
||||
- **Deep Clean (Everything unused):** `docker system prune -a --volumes`
|
||||
- **Clear Build Cache:** `docker builder prune`
|
||||
|
||||
## 1. Hyperparameter Tuning Rationale (SimCLR)
|
||||
|
||||
The training configuration in `scripts_python/CNN_proof_1.py` follows the standard SimCLR framework (Chen et al., 2020) while being optimized for the morphological characteristics of Mass Spectrometry Imaging (MSI) data.
|
||||
|
||||
| Hyperparameter | Value | Rationale |
|
||||
| :--- | :--- | :--- |
|
||||
| **Temperature ($\tau$)** | 0.5 | **Selected via grid search.** We tested $\tau \in \{0.1, 0.5, 1.0\}$. A value of 0.1 led to unstable gradients and collapsed representations, while 1.0 was too permissive. $\tau=0.5$ provided the most stable convergence and distinct cluster boundaries. |
|
||||
| **Projection Dim** | 128 | Standard for SimCLR. Reducing to 64 lost anatomical detail; increasing to 256 did not improve Silhouette scores but increased computational overhead. |
|
||||
| **Training Epochs** | 50 | Observed convergence of NT-Xent loss between epochs 40-50. Further training (tested up to 200 epochs) led to minor overfitting to instrumental artifacts. |
|
||||
| **Batch Size** | 32 | Optimized for the i5 CPU environment to balance memory usage and gradient stability. |
|
||||
|
||||
## 2. Advanced Validation Strategies
|
||||
|
||||
To satisfy rigorous peer review, we implemented three quantitative validation layers in `scripts_python/quantitative_validation.py`:
|
||||
|
||||
### 2.1 Baseline Comparison (PCA vs. SimCLR)
|
||||
* **Method:** We compared our self-supervised approach against a traditional baseline where PCA is applied directly to raw pixel vectors (flattened ion images).
|
||||
* **Result:** SimCLR consistently yields higher Silhouette scores and lower Davies-Bouldin indices. This proves that the neural network's non-linear feature extraction captures anatomical "shapes" better than linear variance-based methods.
|
||||
|
||||
### 2.2 Stability & Reproducibility Analysis
|
||||
* **Metric:** Adjusted Rand Index (ARI).
|
||||
* **Method:** We ran the clustering pipeline across 10 different random seeds. By computing the ARI between all pairs of runs, we demonstrate that the metabolic domains identified (Clusters 0-9) are stable and not artifacts of k-means initialization.
|
||||
|
||||
### 2.3 Isotope Co-localization (Natural Ground Truth)
|
||||
* **Scientific Anchor:** Isotopes ($[M+H]^+$ and $[M+H+1]^+$) must co-localize perfectly in tissue.
|
||||
* **Validation:** We measure the Euclidean distance of isotopic pairs in the latent manifold. A t-test confirms that these distances are significantly smaller than those of random ion pairs, providing objective evidence that the model has learned chemical-biological reality.
|
||||
|
||||
## 3. Biological & Mathematical Rationale for $k=10$
|
||||
|
||||
While mathematical metrics (Elbow method, Silhouette) often suggest $k=2$ or $k=3$ (tissue vs. background), we selected **$k=10$** based on:
|
||||
1. **Anatomical Fidelity:** This threshold is required to resolve the hierarchy between the primary midrib, secondary veins, and apical accumulation zones.
|
||||
2. **Stability:** Metric stabilization was observed in the $k=8$ to $k=12$ range.
|
||||
3. **Metabolic Heatmaps:** Average ion intensities per cluster for scopolamine ($m/z$ 304.1) and atropine ($m/z$ 290.1) show clear sequestration in Clusters 9 (Vascular) and 7 (Apical), validating the biological relevance of the segmentation.
|
||||
|
||||
## 4. Technical Specifications & Citations
|
||||
|
||||
### 4.1 Data Augmentations
|
||||
We implemented **Additive Gaussian Noise ($\sigma=0.01$)**. This is critical for MSI to simulate detector background noise.
|
||||
* *Citation:* DeepION (2024) and Hu et al. (2022, Table S1).
|
||||
|
||||
### 4.2 Model Backbone
|
||||
**EfficientNet-B0** with ImageNet weights was used for transfer learning. The MBConv blocks are highly efficient for CPU-based inference.
|
||||
* *Citation:* Tan & Le (2019).
|
||||
|
||||
### 4.3 Preprocessing (JuliaMSI)
|
||||
* **Spectral Averaging:** Done via `MSI_src.get_average_spectrum`.
|
||||
* **Tolerance:** $\pm 0.05$ Da window for mass slices. This is standard for low-resolution Ion Trap instruments (LCQ Fleet) to ensure all isotopic signal is captured.
|
||||
* *Citation:* Sierra-Álvarez et al. (2025).
|
||||
|
||||
### 4.4 Dimensionality Reduction
|
||||
PCA was used to reduce the 1280-dim (EfficientNet) or 128-dim (Projector) features to **50 components** before k-means. This heuristic stabilizes clustering by removing noise and redundant dimensions.
|
||||
* *Citation:* Jolliffe (2002).
|
||||
|
||||
|
||||
## Benchmarking & Hardware Realism
|
||||
The pipeline is optimized for efficiency on consumer-grade hardware.
|
||||
- **Julia Stage**: Uses `BenchmarkTools.jl` to profile binary parsing.
|
||||
- **Python Stage**: Uses `psutil` and `CUDA` monitoring to track hardware overhead.
|
||||
- **Determinism**: All stochastic steps are locked with `seed=42`.
|
||||
|
||||
## Julia-Python Bridge Details
|
||||
The "two-language problem" is solved by an intermediate file-system bridge. Julia performs binary MSI parsing and **Threshold Intensity Quantization (TrIQ)**. The resulting data is saved as high-contrast 224x224 grayscale PNG images, allowing the Python PyTorch pipeline to consume it efficiently.
|
||||
49
environment/Project.toml
Normal file
@ -0,0 +1,49 @@
|
||||
[deps]
|
||||
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
|
||||
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
||||
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
|
||||
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
|
||||
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
|
||||
CodecBase = "6c391c72-fb7b-5838-ba82-7cfb1bcfecbf"
|
||||
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
|
||||
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
|
||||
ContinuousWavelets = "96eb917e-2868-4417-9cb6-27e7ff17528f"
|
||||
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
|
||||
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
||||
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
|
||||
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
|
||||
Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e"
|
||||
GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353"
|
||||
HistogramThresholding = "2c695a8d-9458-5d45-9878-1b8a99cf7853"
|
||||
ImageBinarization = "cbc4b850-ae4b-5111-9e64-df94c024a13d"
|
||||
ImageComponentAnalysis = "d9b9e9a0-1569-11e9-2cb5-bbca914b0e89"
|
||||
ImageContrastAdjustment = "f332f351-ec65-5f6a-b3d1-319c6670881a"
|
||||
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
|
||||
ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
|
||||
ImageMorphology = "787d08f9-d448-5407-9aad-5290dd7ab264"
|
||||
ImageSegmentation = "80713f31-8817-5129-9cf8-209ff8fb23e1"
|
||||
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
|
||||
Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59"
|
||||
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
|
||||
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
|
||||
Libz = "2ec943e9-cfe8-584d-b93d-64dcb6d567b7"
|
||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612"
|
||||
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
|
||||
NativeFileDialog = "e1fe445b-aa65-4df4-81c1-2041507f0fd4"
|
||||
NaturalSort = "c020b1a1-e9b0-503a-9c33-f039bfc54a85"
|
||||
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
|
||||
Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a"
|
||||
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
|
||||
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
|
||||
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
||||
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
|
||||
SavitzkyGolay = "c4bf5708-b6a6-4fbe-bcd0-6850ed671584"
|
||||
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
|
||||
Setfield = "efcf1570-3423-57d1-acb7-fd33fddbac46"
|
||||
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
|
||||
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
|
||||
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
|
||||
StipplePlotly = "ec984513-233d-481d-95b0-a3b58b97af2b"
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
||||
10
environment/requirements.txt
Normal file
@ -0,0 +1,10 @@
|
||||
torch>=2.0.0
|
||||
torchvision>=0.15.0
|
||||
Pillow>=9.0.0
|
||||
numpy>=1.21.0
|
||||
scikit-learn>=1.0.0
|
||||
matplotlib>=3.5.0
|
||||
pandas>=1.4.0
|
||||
scipy>=1.7.0
|
||||
psutil>=5.9.0
|
||||
plotly>=6.8.0
|
||||
BIN
figures/galleries/Gallery_cluster_0.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
figures/galleries/Gallery_cluster_1.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
figures/galleries/Gallery_cluster_2.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
figures/galleries/Gallery_cluster_3.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
figures/galleries/Gallery_cluster_4.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
figures/galleries/Gallery_cluster_5.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
figures/galleries/Gallery_cluster_6.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
figures/galleries/Gallery_cluster_7.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
figures/galleries/Gallery_cluster_8.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
figures/galleries/Gallery_cluster_9.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
models/msi_encoder_trained.pth
Normal file
589
msi_clusters_results.csv
Normal file
@ -0,0 +1,589 @@
|
||||
ion_image,cluster,mz
|
||||
ion_100.15.png,2,100.15
|
||||
ion_100.48.png,0,100.48
|
||||
ion_100.8.png,2,100.8
|
||||
ion_101.45.png,0,101.45
|
||||
ion_101.78.png,4,101.78
|
||||
ion_102.1.png,2,102.1
|
||||
ion_102.75.png,3,102.75
|
||||
ion_103.73.png,3,103.73
|
||||
ion_104.7.png,1,104.7
|
||||
ion_105.03.png,2,105.03
|
||||
ion_105.68.png,1,105.68
|
||||
ion_106.0.png,2,106.0
|
||||
ion_106.65.png,4,106.65
|
||||
ion_106.98.png,2,106.98
|
||||
ion_107.3.png,9,107.3
|
||||
ion_107.63.png,4,107.63
|
||||
ion_107.95.png,2,107.95
|
||||
ion_108.28.png,8,108.28
|
||||
ion_108.61.png,3,108.61
|
||||
ion_108.93.png,2,108.93
|
||||
ion_109.26.png,1,109.26
|
||||
ion_109.58.png,9,109.58
|
||||
ion_109.91.png,2,109.91
|
||||
ion_110.23.png,8,110.23
|
||||
ion_110.56.png,3,110.56
|
||||
ion_110.88.png,2,110.88
|
||||
ion_111.21.png,2,111.21
|
||||
ion_111.53.png,0,111.53
|
||||
ion_111.86.png,2,111.86
|
||||
ion_112.18.png,2,112.18
|
||||
ion_112.51.png,0,112.51
|
||||
ion_112.83.png,2,112.83
|
||||
ion_113.16.png,2,113.16
|
||||
ion_113.48.png,0,113.48
|
||||
ion_113.81.png,2,113.81
|
||||
ion_114.13.png,2,114.13
|
||||
ion_114.46.png,7,114.46
|
||||
ion_114.78.png,2,114.78
|
||||
ion_115.11.png,2,115.11
|
||||
ion_115.43.png,0,115.43
|
||||
ion_115.76.png,6,115.76
|
||||
ion_116.08.png,2,116.08
|
||||
ion_116.73.png,4,116.73
|
||||
ion_117.06.png,2,117.06
|
||||
ion_117.71.png,4,117.71
|
||||
ion_118.03.png,2,118.03
|
||||
ion_118.68.png,1,118.68
|
||||
ion_119.01.png,2,119.01
|
||||
ion_119.66.png,8,119.66
|
||||
ion_119.98.png,2,119.98
|
||||
ion_120.31.png,3,120.31
|
||||
ion_120.63.png,4,120.63
|
||||
ion_120.96.png,2,120.96
|
||||
ion_121.28.png,8,121.28
|
||||
ion_121.61.png,5,121.61
|
||||
ion_121.94.png,2,121.94
|
||||
ion_122.26.png,8,122.26
|
||||
ion_122.59.png,9,122.59
|
||||
ion_122.91.png,2,122.91
|
||||
ion_123.24.png,8,123.24
|
||||
ion_123.56.png,7,123.56
|
||||
ion_123.89.png,2,123.89
|
||||
ion_124.21.png,6,124.21
|
||||
ion_124.54.png,1,124.54
|
||||
ion_124.86.png,2,124.86
|
||||
ion_125.19.png,6,125.19
|
||||
ion_125.51.png,0,125.51
|
||||
ion_125.84.png,2,125.84
|
||||
ion_126.16.png,2,126.16
|
||||
ion_126.49.png,0,126.49
|
||||
ion_126.81.png,2,126.81
|
||||
ion_127.14.png,6,127.14
|
||||
ion_127.46.png,0,127.46
|
||||
ion_127.79.png,2,127.79
|
||||
ion_128.11.png,2,128.11
|
||||
ion_128.44.png,0,128.44
|
||||
ion_128.76.png,6,128.76
|
||||
ion_129.09.png,2,129.09
|
||||
ion_129.41.png,0,129.41
|
||||
ion_129.74.png,6,129.74
|
||||
ion_130.06.png,2,130.06
|
||||
ion_130.39.png,7,130.39
|
||||
ion_130.71.png,6,130.71
|
||||
ion_131.04.png,2,131.04
|
||||
ion_131.69.png,3,131.69
|
||||
ion_132.01.png,2,132.01
|
||||
ion_132.66.png,1,132.66
|
||||
ion_132.99.png,2,132.99
|
||||
ion_133.64.png,1,133.64
|
||||
ion_133.96.png,2,133.96
|
||||
ion_134.29.png,8,134.29
|
||||
ion_134.61.png,3,134.61
|
||||
ion_134.94.png,2,134.94
|
||||
ion_135.27.png,8,135.27
|
||||
ion_135.59.png,7,135.59
|
||||
ion_135.92.png,2,135.92
|
||||
ion_136.24.png,4,136.24
|
||||
ion_136.57.png,5,136.57
|
||||
ion_136.89.png,2,136.89
|
||||
ion_137.22.png,6,137.22
|
||||
ion_137.54.png,3,137.54
|
||||
ion_137.87.png,6,137.87
|
||||
ion_138.19.png,4,138.19
|
||||
ion_138.52.png,0,138.52
|
||||
ion_138.84.png,2,138.84
|
||||
ion_139.17.png,6,139.17
|
||||
ion_139.49.png,0,139.49
|
||||
ion_139.82.png,2,139.82
|
||||
ion_140.14.png,6,140.14
|
||||
ion_140.47.png,0,140.47
|
||||
ion_140.79.png,2,140.79
|
||||
ion_141.12.png,6,141.12
|
||||
ion_141.44.png,0,141.44
|
||||
ion_141.77.png,8,141.77
|
||||
ion_142.09.png,2,142.09
|
||||
ion_142.74.png,6,142.74
|
||||
ion_143.07.png,2,143.07
|
||||
ion_143.72.png,6,143.72
|
||||
ion_144.04.png,2,144.04
|
||||
ion_144.37.png,1,144.37
|
||||
ion_144.69.png,1,144.69
|
||||
ion_145.02.png,2,145.02
|
||||
ion_145.67.png,3,145.67
|
||||
ion_145.99.png,2,145.99
|
||||
ion_146.32.png,3,146.32
|
||||
ion_146.64.png,1,146.64
|
||||
ion_146.97.png,2,146.97
|
||||
ion_147.29.png,8,147.29
|
||||
ion_147.62.png,3,147.62
|
||||
ion_147.94.png,2,147.94
|
||||
ion_148.27.png,4,148.27
|
||||
ion_148.59.png,7,148.59
|
||||
ion_148.92.png,2,148.92
|
||||
ion_149.25.png,4,149.25
|
||||
ion_149.57.png,0,149.57
|
||||
ion_149.9.png,2,149.9
|
||||
ion_150.22.png,4,150.22
|
||||
ion_150.55.png,5,150.55
|
||||
ion_150.87.png,2,150.87
|
||||
ion_151.2.png,6,151.2
|
||||
ion_151.52.png,0,151.52
|
||||
ion_151.85.png,2,151.85
|
||||
ion_152.17.png,4,152.17
|
||||
ion_152.5.png,0,152.5
|
||||
ion_152.82.png,2,152.82
|
||||
ion_153.15.png,6,153.15
|
||||
ion_153.47.png,0,153.47
|
||||
ion_153.8.png,2,153.8
|
||||
ion_154.12.png,2,154.12
|
||||
ion_154.45.png,9,154.45
|
||||
ion_154.77.png,6,154.77
|
||||
ion_155.1.png,2,155.1
|
||||
ion_155.75.png,8,155.75
|
||||
ion_156.07.png,6,156.07
|
||||
ion_156.4.png,9,156.4
|
||||
ion_156.72.png,6,156.72
|
||||
ion_157.05.png,2,157.05
|
||||
ion_157.7.png,9,157.7
|
||||
ion_158.02.png,2,158.02
|
||||
ion_158.67.png,3,158.67
|
||||
ion_159.0.png,2,159.0
|
||||
ion_159.65.png,9,159.65
|
||||
ion_159.97.png,2,159.97
|
||||
ion_160.3.png,8,160.3
|
||||
ion_160.62.png,9,160.62
|
||||
ion_160.95.png,2,160.95
|
||||
ion_161.27.png,4,161.27
|
||||
ion_161.6.png,0,161.6
|
||||
ion_161.92.png,2,161.92
|
||||
ion_162.25.png,8,162.25
|
||||
ion_162.58.png,7,162.58
|
||||
ion_162.9.png,2,162.9
|
||||
ion_163.23.png,4,163.23
|
||||
ion_163.55.png,7,163.55
|
||||
ion_163.88.png,6,163.88
|
||||
ion_164.2.png,4,164.2
|
||||
ion_164.53.png,0,164.53
|
||||
ion_164.85.png,2,164.85
|
||||
ion_165.18.png,4,165.18
|
||||
ion_165.5.png,0,165.5
|
||||
ion_165.83.png,6,165.83
|
||||
ion_166.15.png,4,166.15
|
||||
ion_166.48.png,0,166.48
|
||||
ion_166.8.png,2,166.8
|
||||
ion_167.13.png,2,167.13
|
||||
ion_167.45.png,9,167.45
|
||||
ion_167.78.png,1,167.78
|
||||
ion_168.1.png,2,168.1
|
||||
ion_168.43.png,5,168.43
|
||||
ion_168.75.png,4,168.75
|
||||
ion_169.08.png,6,169.08
|
||||
ion_169.73.png,1,169.73
|
||||
ion_170.05.png,6,170.05
|
||||
ion_170.7.png,8,170.7
|
||||
ion_171.03.png,2,171.03
|
||||
ion_171.68.png,5,171.68
|
||||
ion_172.0.png,6,172.0
|
||||
ion_172.65.png,9,172.65
|
||||
ion_172.98.png,2,172.98
|
||||
ion_173.63.png,5,173.63
|
||||
ion_173.95.png,4,173.95
|
||||
ion_174.28.png,1,174.28
|
||||
ion_174.6.png,0,174.6
|
||||
ion_174.93.png,2,174.93
|
||||
ion_175.25.png,8,175.25
|
||||
ion_175.58.png,0,175.58
|
||||
ion_175.91.png,6,175.91
|
||||
ion_176.23.png,8,176.23
|
||||
ion_176.56.png,5,176.56
|
||||
ion_176.88.png,2,176.88
|
||||
ion_177.21.png,2,177.21
|
||||
ion_177.53.png,0,177.53
|
||||
ion_177.86.png,4,177.86
|
||||
ion_178.18.png,4,178.18
|
||||
ion_178.83.png,4,178.83
|
||||
ion_179.16.png,4,179.16
|
||||
ion_179.81.png,8,179.81
|
||||
ion_180.13.png,6,180.13
|
||||
ion_180.46.png,5,180.46
|
||||
ion_180.78.png,1,180.78
|
||||
ion_181.11.png,6,181.11
|
||||
ion_181.43.png,9,181.43
|
||||
ion_181.76.png,3,181.76
|
||||
ion_182.08.png,6,182.08
|
||||
ion_182.73.png,8,182.73
|
||||
ion_183.06.png,4,183.06
|
||||
ion_183.71.png,3,183.71
|
||||
ion_184.03.png,6,184.03
|
||||
ion_184.68.png,3,184.68
|
||||
ion_185.01.png,2,185.01
|
||||
ion_185.66.png,7,185.66
|
||||
ion_185.98.png,6,185.98
|
||||
ion_186.31.png,1,186.31
|
||||
ion_186.63.png,5,186.63
|
||||
ion_186.96.png,2,186.96
|
||||
ion_187.61.png,0,187.61
|
||||
ion_187.93.png,4,187.93
|
||||
ion_188.58.png,0,188.58
|
||||
ion_188.91.png,4,188.91
|
||||
ion_189.24.png,1,189.24
|
||||
ion_189.56.png,0,189.56
|
||||
ion_189.89.png,4,189.89
|
||||
ion_190.21.png,8,190.21
|
||||
ion_190.54.png,7,190.54
|
||||
ion_190.86.png,8,190.86
|
||||
ion_191.19.png,4,191.19
|
||||
ion_191.51.png,0,191.51
|
||||
ion_191.84.png,8,191.84
|
||||
ion_192.16.png,4,192.16
|
||||
ion_192.81.png,8,192.81
|
||||
ion_193.14.png,4,193.14
|
||||
ion_193.79.png,1,193.79
|
||||
ion_194.11.png,8,194.11
|
||||
ion_194.76.png,1,194.76
|
||||
ion_195.09.png,6,195.09
|
||||
ion_195.74.png,9,195.74
|
||||
ion_196.06.png,4,196.06
|
||||
ion_196.71.png,1,196.71
|
||||
ion_197.04.png,2,197.04
|
||||
ion_197.69.png,7,197.69
|
||||
ion_198.01.png,4,198.01
|
||||
ion_198.66.png,1,198.66
|
||||
ion_198.99.png,2,198.99
|
||||
ion_199.31.png,1,199.31
|
||||
ion_199.64.png,7,199.64
|
||||
ion_199.96.png,4,199.96
|
||||
ion_200.29.png,1,200.29
|
||||
ion_200.61.png,0,200.61
|
||||
ion_200.94.png,4,200.94
|
||||
ion_201.26.png,8,201.26
|
||||
ion_201.59.png,0,201.59
|
||||
ion_201.91.png,4,201.91
|
||||
ion_202.24.png,1,202.24
|
||||
ion_202.57.png,0,202.57
|
||||
ion_202.89.png,4,202.89
|
||||
ion_203.22.png,8,203.22
|
||||
ion_203.87.png,3,203.87
|
||||
ion_204.19.png,8,204.19
|
||||
ion_204.52.png,3,204.52
|
||||
ion_204.84.png,2,204.84
|
||||
ion_205.17.png,4,205.17
|
||||
ion_205.82.png,1,205.82
|
||||
ion_206.14.png,1,206.14
|
||||
ion_206.79.png,8,206.79
|
||||
ion_207.12.png,6,207.12
|
||||
ion_207.77.png,5,207.77
|
||||
ion_208.09.png,8,208.09
|
||||
ion_208.74.png,3,208.74
|
||||
ion_209.07.png,6,209.07
|
||||
ion_209.72.png,5,209.72
|
||||
ion_210.04.png,4,210.04
|
||||
ion_210.69.png,7,210.69
|
||||
ion_211.02.png,6,211.02
|
||||
ion_211.67.png,0,211.67
|
||||
ion_211.99.png,8,211.99
|
||||
ion_212.64.png,7,212.64
|
||||
ion_212.97.png,6,212.97
|
||||
ion_213.62.png,7,213.62
|
||||
ion_213.94.png,8,213.94
|
||||
ion_214.27.png,1,214.27
|
||||
ion_214.59.png,5,214.59
|
||||
ion_214.92.png,2,214.92
|
||||
ion_215.24.png,8,215.24
|
||||
ion_215.57.png,0,215.57
|
||||
ion_215.89.png,8,215.89
|
||||
ion_216.22.png,1,216.22
|
||||
ion_216.55.png,1,216.55
|
||||
ion_216.87.png,2,216.87
|
||||
ion_217.2.png,6,217.2
|
||||
ion_217.52.png,7,217.52
|
||||
ion_217.85.png,1,217.85
|
||||
ion_218.17.png,1,218.17
|
||||
ion_218.82.png,1,218.82
|
||||
ion_219.15.png,8,219.15
|
||||
ion_219.8.png,3,219.8
|
||||
ion_220.12.png,8,220.12
|
||||
ion_220.77.png,5,220.77
|
||||
ion_221.1.png,4,221.1
|
||||
ion_221.75.png,7,221.75
|
||||
ion_222.07.png,1,222.07
|
||||
ion_222.72.png,1,222.72
|
||||
ion_223.05.png,2,223.05
|
||||
ion_223.7.png,0,223.7
|
||||
ion_224.02.png,1,224.02
|
||||
ion_224.67.png,7,224.67
|
||||
ion_225.0.png,4,225.0
|
||||
ion_225.65.png,0,225.65
|
||||
ion_225.97.png,1,225.97
|
||||
ion_226.62.png,9,226.62
|
||||
ion_226.95.png,6,226.95
|
||||
ion_227.6.png,0,227.6
|
||||
ion_227.92.png,1,227.92
|
||||
ion_228.25.png,1,228.25
|
||||
ion_228.9.png,8,228.9
|
||||
ion_229.22.png,8,229.22
|
||||
ion_229.88.png,1,229.88
|
||||
ion_230.2.png,1,230.2
|
||||
ion_230.85.png,3,230.85
|
||||
ion_231.18.png,1,231.18
|
||||
ion_231.83.png,9,231.83
|
||||
ion_232.15.png,1,232.15
|
||||
ion_232.8.png,9,232.8
|
||||
ion_233.13.png,8,233.13
|
||||
ion_233.78.png,5,233.78
|
||||
ion_234.1.png,1,234.1
|
||||
ion_234.75.png,5,234.75
|
||||
ion_235.08.png,6,235.08
|
||||
ion_235.73.png,7,235.73
|
||||
ion_236.05.png,1,236.05
|
||||
ion_236.7.png,5,236.7
|
||||
ion_237.03.png,8,237.03
|
||||
ion_237.68.png,0,237.68
|
||||
ion_238.0.png,1,238.0
|
||||
ion_238.65.png,0,238.65
|
||||
ion_238.98.png,1,238.98
|
||||
ion_239.95.png,1,239.95
|
||||
ion_240.6.png,0,240.6
|
||||
ion_240.93.png,1,240.93
|
||||
ion_241.9.png,3,241.9
|
||||
ion_242.23.png,3,242.23
|
||||
ion_242.88.png,1,242.88
|
||||
ion_243.21.png,8,243.21
|
||||
ion_243.86.png,9,243.86
|
||||
ion_244.18.png,1,244.18
|
||||
ion_244.83.png,9,244.83
|
||||
ion_245.16.png,1,245.16
|
||||
ion_245.81.png,5,245.81
|
||||
ion_246.13.png,1,246.13
|
||||
ion_246.78.png,7,246.78
|
||||
ion_247.11.png,1,247.11
|
||||
ion_247.76.png,0,247.76
|
||||
ion_248.08.png,3,248.08
|
||||
ion_248.73.png,7,248.73
|
||||
ion_249.06.png,1,249.06
|
||||
ion_249.71.png,7,249.71
|
||||
ion_250.03.png,3,250.03
|
||||
ion_250.68.png,0,250.68
|
||||
ion_251.01.png,1,251.01
|
||||
ion_251.98.png,3,251.98
|
||||
ion_252.63.png,0,252.63
|
||||
ion_252.96.png,1,252.96
|
||||
ion_253.93.png,9,253.93
|
||||
ion_254.91.png,3,254.91
|
||||
ion_255.23.png,1,255.23
|
||||
ion_255.88.png,3,255.88
|
||||
ion_256.21.png,1,256.21
|
||||
ion_256.86.png,5,256.86
|
||||
ion_257.19.png,1,257.19
|
||||
ion_257.84.png,7,257.84
|
||||
ion_258.16.png,3,258.16
|
||||
ion_258.81.png,9,258.81
|
||||
ion_259.14.png,1,259.14
|
||||
ion_259.79.png,5,259.79
|
||||
ion_260.11.png,3,260.11
|
||||
ion_260.76.png,7,260.76
|
||||
ion_261.09.png,1,261.09
|
||||
ion_261.74.png,7,261.74
|
||||
ion_262.06.png,3,262.06
|
||||
ion_262.71.png,7,262.71
|
||||
ion_263.04.png,1,263.04
|
||||
ion_263.69.png,0,263.69
|
||||
ion_264.01.png,9,264.01
|
||||
ion_264.99.png,3,264.99
|
||||
ion_265.96.png,3,265.96
|
||||
ion_266.94.png,3,266.94
|
||||
ion_267.91.png,5,267.91
|
||||
ion_268.89.png,9,268.89
|
||||
ion_269.21.png,1,269.21
|
||||
ion_269.87.png,7,269.87
|
||||
ion_270.19.png,3,270.19
|
||||
ion_270.84.png,5,270.84
|
||||
ion_271.17.png,3,271.17
|
||||
ion_271.82.png,7,271.82
|
||||
ion_272.14.png,3,272.14
|
||||
ion_272.79.png,7,272.79
|
||||
ion_273.12.png,3,273.12
|
||||
ion_273.77.png,7,273.77
|
||||
ion_274.09.png,3,274.09
|
||||
ion_274.74.png,0,274.74
|
||||
ion_275.07.png,3,275.07
|
||||
ion_276.04.png,3,276.04
|
||||
ion_277.02.png,9,277.02
|
||||
ion_277.99.png,9,277.99
|
||||
ion_278.32.png,9,278.32
|
||||
ion_278.64.png,1,278.64
|
||||
ion_278.97.png,2,278.97
|
||||
ion_279.29.png,6,279.29
|
||||
ion_279.62.png,5,279.62
|
||||
ion_279.94.png,1,279.94
|
||||
ion_280.92.png,9,280.92
|
||||
ion_281.89.png,5,281.89
|
||||
ion_282.87.png,5,282.87
|
||||
ion_283.85.png,7,283.85
|
||||
ion_284.82.png,7,284.82
|
||||
ion_285.15.png,3,285.15
|
||||
ion_285.8.png,5,285.8
|
||||
ion_286.12.png,1,286.12
|
||||
ion_286.77.png,5,286.77
|
||||
ion_287.1.png,3,287.1
|
||||
ion_287.75.png,0,287.75
|
||||
ion_288.07.png,3,288.07
|
||||
ion_289.05.png,3,289.05
|
||||
ion_290.02.png,9,290.02
|
||||
ion_291.0.png,5,291.0
|
||||
ion_291.97.png,5,291.97
|
||||
ion_292.95.png,5,292.95
|
||||
ion_293.92.png,7,293.92
|
||||
ion_294.9.png,5,294.9
|
||||
ion_295.87.png,9,295.87
|
||||
ion_296.85.png,5,296.85
|
||||
ion_297.83.png,7,297.83
|
||||
ion_298.15.png,9,298.15
|
||||
ion_298.8.png,7,298.8
|
||||
ion_299.13.png,3,299.13
|
||||
ion_301.08.png,5,301.08
|
||||
ion_302.05.png,5,302.05
|
||||
ion_303.03.png,5,303.03
|
||||
ion_303.68.png,0,303.68
|
||||
ion_304.0.png,3,304.0
|
||||
ion_304.33.png,3,304.33
|
||||
ion_304.98.png,9,304.98
|
||||
ion_305.95.png,9,305.95
|
||||
ion_306.28.png,9,306.28
|
||||
ion_306.93.png,7,306.93
|
||||
ion_307.9.png,7,307.9
|
||||
ion_308.88.png,7,308.88
|
||||
ion_309.85.png,0,309.85
|
||||
ion_310.83.png,7,310.83
|
||||
ion_311.16.png,3,311.16
|
||||
ion_312.78.png,7,312.78
|
||||
ion_313.11.png,8,313.11
|
||||
ion_314.08.png,9,314.08
|
||||
ion_315.06.png,9,315.06
|
||||
ion_318.96.png,5,318.96
|
||||
ion_326.76.png,5,326.76
|
||||
ion_328.71.png,9,328.71
|
||||
ion_329.04.png,1,329.04
|
||||
ion_329.69.png,0,329.69
|
||||
ion_330.01.png,3,330.01
|
||||
ion_330.99.png,9,330.99
|
||||
ion_334.89.png,7,334.89
|
||||
ion_335.21.png,1,335.21
|
||||
ion_336.84.png,0,336.84
|
||||
ion_337.17.png,1,337.17
|
||||
ion_338.14.png,5,338.14
|
||||
ion_339.12.png,5,339.12
|
||||
ion_351.8.png,5,351.8
|
||||
ion_352.12.png,3,352.12
|
||||
ion_353.75.png,7,353.75
|
||||
ion_354.07.png,3,354.07
|
||||
ion_355.05.png,1,355.05
|
||||
ion_370.33.png,5,370.33
|
||||
ion_370.65.png,8,370.65
|
||||
ion_370.98.png,4,370.98
|
||||
ion_371.3.png,1,371.3
|
||||
ion_371.63.png,1,371.63
|
||||
ion_371.95.png,4,371.95
|
||||
ion_372.6.png,7,372.6
|
||||
ion_372.93.png,1,372.93
|
||||
ion_384.63.png,9,384.63
|
||||
ion_387.56.png,5,387.56
|
||||
ion_390.81.png,3,390.81
|
||||
ion_391.14.png,1,391.14
|
||||
ion_401.54.png,5,401.54
|
||||
ion_401.86.png,3,401.86
|
||||
ion_461.69.png,9,461.69
|
||||
ion_59.51.png,5,59.51
|
||||
ion_59.84.png,2,59.84
|
||||
ion_60.16.png,4,60.16
|
||||
ion_60.81.png,8,60.81
|
||||
ion_64.71.png,3,64.71
|
||||
ion_66.66.png,3,66.66
|
||||
ion_66.99.png,2,66.99
|
||||
ion_67.64.png,9,67.64
|
||||
ion_67.96.png,2,67.96
|
||||
ion_68.62.png,8,68.62
|
||||
ion_68.94.png,2,68.94
|
||||
ion_69.27.png,1,69.27
|
||||
ion_69.59.png,6,69.59
|
||||
ion_69.92.png,2,69.92
|
||||
ion_70.24.png,8,70.24
|
||||
ion_70.57.png,0,70.57
|
||||
ion_70.89.png,2,70.89
|
||||
ion_71.54.png,8,71.54
|
||||
ion_71.87.png,2,71.87
|
||||
ion_72.19.png,6,72.19
|
||||
ion_72.52.png,0,72.52
|
||||
ion_72.84.png,2,72.84
|
||||
ion_73.49.png,0,73.49
|
||||
ion_73.82.png,2,73.82
|
||||
ion_74.14.png,2,74.14
|
||||
ion_74.79.png,1,74.79
|
||||
ion_76.74.png,4,76.74
|
||||
ion_77.72.png,8,77.72
|
||||
ion_78.69.png,3,78.69
|
||||
ion_79.02.png,2,79.02
|
||||
ion_79.34.png,0,79.34
|
||||
ion_79.67.png,2,79.67
|
||||
ion_79.99.png,2,79.99
|
||||
ion_80.32.png,5,80.32
|
||||
ion_80.64.png,4,80.64
|
||||
ion_80.97.png,2,80.97
|
||||
ion_81.62.png,1,81.62
|
||||
ion_81.95.png,2,81.95
|
||||
ion_82.27.png,3,82.27
|
||||
ion_82.6.png,8,82.6
|
||||
ion_82.92.png,2,82.92
|
||||
ion_83.25.png,3,83.25
|
||||
ion_83.57.png,2,83.57
|
||||
ion_83.9.png,2,83.9
|
||||
ion_84.22.png,8,84.22
|
||||
ion_84.55.png,7,84.55
|
||||
ion_84.87.png,2,84.87
|
||||
ion_85.52.png,7,85.52
|
||||
ion_85.85.png,2,85.85
|
||||
ion_86.17.png,2,86.17
|
||||
ion_86.5.png,0,86.5
|
||||
ion_86.82.png,2,86.82
|
||||
ion_87.47.png,0,87.47
|
||||
ion_87.8.png,2,87.8
|
||||
ion_88.12.png,2,88.12
|
||||
ion_88.77.png,3,88.77
|
||||
ion_89.75.png,8,89.75
|
||||
ion_90.07.png,4,90.07
|
||||
ion_90.72.png,4,90.72
|
||||
ion_91.05.png,2,91.05
|
||||
ion_91.7.png,0,91.7
|
||||
ion_92.67.png,3,92.67
|
||||
ion_93.0.png,2,93.0
|
||||
ion_93.32.png,7,93.32
|
||||
ion_93.65.png,4,93.65
|
||||
ion_93.97.png,2,93.97
|
||||
ion_94.3.png,8,94.3
|
||||
ion_94.62.png,6,94.62
|
||||
ion_94.95.png,2,94.95
|
||||
ion_95.28.png,8,95.28
|
||||
ion_95.6.png,1,95.6
|
||||
ion_95.93.png,2,95.93
|
||||
ion_96.58.png,1,96.58
|
||||
ion_96.9.png,2,96.9
|
||||
ion_97.23.png,1,97.23
|
||||
ion_97.55.png,3,97.55
|
||||
ion_97.88.png,2,97.88
|
||||
ion_98.2.png,2,98.2
|
||||
ion_98.53.png,0,98.53
|
||||
ion_98.85.png,2,98.85
|
||||
ion_99.5.png,0,99.5
|
||||
ion_99.83.png,2,99.83
|
||||
|
95
scripts_julia/graph_cluster_spectra.jl
Normal file
@ -0,0 +1,95 @@
|
||||
using Pkg
|
||||
Pkg.activate("/app/environment")
|
||||
|
||||
# Handle dynamic environment fallback
|
||||
if !haskey(Pkg.project().dependencies, "Plots")
|
||||
Pkg.add("Plots")
|
||||
end
|
||||
|
||||
ENV["GKSwstype"] = "100" # Headless rendering for Docker container
|
||||
using Plots
|
||||
|
||||
# Load framework core dependencies smoothly
|
||||
juliamsi_path = "/app/JuliaMSI"
|
||||
push!(LOAD_PATH, joinpath(juliamsi_path, "src"))
|
||||
include(joinpath(juliamsi_path, "src", "MSI_src.jl"))
|
||||
using .MSI_src
|
||||
|
||||
using CSV, DataFrames
|
||||
using Statistics
|
||||
using Plots.Measures
|
||||
|
||||
# 1. PATH CONFIGURATIONS
|
||||
data_path = "/app/data/Leaf.imzML"
|
||||
mask_path = "/app/data/region_of_interest.png"
|
||||
clusters_path = "/app/msi_clusters_results.csv"
|
||||
|
||||
println("Loading data arrays...")
|
||||
data = OpenMSIData(data_path)
|
||||
clusters_df = CSV.read(clusters_path, DataFrame)
|
||||
|
||||
# 2. RUN FRAMEWORK ANALYTICS TO DISCOVER PHYSICAL BOUNDARIES
|
||||
println("Syncing Dataset Precomputed Boundaries...")
|
||||
MSI_src.precompute_analytics(data)
|
||||
|
||||
# Extract binned spectrum arrays
|
||||
mzs_avg, ints_avg = MSI_src.get_average_spectrum(data, mask_path=mask_path)
|
||||
|
||||
# --- DYNAMIC AXIS SAFEGUARD ---
|
||||
# Query the atomic fields populated by precompute_analytics
|
||||
# fallback to standard values if the fields return uninitialized values
|
||||
min_vis_mz = data.global_min_mz[] > 0.0 ? data.global_min_mz[] : 50.0
|
||||
max_vis_mz = data.global_max_mz[] > 0.0 ? data.global_max_mz[] : 700.0
|
||||
|
||||
println(" Detected Dynamic Min m/z Limit: ", round(min_vis_mz, digits=2))
|
||||
println(" Detected Dynamic Max m/z Limit: ", round(max_vis_mz, digits=2))
|
||||
|
||||
# 3. Configure Base Plot Layer with Dynamic X-limits
|
||||
p = plot(mzs_avg, ints_avg,
|
||||
linecolor = :lightgray, # Light base so cluster sticks pop out visually
|
||||
linewidth = 0.7,
|
||||
label = "Average Spectrum (Reference)",
|
||||
xlabel = "m/z",
|
||||
ylabel = "Relative Intensity",
|
||||
title = "Cluster Mapping on Isotopic Pattern",
|
||||
size = (1200, 600),
|
||||
legend = :outerright,
|
||||
grid = false,
|
||||
framestyle = :box,
|
||||
xlims = (min_vis_mz, max_vis_mz), # Dynamically locks view strictly to valid mass range
|
||||
left_margin = 15mm,
|
||||
bottom_margin = 15mm
|
||||
)
|
||||
|
||||
# 4. Use the exact qualitative 10-color palette seen in your image
|
||||
cluster_colors = palette(:tab10)
|
||||
|
||||
# 5. Map Cluster Vectors to Binned Positions
|
||||
println("Mapping chemical clusters onto binned coordinates...")
|
||||
for cluster_id in sort(unique(clusters_df.cluster))
|
||||
sub_df = filter(row -> row.cluster == cluster_id, clusters_df)
|
||||
label_ready = false
|
||||
|
||||
for target_mz in sub_df.mz
|
||||
# Find the closest matching bin channel index in the 2000-bin array
|
||||
idx = argmin(abs.(mzs_avg .- target_mz))
|
||||
|
||||
m = mzs_avg[idx]
|
||||
i = ints_avg[idx]
|
||||
|
||||
# Draw the vertical peak highlighter over the base spectrum
|
||||
plot!(p, [m, m], [0, i],
|
||||
linecolor = cluster_colors[cluster_id + 1],
|
||||
linewidth = 1.2,
|
||||
alpha = 0.9,
|
||||
label = label_ready ? "" : "Cluster $cluster_id"
|
||||
)
|
||||
label_ready = true
|
||||
end
|
||||
end
|
||||
|
||||
# 6. Save assets to shared folder mount
|
||||
savefig("/app/espectro_msi_paper_style.png")
|
||||
println("Saved publication-ready plot: /app/espectro_msi_paper_style.png")
|
||||
savefig("/app/espectro_msi_clusters_julia.png")
|
||||
println("Saved backup copy: /app/espectro_msi_clusters_julia.png")
|
||||
119
scripts_julia/julia_msi_bridge.jl
Normal file
@ -0,0 +1,119 @@
|
||||
# BRIDGE SCRIPT: Connects JuliaMSI framework to the CNN Preprocessing pipeline.
|
||||
# VERSION (V3): Relying directly on precomputed analytics and multiple mz slices for max throughtput
|
||||
|
||||
using Pkg
|
||||
juliamsi_path = "/app/JuliaMSI"
|
||||
push!(LOAD_PATH, joinpath(juliamsi_path, "src"))
|
||||
|
||||
# Load JuliaMSI core
|
||||
include(joinpath(juliamsi_path, "src", "MSI_src.jl"))
|
||||
|
||||
using .MSI_src
|
||||
using Statistics, Images, Interpolations, BenchmarkTools
|
||||
|
||||
# CONFIGURATION
|
||||
data_path = "/app/data/Leaf.imzML"
|
||||
mask_path = "/app/data/region_of_interest.png"
|
||||
output_folder = "/app/datos_para_ai"
|
||||
img_size = (256, 256)
|
||||
tolerance = 0.05
|
||||
mkpath(output_folder)
|
||||
|
||||
println("--- JULIA BATCH ENGINE ---")
|
||||
|
||||
function run_production_pipeline()
|
||||
# 1. LOAD DATA
|
||||
println("Step 1: Ingesting Data...")
|
||||
data = OpenMSIData(data_path)
|
||||
|
||||
orig_height = data.image_dims[2]
|
||||
orig_width = data.image_dims[1]
|
||||
if orig_height < 256 || orig_width < 256
|
||||
final_img_size = (orig_height, orig_width)
|
||||
println(" [Notice] Original image is smaller than 256x256. Keeping original size: ", final_img_size)
|
||||
else
|
||||
final_img_size = (256, 256)
|
||||
println(" [Notice] Resizing down to target size: ", final_img_size)
|
||||
end
|
||||
|
||||
# 2. RUN FRAMEWORK ANALYTICS
|
||||
println("Step 2: Checking Dataset Precomputed Boundaries...")
|
||||
# This matches the framework's internal logic to safely discover the true boundaries.
|
||||
# It populates global_min_mz and global_max_mz by reading the binary headers properly.
|
||||
MSI_src.precompute_analytics(data)
|
||||
|
||||
# 3. EXTRACT AVERAGE SPECTRUM
|
||||
println("Step 3: Calculating ROI Average Spectrum...")
|
||||
mzs, avg_ints = MSI_src.get_average_spectrum(data, mask_path=mask_path)
|
||||
|
||||
println(" True Dataset Min m/z: ", round(minimum(mzs), digits=2))
|
||||
println(" True Dataset Max m/z: ", round(maximum(mzs), digits=2))
|
||||
|
||||
# 4. PEAK PICKING USING TRUE SPECTRA LIMITS
|
||||
# Identify relevant ion channels above a 2% intensity base peak threshold
|
||||
threshold = maximum(avg_ints) * 0.02
|
||||
peak_indices = findall(x -> x > threshold, avg_ints)
|
||||
|
||||
min_vis_mz = data.global_min_mz[] > 0.0 ? data.global_min_mz[] : 50.0
|
||||
max_vis_mz = data.global_max_mz[] > 0.0 ? data.global_max_mz[] : 1000.0
|
||||
|
||||
# Isolate targets that fall strictly within the standard CNN metabolic training window
|
||||
target_masses = Float64[]
|
||||
for idx in peak_indices
|
||||
mz_val = mzs[idx]
|
||||
if mz_val >= min_vis_mz && mz_val <= max_vis_mz
|
||||
push!(target_masses, mz_val)
|
||||
end
|
||||
end
|
||||
|
||||
total_ions = length(target_masses)
|
||||
println("Found $total_ions ions within training range ($(min_vis_mz) to $(max_vis_mz) m/z) above threshold.")
|
||||
if total_ions == 0
|
||||
println("Warning: No peaks met the criteria. Check your threshold or mask alignment.")
|
||||
return
|
||||
end
|
||||
|
||||
# 5. MULTI-CHANNEL BATCH EXTRACTION (Single pass over binary data)
|
||||
println("Step 4: Multi-channel Batch Extraction (Optimized I/O)...")
|
||||
slice_dict = MSI_src.get_multiple_mz_slices(data, target_masses, tolerance, mask_path=mask_path)
|
||||
|
||||
# 6. THREADED PROCESSING & EXPORT
|
||||
println("Step 5: Threaded TrIQ Normalization & Safe 256x256 Zero-Padding...")
|
||||
mask_matrix = MSI_src.load_and_prepare_mask(mask_path, (data.image_dims[1], data.image_dims[2]))
|
||||
exported_count = Threads.Atomic{Int}(0)
|
||||
|
||||
# Fixed target dimensions for the CNN
|
||||
target_size = (256, 256)
|
||||
|
||||
Threads.@threads for mz_val in target_masses
|
||||
slice = get(slice_dict, mz_val, nothing)
|
||||
if slice === nothing || all(iszero, slice) continue end
|
||||
|
||||
# 1. Framework-Native TrIQ (0.0 to 255.0 Scaling)
|
||||
slice_quant, _ = MSI_src.TrIQ(slice, 256, 0.98, mask_matrix=mask_matrix)
|
||||
|
||||
# 2. Allocate an empty 256x256 background matrix
|
||||
padded_matrix = zeros(Float32, target_size)
|
||||
|
||||
# 3. Compute structural centering offsets based on true resolution (161x106)
|
||||
h_orig, w_orig = size(slice_quant, 1), size(slice_quant, 2)
|
||||
offset_y = div(target_size[1] - h_orig, 2) + 1
|
||||
offset_x = div(target_size[2] - w_orig, 2) + 1
|
||||
|
||||
# 4. Map the pristine, raw pixels directly into the center
|
||||
padded_matrix[offset_y:offset_y+h_orig-1, offset_x:offset_x+w_orig-1] .= slice_quant
|
||||
|
||||
# 5. Convert safely to 0.0 - 1.0 float mapping for Gray image export
|
||||
normalized_gray = padded_matrix ./ 255.0
|
||||
|
||||
# 6. Save the unblurred, structurally perfect map
|
||||
filename = "ion_$(round(mz_val, digits=2)).png"
|
||||
save(joinpath(output_folder, filename), Gray.(normalized_gray))
|
||||
|
||||
Threads.atomic_add!(exported_count, 1)
|
||||
end
|
||||
|
||||
println("\nSuccess: $(exported_count[]) target image maps exported to $output_folder.")
|
||||
end
|
||||
|
||||
@time run_production_pipeline()
|
||||
64
scripts_julia/legacy/all_ions_mask_triq.jl
Normal file
@ -0,0 +1,64 @@
|
||||
using MSI_src
|
||||
using Statistics, Plots, Images
|
||||
|
||||
# 1. Definir función de contraste (si no la tienes ya en la sesión)
|
||||
function apply_triq(img_matrix)
|
||||
vals = filter(x -> x > 0 && isfinite(x), img_matrix)
|
||||
if isempty(vals) return img_matrix end
|
||||
q = quantile(vals, 0.98)
|
||||
if q == 0 q = maximum(vals) end
|
||||
return clamp.(img_matrix ./ q, 0.0, 1.0)
|
||||
end
|
||||
|
||||
# 2. Configuración de rutas
|
||||
path_data = "Hoja.imzML"
|
||||
path_mask = "region_interes.png"
|
||||
output_folder = "resultados_expertos/batch_triq"
|
||||
mkpath(output_folder)
|
||||
|
||||
# 3. Carga de datos
|
||||
data = OpenMSIData(path_data)
|
||||
|
||||
# 4. Obtener espectro promedio SOLO de la zona de la máscara (ROI)
|
||||
println("Calculando espectro promedio en la ROI...")
|
||||
# Nota: Usamos la función de MSI_src para obtener el promedio filtrado
|
||||
mzs, avg_ints = MSI_src.get_average_spectrum(data, mask_path=path_mask)
|
||||
|
||||
# 5. Identificar picos importantes (ej. > 2% del pico base)
|
||||
max_int = maximum(avg_ints)
|
||||
umbral = max_int * 0.02 # Ajusta este valor (0.01 para 1%, 0.05 para 5%)
|
||||
indices_picos = findall(x -> x > umbral, avg_ints)
|
||||
|
||||
println("Se encontraron $(length(indices_picos)) iones que superan el umbral.")
|
||||
|
||||
# 6. Bucle para generar imágenes con TrIQ
|
||||
for idx in indices_picos
|
||||
mz_val = mzs[idx]
|
||||
|
||||
# Validar rango (ejemplo: m/z 50 a 700)
|
||||
if mz_val < 50.0 || mz_val > 700.0
|
||||
continue
|
||||
end
|
||||
|
||||
println("Procesando m/z: $(round(mz_val, digits=2))")
|
||||
|
||||
# A. Extraer los datos crudos con la máscara aplicada
|
||||
raw_slice = MSI_src.get_mz_slice(data, mz_val, 0.05, mask_path=path_mask)
|
||||
|
||||
# B. Aplicar contraste TrIQ
|
||||
enhanced_slice = apply_triq(raw_slice)
|
||||
|
||||
# C. Crear el mapa de calor (Heatmap)
|
||||
p = heatmap(enhanced_slice,
|
||||
title="Ion m/z: $(round(mz_val, digits=2))",
|
||||
color=:viridis,
|
||||
aspect_ratio=:equal,
|
||||
yflip=true,
|
||||
xaxis=false, yaxis=false)
|
||||
|
||||
# D. Guardar
|
||||
nombre_archivo = "ion_$(round(mz_val, digits=2))_triq.png"
|
||||
savefig(p, joinpath(output_folder, nombre_archivo))
|
||||
end
|
||||
|
||||
println("--- Proceso completado. Revisa la carpeta: $output_folder ---")
|
||||
42
scripts_julia/legacy/prepare_data_simclr.jl
Normal file
@ -0,0 +1,42 @@
|
||||
using MSI_src
|
||||
using Statistics, Images, Interpolations
|
||||
|
||||
# 1. Configuration
|
||||
data_path = "Leaf.imzML"
|
||||
mask_path = "region_of_interest.png"
|
||||
output_folder = "datos_para_ai"
|
||||
img_size = (224, 224) # Standard size for CNN
|
||||
mkpath(output_folder)
|
||||
|
||||
# 2. Data loading
|
||||
data = OpenMSIData(data_path)
|
||||
|
||||
# 3. Get important peaks (you can use your previous logic)
|
||||
mzs, avg_ints = MSI_src.get_average_spectrum(data, mask_path=mask_path)
|
||||
threshold = maximum(avg_ints) * 0.02
|
||||
peak_indices = findall(x -> x > threshold, avg_ints)
|
||||
|
||||
println("Exporting $(length(peak_indices)) ions...")
|
||||
|
||||
for idx in peak_indices
|
||||
mz_val = mzs[idx]
|
||||
if mz_val < 50.0 || mz_val > 700.0 continue end
|
||||
|
||||
# A. Extract slice (raw, no plots)
|
||||
raw_slice = MSI_src.get_mz_slice(data, mz_val, 0.05, mask_path=mask_path)
|
||||
|
||||
# B. TrIQ Normalization (as in your script)
|
||||
vals = filter(x -> x > 0 && isfinite(x), raw_slice)
|
||||
if isempty(vals) continue end
|
||||
q = quantile(vals, 0.98)
|
||||
q = q == 0 ? maximum(vals) : q
|
||||
enhanced = clamp.(raw_slice ./ q, 0.0, 1.0)
|
||||
|
||||
# C. Rescale to 224x224 (Crucial for CNN)
|
||||
# We use imresize from Images.jl
|
||||
img_resized = imresize(enhanced, img_size)
|
||||
|
||||
# D. Save as raw PNG (Grayscale, no axes)
|
||||
filename = "ion_$(round(mz_val, digits=2)).png"
|
||||
save(joinpath(output_folder, filename), Gray.(img_resized))
|
||||
end
|
||||
203
scripts_python/CNN_proof_1.py
Normal file
@ -0,0 +1,203 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import torch.nn.functional as F
|
||||
import numpy as np
|
||||
import random
|
||||
import time
|
||||
import psutil
|
||||
|
||||
def set_seed(seed=42):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
|
||||
set_seed(42)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai")
|
||||
MODEL_SAVE_PATH = os.path.join(BASE_DIR, "..", "models", "msi_encoder_trained.pth")
|
||||
|
||||
# VRAM OPTIMIZATION ENGINE
|
||||
BATCH_SIZE = 8 # Physical batch size (keeps VRAM safely under 3.5 GB)
|
||||
ACCUMULATION_STEPS = 4 # 8 x 4 = Effective Batch Size of 32!
|
||||
EPOCHS = 60 # Slightly extended to give the larger batch size room to converge
|
||||
LEARNING_RATE = 0.0003 # Tuned for an effective batch size of 32
|
||||
TEMPERATURE = 0.5
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
class MSIAugmentations:
|
||||
def __init__(self, size=256): # Enforce 256 dimension consistency
|
||||
self.spatial_transform = transforms.Compose([
|
||||
transforms.Resize((size, size)),
|
||||
transforms.ToTensor()
|
||||
])
|
||||
|
||||
def apply_msi_chemical_noise(self, tensor):
|
||||
x = tensor.clone()
|
||||
|
||||
# 1. Intensity Scaling (Global Concentration Shifts)
|
||||
scale_factor = random.uniform(0.8, 1.2)
|
||||
x = x * scale_factor
|
||||
|
||||
# 2. Poisson Noise Simulation (Shot Noise)
|
||||
# Scaled to avoid breaking the 0.0 - 1.0 float bounds
|
||||
vals = len(torch.unique(x))
|
||||
vals = 2 ** np.ceil(np.log2(vals)) if vals > 0 else 32
|
||||
# Add soft, random Gaussian-Poisson approximation
|
||||
noise = torch.randn_like(x) * (torch.sqrt(x + 1e-5) / float(vals))
|
||||
x = x + noise
|
||||
|
||||
# 3. Random Missing Values (Standard Dropout - e.g., 2% pixels lost)
|
||||
random_dropout_mask = (torch.rand_like(x) > 0.02).float()
|
||||
x = x * random_dropout_mask
|
||||
|
||||
# 4. Intensity-Dependent Missing Values (Low-Signal Clipping)
|
||||
# Pixels with very low values have a higher chance of dropping out completely
|
||||
low_signal_mask = torch.rand_like(x)
|
||||
# Drop threshold scales inversely with intensity: lower values drop more easily
|
||||
drop_threshold = 0.15 * (1.0 - x)
|
||||
x = torch.where(low_signal_mask > drop_threshold, x, torch.zeros_like(x))
|
||||
|
||||
return torch.clamp(x, 0.0, 1.0)
|
||||
|
||||
def __call__(self, x):
|
||||
base_tensor = self.spatial_transform(x)
|
||||
# Create two distinct chemical representations of the same underlying coordinate geometry
|
||||
return self.apply_msi_chemical_noise(base_tensor), self.apply_msi_chemical_noise(base_tensor)
|
||||
|
||||
class SimCLRMSIDataset(Dataset):
|
||||
def __init__(self, img_dir, transform=None):
|
||||
self.img_dir = img_dir
|
||||
self.img_names = [f for f in os.listdir(img_dir) if f.endswith('.png')]
|
||||
self.transform = transform
|
||||
|
||||
def __len__(self):
|
||||
return len(self.img_names)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
img_path = os.path.join(self.img_dir, self.img_names[idx])
|
||||
image = Image.open(img_path).convert("L")
|
||||
if self.transform:
|
||||
xi, xj = self.transform(image)
|
||||
return xi, xj
|
||||
|
||||
class MSI_Structural_Encoder(nn.Module):
|
||||
def __init__(self, projection_dim=128):
|
||||
super(MSI_Structural_Encoder, self).__init__()
|
||||
|
||||
# High-Fidelity Custom Encoder for Padded Data
|
||||
self.features = nn.Sequential(
|
||||
# Input: 256x256x1 (Resize in transform)
|
||||
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
||||
nn.GroupNorm(4, 32), # Replaces BatchNorm for perfect stability at batch size = 8
|
||||
nn.ReLU(),
|
||||
|
||||
# 112x112 -> 56x56
|
||||
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(8, 64),
|
||||
nn.ReLU(),
|
||||
|
||||
# 56x56 -> 28x28 (Captures true raw pixel leaf structures)
|
||||
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(16, 128),
|
||||
nn.ReLU(),
|
||||
|
||||
# 28x28 -> 14x14
|
||||
nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(32, 256),
|
||||
nn.ReLU(),
|
||||
|
||||
nn.AdaptiveAvgPool2d((1, 1)) # Yields a clean 256-dimensional vector
|
||||
)
|
||||
|
||||
self.projector = nn.Sequential(
|
||||
nn.Linear(256, 128),
|
||||
nn.ReLU(),
|
||||
nn.Linear(128, projection_dim)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
h = self.features(x).squeeze(-1).squeeze(-1)
|
||||
h_norm = F.normalize(h, p=2, dim=1) # Projects onto the unit hypersphere
|
||||
z = self.projector(h_norm)
|
||||
return h, z
|
||||
|
||||
def nt_xent_loss(z1, z2, temperature=0.5):
|
||||
batch_size = z1.shape[0]
|
||||
z = torch.cat([z1, z2], dim=0)
|
||||
z = F.normalize(z, p=2, dim=1)
|
||||
|
||||
sim_matrix = torch.mm(z, z.t()) / temperature
|
||||
mask = torch.eye(2 * batch_size, device=z.device).bool()
|
||||
sim_matrix = sim_matrix.masked_fill(mask, -9e15)
|
||||
|
||||
targets = torch.arange(2 * batch_size, device=z.device)
|
||||
targets[:batch_size] += batch_size
|
||||
targets[batch_size:] -= batch_size
|
||||
|
||||
return F.cross_entropy(sim_matrix, targets)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Launching VRAM-Optimized Contrastive Pipeline on: {DEVICE}")
|
||||
print(f"Physical Batch Size: {BATCH_SIZE} | Accumulation Steps: {ACCUMULATION_STEPS} | Effective Batch Size: {BATCH_SIZE * ACCUMULATION_STEPS}")
|
||||
os.makedirs(os.path.dirname(MODEL_SAVE_PATH), exist_ok=True)
|
||||
|
||||
aug_pipeline = MSIAugmentations(size=256)
|
||||
dataset = SimCLRMSIDataset(IMG_DIR, transform=aug_pipeline)
|
||||
loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, drop_last=True, num_workers=2, pin_memory=True)
|
||||
|
||||
model = MSI_Structural_Encoder().to(DEVICE)
|
||||
optimizer = optim.AdamW(model.parameters(), lr=LEARNING_RATE, weight_decay=1e-4)
|
||||
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-6)
|
||||
|
||||
process = psutil.Process(os.getpid())
|
||||
|
||||
# Environment tweak to combat memory fragmentation
|
||||
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
for epoch in range(EPOCHS):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
epoch_start = time.time()
|
||||
|
||||
optimizer.zero_grad() # Initialize gradients outside the accumulation loop
|
||||
|
||||
for batch_idx, (xi, xj) in enumerate(loader):
|
||||
xi, xj = xi.to(DEVICE), xj.to(DEVICE)
|
||||
|
||||
_, z1 = model(xi)
|
||||
_, z2 = model(xj)
|
||||
|
||||
# Scale loss by accumulation steps to ensure correct gradient weighting
|
||||
loss = nt_xent_loss(z1, z2, temperature=TEMPERATURE) / ACCUMULATION_STEPS
|
||||
loss.backward()
|
||||
|
||||
total_loss += loss.item() * ACCUMULATION_STEPS
|
||||
|
||||
# Step the optimizer only after gathering enough gradients
|
||||
if (batch_idx + 1) % ACCUMULATION_STEPS == 0 or (batch_idx + 1) == len(loader):
|
||||
optimizer.step()
|
||||
optimizer.zero_grad()
|
||||
|
||||
scheduler.step()
|
||||
|
||||
# Periodic cache flushing keeps VRAM overhead low
|
||||
if epoch % 5 == 0:
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
print(f"Epoch [{epoch+1}/{EPOCHS}], Loss: {total_loss / len(loader):.4f}, Time: {time.time()-epoch_start:.2f}s, RAM Used: {process.memory_info().rss / 1024 / 1024:.1f} MB")
|
||||
|
||||
print(f"--> Exporting optimized 32-BS encoder weights to: {MODEL_SAVE_PATH}")
|
||||
torch.save(model.features.state_dict(), MODEL_SAVE_PATH)
|
||||
print(f"Model saved to: {MODEL_SAVE_PATH}")
|
||||
156
scripts_python/evaluate_clustering.py
Normal file
@ -0,0 +1,156 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import random
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.decomposition import PCA
|
||||
from sklearn.metrics import silhouette_score, davies_bouldin_score, calinski_harabasz_score
|
||||
import matplotlib.pyplot as plt
|
||||
import torch.nn.functional as F
|
||||
|
||||
# 0. REPRODUCIBILITY
|
||||
def set_seed(seed=42):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
|
||||
set_seed(42)
|
||||
|
||||
# CONFIGURATION
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai")
|
||||
MODEL_PATH = os.path.join(BASE_DIR, "..", "models", "msi_encoder_trained.pth")
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
K_RANGE = range(2, 21) # Testing from 2 to 20 clusters
|
||||
|
||||
class MSI_Structural_Encoder(nn.Module):
|
||||
def __init__(self):
|
||||
super(MSI_Structural_Encoder, self).__init__()
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
||||
nn.GroupNorm(4, 32),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(8, 64),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(16, 128),
|
||||
nn.ReLU(),
|
||||
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):
|
||||
return self.features(x).squeeze(-1).squeeze(-1)
|
||||
|
||||
# 1. LOAD MODEL AND EXTRACT FEATURES
|
||||
def extract_features():
|
||||
model = MSI_Structural_Encoder()
|
||||
if not os.path.exists(MODEL_PATH):
|
||||
raise FileNotFoundError(f"No se encontró el modelo en {MODEL_PATH}")
|
||||
model.features.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
|
||||
transform = transforms.Compose([
|
||||
transforms.Resize((256, 256)),
|
||||
transforms.ToTensor(),
|
||||
])
|
||||
|
||||
features = []
|
||||
if not os.path.exists(IMG_DIR):
|
||||
raise FileNotFoundError(f"No se encontró el directorio de imágenes en {IMG_DIR}")
|
||||
img_names = sorted([f for f in os.listdir(IMG_DIR) if f.endswith('.png')])
|
||||
|
||||
print(f"Extracting features from {len(img_names)} images on {DEVICE}...")
|
||||
with torch.no_grad():
|
||||
for i, name in enumerate(img_names):
|
||||
if i % 100 == 0: print(f"Processing image {i}/{len(img_names)}...")
|
||||
img_path = os.path.join(IMG_DIR, name)
|
||||
img = Image.open(img_path).convert("L")
|
||||
img_t = transform(img).unsqueeze(0).to(DEVICE)
|
||||
feat = model(img_t)
|
||||
# CRITICAL FIX: Project explicitly onto the unit hypersphere
|
||||
feat_norm = F.normalize(feat, p=2, dim=1)
|
||||
features.append(feat_norm.cpu().numpy().flatten())
|
||||
|
||||
return np.array(features)
|
||||
|
||||
# 2. EVALUATE K-MEANS
|
||||
if __name__ == "__main__":
|
||||
X = extract_features()
|
||||
|
||||
pca = PCA(n_components=min(50, X.shape[1]), random_state=42)
|
||||
X_pca = pca.fit_transform(X)
|
||||
# CRITICAL FIX: Re-normalize after PCA to restore unit hypersphere geometry for Spherical K-Means equivalent
|
||||
X_pca = X_pca / np.linalg.norm(X_pca, axis=1, keepdims=True)
|
||||
|
||||
inertias = []
|
||||
silhouettes = []
|
||||
davies_bouldin = []
|
||||
calinski_harabasz = []
|
||||
|
||||
print("Evaluating different K values...")
|
||||
for k in K_RANGE:
|
||||
print(f"Calculating for K={k}...")
|
||||
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
|
||||
labels = kmeans.fit_predict(X_pca)
|
||||
|
||||
inertias.append(kmeans.inertia_)
|
||||
# CRITICAL FIX: Evaluate using cosine distance on the un-reduced features
|
||||
silhouettes.append(silhouette_score(X, labels, metric='cosine'))
|
||||
davies_bouldin.append(davies_bouldin_score(X_pca, labels))
|
||||
calinski_harabasz.append(calinski_harabasz_score(X_pca, labels))
|
||||
|
||||
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
|
||||
|
||||
ax1.plot(K_RANGE, inertias, 'o-', color='tab:red')
|
||||
ax1.set_title('Elbow Method (Inertia)')
|
||||
ax1.set_xlabel('Number of Clusters (k)')
|
||||
ax1.set_ylabel('Sum of Squared Errors')
|
||||
ax1.grid(True)
|
||||
|
||||
ax2.plot(K_RANGE, silhouettes, 's-', color='tab:blue')
|
||||
ax2.set_title('Silhouette Score (Higher is better)')
|
||||
ax2.set_xlabel('Number of Clusters (k)')
|
||||
ax2.set_ylabel('Score')
|
||||
ax2.grid(True)
|
||||
|
||||
ax3.plot(K_RANGE, davies_bouldin, '^-', color='tab:green')
|
||||
ax3.set_title('Davies-Bouldin Index (Lower is better)')
|
||||
ax3.set_xlabel('Number of Clusters (k)')
|
||||
ax3.set_ylabel('Score')
|
||||
ax3.grid(True)
|
||||
|
||||
ax4.plot(K_RANGE, calinski_harabasz, 'd-', color='tab:orange')
|
||||
ax4.set_title('Calinski-Harabasz Index (Higher is better)')
|
||||
ax4.set_xlabel('Number of Clusters (k)')
|
||||
ax4.set_ylabel('Score')
|
||||
ax4.grid(True)
|
||||
|
||||
plt.tight_layout()
|
||||
output_plot = os.path.join(BASE_DIR, "..", "figures", "cluster_validation_detailed_plot.png")
|
||||
os.makedirs(os.path.dirname(output_plot), exist_ok=True)
|
||||
plt.savefig(output_plot)
|
||||
print(f"Plot saved as '{output_plot}'")
|
||||
|
||||
idx_10 = list(K_RANGE).index(10)
|
||||
print(f"\n--- RESULTS FOR K=10 ---")
|
||||
print(f"Silhouette Score: {silhouettes[idx_10]:.4f}")
|
||||
print(f"Davies-Bouldin Index: {davies_bouldin[idx_10]:.4f}")
|
||||
print(f"Calinski-Harabasz Index: {calinski_harabasz[idx_10]:.4f}")
|
||||
|
||||
print(f"\n--- MATHEMATICAL SUGGESTIONS ---")
|
||||
print(f"Best K (Silhouette): {K_RANGE[np.argmax(silhouettes)]}")
|
||||
print(f"Best K (Davies-Bouldin): {K_RANGE[np.argmin(davies_bouldin)]}")
|
||||
print(f"Best K (Calinski-Harabasz): {K_RANGE[np.argmax(calinski_harabasz)]}")
|
||||
182
scripts_python/graficar_tsne_msi.py
Normal file
@ -0,0 +1,182 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import random
|
||||
from sklearn.decomposition import PCA
|
||||
from sklearn.manifold import TSNE
|
||||
from sklearn.cluster import KMeans
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
import torch.nn.functional as F
|
||||
import plotly.express as px
|
||||
|
||||
def set_seed(seed=42):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
|
||||
set_seed(42)
|
||||
|
||||
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")
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
TARGET_K = 10 # Enforce your target 10 metabolic domains
|
||||
|
||||
class MSI_Structural_Encoder(nn.Module):
|
||||
def __init__(self):
|
||||
super(MSI_Structural_Encoder, self).__init__()
|
||||
# Matches the high-fidelity GroupNorm architecture
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
||||
nn.GroupNorm(4, 32),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(8, 64),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(16, 128),
|
||||
nn.ReLU(),
|
||||
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()
|
||||
# Loading only the feature extractor weights
|
||||
model.features.load_state_dict(torch.load(path, map_location=DEVICE))
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
class MSIDatasetInference(Dataset):
|
||||
def __init__(self, img_dir):
|
||||
self.img_dir = img_dir
|
||||
self.img_names = sorted([f for f in os.listdir(img_dir) if f.endswith('.png')])
|
||||
self.transform = transforms.Compose([
|
||||
transforms.Resize((256, 256)),
|
||||
transforms.ToTensor()
|
||||
])
|
||||
|
||||
def __len__(self):
|
||||
return len(self.img_names)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
img_path = os.path.join(self.img_dir, self.img_names[idx])
|
||||
image = Image.open(img_path).convert("L")
|
||||
return self.transform(image), self.img_names[idx]
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Loading specialized custom MSI Structural Encoder...")
|
||||
encoder = load_trained_encoder(ENCODER_PATH)
|
||||
dataset = MSIDatasetInference(IMG_DIR)
|
||||
loader = DataLoader(dataset, batch_size=1, shuffle=False)
|
||||
|
||||
features = []
|
||||
ion_names = []
|
||||
|
||||
print("Extracting and L2-normalizing features from custom manifold...")
|
||||
with torch.no_grad():
|
||||
for img, name in loader:
|
||||
img = img.to(DEVICE)
|
||||
feat = encoder(img)
|
||||
# Re-enforce unit hypersphere mapping
|
||||
feat = F.normalize(feat, p=2, dim=1)
|
||||
features.append(feat.cpu().numpy().flatten())
|
||||
ion_names.append(name[0])
|
||||
|
||||
features = np.array(features)
|
||||
|
||||
# Dimensionality Reduction Optimization
|
||||
pca_km = PCA(n_components=min(50, len(features)), random_state=42)
|
||||
features_pca_all = pca_km.fit_transform(features)
|
||||
|
||||
# FIX: Slice out the first principal component (index 0)
|
||||
# if it continues to hold the global brightness/intensity bias
|
||||
features_pca_km = features_pca_all[:, 1:]
|
||||
|
||||
# CRITICAL FIX: Re-normalize strictly for K-Means to ensure objective Cosine geometry
|
||||
features_pca_km_norm = features_pca_km / np.linalg.norm(features_pca_km, axis=1, keepdims=True)
|
||||
|
||||
# ENFORCE EXACTLY 10 METABOLIC DOMAINS
|
||||
print(f"Executing K-Means clustering strictly constrained to K={TARGET_K}...")
|
||||
clusterer = KMeans(n_clusters=TARGET_K, init='k-means++', n_init=20, random_state=42)
|
||||
labels = clusterer.fit_predict(features_pca_km_norm)
|
||||
|
||||
print("Projecting t-SNE map...")
|
||||
tsne = TSNE(
|
||||
n_components=2,
|
||||
perplexity=15,
|
||||
early_exaggeration=24.0, # Boosted significantly to force tighter, more definitive visual clusters
|
||||
learning_rate='auto',
|
||||
init='pca',
|
||||
random_state=42
|
||||
)
|
||||
# Feed the unnormalized PCA vectors to t-SNE to allow intensity variance to separate the visual blobs
|
||||
embeddings_2d = tsne.fit_transform(features_pca_km)
|
||||
|
||||
# Plotting
|
||||
plt.figure(figsize=(10, 8))
|
||||
scatter = plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1], c=labels, cmap='tab10', alpha=0.9, edgecolors='none', s=45)
|
||||
cbar = plt.colorbar(scatter, ticks=range(TARGET_K))
|
||||
cbar.set_label('Metabolic Domain ID (K=10)')
|
||||
|
||||
plt.title("Objectively Segmented Leaf Manifold (Custom Encoder)")
|
||||
plt.xlabel("t-SNE Dimension 1")
|
||||
plt.ylabel("t-SNE Dimension 2")
|
||||
plt.grid(True, linestyle='--', alpha=0.3)
|
||||
|
||||
plt.savefig(os.path.join(BASE_DIR, "..", "msi_tsne_corregido.png"), dpi=300, bbox_inches='tight')
|
||||
plt.close()
|
||||
|
||||
# INTERACTIVE PLOTLY EXPORT
|
||||
print("Exporting Interactive Plotly t-SNE...")
|
||||
mz_values = [float(n.replace("ion_", "").replace(".png", "")) for n in ion_names]
|
||||
df_plot = pd.DataFrame({
|
||||
't-SNE Dimension 1': embeddings_2d[:, 0],
|
||||
't-SNE Dimension 2': embeddings_2d[:, 1],
|
||||
'Cluster': [str(lbl) for lbl in labels],
|
||||
'm/z': mz_values,
|
||||
'Filename': ion_names
|
||||
})
|
||||
|
||||
fig = px.scatter(
|
||||
df_plot,
|
||||
x='t-SNE Dimension 1',
|
||||
y='t-SNE Dimension 2',
|
||||
color='Cluster',
|
||||
hover_data=['m/z', 'Filename'],
|
||||
title="Interactive Segmented Leaf Manifold (Custom Encoder)",
|
||||
category_orders={"Cluster": [str(i) for i in range(TARGET_K)]}
|
||||
)
|
||||
|
||||
plotly_out = os.path.join(BASE_DIR, "..", "figures", "msi_tsne_interactive.html")
|
||||
os.makedirs(os.path.dirname(plotly_out), exist_ok=True)
|
||||
fig.write_html(plotly_out)
|
||||
print(f"Interactive t-SNE saved to: {plotly_out}")
|
||||
|
||||
# Save outputs
|
||||
print("Exporting synchronized cluster assignments...")
|
||||
df_results = pd.DataFrame({'ion_image': ion_names, 'cluster': labels})
|
||||
df_results['mz'] = df_results['ion_image'].str.extract(r'(\d+\.\d+)').astype(float)
|
||||
df_results.to_csv(os.path.join(BASE_DIR, "..", "msi_clusters_results.csv"), index=False)
|
||||
|
||||
print("Process complete.")
|
||||
195
scripts_python/guided_vascular.py
Normal file
@ -0,0 +1,195 @@
|
||||
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.")
|
||||
116
scripts_python/legacy/cluster_msi_direct.py
Normal file
@ -0,0 +1,116 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.decomposition import PCA
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.manifold import TSNE
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# 1. CONFIGURACIÓN
|
||||
IMG_DIR = "datos_para_ai"
|
||||
OUTPUT_DIR = "pruebas_clusters"
|
||||
N_CLUSTERS = 10
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# 2. CARGAR MODELO PRE-ENTRENADO (Directo de PyTorch)
|
||||
def get_pretrained_extractor():
|
||||
# Usamos los pesos por defecto de ImageNet
|
||||
model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT)
|
||||
model.classifier = nn.Identity() # Quitamos la capa de clasificación
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
# 3. DATASET CON NORMALIZACIÓN ESTÁNDAR
|
||||
class MSIDatasetDirect(Dataset):
|
||||
def __init__(self, img_dir):
|
||||
self.img_dir = img_dir
|
||||
self.img_names = sorted([f for f in os.listdir(img_dir) if f.endswith('.png')])
|
||||
self.transform = transforms.Compose([
|
||||
transforms.Resize((224, 224)),
|
||||
transforms.ToTensor(),
|
||||
# Normalización obligatoria para modelos pre-entrenados
|
||||
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
||||
])
|
||||
|
||||
def __len__(self):
|
||||
return len(self.img_names)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
img_path = os.path.join(self.img_dir, self.img_names[idx])
|
||||
image = Image.open(img_path).convert("RGB")
|
||||
return self.transform(image), self.img_names[idx]
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Extrayendo características con red pre-entrenada en {DEVICE}...")
|
||||
model = get_pretrained_extractor()
|
||||
dataset = MSIDatasetDirect(IMG_DIR)
|
||||
loader = DataLoader(dataset, batch_size=1, shuffle=False)
|
||||
|
||||
features = []
|
||||
ion_names = []
|
||||
|
||||
with torch.no_grad():
|
||||
for img, name in loader:
|
||||
img = img.to(DEVICE)
|
||||
feat = model(img)
|
||||
# Aplanar el vector de características (1280 dimensiones para EffNet-B0)
|
||||
features.append(feat.cpu().numpy().flatten())
|
||||
ion_names.append(name[0])
|
||||
|
||||
features = np.array(features)
|
||||
print(f"Características extraídas: {features.shape}")
|
||||
|
||||
# 4. REDUCCIÓN DE DIMENSIONALIDAD (PCA)
|
||||
# Esto ayuda a que el clustering no se pierda en dimensiones irrelevantes
|
||||
print("Reduciendo ruido con PCA...")
|
||||
features_scaled = StandardScaler().fit_transform(features)
|
||||
pca = PCA(n_components=0.95) # Mantenemos el 95% de la varianza
|
||||
features_pca = pca.fit_transform(features_scaled)
|
||||
print(f"Dimensiones tras PCA: {features_pca.shape[1]}")
|
||||
|
||||
# 5. CLUSTERING K-MEANS
|
||||
print(f"Agrupando en {N_CLUSTERS} clusters...")
|
||||
km = KMeans(n_clusters=N_CLUSTERS, n_init=20, random_state=42)
|
||||
labels = km.fit_predict(features_pca)
|
||||
|
||||
# 6. VISUALIZACIÓN (t-SNE)
|
||||
print("Generando visualización t-SNE para verificar consistencia...")
|
||||
tsne = TSNE(n_components=2, random_state=42)
|
||||
features_tsne = tsne.fit_transform(features_pca)
|
||||
|
||||
plt.figure(figsize=(10, 8))
|
||||
scatter = plt.scatter(features_tsne[:, 0], features_tsne[:, 1], c=labels, cmap='tab10', alpha=0.7)
|
||||
plt.colorbar(scatter, ticks=range(N_CLUSTERS), label='Cluster ID')
|
||||
plt.title(f"Visualización de Clusters (K={N_CLUSTERS}) - Directo")
|
||||
plt.xlabel("t-SNE 1")
|
||||
plt.ylabel("t-SNE 2")
|
||||
plt.grid(True, linestyle='--', alpha=0.5)
|
||||
plt.savefig(os.path.join(OUTPUT_DIR, "msi_clusters_direct_plot.png"))
|
||||
print(f"Gráfico de consistencia guardado en '{OUTPUT_DIR}/msi_clusters_direct_plot.png'.")
|
||||
|
||||
# 7. GUARDAR Y REPORTAR
|
||||
df = pd.DataFrame({'ion_image': ion_names, 'cluster': labels})
|
||||
df['mz'] = df['ion_image'].str.extract(r'(\d+\.\d+)').astype(float)
|
||||
df.to_csv(os.path.join(OUTPUT_DIR, "msi_clusters_direct_results.csv"), index=False)
|
||||
|
||||
# Crear un resumen de cuántos iones hay por cluster
|
||||
print("\nResumen de Clusters (Consistente con el CSV y el Gráfico):")
|
||||
counts = df['cluster'].value_counts().sort_index()
|
||||
print(counts)
|
||||
|
||||
# Guardar también el resumen para evitar confusiones con otros archivos
|
||||
summary_df = counts.reset_index()
|
||||
summary_df.columns = ['Cluster_ID', 'Ion_Count']
|
||||
summary_df.to_csv(os.path.join(OUTPUT_DIR, "msi_clusters_direct_summary.csv"), index=False)
|
||||
|
||||
print(f"\nResultados guardados en '{OUTPUT_DIR}/msi_clusters_direct_results.csv' y '{OUTPUT_DIR}/msi_clusters_direct_summary.csv'.")
|
||||
print(f"Usa '{OUTPUT_DIR}/msi_clusters_direct_plot.png' para validar visualmente los puntos.")
|
||||
109
scripts_python/legacy/graficar_tsne_msi_old.py
Normal file
@ -0,0 +1,109 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.manifold import TSNE
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
|
||||
# 1. CONFIGURACIÓN
|
||||
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")
|
||||
N_CLUSTERS = 10
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
# 2. CARGAR EL MODELO ENTRENADO
|
||||
def load_trained_encoder(path):
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"No se encontró el modelo en {path}")
|
||||
|
||||
model = models.efficientnet_b0()
|
||||
model.classifier = nn.Identity()
|
||||
|
||||
state_dict = torch.load(path, map_location=DEVICE)
|
||||
model.load_state_dict(state_dict)
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
# 3. DATASET PARA INFERENCIA
|
||||
class MSIDatasetInference(Dataset):
|
||||
def __init__(self, img_dir):
|
||||
self.img_dir = img_dir
|
||||
self.img_names = sorted([f for f in os.listdir(img_dir) if f.endswith('.png')])
|
||||
self.transform = transforms.Compose([
|
||||
transforms.Resize((224, 224)),
|
||||
transforms.ToTensor(),
|
||||
])
|
||||
|
||||
def __len__(self):
|
||||
return len(self.img_names)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
img_path = os.path.join(self.img_dir, self.img_names[idx])
|
||||
image = Image.open(img_path).convert("RGB")
|
||||
return self.transform(image), self.img_names[idx]
|
||||
|
||||
# --- PROCESO ---
|
||||
if __name__ == "__main__":
|
||||
print("Extrayendo características con el encoder...")
|
||||
try:
|
||||
encoder = load_trained_encoder(ENCODER_PATH)
|
||||
except Exception as e:
|
||||
print(f"Error al cargar el modelo: {e}")
|
||||
exit(1)
|
||||
|
||||
dataset = MSIDatasetInference(IMG_DIR)
|
||||
if len(dataset) == 0:
|
||||
print(f"No se encontraron imágenes en {IMG_DIR}")
|
||||
exit(1)
|
||||
|
||||
loader = DataLoader(dataset, batch_size=1, shuffle=False)
|
||||
|
||||
features = []
|
||||
ion_names = []
|
||||
|
||||
with torch.no_grad():
|
||||
for img, name in loader:
|
||||
img = img.to(DEVICE)
|
||||
feat = encoder(img)
|
||||
# Normalización L2
|
||||
feat = feat / feat.norm(p=2, dim=1, keepdim=True)
|
||||
features.append(feat.cpu().numpy().flatten())
|
||||
ion_names.append(name[0])
|
||||
|
||||
features = np.array(features)
|
||||
|
||||
# 4. CLUSTERING K-MEANS
|
||||
print(f"Realizando KMeans con {N_CLUSTERS} clusters...")
|
||||
km = KMeans(n_clusters=N_CLUSTERS, n_init=20, random_state=42)
|
||||
labels = km.fit_predict(features)
|
||||
|
||||
# 5. VISUALIZACIÓN t-SNE CORREGIDA
|
||||
print("Generando visualización t-SNE con escala corregida...")
|
||||
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
|
||||
embeddings_2d = tsne.fit_transform(features)
|
||||
|
||||
plt.figure(figsize=(10, 8))
|
||||
# Aplicando tu corrección específica:
|
||||
scatter = plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1], c=labels, cmap='tab10', alpha=0.8, edgecolors='w', s=50)
|
||||
|
||||
# Configuración de la barra de colores con los ticks exactos para 10 clusters
|
||||
cbar = plt.colorbar(scatter, ticks=range(N_CLUSTERS))
|
||||
cbar.set_label('Cluster ID')
|
||||
|
||||
plt.title(f"t-SNE Visualization of MSI Ions (K={N_CLUSTERS})")
|
||||
plt.xlabel("t-SNE dimension 1")
|
||||
plt.ylabel("t-SNE dimension 2")
|
||||
plt.grid(True, linestyle='--', alpha=0.5)
|
||||
|
||||
output_file = "msi_tsne_corregido.png"
|
||||
plt.savefig(output_file, dpi=300, bbox_inches='tight')
|
||||
# plt.show()
|
||||
|
||||
print(f"Visualización guardada exitosamente en '{output_file}'.")
|
||||
117
scripts_python/legacy/graficar_tsne_plotly.py
Normal file
@ -0,0 +1,117 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from sklearn.manifold import TSNE
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
|
||||
# 1. CONFIGURACIÓN
|
||||
IMG_DIR = "/home/sierra/Documentos/Prueba teams/Julia_msi_GUI-main/datos_para_ai"
|
||||
ENCODER_PATH = "Reporte/Model/msi_encoder_trained.pth"
|
||||
CSV_PATH = "msi_clusters_results.csv" # Fuente de verdad
|
||||
N_CLUSTERS = 10
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
# 2. CARGAR EL MODELO ENTRENADO
|
||||
def load_trained_encoder(path):
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"No se encontró el modelo en {path}")
|
||||
|
||||
model = models.efficientnet_b0()
|
||||
model.classifier = nn.Identity()
|
||||
|
||||
state_dict = torch.load(path, map_location=DEVICE)
|
||||
model.load_state_dict(state_dict)
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
# 3. DATASET BASADO EN EL CSV
|
||||
class MSIDatasetFromCSV(Dataset):
|
||||
def __init__(self, img_dir, img_names):
|
||||
self.img_dir = img_dir
|
||||
self.img_names = img_names
|
||||
self.transform = transforms.Compose([
|
||||
transforms.Resize((224, 224)),
|
||||
transforms.ToTensor(),
|
||||
])
|
||||
|
||||
def __len__(self):
|
||||
return len(self.img_names)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
img_path = os.path.join(self.img_dir, self.img_names[idx])
|
||||
image = Image.open(img_path).convert("RGB")
|
||||
return self.transform(image)
|
||||
|
||||
# --- PROCESO ---
|
||||
if __name__ == "__main__":
|
||||
# 4. CARGAR EL CSV DE RESULTADOS
|
||||
print(f"Cargando datos desde {CSV_PATH}...")
|
||||
df = pd.read_csv(CSV_PATH)
|
||||
ion_names = df['ion_image'].tolist()
|
||||
|
||||
# Asegurarnos de que cluster sea categórico para Plotly
|
||||
df['cluster'] = df['cluster'].astype(str)
|
||||
|
||||
print("Extrayendo características con el encoder...")
|
||||
encoder = load_trained_encoder(ENCODER_PATH)
|
||||
dataset = MSIDatasetFromCSV(IMG_DIR, ion_names)
|
||||
loader = DataLoader(dataset, batch_size=1, shuffle=False)
|
||||
|
||||
features = []
|
||||
with torch.no_grad():
|
||||
for i, img in enumerate(loader):
|
||||
img = img.to(DEVICE)
|
||||
feat = encoder(img)
|
||||
# Normalización L2
|
||||
feat = feat / feat.norm(p=2, dim=1, keepdim=True)
|
||||
features.append(feat.cpu().numpy().flatten())
|
||||
if (i+1) % 100 == 0:
|
||||
print(f"Iones procesados: {i+1}/{len(ion_names)}")
|
||||
|
||||
features = np.array(features)
|
||||
|
||||
# 5. t-SNE
|
||||
print("Calculando t-SNE...")
|
||||
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
|
||||
embeddings_2d = tsne.fit_transform(features)
|
||||
|
||||
# Agregar coordenadas t-SNE al DataFrame original
|
||||
df['tsne_1'] = embeddings_2d[:, 0]
|
||||
df['tsne_2'] = embeddings_2d[:, 1]
|
||||
|
||||
# 6. VISUALIZACIÓN INTERACTIVA CON PLOTLY
|
||||
print("Generando gráfica interactiva con Plotly...")
|
||||
|
||||
# Usar escala de colores similar a tab10 o cualitativa estándar
|
||||
fig = px.scatter(
|
||||
df,
|
||||
x='tsne_1',
|
||||
y='tsne_2',
|
||||
color='cluster',
|
||||
hover_data={'ion_image': True, 'mz': True, 'cluster': True, 'tsne_1': False, 'tsne_2': False},
|
||||
title=f"t-SNE Interactivo de Iones MSI (Consistente con {CSV_PATH})",
|
||||
labels={'tsne_1': 't-SNE 1', 'tsne_2': 't-SNE 2'},
|
||||
category_orders={"cluster": [str(i) for i in range(N_CLUSTERS)]},
|
||||
color_discrete_sequence=px.colors.qualitative.Plotly # O T10 si está disponible
|
||||
)
|
||||
|
||||
# Mejorar el diseño
|
||||
fig.update_traces(marker=dict(size=10, opacity=0.8, line=dict(width=1, color='DarkSlateGrey')))
|
||||
fig.update_layout(
|
||||
legend_title_text='Cluster ID',
|
||||
font=dict(size=14),
|
||||
hoverlabel=dict(bgcolor="white", font_size=16)
|
||||
)
|
||||
|
||||
# Guardar como HTML
|
||||
output_html = "msi_tsne_interactivo.html"
|
||||
fig.write_html(output_html)
|
||||
|
||||
print(f"\n¡Éxito! Gráfica interactiva guardada en '{output_html}'.")
|
||||
print("Puedes abrir este archivo en cualquier navegador para explorar los clusters.")
|
||||
197
scripts_python/quantitative_validation.py
Normal file
@ -0,0 +1,197 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import random
|
||||
import pandas as pd
|
||||
from sklearn.cluster import KMeans
|
||||
from sklearn.decomposition import PCA
|
||||
from sklearn.metrics import silhouette_score, davies_bouldin_score, calinski_harabasz_score, adjusted_rand_score
|
||||
import matplotlib.pyplot as plt
|
||||
from scipy.stats import ttest_ind
|
||||
import torch.nn.functional as F
|
||||
|
||||
# 0. REPRODUCIBILITY
|
||||
def set_seed(seed=42):
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.manual_seed(seed)
|
||||
torch.cuda.manual_seed_all(seed)
|
||||
torch.backends.cudnn.deterministic = True
|
||||
torch.backends.cudnn.benchmark = False
|
||||
|
||||
set_seed(42)
|
||||
|
||||
# CONFIGURATION
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai")
|
||||
MODEL_PATH = os.path.join(BASE_DIR, "..", "models", "msi_encoder_trained.pth")
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
K = 10
|
||||
|
||||
class MSI_Structural_Encoder(nn.Module):
|
||||
def __init__(self):
|
||||
super(MSI_Structural_Encoder, self).__init__()
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
||||
nn.GroupNorm(4, 32),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(8, 64),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(16, 128),
|
||||
nn.ReLU(),
|
||||
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):
|
||||
return self.features(x).squeeze(-1).squeeze(-1)
|
||||
|
||||
# 1. DATA LOADING
|
||||
def load_images():
|
||||
img_names = sorted([f for f in os.listdir(IMG_DIR) if f.endswith('.png')])
|
||||
raw_pixels = []
|
||||
mzs = []
|
||||
|
||||
print(f"Loading {len(img_names)} images for validation...")
|
||||
for name in img_names:
|
||||
img_path = os.path.join(IMG_DIR, name)
|
||||
img = Image.open(img_path).convert("L")
|
||||
raw_pixels.append(np.array(img).flatten())
|
||||
|
||||
try:
|
||||
mz = float(name.split('_')[1].replace('.png', ''))
|
||||
mzs.append(mz)
|
||||
except:
|
||||
mzs.append(0.0)
|
||||
|
||||
return np.array(raw_pixels), np.array(mzs), img_names
|
||||
|
||||
def extract_simclr_features(img_names):
|
||||
model = MSI_Structural_Encoder()
|
||||
if os.path.exists(MODEL_PATH):
|
||||
model.features.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
|
||||
transform = transforms.Compose([
|
||||
transforms.Resize((256, 256)),
|
||||
transforms.ToTensor(),
|
||||
])
|
||||
|
||||
features = []
|
||||
print(f"Extracting SimCLR features on {DEVICE}...")
|
||||
with torch.no_grad():
|
||||
for name in img_names:
|
||||
img_path = os.path.join(IMG_DIR, name)
|
||||
img = Image.open(img_path).convert("L")
|
||||
img_t = transform(img).unsqueeze(0).to(DEVICE)
|
||||
feat = model(img_t)
|
||||
# CRITICAL FIX: Project explicitly onto the unit hypersphere
|
||||
feat_norm = F.normalize(feat, p=2, dim=1)
|
||||
features.append(feat_norm.cpu().numpy().flatten())
|
||||
return np.array(features)
|
||||
|
||||
# 2. VALIDATION TASKS
|
||||
def baseline_comparison(X_raw, X_simclr):
|
||||
print("\n--- TASK 1: BASELINE COMPARISON (PCA vs SimCLR) ---")
|
||||
pca_raw = PCA(n_components=50, random_state=42)
|
||||
X_pca_raw = pca_raw.fit_transform(X_raw)
|
||||
|
||||
pca_simclr = PCA(n_components=50, random_state=42)
|
||||
X_pca_simclr = pca_simclr.fit_transform(X_simclr)
|
||||
|
||||
km_raw = KMeans(n_clusters=K, random_state=42, n_init=10)
|
||||
labels_raw = km_raw.fit_predict(X_pca_raw)
|
||||
|
||||
km_sim = KMeans(n_clusters=K, random_state=42, n_init=10)
|
||||
labels_sim = km_sim.fit_predict(X_pca_simclr)
|
||||
|
||||
metrics = {
|
||||
"Method": ["Raw PCA", "SimCLR Encoder"],
|
||||
"Silhouette": [silhouette_score(X_pca_raw, labels_raw), silhouette_score(X_pca_simclr, labels_sim)],
|
||||
"Davies-Bouldin": [davies_bouldin_score(X_pca_raw, labels_raw), davies_bouldin_score(X_pca_simclr, labels_sim)],
|
||||
"Calinski-Harabasz": [calinski_harabasz_score(X_pca_raw, labels_raw), calinski_harabasz_score(X_pca_simclr, labels_sim)]
|
||||
}
|
||||
|
||||
df_metrics = pd.DataFrame(metrics)
|
||||
print(df_metrics.to_string(index=False))
|
||||
return df_metrics
|
||||
|
||||
def stability_analysis(X_features):
|
||||
print("\n--- TASK 2: STABILITY ANALYSIS (Multiple Seeds) ---")
|
||||
seeds = [10, 20, 30, 42, 50, 60, 70, 80, 90, 100]
|
||||
all_labels = []
|
||||
metrics_list = []
|
||||
|
||||
pca = PCA(n_components=50, random_state=42)
|
||||
X_pca = pca.fit_transform(X_features)
|
||||
|
||||
for seed in seeds:
|
||||
km = KMeans(n_clusters=K, random_state=seed, n_init=10)
|
||||
labels = km.fit_predict(X_pca)
|
||||
all_labels.append(labels)
|
||||
metrics_list.append(silhouette_score(X_pca, labels))
|
||||
|
||||
ari_scores = []
|
||||
for i in range(len(all_labels)):
|
||||
for j in range(i + 1, len(all_labels)):
|
||||
ari_scores.append(adjusted_rand_score(all_labels[i], all_labels[j]))
|
||||
|
||||
print(f"Mean Silhouette Score: {np.mean(metrics_list):.4f} ± {np.std(metrics_list):.4f}")
|
||||
print(f"Mean Adjusted Rand Index (ARI) between runs: {np.mean(ari_scores):.4f} (1.0 is perfect stability)")
|
||||
|
||||
def isotope_validation(X_features, mzs):
|
||||
print("\n--- TASK 3: ISOTOPE CO-LOCALIZATION VALIDATION ---")
|
||||
X_norm = X_features / np.linalg.norm(X_features, axis=1, keepdims=True)
|
||||
|
||||
isotope_dist = []
|
||||
random_dist = []
|
||||
|
||||
for i in range(len(mzs)):
|
||||
for j in range(i + 1, len(mzs)):
|
||||
diff = abs(mzs[i] - mzs[j])
|
||||
if 0.95 <= diff <= 1.05:
|
||||
dist = np.linalg.norm(X_norm[i] - X_norm[j])
|
||||
isotope_dist.append(dist)
|
||||
|
||||
if len(isotope_dist) > 0:
|
||||
for _ in range(len(isotope_dist)):
|
||||
idx1 = random.randint(0, len(mzs)-1)
|
||||
idx2 = random.randint(0, len(mzs)-1)
|
||||
if idx1 != idx2:
|
||||
dist = np.linalg.norm(X_norm[idx1] - X_norm[idx2])
|
||||
random_dist.append(dist)
|
||||
|
||||
print(f"Number of isotopic pairs found: {len(isotope_dist)}")
|
||||
print(f"Mean distance (Isotopes): {np.mean(isotope_dist):.4f}")
|
||||
print(f"Mean distance (Random): {np.mean(random_dist):.4f}")
|
||||
|
||||
t_stat, p_val = ttest_ind(isotope_dist, random_dist)
|
||||
print(f"T-test p-value: {p_val:.4e} (Significant if < 0.05)")
|
||||
|
||||
plt.figure(figsize=(8, 6))
|
||||
plt.boxplot([isotope_dist, random_dist], tick_labels=['Isotopic Pairs', 'Random Pairs'])
|
||||
plt.title('Latent Space Distances: Isotopes vs Random')
|
||||
plt.ylabel('Euclidean Distance (Normalized Latent Space)')
|
||||
plt.savefig(os.path.join(BASE_DIR, "..", "figures", "isotope_validation_plot.png"))
|
||||
print(f"Isotope validation plot saved to figures/isotope_validation_plot.png")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if not os.path.exists(IMG_DIR):
|
||||
print(f"Error: IMG_DIR {IMG_DIR} not found. Generate ion images first.")
|
||||
else:
|
||||
X_raw, mzs, img_names = load_images()
|
||||
X_simclr = extract_simclr_features(img_names)
|
||||
|
||||
baseline_comparison(X_raw, X_simclr)
|
||||
stability_analysis(X_simclr)
|
||||
isotope_validation(X_simclr, mzs)
|
||||
73
scripts_python/test_grouping.py
Normal file
@ -0,0 +1,73 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
from torchvision import models, transforms
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
from sklearn.cluster import KMeans
|
||||
import torch.nn.functional as F
|
||||
|
||||
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")
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
class MSI_Structural_Encoder(nn.Module):
|
||||
def __init__(self):
|
||||
super(MSI_Structural_Encoder, self).__init__()
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
||||
nn.GroupNorm(4, 32),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(8, 64),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(16, 128),
|
||||
nn.ReLU(),
|
||||
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):
|
||||
return self.features(x).squeeze(-1).squeeze(-1)
|
||||
|
||||
def load_encoder_diagnostic(path):
|
||||
model = MSI_Structural_Encoder()
|
||||
if os.path.exists(path):
|
||||
state_dict = torch.load(path, map_location=DEVICE)
|
||||
model.features.load_state_dict(state_dict)
|
||||
model.to(DEVICE)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
if __name__ == "__main__":
|
||||
encoder = load_encoder_diagnostic(ENCODER_PATH)
|
||||
|
||||
# Load targets directly to test raw alignment
|
||||
img_155 = os.path.join(IMG_DIR, "ion_155.75.png")
|
||||
img_156 = os.path.join(IMG_DIR, "ion_156.07.png")
|
||||
|
||||
transform = transforms.Compose([
|
||||
transforms.Resize((256, 256)),
|
||||
transforms.ToTensor()
|
||||
])
|
||||
|
||||
if os.path.exists(img_155) and os.path.exists(img_156):
|
||||
# Convert to Grayscale "L"
|
||||
t_155 = transform(Image.open(img_155).convert("L")).unsqueeze(0).to(DEVICE)
|
||||
t_156 = transform(Image.open(img_156).convert("L")).unsqueeze(0).to(DEVICE)
|
||||
|
||||
with torch.no_grad():
|
||||
f_155 = encoder(t_155)
|
||||
f_156 = encoder(t_156)
|
||||
|
||||
# Check directional similarity
|
||||
sim = F.cosine_similarity(f_155, f_156)
|
||||
print(f"\n--- Feature Diagnostics ---")
|
||||
print(f"Cosine Similarity between Vein Ions (155.75 vs 156.07): {sim.item():.4f}")
|
||||
print("If this value is below 0.85, the loaded weight file is treating them as different shapes.")
|
||||
|
||||
84
scripts_python/verify_coherence.py
Normal file
@ -0,0 +1,84 @@
|
||||
import os
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from torchvision import transforms
|
||||
|
||||
# Copy the exact encoder structure from your scripts
|
||||
import torch.nn as nn
|
||||
class MSI_Structural_Encoder(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.features = nn.Sequential(
|
||||
nn.Conv2d(1, 32, kernel_size=5, stride=2, padding=2, bias=False),
|
||||
nn.GroupNorm(4, 32),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(8, 64),
|
||||
nn.ReLU(),
|
||||
nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=False),
|
||||
nn.GroupNorm(16, 128),
|
||||
nn.ReLU(),
|
||||
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):
|
||||
return self.features(x).squeeze(-1).squeeze(-1)
|
||||
|
||||
DEVICE = torch.device("cpu")
|
||||
IMG_DIR = "datos_para_ai"
|
||||
MODEL_PATH = "models/msi_encoder_trained.pth"
|
||||
|
||||
if not os.path.exists(MODEL_PATH):
|
||||
print("Model not found. Please ensure you are in the correct directory.")
|
||||
exit()
|
||||
|
||||
model = MSI_Structural_Encoder()
|
||||
model.features.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
|
||||
model.eval()
|
||||
|
||||
transform = transforms.Compose([transforms.Resize((256, 256)), transforms.ToTensor()])
|
||||
|
||||
def get_vector(mz_target):
|
||||
# Find the file closest to mz_target
|
||||
files = [f for f in os.listdir(IMG_DIR) if f.endswith('.png')]
|
||||
mzs = [float(f.replace("ion_", "").replace(".png", "")) for f in files]
|
||||
idx = np.argmin(np.abs(np.array(mzs) - mz_target))
|
||||
closest_mz = mzs[idx]
|
||||
|
||||
if abs(closest_mz - mz_target) > 0.1:
|
||||
return None, closest_mz
|
||||
|
||||
img_path = os.path.join(IMG_DIR, files[idx])
|
||||
img = Image.open(img_path).convert("L")
|
||||
tensor = transform(img).unsqueeze(0)
|
||||
|
||||
with torch.no_grad():
|
||||
feat = model(tensor)
|
||||
feat_norm = F.normalize(feat, p=2, dim=1)
|
||||
return feat_norm.numpy().flatten(), closest_mz
|
||||
|
||||
print("--- INTERNAL VALIDATION: ISOTOPIC & STRUCTURAL COHERENCE ---")
|
||||
|
||||
# Test 1: Isotopic Coherence (Atropine and its +1 Isotope)
|
||||
v1, mz1 = get_vector(290.02)
|
||||
v2, mz2 = get_vector(291.00)
|
||||
if v1 is not None and v2 is not None:
|
||||
sim = np.dot(v1, v2)
|
||||
print(f"Isotopic Match -> m/z {mz1:.2f} (Atropine) vs m/z {mz2:.2f} (+1 Isotope): Cosine Similarity = {sim:.4f}")
|
||||
|
||||
# Test 2: Structural Coherence (Atropine and Scopolamine - both vascular)
|
||||
v3, mz3 = get_vector(304.00)
|
||||
if v1 is not None and v3 is not None:
|
||||
sim = np.dot(v1, v3)
|
||||
print(f"Biological Match -> m/z {mz1:.2f} (Atropine) vs m/z {mz3:.2f} (Scopolamine): Cosine Similarity = {sim:.4f}")
|
||||
|
||||
# Test 3: Unrelated Coherence (Atropine vs random background or non-vascular)
|
||||
v4, mz4 = get_vector(142.09) # Tropine (paravascular, should be lower than scopolamine)
|
||||
if v1 is not None and v4 is not None:
|
||||
sim = np.dot(v1, v4)
|
||||
print(f"Divergent Match -> m/z {mz1:.2f} (Atropine) vs m/z {mz4:.2f} (Tropine): Cosine Similarity = {sim:.4f}")
|
||||
55
scripts_python/visualize_clusters.py
Normal file
@ -0,0 +1,55 @@
|
||||
import pandas as pd
|
||||
import os
|
||||
import matplotlib.pyplot as plt
|
||||
from PIL import Image
|
||||
|
||||
# 1. CONFIGURATION (Relative paths for portability)
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# Strictly use the root results file generated by graficar_tsne_msi.py
|
||||
CSV_PATH = os.path.join(BASE_DIR, "..", "msi_clusters_results.csv")
|
||||
|
||||
IMG_DIR = os.path.join(BASE_DIR, "..", "datos_para_ai")
|
||||
OUTPUT_DIR = os.path.join(BASE_DIR, "..", "figures", "galleries")
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# 2. PROCESSING
|
||||
if os.path.exists(CSV_PATH):
|
||||
print(f"Loading results from: {CSV_PATH}")
|
||||
df = pd.read_csv(CSV_PATH)
|
||||
|
||||
# Detect the correct column for ion image names
|
||||
possible_cols = ['ion_image', 'ion', 'filename']
|
||||
img_col = next((c for c in possible_cols if c in df.columns), None)
|
||||
|
||||
if not img_col:
|
||||
print(f"Error: Could not find image column in {CSV_PATH}. Available: {df.columns.tolist()}")
|
||||
else:
|
||||
for cluster_id in sorted(df['cluster'].unique()):
|
||||
print(f"Generating gallery for Cluster {cluster_id}...")
|
||||
# Take a 3x3 sample of the cluster
|
||||
cluster_ions = df[df['cluster'] == cluster_id][img_col].head(9).tolist()
|
||||
|
||||
fig, axes = plt.subplots(3, 3, figsize=(10, 10))
|
||||
fig.suptitle(f"Cluster {cluster_id} - Ion Morphologies", fontsize=16)
|
||||
|
||||
for i, ax in enumerate(axes.flat):
|
||||
if i < len(cluster_ions):
|
||||
img_path = os.path.join(IMG_DIR, cluster_ions[i])
|
||||
if os.path.exists(img_path):
|
||||
img = Image.open(img_path)
|
||||
# Using 'viridis' improves human visibility of low-concentration transport traces in the veins
|
||||
ax.imshow(img, cmap='viridis')
|
||||
ax.set_title(cluster_ions[i], fontsize=8)
|
||||
else:
|
||||
ax.text(0.5, 0.5, 'Image Not Found', ha='center', va='center', fontsize=6)
|
||||
ax.axis('off')
|
||||
|
||||
plt.tight_layout()
|
||||
# Standardized filename for consistency
|
||||
plt.savefig(f"{OUTPUT_DIR}/Gallery_cluster_{cluster_id}.png")
|
||||
plt.close()
|
||||
|
||||
print(f"\nGalleries successfully saved in: {OUTPUT_DIR}")
|
||||
else:
|
||||
print(f"Error: Missing required results file: {CSV_PATH}")
|
||||
print("Please run 'python scripts_python/graficar_tsne_msi.py' first to generate results.")
|
||||