bulk and batch processing for multiple imzml and multiple m/z is now available, folder organization for imzml slices in public folder, JSON tracking of processed datasets to save memory time, multiple performance optimizations and UI bug fixes, metadata viewing for all processed files, comparative view between multiple datasets

This commit is contained in:
Pixelguy14 2025-10-21 11:56:15 -06:00
parent 39fe2bf52c
commit 39c92ba850
12 changed files with 1437 additions and 585 deletions

3
.gitignore vendored
View File

@ -1,9 +1,6 @@
public/*
public/css/imgOver.png
!public/css/
!public/masks/
public/masks/*
!public/masks/static.txt
log/*
R_original_scripts/
test/results/

View File

@ -2,7 +2,7 @@
julia_version = "1.11.7"
manifest_format = "2.0"
project_hash = "d4aecbadf6a54893101b3b89b57e8ae0ab391000"
project_hash = "b3d73d786a430f468e0f2f1b3b05da6a3addc7fe"
[[deps.ATK_jll]]
deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"]

View File

@ -13,6 +13,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e"
GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353"
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Libz = "2ec943e9-cfe8-584d-b93d-64dcb6d567b7"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"

View File

@ -22,7 +22,7 @@ https://codeberg.org/LabABI/JuliaMSI
~/Downloads/JuliaMSI-main/juliamsi
2. Without entering the Julia environment, launch the project in your terminal with the following command (which works for all operating systems):
```
julia --threads auto --project=. start_MSI_GUI.jl
julia--project=. start_MSI_GUI.jl
```
3. After the script has finished loading, you can open a [page](http://127.0.0.1:1481/) in your browser with the web app running.

1558
app.jl

File diff suppressed because it is too large Load Diff

View File

@ -17,19 +17,42 @@
<q-tab-panel name="generator">
<div class="text-h6">imzML & mzML Data Processor</div>
<p>Please make sure the ibd and imzML file are located in the same directory and have the same name.
<br>It may take a while to generate the image and the plot, please be patient.
<br>To generate the contour or surface plots, you have to select the desired image first using the interface.</p>
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="full_route" readonly label="Select your imzML or mzML file" v-on:click="btnSearch=true">
<template v-slot:append>
<q-icon name="search" v:onclick="btnSearch=true" class="cursor-pointer" />
</template>
</q-input>
<!-- Variable Manipulation -->
<br>It may take a while to generate the slice / spectrum, please be patient.
<br>To generate the contour or surface plots, you have to select the desired slice first using the interface.</p>
<div class="row items-center">
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer col"
v-model="full_route" readonly
:label="batch_file_count > 0 ? batch_file_count + ' file(s) in batch' : 'Select an imzML file'"
v-on:click="btnSearch=true">
<template v-slot:append>
<q-icon name="search" v-on:click="btnSearch=true" class="cursor-pointer" />
</template>
</q-input>
<q-btn class="q-ma-sm" icon="add" v-on:click="btnAddBatch=true" label="Add"></q-btn>
<q-btn class="q-ma-sm" icon="clear" v-on:click="clear_batch_btn=true"
:disable="batch_file_count === 0" label="Clear"></q-btn>
</div>
<q-list bordered separator v-if="selected_files.length > 0">
<q-item v-for="(file, index) in selected_files" :key="index">
<q-item-section>
{{ file }}
</q-item-section>
<q-item-section side>
<q-btn flat round icon="delete" size="sm" v-on:click="selected_files.splice(index, 1)"></q-btn>
</q-item-section>
</q-item>
</q-list>
<!-- Variable Manipulation -->
<div class="row">
<div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textNmass" step="0.001" v-model="Nmass"
label="Mass-to-charge ratio of interest" type="number"
:rules="[ val => !!val || '* Required', val => val >= 0.0 || 'Need positive mass values']"></q-input>
<q-input standout="custom-standout" id="textNmass" v-model="Nmass"
label="Mass-to-charge ratio(s) of interest" type="text"
:rules="[
val => !!val || '* Required',
val => val.split(',').every(m => !isNaN(parseFloat(m.trim())) && parseFloat(m.trim()) > 0) || 'Need comma-separated positive numbers'
]">
</q-input>
</div>
<div class="st-col col-4 col-sm q-ma-sm">
<q-input standout="custom-standout" id="textTol" step="0.005" v-model="Tol"
@ -104,20 +127,24 @@
</div>
</div>
<div class="row">
<q-btn :loading="progress" class="q-ma-sm btn-style" :disabled="btnStartDisable" icon="play_arrow"
v-on:click="mainProcess=true" padding="lg" label="Generate Slice">
<template v-slot:loading>
<q-spinner-hourglass class="on-left" />
Loading...
</template>
</q-btn>
<q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm"
label="Compare"></q-btn>
<q-btn class="q-ma-sm btn-style" :disable="btnMetadataDisable"
v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>
</div>
<p>{{msg}}</p>
<q-btn :loading="progress" class="q-ma-sm btn-style" :disabled="btnStartDisable" icon="play_arrow"
v-on:click="mainProcess=true" padding="lg" label="Generate Slice">
<template v-slot:loading>
<q-spinner-hourglass class="on-left" />
Loading...
</template>
</q-btn>
<q-btn icon="zoom_out_map" class="q-ma-sm on-right btn-style" v-on:click="compareBtn=true" padding="sm"
label="Compare"></q-btn>
<!--<q-btn class="q-ma-sm btn-style" :disable="btnMetadataDisable"
v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>-->
<q-btn class="q-ma-sm btn-style" v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>
<div class="q-pa-md row items-center" v-show="progress">
<q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner>
<div class="text-caption">{{ progress_message }}</div>
</div>
</div>
<p>{{msg}}</p>
<div class="row st-col col-12">
<q-btn-dropdown class="q-ma-sm btn-style" :loading="progressPlot" :disable="btnPlotDisable"
label="Generate Plots" icon="play_arrow">
@ -177,13 +204,13 @@
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="mzml_full_route" readonly label="Select your .mzML file" v-on:click="btnSearchMzml=true">
<template v-slot:append>
<q-icon name="search" v:onclick="btnSearchMzml=true" class="cursor-pointer" />
<q-icon name="search" v-on:click="btnSearchMzml=true" class="cursor-pointer" />
</template>
</q-input>
<q-input standout="custom-standout" class="q-ma-sm cursor-pointer" v-model="sync_full_route" readonly label="Select your .txt sync file" v-on:click="btnSearchSync=true">
<template v-slot:append>
<q-icon name="search" v:onclick="btnSearchSync=true" class="cursor-pointer" />
<q-icon name="search" v-on:click="btnSearchSync=true" class="cursor-pointer" />
</template>
</q-input>
@ -205,9 +232,10 @@
<q-tab-panels v-model="selectedTab">
<q-tab-panel name="tab0">
<!-- Content for Tab 0 -->
<!-- Btn image changer -->
<h6>Image visualizer</h6>
<div>
<div class="row items-center">
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"></q-btn>
</div>
@ -221,14 +249,15 @@
<q-img id="colorbar-normal" class="q-ma-none q-pa-none" :src="colorbar"></q-img>
</div>
</div>
<p>{{msgimg}}</p>
<p v-html="msgimg"></p>
</q-tab-panel>
<q-tab-panel name="tab1">
<!-- Content for Tab 1 -->
<!-- Triq Btn image changer -->
<h6>TrIQ visualizer</h6>
<div>
<div class="row items-center">
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"></q-btn>
</div>
@ -242,11 +271,38 @@
<q-img id="colorbar-triq" class="q-ma-none q-pa-none" :src="colorbarT"></q-img>
</div>
</div>
<p>{{msgtriq}}</p>
<p v-html="msgtriq"></p>
</q-tab-panel>
<q-tab-panel name="tab2">
<!-- Content for Tab 2 -->
<div class="row items-center">
<q-select v-model="selected_folder_main" :options="available_folders" label="Select Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-btn-dropdown class="q-ma-sm btn-style" :loading="progressSpectraPlot" :disable="btnSpectraDisable"
label="Generate Spectra" icon="play_arrow">
<template v-slot:loading>
<q-spinner-hourglass class="on-left" />
Loading plot
</template>
<q-list>
<q-item clickable v-close-popup v-on:click="createMeanPlot=true">
<q-item-section>
<q-item-label>Mean spectrum plot</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup v-on:click="createSumPlot=true">
<q-item-section>
<q-item-label>Sum Spectrum plot</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup v-on:click="createXYPlot=true">
<q-item-section>
<q-item-label>Spectrum plot (X,Y)</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
<plotly id="plotSpectra" :data="plotdata" :layout="plotlayout" class="q-pa-none q-ma-none"></plotly>
</q-tab-panel>
@ -281,8 +337,8 @@
<q-dialog v-model="CompareDialog" full-width>
<q-card>
<q-card-section class="row">
<div class="text-h6 q-ma-sm">Compare two diferent images or plots</div>
<q-card-section class="row items-center">
<div class="text-h6 q-ma-sm">Compare two different images or plots</div>
<div class="q-mx-sm">
<q-slider color="black" v-model="imgTrans" :min="0.0" :max="1" :step="0.1" :disable="btnOpticalDisable" />
<q-badge style="background-color: #009f90;"> Transparency: {{ imgTrans }}</q-badge>
@ -294,46 +350,50 @@
<q-card-section class="q-pt-none col-12">
<div class="row">
<div class="col-6">
<st-tabs id="tabHeaderCompareLeft" :ids="tabIDs" :labels="tabLabels" v-model="selectedTab"></st-tabs>
<q-tab-panels v-model="selectedTab">
<div class="row items-center">
<q-select v-model="selected_folder_compare_left" :options="image_available_folders" label="Select Left Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<st-tabs id="tabHeaderCompareLeft" :ids="CompTabIDsLeft" :labels="CompTabLabelsLeft" v-model="CompSelectedTabLeft"></st-tabs>
</div>
<q-tab-panels v-model="CompSelectedTabLeft">
<q-tab-panel name="tab0">
<!-- Content for Tab 0 -->
<!-- Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinus=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlus=true"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompLeft=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusCompLeft=true"></q-btn>
</div>
<!-- Image manager -->
<div id="image-container-compare-left-normal" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgCompareLeft" :data="plotdataImg" :layout="plotlayoutImg" class="q-pa-none q-ma-none">
<plotly id="plotImgCompareLeft" :data="plotdataImgCompLeft" :layout="plotlayoutImgCompLeft" class="q-pa-none q-ma-none">
</plotly>
</div>
<div class="col-2">
<q-img id="colorbarCompareLeft" class="q-ma-none q-pa-none" :src="colorbar"></q-img>
<q-img id="colorbarCompareLeft" class="q-ma-none q-pa-none" :src="colorbarCompLeft"></q-img>
</div>
</div>
<p>{{msgimg}}</p>
<p v-html="msgimgCompLeft"></p>
</q-tab-panel>
<q-tab-panel name="tab1">
<!-- Content for Tab 1 -->
<!-- Triq Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusT=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusT=true"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompLeft=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="imgPlusTCompLeft=true"></q-btn>
</div>
<!-- Triq Image manager -->
<div id="image-container-compare-left-triq" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgTCompareLeft" :data="plotdataImgT" :layout="plotlayoutImgT" class="q-pa-none q-ma-none">
<plotly id="plotImgTCompareLeft" :data="plotdataImgTCompLeft" :layout="plotlayoutImgTCompLeft" class="q-pa-none q-ma-none">
</plotly>
</div>
<div class="col-2">
<q-img id="colorbarTCompareLeft" class="q-ma-none q-pa-none" :src="colorbarT"></q-img>
<q-img id="colorbarTCompareLeft" class="q-ma-none q-pa-none" :src="colorbarTCompLeft"></q-img>
</div>
</div>
<p>{{msgtriq}}</p>
<p v-html="msgtriqCompLeft"></p>
</q-tab-panel>
<q-tab-panel name="tab2">
<!-- Content for Tab 2 -->
@ -350,51 +410,54 @@
</q-tab-panels>
</div>
<div class="col-6">
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDs" :labels="CompTabLabels" v-model="CompSelectedTab" outside-arrows
mobile-arrows></st-tabs>
<q-tab-panels v-model="CompSelectedTab">
<div class="row items-center">
<q-select v-model="selected_folder_compare_right" :options="image_available_folders" label="Select Right Dataset" class="q-ma-sm" style="min-width: 200px;"></q-select>
<q-space></q-space>
<st-tabs id="tabHeaderCompareRight" :ids="CompTabIDsRight" :labels="CompTabLabelsRight" v-model="CompSelectedTabRight"></st-tabs>
</div>
<q-tab-panels v-model="CompSelectedTabRight">
<q-tab-panel name="tab0">
<!-- Content for Tab 0 -->
<!-- Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusComp=true"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusCompRight=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusComp=true"></q-btn>
v-on:click="imgPlusCompRight=true"></q-btn>
</div>
<!-- Image manager -->
<div id="image-container-compare-right-normal" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgCompareRight" :data="plotdataImgComp" :layout="plotlayoutImgComp"
<plotly id="plotImgCompareRight" :data="plotdataImgCompRight" :layout="plotlayoutImgCompRight"
class="q-pa-none q-ma-none">
</plotly>
</div>
<div class="col-2 q-pa-none q-ma-none ">
<q-img id="colorbarComp" class="q-ma-none q-pa-none" :src="colorbarComp"></q-img>
<q-img id="colorbarCompRight" class="q-ma-none q-pa-none" :src="colorbarCompRight"></q-img>
</div>
</div>
<p>{{msgimgComp}}</p>
<p v-html="msgimgCompRight"></p>
</q-tab-panel>
<q-tab-panel name="tab1">
<!-- Content for Tab 1 -->
<!-- Triq Btn image changer -->
<div>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTComp=true"></q-btn>
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="imgMinusTCompRight=true"></q-btn>
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style"
v-on:click="imgPlusTComp=true"></q-btn>
v-on:click="imgPlusTCompRight=true"></q-btn>
</div>
<!-- Triq Image manager -->
<div id="image-container-compare-right-triq" class="row st-col col-12">
<div class="col-10 q-pa-none q-ma-none ">
<plotly id="plotImgTCompareRight" :data="plotdataImgTComp" :layout="plotlayoutImgTComp"
<plotly id="plotImgTCompareRight" :data="plotdataImgTCompRight" :layout="plotlayoutImgTCompRight"
class="q-pa-none q-ma-none">
</plotly>
</div>
<div class="col-2 q-pa-none q-ma-none ">
<q-img id="colorbarTComp" class="q-ma-none q-pa-none" :src="colorbarTComp"></q-img>
<q-img id="colorbarTCompRight" class="q-ma-none q-pa-none" :src="colorbarTCompRight"></q-img>
</div>
</div>
<p>{{msgtriqComp}}</p>
<p v-html="msgtriqCompRight"></p>
</q-tab-panel>
<q-tab-panel name="tab2">
<!-- Content for Tab 2 -->
@ -419,23 +482,53 @@
</q-card>
</q-dialog>
<q-dialog v-model="showBatchSummary">
<q-card>
<q-card-section>
<div class="text-h6">Batch Process Summary</div>
</q-card-section>
<q-card-section class="q-pt-none">
<pre>{{ batch_summary }}</pre>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Ok" style="color:#009f90" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="showMetadataDialog" full-width full-height>
<q-card>
<q-card-section>
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Dataset Summary</div>
<q-space />
<q-select
v-model="selected_folder_metadata"
:options="available_folders"
label="Select Dataset"
class="q-ma-sm"
style="min-width: 250px;"
standout="custom-standout"
></q-select>
</q-card-section>
<q-card-section class="q-pt-none">
<q-list bordered separator>
<q-list bordered separator v-if="metadata_rows.length > 0">
<q-item v-for="row in metadata_rows" :key="row.parameter">
<q-item-section>
<q-item-label>{{ row.parameter }}</q-item-label>
<q-item-label class="text-body1">{{ row.parameter }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-item-label caption>{{ row.value }}</q-item-label>
<q-item-label caption class="text-body1">{{ row.value }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
<div v-else class="text-center q-pa-md text-grey-7">
<q-icon name="info" size="3em" />
<p class="q-mt-md">No metadata found in registry for this dataset.</p>
<p class="text-caption">You may need to process the file first.</p>
</div>
</q-card-section>
<q-card-actions align="right">

View File

@ -452,10 +452,11 @@ end
# returns the layout and data for the surface plotly plot
# this function loads the spectra data and makes a mean to display
# its values in the spectrum plot
function meanSpectrumPlot(data::MSIData)
function meanSpectrumPlot(data::MSIData, dataset_name::String="")
title_text = isempty(dataset_name) ? "Average Spectrum Plot" : "Average Spectrum for: $dataset_name"
layout = PlotlyBase.Layout(
title=PlotlyBase.attr(
text="Average Spectrum Plot",
text=title_text,
font=PlotlyBase.attr(
family="Roboto, Lato, sans-serif",
size=18,
@ -480,9 +481,9 @@ function meanSpectrumPlot(data::MSIData)
if isempty(xSpectraMz)
@warn "Average spectrum is empty."
trace = PlotlyBase.stem(x=Float64[], y=Float64[])
trace = PlotlyBase.scatter(x=Float64[], y=Float64[])
else
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Average", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
end
@ -491,7 +492,7 @@ function meanSpectrumPlot(data::MSIData)
return plotdata, plotlayout, xSpectraMz, ySpectraMz
end
function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int)
function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int, imgHeight::Int, dataset_name::String="")
local mz::AbstractVector, intensity::AbstractVector
local plot_title::String
@ -502,24 +503,24 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
x = clamp(xCoord, 1, imgWidth)
y = clamp(yCoord, 1, imgHeight)
# mz, intensity = GetSpectrum(data, Int(x), Int(y))
process_spectrum(data, Int(x), Int(y)) do recieved_mz, recieved_intensity
mz = recieved_mz
intensity = recieved_intensity
end
plot_title = "Spectrum at ($x, $y)"
base_title = "Spectrum at ($x, $y)"
else
# For non-imaging data, treat xCoord as the spectrum index
index = clamp(xCoord, 1, length(data.spectra_metadata))
# mz, intensity = GetSpectrum(data, index)
process_spectrum(data, index) do recieved_mz, recieved_intensity
mz = recieved_mz
intensity = recieved_intensity
end
plot_title = "Spectrum #$index"
base_title = "Spectrum #$index"
end
plot_title = isempty(dataset_name) ? base_title : "$base_title for: $dataset_name"
layout = PlotlyBase.Layout(
title=PlotlyBase.attr(
text=plot_title,
@ -545,7 +546,7 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
# Downsample for plotting performance
mz_down, int_down = MSI_src.downsample_spectrum(mz, intensity)
trace = PlotlyBase.stem(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
trace = PlotlyBase.scatter(x=mz_down, y=int_down, marker=attr(size=1, color="blue", opacity=0.5), name="Spectrum", hoverinfo="x", hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
plotdata = [trace]
plotlayout = layout
@ -553,10 +554,11 @@ function xySpectrumPlot(data::MSIData, xCoord::Int, yCoord::Int, imgWidth::Int,
return plotdata, plotlayout, mz, intensity
end
function sumSpectrumPlot(data::MSIData)
function sumSpectrumPlot(data::MSIData, dataset_name::String="")
title_text = isempty(dataset_name) ? "Total Spectrum Plot" : "Total Spectrum for: $dataset_name"
layout = PlotlyBase.Layout(
title=PlotlyBase.attr(
text="Total Spectrum Plot",
text=title_text,
font=PlotlyBase.attr(
family="Roboto, Lato, sans-serif",
size=18,
@ -581,12 +583,47 @@ function sumSpectrumPlot(data::MSIData)
if isempty(xSpectraMz)
@warn "Total spectrum is empty."
trace = PlotlyBase.stem(x=Float64[], y=Float64[])
trace = PlotlyBase.scatter(x=Float64[], y=Float64[])
else
trace = PlotlyBase.stem(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
trace = PlotlyBase.scatter(x=xSpectraMz, y=ySpectraMz, marker=attr(size=1, color="blue", opacity=0.5), name="Total", hoverinfo="x",hovertemplate="<b>m/z</b>: %{x:.4f}<extra></extra>")
end
plotdata = [trace]
plotlayout = layout
return plotdata, plotlayout, xSpectraMz, ySpectraMz
end
function warmup_init()
@async begin
println("Pre-compiling functions at startup...")
# Create a dummy MSIData object to be used for pre-compilation
# dummy_source = ImzMLSource("dummy.ibd", Float32, Float32)
# dummy_meta = MSI_src.SpectrumMetadata(0,0,"",MSI_src.UNKNOWN, MSI_src.SpectrumAsset(Float32,false,0,0,:mz), MSI_src.SpectrumAsset(Float32,false,0,0,:intensity))
# dummy_msi_data = MSIData(dummy_source, [dummy_meta], (1,1), zeros(Int,1,1), 0)
# Pre-compile functions from btnSearch
# try OpenMSIData("dummy.imzML") catch end
# try precompute_analytics(dummy_msi_data) catch end
# Pre-compile functions from mainProcess
# try get_mz_slice(dummy_msi_data, 1.0, 1.0) catch end
try TrIQ(zeros(10,10), 256, 0.98) catch end
try quantize_intensity(zeros(10,10), 256) catch end
dummy_bmp_path = joinpath("public", "dummy.bmp")
dummy_png_path = joinpath("public", "dummy.png")
try
save_bitmap(dummy_bmp_path, zeros(UInt8, 10, 10), ViridisPalette)
loadImgPlot("/dummy.bmp")
generate_colorbar_image(zeros(10,10), 256, dummy_png_path)
catch e
@warn "Pre-compilation step failed (this is expected if dummy files can't be created/read)"
finally
rm(dummy_bmp_path, force=true)
rm(dummy_png_path, force=true)
end
println("Pre-compilation finished.")
end
end

View File

@ -82,7 +82,7 @@
}
#intDivStyle-left .q-tab-panels {
height: 670px; /* Set this to accommodate your tallest content */
height: 700px; /* Set this to accommodate your tallest content */
}
#intDivStyle-left .q-tab-panel {

