Cleaned code repository by separating from manuscript files

This commit is contained in:
Pixelguy14 2026-06-30 13:29:06 -06:00
commit 9feb023852
40 changed files with 3119 additions and 0 deletions

View 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
View 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
View 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
View 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
View 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
View 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 (400450 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* 50700) 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 Julias 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*.
![Quantitative validation of the clustering architecture for MSI representation learning. To ensure the objectivity of the anatomical mapping, four independent clustering metrics were evaluated across a range of $k$ values using Spherical K-Means on the re-normalized hypersphere: (A) The Elbow Method (Inertia) illustrates the rate of variance reduction, showing a gradual stabilization beyond $k=8$. (B) The Silhouette Score indicates the internal cohesion of the clusters; although the global maximum occurs at $k=2$ (tissue vs. background), the score achieves 0.6894 at $k=10$, confirming a well-defined structure in the latent manifold. (C) The Davies-Bouldin Index—where lower values indicate superior cluster separation—achieves 0.6243 at $k=10$, well below the critical threshold of 1.0, demonstrating distinct boundaries between metabolic domains. (D) The Calinski-Harabasz Index —where higher values indicate better dispersion— shows a robust score of 2054.20 at the selected $k$. Collectively, these metrics justify the selection of $k=10$ as a balanced threshold that maximizes both mathematical stability and the resolution of specialized biological features, such as paravascular and apical accumulation zones.](Figure1.png)
---
## 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* 80320 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).
![Mirrored MS/MS spectra for the confirmation of scopolamine (*m/z* 304.17) acquired via LD-LTP-MS in positive ion mode. The upper panel (blue) represents the fragmentation profile obtained directly from the leaf surface of *D. innoxia*, while the lower panel (red) shows the commercial reference standard. Data were acquired in profile mode and are presented as refined centroids after baseline subtraction. Both spectra were generated using collision-induced dissociation (CID) at 20 eV. The axes denote relative abundance (%) and the mass-to-charge ratio (*m/z*), respectively.](Figure2.png)
### 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.)
![Visualizing metabolic specialization across anatomical zones of a *D. innoxia* leaf using t-SNE. This scatter plot displays the t-SNE dimensionality reduction of MSI data acquired from a leaf section at a spatial resolution of 150 µm. The original dataset of 588 *m/z* feature intensities was projected into a 2D space to visualize metabolic similarity. Ten distinct clusters are identified via automated segmentation of the co-localized ion images and are represented here by a qualitative color bar (right). Points of the same color indicate pixels with closely related metabolic signatures, corresponding to different anatomical regions within the leaf. The strong segregation between groups highlights a sharp molecular differentiation of metabolic functions across the tissue.](Figure3.png)
### 3.3 Chemical-Anatomical Segmentation in *D. innoxia*
The unsupervised decomposition of the latent space yielded ten distinct molecular domains (Clusters 09), 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).
![t-SNE-based clustering reveals spatially resolved chemical domains in *D. innoxia* leaves ($k = 10$). The t-distributed stochastic neighbor embedding (t-SNE) projection of MSI data separates the dataset into ten distinct clusters (Clusters 09), each corresponding to a reproducible spatial ion distribution pattern. Representative ion images (one per cluster) illustrate the characteristic spatial topology associated with each molecular domain. Cluster 0 is characterized by large defined islands concentrated near the apex of the leaf. Cluster 1 displays a fully apical distribution, with highly defined signal concentrated exclusively at the leaf tip. Cluster 2 shows broad, uniform presence across the entire leaf surface, reflecting a vascular-parenchymal distribution of biosynthetic precursors. Cluster 3 combines midrib signal with small islands distributed across the lamina and enrichment toward the tip. Cluster 4 exhibits a mixed pattern encompassing the tip, midrib, and large islands distributed across the leaf. Cluster 5 presents medium-sized islands throughout the leaf in conjunction with midrib-associated signal. Cluster 6 shows combined presence at the tip, midrib, and across the entire leaf surface. Cluster 7 is dominated by large islands across the leaf with a minor component at the tip. Cluster 8 displays signal in the midrib, tip, and medium-sized islands across the leaf. Cluster 9 shows large and small islands distributed across the leaf with moderate apical presence. A representative photograph of a *D. innoxia* leaf is included to provide anatomical reference for the spatial patterns observed in the MSI-derived clusters.](Figure4.png)
**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.42.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. 15971607). 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), 90979104. 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), 9098. 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, 133145. 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), 27342743. 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), 39994004. 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, 219225. 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), 2469724705. 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), 38293836. 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), 51065110. 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. 61056114). 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), 25792605.
* Ziegler, J., & Facchini, P. J. (2008). Alkaloid Biosynthesis: Metabolism and Trafficking. Annual Review of Plant Biology, 59(1), 735769. 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), 99459954. 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, 1690316916.
* Diaconis, P., Goel, S., & Holmes, S. (2008). Horseshoes in multidimensional scaling and local kernel methods. The Annals of Applied Statistics, 2(3), 777807. 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), 14821492. 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), 122. https://doi.org/10.18637/jss.v050.i10

119
README.md Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
data/region_of_interest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

73
documentation.md Normal file
View 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
View 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"

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

589
msi_clusters_results.csv Normal file
View 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
1 ion_image cluster mz
2 ion_100.15.png 2 100.15
3 ion_100.48.png 0 100.48
4 ion_100.8.png 2 100.8
5 ion_101.45.png 0 101.45
6 ion_101.78.png 4 101.78
7 ion_102.1.png 2 102.1
8 ion_102.75.png 3 102.75
9 ion_103.73.png 3 103.73
10 ion_104.7.png 1 104.7
11 ion_105.03.png 2 105.03
12 ion_105.68.png 1 105.68
13 ion_106.0.png 2 106.0
14 ion_106.65.png 4 106.65
15 ion_106.98.png 2 106.98
16 ion_107.3.png 9 107.3
17 ion_107.63.png 4 107.63
18 ion_107.95.png 2 107.95
19 ion_108.28.png 8 108.28
20 ion_108.61.png 3 108.61
21 ion_108.93.png 2 108.93
22 ion_109.26.png 1 109.26
23 ion_109.58.png 9 109.58
24 ion_109.91.png 2 109.91
25 ion_110.23.png 8 110.23
26 ion_110.56.png 3 110.56
27 ion_110.88.png 2 110.88
28 ion_111.21.png 2 111.21
29 ion_111.53.png 0 111.53
30 ion_111.86.png 2 111.86
31 ion_112.18.png 2 112.18
32 ion_112.51.png 0 112.51
33 ion_112.83.png 2 112.83
34 ion_113.16.png 2 113.16
35 ion_113.48.png 0 113.48
36 ion_113.81.png 2 113.81
37 ion_114.13.png 2 114.13
38 ion_114.46.png 7 114.46
39 ion_114.78.png 2 114.78
40 ion_115.11.png 2 115.11
41 ion_115.43.png 0 115.43
42 ion_115.76.png 6 115.76
43 ion_116.08.png 2 116.08
44 ion_116.73.png 4 116.73
45 ion_117.06.png 2 117.06
46 ion_117.71.png 4 117.71
47 ion_118.03.png 2 118.03
48 ion_118.68.png 1 118.68
49 ion_119.01.png 2 119.01
50 ion_119.66.png 8 119.66
51 ion_119.98.png 2 119.98
52 ion_120.31.png 3 120.31
53 ion_120.63.png 4 120.63
54 ion_120.96.png 2 120.96
55 ion_121.28.png 8 121.28
56 ion_121.61.png 5 121.61
57 ion_121.94.png 2 121.94
58 ion_122.26.png 8 122.26
59 ion_122.59.png 9 122.59
60 ion_122.91.png 2 122.91
61 ion_123.24.png 8 123.24
62 ion_123.56.png 7 123.56
63 ion_123.89.png 2 123.89
64 ion_124.21.png 6 124.21
65 ion_124.54.png 1 124.54
66 ion_124.86.png 2 124.86
67 ion_125.19.png 6 125.19
68 ion_125.51.png 0 125.51
69 ion_125.84.png 2 125.84
70 ion_126.16.png 2 126.16
71 ion_126.49.png 0 126.49
72 ion_126.81.png 2 126.81
73 ion_127.14.png 6 127.14
74 ion_127.46.png 0 127.46
75 ion_127.79.png 2 127.79
76 ion_128.11.png 2 128.11
77 ion_128.44.png 0 128.44
78 ion_128.76.png 6 128.76
79 ion_129.09.png 2 129.09
80 ion_129.41.png 0 129.41
81 ion_129.74.png 6 129.74
82 ion_130.06.png 2 130.06
83 ion_130.39.png 7 130.39
84 ion_130.71.png 6 130.71
85 ion_131.04.png 2 131.04
86 ion_131.69.png 3 131.69
87 ion_132.01.png 2 132.01
88 ion_132.66.png 1 132.66
89 ion_132.99.png 2 132.99
90 ion_133.64.png 1 133.64
91 ion_133.96.png 2 133.96
92 ion_134.29.png 8 134.29
93 ion_134.61.png 3 134.61
94 ion_134.94.png 2 134.94
95 ion_135.27.png 8 135.27
96 ion_135.59.png 7 135.59
97 ion_135.92.png 2 135.92
98 ion_136.24.png 4 136.24
99 ion_136.57.png 5 136.57
100 ion_136.89.png 2 136.89
101 ion_137.22.png 6 137.22
102 ion_137.54.png 3 137.54
103 ion_137.87.png 6 137.87
104 ion_138.19.png 4 138.19
105 ion_138.52.png 0 138.52
106 ion_138.84.png 2 138.84
107 ion_139.17.png 6 139.17
108 ion_139.49.png 0 139.49
109 ion_139.82.png 2 139.82
110 ion_140.14.png 6 140.14
111 ion_140.47.png 0 140.47
112 ion_140.79.png 2 140.79
113 ion_141.12.png 6 141.12
114 ion_141.44.png 0 141.44
115 ion_141.77.png 8 141.77
116 ion_142.09.png 2 142.09
117 ion_142.74.png 6 142.74
118 ion_143.07.png 2 143.07
119 ion_143.72.png 6 143.72
120 ion_144.04.png 2 144.04
121 ion_144.37.png 1 144.37
122 ion_144.69.png 1 144.69
123 ion_145.02.png 2 145.02
124 ion_145.67.png 3 145.67
125 ion_145.99.png 2 145.99
126 ion_146.32.png 3 146.32
127 ion_146.64.png 1 146.64
128 ion_146.97.png 2 146.97
129 ion_147.29.png 8 147.29
130 ion_147.62.png 3 147.62
131 ion_147.94.png 2 147.94
132 ion_148.27.png 4 148.27
133 ion_148.59.png 7 148.59
134 ion_148.92.png 2 148.92
135 ion_149.25.png 4 149.25
136 ion_149.57.png 0 149.57
137 ion_149.9.png 2 149.9
138 ion_150.22.png 4 150.22
139 ion_150.55.png 5 150.55
140 ion_150.87.png 2 150.87
141 ion_151.2.png 6 151.2
142 ion_151.52.png 0 151.52
143 ion_151.85.png 2 151.85
144 ion_152.17.png 4 152.17
145 ion_152.5.png 0 152.5
146 ion_152.82.png 2 152.82
147 ion_153.15.png 6 153.15
148 ion_153.47.png 0 153.47
149 ion_153.8.png 2 153.8
150 ion_154.12.png 2 154.12
151 ion_154.45.png 9 154.45
152 ion_154.77.png 6 154.77
153 ion_155.1.png 2 155.1
154 ion_155.75.png 8 155.75
155 ion_156.07.png 6 156.07
156 ion_156.4.png 9 156.4
157 ion_156.72.png 6 156.72
158 ion_157.05.png 2 157.05
159 ion_157.7.png 9 157.7
160 ion_158.02.png 2 158.02
161 ion_158.67.png 3 158.67
162 ion_159.0.png 2 159.0
163 ion_159.65.png 9 159.65
164 ion_159.97.png 2 159.97
165 ion_160.3.png 8 160.3
166 ion_160.62.png 9 160.62
167 ion_160.95.png 2 160.95
168 ion_161.27.png 4 161.27
169 ion_161.6.png 0 161.6
170 ion_161.92.png 2 161.92
171 ion_162.25.png 8 162.25
172 ion_162.58.png 7 162.58
173 ion_162.9.png 2 162.9
174 ion_163.23.png 4 163.23
175 ion_163.55.png 7 163.55
176 ion_163.88.png 6 163.88
177 ion_164.2.png 4 164.2
178 ion_164.53.png 0 164.53
179 ion_164.85.png 2 164.85
180 ion_165.18.png 4 165.18
181 ion_165.5.png 0 165.5
182 ion_165.83.png 6 165.83
183 ion_166.15.png 4 166.15
184 ion_166.48.png 0 166.48
185 ion_166.8.png 2 166.8
186 ion_167.13.png 2 167.13
187 ion_167.45.png 9 167.45
188 ion_167.78.png 1 167.78
189 ion_168.1.png 2 168.1
190 ion_168.43.png 5 168.43
191 ion_168.75.png 4 168.75
192 ion_169.08.png 6 169.08
193 ion_169.73.png 1 169.73
194 ion_170.05.png 6 170.05
195 ion_170.7.png 8 170.7
196 ion_171.03.png 2 171.03
197 ion_171.68.png 5 171.68
198 ion_172.0.png 6 172.0
199 ion_172.65.png 9 172.65
200 ion_172.98.png 2 172.98
201 ion_173.63.png 5 173.63
202 ion_173.95.png 4 173.95
203 ion_174.28.png 1 174.28
204 ion_174.6.png 0 174.6
205 ion_174.93.png 2 174.93
206 ion_175.25.png 8 175.25
207 ion_175.58.png 0 175.58
208 ion_175.91.png 6 175.91
209 ion_176.23.png 8 176.23
210 ion_176.56.png 5 176.56
211 ion_176.88.png 2 176.88
212 ion_177.21.png 2 177.21
213 ion_177.53.png 0 177.53
214 ion_177.86.png 4 177.86
215 ion_178.18.png 4 178.18
216 ion_178.83.png 4 178.83
217 ion_179.16.png 4 179.16
218 ion_179.81.png 8 179.81
219 ion_180.13.png 6 180.13
220 ion_180.46.png 5 180.46
221 ion_180.78.png 1 180.78
222 ion_181.11.png 6 181.11
223 ion_181.43.png 9 181.43
224 ion_181.76.png 3 181.76
225 ion_182.08.png 6 182.08
226 ion_182.73.png 8 182.73
227 ion_183.06.png 4 183.06
228 ion_183.71.png 3 183.71
229 ion_184.03.png 6 184.03
230 ion_184.68.png 3 184.68
231 ion_185.01.png 2 185.01
232 ion_185.66.png 7 185.66
233 ion_185.98.png 6 185.98
234 ion_186.31.png 1 186.31
235 ion_186.63.png 5 186.63
236 ion_186.96.png 2 186.96
237 ion_187.61.png 0 187.61
238 ion_187.93.png 4 187.93
239 ion_188.58.png 0 188.58
240 ion_188.91.png 4 188.91
241 ion_189.24.png 1 189.24
242 ion_189.56.png 0 189.56
243 ion_189.89.png 4 189.89
244 ion_190.21.png 8 190.21
245 ion_190.54.png 7 190.54
246 ion_190.86.png 8 190.86
247 ion_191.19.png 4 191.19
248 ion_191.51.png 0 191.51
249 ion_191.84.png 8 191.84
250 ion_192.16.png 4 192.16
251 ion_192.81.png 8 192.81
252 ion_193.14.png 4 193.14
253 ion_193.79.png 1 193.79
254 ion_194.11.png 8 194.11
255 ion_194.76.png 1 194.76
256 ion_195.09.png 6 195.09
257 ion_195.74.png 9 195.74
258 ion_196.06.png 4 196.06
259 ion_196.71.png 1 196.71
260 ion_197.04.png 2 197.04
261 ion_197.69.png 7 197.69
262 ion_198.01.png 4 198.01
263 ion_198.66.png 1 198.66
264 ion_198.99.png 2 198.99
265 ion_199.31.png 1 199.31
266 ion_199.64.png 7 199.64
267 ion_199.96.png 4 199.96
268 ion_200.29.png 1 200.29
269 ion_200.61.png 0 200.61
270 ion_200.94.png 4 200.94
271 ion_201.26.png 8 201.26
272 ion_201.59.png 0 201.59
273 ion_201.91.png 4 201.91
274 ion_202.24.png 1 202.24
275 ion_202.57.png 0 202.57
276 ion_202.89.png 4 202.89
277 ion_203.22.png 8 203.22
278 ion_203.87.png 3 203.87
279 ion_204.19.png 8 204.19
280 ion_204.52.png 3 204.52
281 ion_204.84.png 2 204.84
282 ion_205.17.png 4 205.17
283 ion_205.82.png 1 205.82
284 ion_206.14.png 1 206.14
285 ion_206.79.png 8 206.79
286 ion_207.12.png 6 207.12
287 ion_207.77.png 5 207.77
288 ion_208.09.png 8 208.09
289 ion_208.74.png 3 208.74
290 ion_209.07.png 6 209.07
291 ion_209.72.png 5 209.72
292 ion_210.04.png 4 210.04
293 ion_210.69.png 7 210.69
294 ion_211.02.png 6 211.02
295 ion_211.67.png 0 211.67
296 ion_211.99.png 8 211.99
297 ion_212.64.png 7 212.64
298 ion_212.97.png 6 212.97
299 ion_213.62.png 7 213.62
300 ion_213.94.png 8 213.94
301 ion_214.27.png 1 214.27
302 ion_214.59.png 5 214.59
303 ion_214.92.png 2 214.92
304 ion_215.24.png 8 215.24
305 ion_215.57.png 0 215.57
306 ion_215.89.png 8 215.89
307 ion_216.22.png 1 216.22
308 ion_216.55.png 1 216.55
309 ion_216.87.png 2 216.87
310 ion_217.2.png 6 217.2
311 ion_217.52.png 7 217.52
312 ion_217.85.png 1 217.85
313 ion_218.17.png 1 218.17
314 ion_218.82.png 1 218.82
315 ion_219.15.png 8 219.15
316 ion_219.8.png 3 219.8
317 ion_220.12.png 8 220.12
318 ion_220.77.png 5 220.77
319 ion_221.1.png 4 221.1
320 ion_221.75.png 7 221.75
321 ion_222.07.png 1 222.07
322 ion_222.72.png 1 222.72
323 ion_223.05.png 2 223.05
324 ion_223.7.png 0 223.7
325 ion_224.02.png 1 224.02
326 ion_224.67.png 7 224.67
327 ion_225.0.png 4 225.0
328 ion_225.65.png 0 225.65
329 ion_225.97.png 1 225.97
330 ion_226.62.png 9 226.62
331 ion_226.95.png 6 226.95
332 ion_227.6.png 0 227.6
333 ion_227.92.png 1 227.92
334 ion_228.25.png 1 228.25
335 ion_228.9.png 8 228.9
336 ion_229.22.png 8 229.22
337 ion_229.88.png 1 229.88
338 ion_230.2.png 1 230.2
339 ion_230.85.png 3 230.85
340 ion_231.18.png 1 231.18
341 ion_231.83.png 9 231.83
342 ion_232.15.png 1 232.15
343 ion_232.8.png 9 232.8
344 ion_233.13.png 8 233.13
345 ion_233.78.png 5 233.78
346 ion_234.1.png 1 234.1
347 ion_234.75.png 5 234.75
348 ion_235.08.png 6 235.08
349 ion_235.73.png 7 235.73
350 ion_236.05.png 1 236.05
351 ion_236.7.png 5 236.7
352 ion_237.03.png 8 237.03
353 ion_237.68.png 0 237.68
354 ion_238.0.png 1 238.0
355 ion_238.65.png 0 238.65
356 ion_238.98.png 1 238.98
357 ion_239.95.png 1 239.95
358 ion_240.6.png 0 240.6
359 ion_240.93.png 1 240.93
360 ion_241.9.png 3 241.9
361 ion_242.23.png 3 242.23
362 ion_242.88.png 1 242.88
363 ion_243.21.png 8 243.21
364 ion_243.86.png 9 243.86
365 ion_244.18.png 1 244.18
366 ion_244.83.png 9 244.83
367 ion_245.16.png 1 245.16
368 ion_245.81.png 5 245.81
369 ion_246.13.png 1 246.13
370 ion_246.78.png 7 246.78
371 ion_247.11.png 1 247.11
372 ion_247.76.png 0 247.76
373 ion_248.08.png 3 248.08
374 ion_248.73.png 7 248.73
375 ion_249.06.png 1 249.06
376 ion_249.71.png 7 249.71
377 ion_250.03.png 3 250.03
378 ion_250.68.png 0 250.68
379 ion_251.01.png 1 251.01
380 ion_251.98.png 3 251.98
381 ion_252.63.png 0 252.63
382 ion_252.96.png 1 252.96
383 ion_253.93.png 9 253.93
384 ion_254.91.png 3 254.91
385 ion_255.23.png 1 255.23
386 ion_255.88.png 3 255.88
387 ion_256.21.png 1 256.21
388 ion_256.86.png 5 256.86
389 ion_257.19.png 1 257.19
390 ion_257.84.png 7 257.84
391 ion_258.16.png 3 258.16
392 ion_258.81.png 9 258.81
393 ion_259.14.png 1 259.14
394 ion_259.79.png 5 259.79
395 ion_260.11.png 3 260.11
396 ion_260.76.png 7 260.76
397 ion_261.09.png 1 261.09
398 ion_261.74.png 7 261.74
399 ion_262.06.png 3 262.06
400 ion_262.71.png 7 262.71
401 ion_263.04.png 1 263.04
402 ion_263.69.png 0 263.69
403 ion_264.01.png 9 264.01
404 ion_264.99.png 3 264.99
405 ion_265.96.png 3 265.96
406 ion_266.94.png 3 266.94
407 ion_267.91.png 5 267.91
408 ion_268.89.png 9 268.89
409 ion_269.21.png 1 269.21
410 ion_269.87.png 7 269.87
411 ion_270.19.png 3 270.19
412 ion_270.84.png 5 270.84
413 ion_271.17.png 3 271.17
414 ion_271.82.png 7 271.82
415 ion_272.14.png 3 272.14
416 ion_272.79.png 7 272.79
417 ion_273.12.png 3 273.12
418 ion_273.77.png 7 273.77
419 ion_274.09.png 3 274.09
420 ion_274.74.png 0 274.74
421 ion_275.07.png 3 275.07
422 ion_276.04.png 3 276.04
423 ion_277.02.png 9 277.02
424 ion_277.99.png 9 277.99
425 ion_278.32.png 9 278.32
426 ion_278.64.png 1 278.64
427 ion_278.97.png 2 278.97
428 ion_279.29.png 6 279.29
429 ion_279.62.png 5 279.62
430 ion_279.94.png 1 279.94
431 ion_280.92.png 9 280.92
432 ion_281.89.png 5 281.89
433 ion_282.87.png 5 282.87
434 ion_283.85.png 7 283.85
435 ion_284.82.png 7 284.82
436 ion_285.15.png 3 285.15
437 ion_285.8.png 5 285.8
438 ion_286.12.png 1 286.12
439 ion_286.77.png 5 286.77
440 ion_287.1.png 3 287.1
441 ion_287.75.png 0 287.75
442 ion_288.07.png 3 288.07
443 ion_289.05.png 3 289.05
444 ion_290.02.png 9 290.02
445 ion_291.0.png 5 291.0
446 ion_291.97.png 5 291.97
447 ion_292.95.png 5 292.95
448 ion_293.92.png 7 293.92
449 ion_294.9.png 5 294.9
450 ion_295.87.png 9 295.87
451 ion_296.85.png 5 296.85
452 ion_297.83.png 7 297.83
453 ion_298.15.png 9 298.15
454 ion_298.8.png 7 298.8
455 ion_299.13.png 3 299.13
456 ion_301.08.png 5 301.08
457 ion_302.05.png 5 302.05
458 ion_303.03.png 5 303.03
459 ion_303.68.png 0 303.68
460 ion_304.0.png 3 304.0
461 ion_304.33.png 3 304.33
462 ion_304.98.png 9 304.98
463 ion_305.95.png 9 305.95
464 ion_306.28.png 9 306.28
465 ion_306.93.png 7 306.93
466 ion_307.9.png 7 307.9
467 ion_308.88.png 7 308.88
468 ion_309.85.png 0 309.85
469 ion_310.83.png 7 310.83
470 ion_311.16.png 3 311.16
471 ion_312.78.png 7 312.78
472 ion_313.11.png 8 313.11
473 ion_314.08.png 9 314.08
474 ion_315.06.png 9 315.06
475 ion_318.96.png 5 318.96
476 ion_326.76.png 5 326.76
477 ion_328.71.png 9 328.71
478 ion_329.04.png 1 329.04
479 ion_329.69.png 0 329.69
480 ion_330.01.png 3 330.01
481 ion_330.99.png 9 330.99
482 ion_334.89.png 7 334.89
483 ion_335.21.png 1 335.21
484 ion_336.84.png 0 336.84
485 ion_337.17.png 1 337.17
486 ion_338.14.png 5 338.14
487 ion_339.12.png 5 339.12
488 ion_351.8.png 5 351.8
489 ion_352.12.png 3 352.12
490 ion_353.75.png 7 353.75
491 ion_354.07.png 3 354.07
492 ion_355.05.png 1 355.05
493 ion_370.33.png 5 370.33
494 ion_370.65.png 8 370.65
495 ion_370.98.png 4 370.98
496 ion_371.3.png 1 371.3
497 ion_371.63.png 1 371.63
498 ion_371.95.png 4 371.95
499 ion_372.6.png 7 372.6
500 ion_372.93.png 1 372.93
501 ion_384.63.png 9 384.63
502 ion_387.56.png 5 387.56
503 ion_390.81.png 3 390.81
504 ion_391.14.png 1 391.14
505 ion_401.54.png 5 401.54
506 ion_401.86.png 3 401.86
507 ion_461.69.png 9 461.69
508 ion_59.51.png 5 59.51
509 ion_59.84.png 2 59.84
510 ion_60.16.png 4 60.16
511 ion_60.81.png 8 60.81
512 ion_64.71.png 3 64.71
513 ion_66.66.png 3 66.66
514 ion_66.99.png 2 66.99
515 ion_67.64.png 9 67.64
516 ion_67.96.png 2 67.96
517 ion_68.62.png 8 68.62
518 ion_68.94.png 2 68.94
519 ion_69.27.png 1 69.27
520 ion_69.59.png 6 69.59
521 ion_69.92.png 2 69.92
522 ion_70.24.png 8 70.24
523 ion_70.57.png 0 70.57
524 ion_70.89.png 2 70.89
525 ion_71.54.png 8 71.54
526 ion_71.87.png 2 71.87
527 ion_72.19.png 6 72.19
528 ion_72.52.png 0 72.52
529 ion_72.84.png 2 72.84
530 ion_73.49.png 0 73.49
531 ion_73.82.png 2 73.82
532 ion_74.14.png 2 74.14
533 ion_74.79.png 1 74.79
534 ion_76.74.png 4 76.74
535 ion_77.72.png 8 77.72
536 ion_78.69.png 3 78.69
537 ion_79.02.png 2 79.02
538 ion_79.34.png 0 79.34
539 ion_79.67.png 2 79.67
540 ion_79.99.png 2 79.99
541 ion_80.32.png 5 80.32
542 ion_80.64.png 4 80.64
543 ion_80.97.png 2 80.97
544 ion_81.62.png 1 81.62
545 ion_81.95.png 2 81.95
546 ion_82.27.png 3 82.27
547 ion_82.6.png 8 82.6
548 ion_82.92.png 2 82.92
549 ion_83.25.png 3 83.25
550 ion_83.57.png 2 83.57
551 ion_83.9.png 2 83.9
552 ion_84.22.png 8 84.22
553 ion_84.55.png 7 84.55
554 ion_84.87.png 2 84.87
555 ion_85.52.png 7 85.52
556 ion_85.85.png 2 85.85
557 ion_86.17.png 2 86.17
558 ion_86.5.png 0 86.5
559 ion_86.82.png 2 86.82
560 ion_87.47.png 0 87.47
561 ion_87.8.png 2 87.8
562 ion_88.12.png 2 88.12
563 ion_88.77.png 3 88.77
564 ion_89.75.png 8 89.75
565 ion_90.07.png 4 90.07
566 ion_90.72.png 4 90.72
567 ion_91.05.png 2 91.05
568 ion_91.7.png 0 91.7
569 ion_92.67.png 3 92.67
570 ion_93.0.png 2 93.0
571 ion_93.32.png 7 93.32
572 ion_93.65.png 4 93.65
573 ion_93.97.png 2 93.97
574 ion_94.3.png 8 94.3
575 ion_94.62.png 6 94.62
576 ion_94.95.png 2 94.95
577 ion_95.28.png 8 95.28
578 ion_95.6.png 1 95.6
579 ion_95.93.png 2 95.93
580 ion_96.58.png 1 96.58
581 ion_96.9.png 2 96.9
582 ion_97.23.png 1 97.23
583 ion_97.55.png 3 97.55
584 ion_97.88.png 2 97.88
585 ion_98.2.png 2 98.2
586 ion_98.53.png 0 98.53
587 ion_98.85.png 2 98.85
588 ion_99.5.png 0 99.5
589 ion_99.83.png 2 99.83

View 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")

View 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()

View 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 ---")

View 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

View 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}")

View 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)]}")

View 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.")

View 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.")

View 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.")

View 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}'.")

View 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.")

View 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)

View 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.")

View 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}")

View 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.")