View File

@ -497,7 +497,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
open(target_ibd_file, "w") do ibd_stream
write(ibd_stream, zeros(UInt8, 16)) # UUID placeholder
end
return BinaryMetadata[], Tuple{Int, Int}[], (0, 0)
return BinaryMetadata[], Tuple{Int, Int}[], (0, 0), UNKNOWN
end
width = maximum(timing_matrix[:, 1])
@ -505,6 +505,11 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
msi_data = OpenMSIData(source_file)
source_mode = UNKNOWN
if !isempty(msi_data.spectra_metadata)
source_mode = msi_data.spectra_metadata[1].mode
end
scan_time_deltas = zeros(Int64, size(scans, 1))
if size(scans, 1) > 1
for i in 1:(size(scans, 1) - 1)
@ -553,7 +558,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
# Write m/z array
mz_offset = position(ibd_stream)
for val in mz
write(ibd_stream, htol(Float64(val)))
write(ibd_stream, htol(Float32(val)))
end
mz_length = position(ibd_stream) - mz_offset
@ -569,7 +574,7 @@ function ConvertMzmlToImzml(source_file::String, target_ibd_file::String, timing
end
@info "Found and processed $empty_pixel_count empty pixels out of $(size(timing_matrix, 1)) total."
return binary_meta_vec, coords_vec, (width, height)
return binary_meta_vec, coords_vec, (width, height), source_mode
end
"""
@ -589,7 +594,7 @@ experiment and data format.
# Returns
- `true` on success, `false` on failure.
"""
function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, coords::Vector{Tuple{Int, Int}}, dims::Tuple{Int, Int})
function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, coords::Vector{Tuple{Int, Int}}, dims::Tuple{Int, Int}, mode::SpectrumMode)
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
if isempty(binary_meta)
@ -604,8 +609,7 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
open(target_file, "w") do imzml_stream
# XML Header
write(imzml_stream, """<?xml version="1.0" encoding="ISO-8859-1"?>
<indexedmzML xmlns="http://psi.hupo.org/ms/mzml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.0_idx.xsd">
<mzML version="1.1" id="$(splitext(basename(target_file))[1])">
<mzML xmlns="http://psi.hupo.org/ms/mzml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://psi.hupo.org/ms/mzml http://psidev.info/files/ms/mzML/xsd/mzML1.1.0.xsd" version="1.1" id="$(splitext(basename(target_file))[1])">
""")
# CV List, File Description, etc. (static parts)
@ -627,13 +631,11 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
<referenceableParamGroup id="mzArray">
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
<cvParam cvRef="MS" accession="MS:1000514" name="m/z array" unitCvRef="MS" unitAccession="MS:1000040" unitName="m/z"/>
<cvParam cvRef="IMS" accession="IMS:1000101" name="external data" value="true"/>
<cvParam cvRef="MS" accession="MS:1000523" name="64-bit float"/>
<cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/>
</referenceableParamGroup>
<referenceableParamGroup id="intensityArray">
<cvParam cvRef="MS" accession="MS:1000576" name="no compression"/>
<cvParam cvRef="MS" accession="MS:1000515" name="intensity array" unitCvRef="MS" unitAccession="MS:1000131" unitName="number of counts"/>
<cvParam cvRef="IMS" accession="IMS:1000101" name="external data" value="true"/>
<cvParam cvRef="MS" accession="MS:1000521" name="32-bit float"/>
</referenceableParamGroup>
</referenceableParamGroupList>
@ -699,13 +701,20 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
push!(spectrum_offsets, spectrum_start)
# Calculate number of points from byte length
mz_points = meta.mz_length ÷ sizeof(Float64)
mz_points = meta.mz_length ÷ sizeof(Float32)
int_points = meta.int_length ÷ sizeof(Float32)
write(imzml_stream, """ <spectrum id="Scan=$(i)" defaultArrayLength="$(mz_points)" index="$(i-1)">
<cvParam cvRef="MS" accession="MS:1000511" name="ms level" value="1"/>
<cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/>
<scanList count="1">
""")
if mode == CENTROID
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000127" name="centroid spectrum"/>
""")
else
write(imzml_stream, """ <cvParam cvRef="MS" accession="MS:1000128" name="profile spectrum"/>
""")
end
write(imzml_stream, """ <scanList count="1">
<scan instrumentConfigurationRef="instrument1">
<cvParam cvRef="IMS" accession="IMS:1000050" name="position x" value="$(x)"/>
<cvParam cvRef="IMS" accession="IMS:1000051" name="position y" value="$(y)"/>
@ -714,16 +723,14 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
<binaryDataArrayList count="2">
<binaryDataArray encodedLength="0">
<referenceableParamGroupRef ref="mzArray"/>
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.mz_offset)"/>
<cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(mz_points)"/>
<cvParam cvRef="IMS" accession="IMS:1000104" name="external encoded length" value="$(meta.mz_length)"/>
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.mz_offset)"/>
<binary/>
</binaryDataArray>
<binaryDataArray encodedLength="0">
<referenceableParamGroupRef ref="intensityArray"/>
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.int_offset)"/>
<cvParam cvRef="IMS" accession="IMS:1000103" name="external array length" value="$(int_points)"/>
<cvParam cvRef="IMS" accession="IMS:1000104" name="external encoded length" value="$(meta.int_length)"/>
<cvParam cvRef="IMS" accession="IMS:1000102" name="external offset" value="$(meta.int_offset)"/>
<binary/>
</binaryDataArray>
</binaryDataArrayList>
@ -734,19 +741,6 @@ function ExportImzml(target_file::String, binary_meta::Vector{BinaryMetadata}, c
write(imzml_stream, """ </spectrumList>
</run>
</mzML>
""")
# Index List
index_list_start = position(imzml_stream)
write(imzml_stream, """ <indexList count="1">
<index name="spectrum">
""")
for (i, offset) in enumerate(spectrum_offsets)
write(imzml_stream, " <offset idRef=\"Scan=$(i)\">$offset</offset>\n")
end
write(imzml_stream, """ </index>
</indexList>
<indexListOffset>$index_list_start</indexListOffset>
</indexedmzML>
""")
end
@ -784,13 +778,13 @@ function ImportMzmlFile(source_file::String, sync_file::String, target_file::Str
println("Step 3: Converting spectra and writing .ibd file...")
ibd_file = replace(target_file, r"\.imzML$"i => ".ibd")
binary_meta, coords, (width, height) = ConvertMzmlToImzml(source_file, ibd_file, timing_matrix, scans)
binary_meta, coords, (width, height), source_mode = ConvertMzmlToImzml(source_file, ibd_file, timing_matrix, scans)
# Flip image vertically to match R script output
flipped_coords = [(x, height - y + 1) for (x, y) in coords]
println("Step 4: Exporting .imzML metadata file...")
success = ExportImzml(target_file, binary_meta, flipped_coords, (width, height))
success = ExportImzml(target_file, binary_meta, flipped_coords, (width, height), source_mode)
if success
println("Conversion successful: $target_file")

View File

@ -793,6 +793,84 @@ function get_mz_slice(data::MSIData, mass::Real, tolerance::Real)
end
"""
get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real)
Extracts multiple image slices for a given list of m/z values in a single pass.
This is a highly performant function that iterates through the full dataset only once.
# Returns
- A `Dict{Real, Matrix{Float64}}` mapping each mass to its intensity slice matrix.
"""
function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real)
width, height = data.image_dims
# 1. Initialize a dictionary to hold the output slice matrices
slice_dict = Dict{Real, Matrix{Float64}}()
for mass in masses
slice_dict[mass] = zeros(Float64, height, width)
end
# 2. Ensure analytics are computed for filtering.
if data.spectrum_stats_df === nothing || !hasproperty(data.spectrum_stats_df, :MinMZ)
println("Per-spectrum metadata not found. Running one-time analytics computation...")
precompute_analytics(data)
end
println("Filtering candidate spectra for $(length(masses)) m/z values...")
stats_df = data.spectrum_stats_df
candidate_indices = Set{Int}()
# 3. Find all spectra that could contain *any* of the requested masses.
for mass in masses
target_min = mass - tolerance
target_max = mass + tolerance
for i in 1:length(data.spectra_metadata)
# If already a candidate, no need to check again
if i in candidate_indices
continue
end
spec_min_mz = stats_df.MinMZ[i]
spec_max_mz = stats_df.MaxMZ[i]
if target_max >= spec_min_mz && target_min <= spec_max_mz
push!(candidate_indices, i)
end
end
end
println("Found $(length(candidate_indices)) total candidate spectra.")
# 4. Iterate through the data a single time using the optimized iterator.
_iterate_spectra_fast(data) do idx, mz_array, intensity_array
# Process only the spectra that are candidates
if idx in candidate_indices
meta = data.spectra_metadata[idx]
# For this single spectrum, check all masses of interest
for mass in masses
# Check if this spectrum's range actually covers the current mass
# This is a finer-grained check than the initial filtering
if !isempty(mz_array) && (mass + tolerance) >= first(mz_array) && (mass - tolerance) <= last(mz_array)
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
if intensity > 0.0
if 1 <= meta.x <= width && 1 <= meta.y <= height
slice_dict[mass][meta.y, meta.x] = intensity
end
end
end
end
end
end
# 5. Clean up and return
for mass in masses
replace!(slice_dict[mass], NaN => 0.0)
end
println("Finished generating $(length(masses)) slices in a single pass.")
return slice_dict
end
"""
plot_slice(msi_data::MSIData, mass::Float64, tolerance::Float64, output_dir::String; stage_name="slice", bins=256)

View File

@ -103,9 +103,37 @@ the m/z and intensity array metadata.
function parse_spectrum_metadata(stream::IO, offset::Int64)
seek(stream, offset)
id_match = find_tag(stream, r"<spectrum\s+index=\"\d+\"\s+id=\"([^\"]+)" )
# Read the whole spectrum block to parse mode
spectrum_start_pos = position(stream)
line = ""
spectrum_buffer = IOBuffer()
while !eof(stream)
line = readline(stream)
write(spectrum_buffer, line)
if occursin("</spectrum>", line)
break
end
end
spectrum_xml = String(take!(spectrum_buffer))
seek(stream, spectrum_start_pos) # Reset for other parsing
id_match = match(r"<spectrum\s+index=\"\d+\"\s+id=\"([^\"]+)", spectrum_xml)
id = id_match === nothing ? "" : id_match.captures[1]
# Determine mode from the XML block
mode = UNKNOWN
if occursin("MS:1000127", spectrum_xml)
mode = CENTROID
elseif occursin("MS:1000128", spectrum_xml)
mode = PROFILE
end
# Find where the binary data list starts to parse assets
binary_list_match = findfirst("<binaryDataArrayList", spectrum_xml)
if binary_list_match !== nothing
seek(stream, spectrum_start_pos + binary_list_match.start - 1)
end
asset1 = get_spectrum_asset_metadata(stream)
asset2 = get_spectrum_asset_metadata(stream)
@ -118,7 +146,7 @@ function parse_spectrum_metadata(stream::IO, offset::Int64)
# Create the new unified metadata object
# For mzML, x and y coordinates are not applicable, so we use 0.
return SpectrumMetadata(Int32(0), Int32(0), id, UNKNOWN, mz_asset, int_asset)
return SpectrumMetadata(Int32(0), Int32(0), id, mode, mz_asset, int_asset)
end
"""