Added mask interface accessible from the UI, with it you can create slices from your masks or your images of the samples, included masking in the main processing, on the spectra creation, and on the 3d surface plot, started with interface for pre processing spectral data
This commit is contained in:
parent
e13b5ef0cc
commit
3439a8a4dd
491
app.jl
491
app.jl
@ -1,3 +1,5 @@
|
|||||||
|
# app.jl
|
||||||
|
|
||||||
module App
|
module App
|
||||||
# ==Packages ==
|
# ==Packages ==
|
||||||
using GenieFramework # Set up Genie development environment.
|
using GenieFramework # Set up Genie development environment.
|
||||||
@ -6,7 +8,6 @@ using Libz
|
|||||||
using PlotlyBase
|
using PlotlyBase
|
||||||
using CairoMakie
|
using CairoMakie
|
||||||
using Colors
|
using Colors
|
||||||
# using julia_mzML_imzML
|
|
||||||
using MSI_src # Import the new MSIData library
|
using MSI_src # Import the new MSIData library
|
||||||
using Statistics
|
using Statistics
|
||||||
using NaturalSort
|
using NaturalSort
|
||||||
@ -20,9 +21,50 @@ using JSON
|
|||||||
using Dates
|
using Dates
|
||||||
|
|
||||||
# Bring MSIData into App module's scope
|
# Bring MSIData into App module's scope
|
||||||
using .MSI_src: MSIData, OpenMSIData, #=GetSpectrum,=# process_spectrum, IterateSpectra, ImzMLSource, _iterate_spectra_fast, MzMLSource, find_mass, ViridisPalette, get_mz_slice, get_multiple_mz_slices, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile
|
using .MSI_src: MSIData, OpenMSIData, process_spectrum, IterateSpectra, ImzMLSource, _iterate_spectra_fast, MzMLSource, find_mass, ViridisPalette, get_mz_slice, get_multiple_mz_slices, quantize_intensity, save_bitmap, median_filter, save_bitmap, downsample_spectrum, TrIQ, precompute_analytics, ImportMzmlFile, generate_colorbar_image, load_and_prepare_mask
|
||||||
|
|
||||||
include("./julia_imzML_visual.jl")
|
if !@isdefined(increment_image)
|
||||||
|
include("./julia_imzML_visual.jl")
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Memory Validation Logging ---
|
||||||
|
if get(ENV, "GENIE_ENV", "dev") != "prod"
|
||||||
|
function get_rss_mb()
|
||||||
|
if !Sys.islinux()
|
||||||
|
return 0.0
|
||||||
|
end
|
||||||
|
try
|
||||||
|
pid = getpid()
|
||||||
|
cmd = `ps -p $pid -o rss=`
|
||||||
|
rss_kb_str = read(cmd, String)
|
||||||
|
rss_kb = parse(Int, strip(rss_kb_str))
|
||||||
|
return round(rss_kb / 1024, digits=2)
|
||||||
|
catch e
|
||||||
|
@warn "Could not get RSS via `ps` command. Error: $e"
|
||||||
|
return 0.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function log_memory_usage(context::String, msi_data_val)
|
||||||
|
rss_mb = get_rss_mb()
|
||||||
|
|
||||||
|
msi_data_size_mb = 0
|
||||||
|
if msi_data_val !== nothing
|
||||||
|
msi_data_size_mb = round(Base.summarysize(msi_data_val) / (1024^2), digits=2)
|
||||||
|
end
|
||||||
|
|
||||||
|
gc_time_s = round(GC.time(), digits=3)
|
||||||
|
|
||||||
|
println("--- MEMORY LOG [$(context)] ---")
|
||||||
|
println(" Timestamp: $(now())")
|
||||||
|
println(" Process RSS: $(rss_mb) MB")
|
||||||
|
println(" msi_data size: $(msi_data_size_mb) MB")
|
||||||
|
println(" Cumulative GC time: $(gc_time_s) s")
|
||||||
|
println("--------------------------")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log_memory_usage(context::String, msi_data_val) = nothing # No-op for production
|
||||||
|
end
|
||||||
|
|
||||||
@genietools
|
@genietools
|
||||||
|
|
||||||
@ -47,6 +89,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
@in triqEnabled=false
|
@in triqEnabled=false
|
||||||
@in SpectraEnabled=false
|
@in SpectraEnabled=false
|
||||||
@in MFilterEnabled=false
|
@in MFilterEnabled=false
|
||||||
|
@in maskEnabled=false
|
||||||
# Dialogs
|
# Dialogs
|
||||||
@in warning_msg=false
|
@in warning_msg=false
|
||||||
@in CompareDialog=false
|
@in CompareDialog=false
|
||||||
@ -160,6 +203,9 @@ include("./julia_imzML_visual.jl")
|
|||||||
@out msg_conversion = ""
|
@out msg_conversion = ""
|
||||||
@out btnConvertDisable = true
|
@out btnConvertDisable = true
|
||||||
|
|
||||||
|
# == Pre Processing Variables ==
|
||||||
|
@in pre_tab = "stabilization"
|
||||||
|
|
||||||
# == Batch Summary Dialog ==
|
# == Batch Summary Dialog ==
|
||||||
@in showBatchSummary = false
|
@in showBatchSummary = false
|
||||||
@out batch_summary = ""
|
@out batch_summary = ""
|
||||||
@ -271,7 +317,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
margin=attr(l=0,r=0,t=120,b=0,pad=0)
|
margin=attr(l=0,r=0,t=120,b=0,pad=0)
|
||||||
)
|
)
|
||||||
# Dummy 2D scatter plot
|
# Dummy 2D scatter plot
|
||||||
traceSpectra=PlotlyBase.scatter(x=Vector{Float64}(), y=Vector{Float64}(),marker=attr(size=1, color="blue", opacity=0.1))
|
traceSpectra=PlotlyBase.stem(x=Vector{Float64}(), y=Vector{Float64}(),marker=attr(size=1, color="blue", opacity=0.1))
|
||||||
# Create conection to frontend
|
# Create conection to frontend
|
||||||
@out plotdata=[traceSpectra]
|
@out plotdata=[traceSpectra]
|
||||||
@out plotlayout=layoutSpectra
|
@out plotlayout=layoutSpectra
|
||||||
@ -384,6 +430,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
imgWidth, imgHeight = dims[1], dims[2]
|
imgWidth, imgHeight = dims[1], dims[2]
|
||||||
|
|
||||||
msi_data = nothing # Ensure data is not held in memory
|
msi_data = nothing # Ensure data is not held in memory
|
||||||
|
log_memory_usage("Fast Load (msi_data cleared)", msi_data)
|
||||||
btnMetadataDisable = false
|
btnMetadataDisable = false
|
||||||
btnStartDisable = false
|
btnStartDisable = false
|
||||||
btnPlotDisable = false
|
btnPlotDisable = false
|
||||||
@ -443,6 +490,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
|
|
||||||
selected_folder_main = dataset_name
|
selected_folder_main = dataset_name
|
||||||
msi_data = loaded_data
|
msi_data = loaded_data
|
||||||
|
log_memory_usage("Full Load", msi_data)
|
||||||
|
|
||||||
eTime = round(time() - sTime, digits=3)
|
eTime = round(time() - sTime, digits=3)
|
||||||
msg = "Active file loaded in $(eTime) seconds. Dataset '$(dataset_name)' is ready for analysis."
|
msg = "Active file loaded in $(eTime) seconds. Dataset '$(dataset_name)' is ready for analysis."
|
||||||
@ -612,7 +660,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
overall_progress = 0.0
|
overall_progress = 0.0
|
||||||
progress_message = "Preparing batch process..."
|
progress_message = "Preparing batch process..."
|
||||||
|
|
||||||
# --- CAPTURE CURRENT VALUES HERE (NO []) ---
|
# --- CAPTURE CURRENT VALUES HERE ---
|
||||||
current_selected_files = selected_files
|
current_selected_files = selected_files
|
||||||
current_nmass = Nmass
|
current_nmass = Nmass
|
||||||
current_tol = Tol
|
current_tol = Tol
|
||||||
@ -620,6 +668,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
current_triq_enabled = triqEnabled
|
current_triq_enabled = triqEnabled
|
||||||
current_triq_prob = triqProb
|
current_triq_prob = triqProb
|
||||||
current_mfilter_enabled = MFilterEnabled
|
current_mfilter_enabled = MFilterEnabled
|
||||||
|
current_mask_enabled = maskEnabled
|
||||||
current_registry_path = registry_path
|
current_registry_path = registry_path
|
||||||
|
|
||||||
println("starting main process with $(length(current_selected_files)) files")
|
println("starting main process with $(length(current_selected_files)) files")
|
||||||
@ -635,62 +684,33 @@ include("./julia_imzML_visual.jl")
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
println("Nmass value: '$current_nmass'")
|
masses = Float64[]
|
||||||
println("Type of current_nmass: $(typeof(current_nmass))")
|
|
||||||
|
|
||||||
masses_str = split(current_nmass, ',', keepempty=false)
|
|
||||||
println("Parsed masses strings: $masses_str")
|
|
||||||
|
|
||||||
masses = Float64[]
|
|
||||||
try
|
try
|
||||||
masses = [parse(Float64, strip(m)) for m in masses_str]
|
masses = [parse(Float64, strip(m)) for m in split(current_nmass, ',', keepempty=false)]
|
||||||
println("Parsed masses: $masses")
|
|
||||||
catch e
|
catch e
|
||||||
progress_message = "Invalid m/z value(s). Please provide a comma-separated list of numbers. Error: $e"
|
progress_message = "Invalid m/z value(s). Please provide a comma-separated list of numbers. Error: $e"
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
println(progress_message)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
println("Masses array: $masses, type: $(typeof(masses))")
|
if isempty(masses)
|
||||||
|
|
||||||
if !(masses isa AbstractArray) || isempty(masses)
|
|
||||||
progress_message = "No valid m/z values found. Please provide comma-separated positive numbers."
|
progress_message = "No valid m/z values found. Please provide comma-separated positive numbers."
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
println(progress_message)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check other parameters
|
|
||||||
if !(0 < current_tol <= 1)
|
|
||||||
progress_message = "Tolerance must be between 0 and 1."
|
|
||||||
warning_msg = true
|
|
||||||
println(progress_message)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if !(1 < current_color_level < 257)
|
|
||||||
progress_message = "Color levels must be between 2 and 256."
|
|
||||||
warning_msg = true
|
|
||||||
println(progress_message)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
println("entering batch processing loop with masses: $masses")
|
|
||||||
|
|
||||||
# --- 2. Batch Processing Loop ---
|
# --- 2. Batch Processing Loop ---
|
||||||
num_files = length(current_selected_files)
|
num_files = length(current_selected_files)
|
||||||
total_steps = num_files
|
total_steps = num_files
|
||||||
current_step = 0
|
current_step = 0
|
||||||
errors = Dict("load_errors" => String[], "slice_errors" => String[], "io_errors" => String[])
|
errors = Dict("load_errors" => String[], "slice_errors" => String[], "io_errors" => String[])
|
||||||
newly_created_folders = String[]
|
newly_created_folders = String[]
|
||||||
|
files_without_mask = 0
|
||||||
|
|
||||||
for (file_idx, file_path) in enumerate(current_selected_files)
|
for (file_idx, file_path) in enumerate(current_selected_files)
|
||||||
# Update progress for current file
|
|
||||||
progress_message = "Processing file $file_idx/$num_files: $(basename(file_path))"
|
progress_message = "Processing file $file_idx/$num_files: $(basename(file_path))"
|
||||||
overall_progress = current_step / total_steps
|
overall_progress = current_step / total_steps
|
||||||
|
|
||||||
# Create parameters for this specific file
|
|
||||||
all_params = (
|
all_params = (
|
||||||
tolerance = current_tol,
|
tolerance = current_tol,
|
||||||
colorL = current_color_level,
|
colorL = current_color_level,
|
||||||
@ -702,14 +722,12 @@ include("./julia_imzML_visual.jl")
|
|||||||
nFiles = num_files
|
nFiles = num_files
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process the file
|
success, error_msg = process_file_safely(file_path, masses, all_params, progress_message, overall_progress, use_mask=current_mask_enabled)
|
||||||
success, error_msg = process_file_safely(file_path, masses, all_params, progress_message, overall_progress)
|
|
||||||
|
|
||||||
if !success
|
if !success
|
||||||
push!(errors["load_errors"], error_msg)
|
push!(errors["load_errors"], error_msg)
|
||||||
else
|
else
|
||||||
dataset_name = replace(basename(file_path), r"\.imzML$"i => "")
|
push!(newly_created_folders, replace(basename(file_path), r"\.imzML$"i => ""))
|
||||||
push!(newly_created_folders, dataset_name)
|
|
||||||
end
|
end
|
||||||
current_step += 1
|
current_step += 1
|
||||||
end
|
end
|
||||||
@ -717,7 +735,6 @@ include("./julia_imzML_visual.jl")
|
|||||||
# --- 3. Final Report ---
|
# --- 3. Final Report ---
|
||||||
total_time_end = round(time() - total_time_start, digits=3)
|
total_time_end = round(time() - total_time_start, digits=3)
|
||||||
|
|
||||||
# Update folder lists in UI
|
|
||||||
registry = load_registry(current_registry_path)
|
registry = load_registry(current_registry_path)
|
||||||
all_folders = sort(collect(keys(registry)), lt=natural)
|
all_folders = sort(collect(keys(registry)), lt=natural)
|
||||||
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)
|
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)
|
||||||
@ -738,8 +755,11 @@ include("./julia_imzML_visual.jl")
|
|||||||
warning_msg = true
|
warning_msg = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
mask_summary = current_mask_enabled ? "\nFiles processed without a mask: $(files_without_mask)" : ""
|
||||||
|
|
||||||
batch_summary = """
|
batch_summary = """
|
||||||
Processed $(successful_files)/$(num_files) files successfully.
|
Processed $(successful_files)/$(num_files) files successfully.
|
||||||
|
$(mask_summary)
|
||||||
|
|
||||||
Errors by category:
|
Errors by category:
|
||||||
• Load failures: $(length(errors["load_errors"]))
|
• Load failures: $(length(errors["load_errors"]))
|
||||||
@ -751,6 +771,56 @@ include("./julia_imzML_visual.jl")
|
|||||||
"""
|
"""
|
||||||
showBatchSummary = true
|
showBatchSummary = true
|
||||||
|
|
||||||
|
# Update UI to display the last generated image
|
||||||
|
if !isempty(newly_created_folders)
|
||||||
|
timestamp = string(time_ns())
|
||||||
|
folder_path = joinpath("public", selected_folder_main)
|
||||||
|
|
||||||
|
if current_triq_enabled
|
||||||
|
triq_files = filter(filename -> startswith(filename, "TrIQ_") && endswith(filename, ".bmp"), readdir(folder_path))
|
||||||
|
col_triq_files = filter(filename -> startswith(filename, "colorbar_TrIQ_") && endswith(filename, ".png"), readdir(folder_path))
|
||||||
|
|
||||||
|
if !isempty(triq_files)
|
||||||
|
latest_triq = triq_files[argmax([mtime(joinpath(folder_path, f)) for f in triq_files])]
|
||||||
|
current_triq = latest_triq
|
||||||
|
imgIntT = "/$(selected_folder_main)/$(current_triq)?t=$(timestamp)"
|
||||||
|
plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT)
|
||||||
|
text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "")
|
||||||
|
msgtriq = "TrIQ <i>m/z</i>: $(replace(text_nmass, "_" => "."))"
|
||||||
|
|
||||||
|
if !isempty(col_triq_files)
|
||||||
|
latest_col_triq = col_triq_files[argmax([mtime(joinpath(folder_path, f)) for f in col_triq_files])]
|
||||||
|
current_col_triq = latest_col_triq
|
||||||
|
colorbarT = "/$(selected_folder_main)/$(current_col_triq)?t=$(timestamp)"
|
||||||
|
else
|
||||||
|
colorbarT = ""
|
||||||
|
end
|
||||||
|
selectedTab = "tab1"
|
||||||
|
end
|
||||||
|
else # Not TrIQ enabled, display regular MSI image
|
||||||
|
msi_files = filter(filename -> startswith(filename, "MSI_") && endswith(filename, ".bmp"), readdir(folder_path))
|
||||||
|
col_msi_files = filter(filename -> startswith(filename, "colorbar_MSI_") && endswith(filename, ".png"), readdir(folder_path))
|
||||||
|
|
||||||
|
if !isempty(msi_files)
|
||||||
|
latest_msi = msi_files[argmax([mtime(joinpath(folder_path, f)) for f in msi_files])]
|
||||||
|
current_msi = latest_msi
|
||||||
|
imgInt = "/$(selected_folder_main)/$(current_msi)?t=$(timestamp)"
|
||||||
|
plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt)
|
||||||
|
text_nmass = replace(current_msi, r"MSI_|.bmp" => "")
|
||||||
|
msgimg = "<i>m/z</i>: $(replace(text_nmass, "_" => "."))"
|
||||||
|
|
||||||
|
if !isempty(col_msi_files)
|
||||||
|
latest_col_msi = col_msi_files[argmax([mtime(joinpath(folder_path, f)) for f in col_msi_files])]
|
||||||
|
current_col_msi = latest_col_msi
|
||||||
|
colorbar = "/$(selected_folder_main)/$(current_col_msi)?t=$(timestamp)"
|
||||||
|
else
|
||||||
|
colorbar = ""
|
||||||
|
end
|
||||||
|
selectedTab = "tab0"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
catch e
|
catch e
|
||||||
println("Error in main process: $e")
|
println("Error in main process: $e")
|
||||||
msg = "Batch processing failed: $e"
|
msg = "Batch processing failed: $e"
|
||||||
@ -766,6 +836,10 @@ include("./julia_imzML_visual.jl")
|
|||||||
SpectraEnabled = true
|
SpectraEnabled = true
|
||||||
overall_progress = 0.0
|
overall_progress = 0.0
|
||||||
println("Done")
|
println("Done")
|
||||||
|
GC.gc()
|
||||||
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -786,7 +860,9 @@ include("./julia_imzML_visual.jl")
|
|||||||
try
|
try
|
||||||
sTime = time()
|
sTime = time()
|
||||||
registry = load_registry(registry_path)
|
registry = load_registry(registry_path)
|
||||||
target_path = registry[selected_folder_main]["source_path"]
|
entry = registry[selected_folder_main]
|
||||||
|
target_path = entry["source_path"]
|
||||||
|
|
||||||
if target_path == "unknown (manually added)"
|
if target_path == "unknown (manually added)"
|
||||||
msg = "Dataset selected contained no route."
|
msg = "Dataset selected contained no route."
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
@ -797,22 +873,29 @@ include("./julia_imzML_visual.jl")
|
|||||||
msg = "Reloading $(basename(target_path)) for analysis..."
|
msg = "Reloading $(basename(target_path)) for analysis..."
|
||||||
full_route = target_path
|
full_route = target_path
|
||||||
msi_data = OpenMSIData(target_path)
|
msi_data = OpenMSIData(target_path)
|
||||||
|
if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing
|
||||||
existing_entry = get(registry, selected_folder_main, nothing)
|
msi_data.global_min_mz = entry["metadata"]["global_min_mz"]
|
||||||
if existing_entry !== nothing && haskey(get(existing_entry, "metadata", Dict()), "global_min_mz") && existing_entry["metadata"]["global_min_mz"] !== nothing
|
msi_data.global_max_mz = entry["metadata"]["global_max_mz"]
|
||||||
println("Injecting cached m/z range to skip Pass 1...")
|
|
||||||
msi_data.global_min_mz = existing_entry["metadata"]["global_min_mz"]
|
|
||||||
msi_data.global_max_mz = existing_entry["metadata"]["global_max_mz"]
|
|
||||||
else
|
else
|
||||||
precompute_analytics(msi_data)
|
precompute_analytics(msi_data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data, selected_folder_main)
|
local mask_path_for_plot::Union{String, Nothing} = nothing
|
||||||
|
if maskEnabled && get(entry, "has_mask", false)
|
||||||
|
mask_path_for_plot = get(entry, "mask_path", "")
|
||||||
|
if !isfile(mask_path_for_plot)
|
||||||
|
@warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask."
|
||||||
|
mask_path_for_plot = nothing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
plotdata, plotlayout, xSpectraMz, ySpectraMz = meanSpectrumPlot(msi_data, selected_folder_main, mask_path=mask_path_for_plot)
|
||||||
selectedTab = "tab2"
|
selectedTab = "tab2"
|
||||||
fTime = time()
|
fTime = time()
|
||||||
eTime = round(fTime - sTime, digits=3)
|
eTime = round(fTime - sTime, digits=3)
|
||||||
msg = "Plot loaded in $(eTime) seconds"
|
msg = "Plot loaded in $(eTime) seconds"
|
||||||
|
log_memory_usage("Mean Plot Generated", msi_data)
|
||||||
catch e
|
catch e
|
||||||
msg = "Could not generate mean spectrum plot: $e"
|
msg = "Could not generate mean spectrum plot: $e"
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
@ -822,6 +905,10 @@ include("./julia_imzML_visual.jl")
|
|||||||
btnPlotDisable = false
|
btnPlotDisable = false
|
||||||
btnSpectraDisable = false
|
btnSpectraDisable = false
|
||||||
btnStartDisable = false
|
btnStartDisable = false
|
||||||
|
GC.gc()
|
||||||
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -842,7 +929,9 @@ include("./julia_imzML_visual.jl")
|
|||||||
try
|
try
|
||||||
sTime = time()
|
sTime = time()
|
||||||
registry = load_registry(registry_path)
|
registry = load_registry(registry_path)
|
||||||
target_path = registry[selected_folder_main]["source_path"]
|
entry = registry[selected_folder_main]
|
||||||
|
target_path = entry["source_path"]
|
||||||
|
|
||||||
if target_path == "unknown (manually added)"
|
if target_path == "unknown (manually added)"
|
||||||
msg = "Dataset selected contained no route."
|
msg = "Dataset selected contained no route."
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
@ -853,22 +942,29 @@ include("./julia_imzML_visual.jl")
|
|||||||
msg = "Reloading $(basename(target_path)) for analysis..."
|
msg = "Reloading $(basename(target_path)) for analysis..."
|
||||||
full_route = target_path
|
full_route = target_path
|
||||||
msi_data = OpenMSIData(target_path)
|
msi_data = OpenMSIData(target_path)
|
||||||
|
if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing
|
||||||
existing_entry = get(registry, selected_folder_main, nothing)
|
msi_data.global_min_mz = entry["metadata"]["global_min_mz"]
|
||||||
if existing_entry !== nothing && haskey(get(existing_entry, "metadata", Dict()), "global_min_mz") && existing_entry["metadata"]["global_min_mz"] !== nothing
|
msi_data.global_max_mz = entry["metadata"]["global_max_mz"]
|
||||||
println("Injecting cached m/z range to skip Pass 1...")
|
|
||||||
msi_data.global_min_mz = existing_entry["metadata"]["global_min_mz"]
|
|
||||||
msi_data.global_max_mz = existing_entry["metadata"]["global_max_mz"]
|
|
||||||
else
|
else
|
||||||
precompute_analytics(msi_data)
|
precompute_analytics(msi_data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data, selected_folder_main)
|
local mask_path_for_plot::Union{String, Nothing} = nothing
|
||||||
|
if maskEnabled && get(entry, "has_mask", false)
|
||||||
|
mask_path_for_plot = get(entry, "mask_path", "")
|
||||||
|
if !isfile(mask_path_for_plot)
|
||||||
|
@warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask."
|
||||||
|
mask_path_for_plot = nothing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
plotdata, plotlayout, xSpectraMz, ySpectraMz = sumSpectrumPlot(msi_data, selected_folder_main, mask_path=mask_path_for_plot)
|
||||||
selectedTab = "tab2"
|
selectedTab = "tab2"
|
||||||
fTime = time()
|
fTime = time()
|
||||||
eTime = round(fTime - sTime, digits=3)
|
eTime = round(fTime - sTime, digits=3)
|
||||||
msg = "Total plot loaded in $(eTime) seconds"
|
msg = "Total plot loaded in $(eTime) seconds"
|
||||||
|
log_memory_usage("Sum Plot Generated", msi_data)
|
||||||
catch e
|
catch e
|
||||||
msg = "Could not generate total spectrum plot: $e"
|
msg = "Could not generate total spectrum plot: $e"
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
@ -878,6 +974,10 @@ include("./julia_imzML_visual.jl")
|
|||||||
btnPlotDisable = false
|
btnPlotDisable = false
|
||||||
btnSpectraDisable = false
|
btnSpectraDisable = false
|
||||||
btnStartDisable = false
|
btnStartDisable = false
|
||||||
|
GC.gc()
|
||||||
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -899,7 +999,17 @@ include("./julia_imzML_visual.jl")
|
|||||||
try
|
try
|
||||||
sTime = time()
|
sTime = time()
|
||||||
registry = load_registry(registry_path)
|
registry = load_registry(registry_path)
|
||||||
target_path = registry[selected_folder_main]["source_path"]
|
|
||||||
|
# Add error handling for registry access
|
||||||
|
if !haskey(registry, selected_folder_main)
|
||||||
|
msg = "Dataset '$selected_folder_main' not found in registry."
|
||||||
|
warning_msg = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
entry = registry[selected_folder_main]
|
||||||
|
target_path = entry["source_path"]
|
||||||
|
|
||||||
if target_path == "unknown (manually added)"
|
if target_path == "unknown (manually added)"
|
||||||
msg = "Dataset selected contained no route."
|
msg = "Dataset selected contained no route."
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
@ -910,25 +1020,62 @@ include("./julia_imzML_visual.jl")
|
|||||||
msg = "Reloading $(basename(target_path)) for analysis..."
|
msg = "Reloading $(basename(target_path)) for analysis..."
|
||||||
full_route = target_path
|
full_route = target_path
|
||||||
msi_data = OpenMSIData(target_path)
|
msi_data = OpenMSIData(target_path)
|
||||||
|
if haskey(get(entry, "metadata", Dict()), "global_min_mz") && entry["metadata"]["global_min_mz"] !== nothing
|
||||||
existing_entry = get(registry, selected_folder_main, nothing)
|
msi_data.global_min_mz = entry["metadata"]["global_min_mz"]
|
||||||
if existing_entry !== nothing && haskey(get(existing_entry, "metadata", Dict()), "global_min_mz") && existing_entry["metadata"]["global_min_mz"] !== nothing
|
msi_data.global_max_mz = entry["metadata"]["global_max_mz"]
|
||||||
println("Injecting cached m/z range to skip Pass 1...")
|
|
||||||
msi_data.global_min_mz = existing_entry["metadata"]["global_min_mz"]
|
|
||||||
msi_data.global_max_mz = existing_entry["metadata"]["global_max_mz"]
|
|
||||||
else
|
else
|
||||||
precompute_analytics(msi_data)
|
precompute_analytics(msi_data)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
y = yCoord < 0 ? abs(yCoord) : yCoord
|
local mask_path_for_plot::Union{String, Nothing} = nothing
|
||||||
plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y, imgWidth, imgHeight, selected_folder_main)
|
if maskEnabled && get(entry, "has_mask", false)
|
||||||
xCoord = plotlayout.title == "Spectrum #$(xCoord)" ? xCoord : clamp(xCoord, 1, imgWidth)
|
mask_path_for_plot = get(entry, "mask_path", "")
|
||||||
yCoord = plotlayout.title == "Spectrum #$(xCoord)" ? 0 : -clamp(y, 1, imgHeight)
|
if !isfile(mask_path_for_plot)
|
||||||
|
@warn "Mask not found for plotting: $(mask_path_for_plot). Plotting without mask."
|
||||||
|
mask_path_for_plot = nothing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Convert to positive coordinates for processing
|
||||||
|
y_positive = yCoord < 0 ? abs(yCoord) : yCoord
|
||||||
|
plotdata, plotlayout, xSpectraMz, ySpectraMz = xySpectrumPlot(msi_data, xCoord, y_positive, imgWidth, imgHeight, selected_folder_main, mask_path=mask_path_for_plot)
|
||||||
|
|
||||||
|
# Update coordinates based on actual plot title
|
||||||
|
# Extract title text from the Dict safely
|
||||||
|
actual_title = if plotlayout.title isa Dict && haskey(plotlayout.title, :text)
|
||||||
|
plotlayout.title[:text]
|
||||||
|
elseif plotlayout.title isa Dict && haskey(plotlayout.title, "text")
|
||||||
|
plotlayout.title["text"]
|
||||||
|
else
|
||||||
|
string(plotlayout.title) # Fallback
|
||||||
|
end
|
||||||
|
|
||||||
|
if occursin("Masked Spectrum at", actual_title)
|
||||||
|
# Extract coordinates from masked spectrum title
|
||||||
|
coords_match = match(r"Masked Spectrum at \((\d+), (\d+)\)", actual_title)
|
||||||
|
if coords_match !== nothing
|
||||||
|
xCoord = parse(Int, coords_match.captures[1])
|
||||||
|
yCoord = -parse(Int, coords_match.captures[2]) # Negative for display
|
||||||
|
end
|
||||||
|
elseif occursin("Spectrum at", actual_title)
|
||||||
|
# Extract coordinates from regular spectrum title
|
||||||
|
coords_match = match(r"Spectrum at \((\d+), (\d+)\)", actual_title)
|
||||||
|
if coords_match !== nothing
|
||||||
|
xCoord = parse(Int, coords_match.captures[1])
|
||||||
|
yCoord = -parse(Int, coords_match.captures[2]) # Negative for display
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# For non-imaging data or fallback, just clamp the coordinates
|
||||||
|
xCoord = clamp(xCoord, 1, imgWidth)
|
||||||
|
yCoord = yCoord < 0 ? yCoord : -clamp(yCoord, 1, imgHeight)
|
||||||
|
end
|
||||||
|
|
||||||
selectedTab = "tab2"
|
selectedTab = "tab2"
|
||||||
fTime = time()
|
fTime = time()
|
||||||
eTime = round(fTime - sTime, digits=3)
|
eTime = round(fTime - sTime, digits=3)
|
||||||
msg = "Plot loaded in $(eTime) seconds"
|
msg = "Plot loaded in $(eTime) seconds"
|
||||||
|
log_memory_usage("XY Plot Generated", msi_data)
|
||||||
catch e
|
catch e
|
||||||
msg = "Could not retrieve spectrum: $e"
|
msg = "Could not retrieve spectrum: $e"
|
||||||
warning_msg = true
|
warning_msg = true
|
||||||
@ -938,6 +1085,10 @@ include("./julia_imzML_visual.jl")
|
|||||||
btnPlotDisable = false
|
btnPlotDisable = false
|
||||||
btnSpectraDisable = false
|
btnSpectraDisable = false
|
||||||
btnStartDisable = false
|
btnStartDisable = false
|
||||||
|
GC.gc()
|
||||||
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1213,6 +1364,13 @@ include("./julia_imzML_visual.jl")
|
|||||||
|
|
||||||
# This handler will now correctly load the first image from the newly selected folder.
|
# This handler will now correctly load the first image from the newly selected folder.
|
||||||
@onchange selected_folder_main begin
|
@onchange selected_folder_main begin
|
||||||
|
msi_data = nothing
|
||||||
|
log_memory_usage("Folder Changed (msi_data cleared)", msi_data)
|
||||||
|
GC.gc()
|
||||||
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
|
end
|
||||||
|
|
||||||
if !isempty(selected_folder_main)
|
if !isempty(selected_folder_main)
|
||||||
folder_path = joinpath("public", selected_folder_main)
|
folder_path = joinpath("public", selected_folder_main)
|
||||||
if !isdir(folder_path)
|
if !isdir(folder_path)
|
||||||
@ -1226,6 +1384,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
plotlayoutImg = layoutImg
|
plotlayoutImg = layoutImg
|
||||||
plotdataImgT = [traceImg]
|
plotdataImgT = [traceImg]
|
||||||
plotlayoutImgT = layoutImg
|
plotlayoutImgT = layoutImg
|
||||||
|
imgWidth, imgHeight = 0, 0
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1236,7 +1395,8 @@ include("./julia_imzML_visual.jl")
|
|||||||
if !isempty(msi_bmp)
|
if !isempty(msi_bmp)
|
||||||
current_msi = first(msi_bmp)
|
current_msi = first(msi_bmp)
|
||||||
imgInt = "/$(selected_folder_main)/$(current_msi)"
|
imgInt = "/$(selected_folder_main)/$(current_msi)"
|
||||||
plotdataImg, plotlayoutImg, _, _ = loadImgPlot(imgInt)
|
plotdataImg, plotlayoutImg, w, h = loadImgPlot(imgInt)
|
||||||
|
imgWidth, imgHeight = w, h
|
||||||
text_nmass = replace(current_msi, r"MSI_|.bmp" => "")
|
text_nmass = replace(current_msi, r"MSI_|.bmp" => "")
|
||||||
msgimg = "<i>m/z</i>: $(replace(text_nmass, "_" => "."))"
|
msgimg = "<i>m/z</i>: $(replace(text_nmass, "_" => "."))"
|
||||||
if !isempty(col_msi_png)
|
if !isempty(col_msi_png)
|
||||||
@ -1260,7 +1420,11 @@ include("./julia_imzML_visual.jl")
|
|||||||
if !isempty(triq_bmp)
|
if !isempty(triq_bmp)
|
||||||
current_triq = first(triq_bmp)
|
current_triq = first(triq_bmp)
|
||||||
imgIntT = "/$(selected_folder_main)/$(current_triq)"
|
imgIntT = "/$(selected_folder_main)/$(current_triq)"
|
||||||
plotdataImgT, plotlayoutImgT, _, _ = loadImgPlot(imgIntT)
|
plotdataImgT, plotlayoutImgT, w, h = loadImgPlot(imgIntT)
|
||||||
|
# If no MSI image was loaded, dimensions from TrIQ image are used.
|
||||||
|
if isempty(msi_bmp)
|
||||||
|
imgWidth, imgHeight = w, h
|
||||||
|
end
|
||||||
text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "")
|
text_nmass = replace(current_triq, r"TrIQ_|.bmp" => "")
|
||||||
msgtriq = "TrIQ <i>m/z</i>: $(replace(text_nmass, "_" => "."))"
|
msgtriq = "TrIQ <i>m/z</i>: $(replace(text_nmass, "_" => "."))"
|
||||||
if !isempty(col_triq_png)
|
if !isempty(col_triq_png)
|
||||||
@ -1276,6 +1440,10 @@ include("./julia_imzML_visual.jl")
|
|||||||
plotdataImgT = [traceImg]
|
plotdataImgT = [traceImg]
|
||||||
plotlayoutImgT = layoutImg
|
plotlayoutImgT = layoutImg
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if isempty(msi_bmp) && isempty(triq_bmp)
|
||||||
|
imgWidth, imgHeight = 0, 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1391,90 +1559,130 @@ include("./julia_imzML_visual.jl")
|
|||||||
|
|
||||||
# 3d plot
|
# 3d plot
|
||||||
@onbutton image3dPlot begin
|
@onbutton image3dPlot begin
|
||||||
msg="Image 3D plot selected"
|
msg = "Image 3D plot selected"
|
||||||
cleaned_imgInt=replace(imgInt, r"\?.*" => "")
|
cleaned_imgInt = replace(imgInt, r"\?.*" => "")
|
||||||
cleaned_imgInt=lstrip(cleaned_imgInt, '/')
|
cleaned_imgInt = lstrip(cleaned_imgInt, '/')
|
||||||
var=joinpath( "./public", cleaned_imgInt )
|
var = joinpath("./public", cleaned_imgInt)
|
||||||
|
|
||||||
if !isfile(var)
|
if !isfile(var)
|
||||||
msg="Image could not be 3d plotted"
|
msg = "Image could not be 3d plotted"
|
||||||
warning_msg=true
|
warning_msg = true
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
progressPlot=true
|
progressPlot = true
|
||||||
btnPlotDisable=true
|
btnPlotDisable = true
|
||||||
btnStartDisable=true
|
btnStartDisable = true
|
||||||
btnSpectraDisable=true
|
btnSpectraDisable = true
|
||||||
|
|
||||||
@async begin
|
@async begin
|
||||||
try
|
try
|
||||||
sTime=time()
|
# --- Get Mask Path ---
|
||||||
plotdata3d, plotlayout3d=loadSurfacePlot(imgInt)
|
local mask_path_for_plot::Union{String, Nothing} = nothing
|
||||||
GC.gc() # Trigger garbage collection
|
if maskEnabled && !isempty(selected_folder_main)
|
||||||
if Sys.islinux()
|
registry = load_registry(registry_path)
|
||||||
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure julia returns the freed memory to OS
|
entry = get(registry, selected_folder_main, nothing)
|
||||||
|
if entry !== nothing && get(entry, "has_mask", false)
|
||||||
|
mask_path_candidate = get(entry, "mask_path", "")
|
||||||
|
if isfile(mask_path_candidate)
|
||||||
|
mask_path_for_plot = mask_path_candidate
|
||||||
|
else
|
||||||
|
@warn "Mask enabled but file not found: $(mask_path_candidate). Plotting without mask."
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
selectedTab="tab4"
|
# ---
|
||||||
fTime=time()
|
|
||||||
eTime=round(fTime-sTime,digits=3)
|
sTime = time()
|
||||||
msg="Plot loaded in $(eTime) seconds"
|
if mask_path_for_plot !== nothing
|
||||||
catch e
|
plotdata3d, plotlayout3d = loadSurfacePlot(imgInt, mask_path_for_plot)
|
||||||
msg="Failed to load and process image: $e"
|
else
|
||||||
warning_msg=true
|
plotdata3d, plotlayout3d = loadSurfacePlot(imgInt)
|
||||||
|
end
|
||||||
|
|
||||||
|
selectedTab = "tab4"
|
||||||
|
fTime = time()
|
||||||
|
eTime = round(fTime - sTime, digits=3)
|
||||||
|
msg = "Plot loaded in $(eTime) seconds"
|
||||||
|
log_memory_usage("Mean Plot Generated", msi_data)
|
||||||
|
catch e
|
||||||
|
msg = "Failed to load and process image: $e"
|
||||||
|
warning_msg = true
|
||||||
|
@error "3D plot generation failed" exception=(e, catch_backtrace())
|
||||||
finally
|
finally
|
||||||
progressPlot=false
|
progressPlot=false
|
||||||
btnPlotDisable=false
|
btnPlotDisable=false
|
||||||
btnStartDisable=false
|
btnStartDisable=false
|
||||||
if msi_data !== nothing
|
btnSpectraDisable=false
|
||||||
# We enable coord search and spectra plot creation
|
SpectraEnabled=true
|
||||||
btnSpectraDisable=false
|
GC.gc()
|
||||||
SpectraEnabled=true
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # 3d plot for TrIQ
|
end
|
||||||
|
|
||||||
@onbutton triq3dPlot begin
|
@onbutton triq3dPlot begin
|
||||||
msg="TrIQ 3D plot selected"
|
msg = "TrIQ 3D plot selected"
|
||||||
cleaned_imgIntT=replace(imgIntT, r"\?.*" => "")
|
cleaned_imgIntT = replace(imgIntT, r"\?.*" => "")
|
||||||
cleaned_imgIntT=lstrip(cleaned_imgIntT, '/')
|
cleaned_imgIntT = lstrip(cleaned_imgIntT, '/')
|
||||||
var=joinpath( "./public", cleaned_imgIntT )
|
var = joinpath("./public", cleaned_imgIntT)
|
||||||
|
|
||||||
if !isfile(var)
|
if !isfile(var)
|
||||||
msg="Image could not be 3d plotted"
|
msg = "Image could not be 3d plotted"
|
||||||
warning_msg=true
|
warning_msg = true
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
progressPlot=true
|
progressPlot = true
|
||||||
btnPlotDisable=true
|
btnPlotDisable = true
|
||||||
btnStartDisable=true
|
btnStartDisable = true
|
||||||
btnSpectraDisable=true
|
btnSpectraDisable = true
|
||||||
|
|
||||||
@async begin
|
@async begin
|
||||||
try
|
try
|
||||||
sTime=time()
|
# --- Get Mask Path ---
|
||||||
plotdata3d, plotlayout3d=loadSurfacePlot(imgIntT)
|
local mask_path_for_plot::Union{String, Nothing} = nothing
|
||||||
GC.gc() # Trigger garbage collection
|
if maskEnabled[] && !isempty(selected_folder_main)
|
||||||
if Sys.islinux()
|
registry = load_registry(registry_path)
|
||||||
ccall(:malloc_trim, Int32, (Int32,), 0) # Ensure julia returns the freed memory to OS
|
entry = get(registry, selected_folder_main, nothing)
|
||||||
|
if entry !== nothing && get(entry, "has_mask", false)
|
||||||
|
mask_path_candidate = get(entry, "mask_path", "")
|
||||||
|
if isfile(mask_path_candidate)
|
||||||
|
mask_path_for_plot = mask_path_candidate
|
||||||
|
else
|
||||||
|
@warn "Mask enabled but file not found: $(mask_path_candidate). Plotting without mask."
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
selectedTab="tab4"
|
# ---
|
||||||
fTime=time()
|
|
||||||
eTime=round(fTime-sTime,digits=3)
|
sTime = time()
|
||||||
msg="Plot loaded in $(eTime) seconds"
|
if mask_path_for_plot !== nothing
|
||||||
catch e
|
plotdata3d, plotlayout3d = loadSurfacePlot(imgIntT, mask_path_for_plot)
|
||||||
msg="Failed to load and process image: $e"
|
else
|
||||||
warning_msg=true
|
plotdata3d, plotlayout3d = loadSurfacePlot(imgIntT)
|
||||||
|
end
|
||||||
|
|
||||||
|
selectedTab = "tab4"
|
||||||
|
fTime = time()
|
||||||
|
eTime = round(fTime - sTime, digits=3)
|
||||||
|
msg = "Plot loaded in $(eTime) seconds"
|
||||||
|
log_memory_usage("Mean Plot Generated", msi_data)
|
||||||
|
catch e
|
||||||
|
msg = "Failed to load and process image: $e"
|
||||||
|
warning_msg = true
|
||||||
|
@error "3D TrIQ plot generation failed" exception=(e, catch_backtrace())
|
||||||
finally
|
finally
|
||||||
progressPlot=false
|
progressPlot=false
|
||||||
btnPlotDisable=false
|
btnPlotDisable=false
|
||||||
btnStartDisable=false
|
btnStartDisable=false
|
||||||
if msi_data !== nothing
|
btnSpectraDisable=false
|
||||||
# We enable coord search and spectra plot creation
|
SpectraEnabled=true
|
||||||
btnSpectraDisable=false
|
GC.gc()
|
||||||
SpectraEnabled=true
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1517,10 +1725,11 @@ include("./julia_imzML_visual.jl")
|
|||||||
progressPlot=false
|
progressPlot=false
|
||||||
btnPlotDisable=false
|
btnPlotDisable=false
|
||||||
btnStartDisable=false
|
btnStartDisable=false
|
||||||
if msi_data !== nothing
|
btnSpectraDisable=false
|
||||||
# We enable coord search and spectra plot creation
|
SpectraEnabled=true
|
||||||
btnSpectraDisable=false
|
GC.gc()
|
||||||
SpectraEnabled=true
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1562,10 +1771,11 @@ include("./julia_imzML_visual.jl")
|
|||||||
progressPlot=false
|
progressPlot=false
|
||||||
btnPlotDisable=false
|
btnPlotDisable=false
|
||||||
btnStartDisable=false
|
btnStartDisable=false
|
||||||
if msi_data !== nothing
|
btnSpectraDisable=false
|
||||||
# We enable coord search and spectra plot creation
|
SpectraEnabled=true
|
||||||
btnSpectraDisable=false
|
GC.gc()
|
||||||
SpectraEnabled=true
|
if Sys.islinux()
|
||||||
|
ccall(:malloc_trim, Int32, (Int32,), 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1579,7 +1789,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
@onchange Nmass begin
|
@onchange Nmass begin
|
||||||
if !isempty(xSpectraMz)
|
if !isempty(xSpectraMz)
|
||||||
# Main spectrum trace
|
# Main spectrum trace
|
||||||
traceSpectra = PlotlyBase.scatter(
|
traceSpectra = PlotlyBase.stem(
|
||||||
x=xSpectraMz,
|
x=xSpectraMz,
|
||||||
y=ySpectraMz,
|
y=ySpectraMz,
|
||||||
marker=attr(size=1, color="blue", opacity=0.5),
|
marker=attr(size=1, color="blue", opacity=0.5),
|
||||||
@ -1651,13 +1861,13 @@ include("./julia_imzML_visual.jl")
|
|||||||
|
|
||||||
@onchange xCoord, yCoord begin
|
@onchange xCoord, yCoord begin
|
||||||
if selectedTab == "tab1"
|
if selectedTab == "tab1"
|
||||||
plotdataImgT = filter(trace -> !(get(trace, :name, "") in ["Line X", "Line Y"]), plotdataImgT)
|
main_trace = plotdataImgT[1] # The heatmap/image trace
|
||||||
trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight)
|
trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight)
|
||||||
plotdataImgT = append!(plotdataImgT, [trace1, trace2])
|
plotdataImgT = [main_trace, trace1, trace2] # Fresh array every time
|
||||||
elseif selectedTab == "tab0"
|
elseif selectedTab == "tab0"
|
||||||
plotdataImg = filter(trace -> !(get(trace, :name, "") in ["Line X", "Line Y", "Optical"]), plotdataImg)
|
main_trace = plotdataImg[1] # The heatmap/image trace
|
||||||
trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight)
|
trace1, trace2 = crossLinesPlot(xCoord, yCoord, imgWidth, -imgHeight)
|
||||||
plotdataImg = append!(plotdataImg, [trace1, trace2])
|
plotdataImg = [main_trace, trace1, trace2]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1769,6 +1979,7 @@ include("./julia_imzML_visual.jl")
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
log_memory_usage("App Ready", msi_data)
|
||||||
warmup_init()
|
warmup_init()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
1032
app.jl.html
1032
app.jl.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
13
mask.jl
13
mask.jl
@ -9,14 +9,12 @@ using Statistics, NaturalSort, LinearAlgebra, StipplePlotly
|
|||||||
using Base.Filesystem: mv
|
using Base.Filesystem: mv
|
||||||
|
|
||||||
using MSI_src
|
using MSI_src
|
||||||
using .MSI_src: MSIData
|
using .MSI_src: MSIData, process_image_pipeline
|
||||||
|
|
||||||
# Plot Handling
|
# Plot Handling
|
||||||
include("./julia_imzML_visual.jl")
|
include("./julia_imzML_visual.jl")
|
||||||
|
|
||||||
# Image Processing Pipeline
|
# Image Processing Pipeline
|
||||||
include("src/ImageProcessing.jl")
|
|
||||||
using .ImageProcessing
|
|
||||||
using ImageBinarization
|
using ImageBinarization
|
||||||
|
|
||||||
function load_and_binarize_mask(path)
|
function load_and_binarize_mask(path)
|
||||||
@ -46,7 +44,7 @@ function alter_image(img_path, otsu_scale, noise_size_percent, hole_size_percent
|
|||||||
gray_img = Float32.(ensure_grayscale(original_img))
|
gray_img = Float32.(ensure_grayscale(original_img))
|
||||||
|
|
||||||
binary, noise_removed, holes_filled, smoothed =
|
binary, noise_removed, holes_filled, smoothed =
|
||||||
ImageProcessing.process_image_pipeline(gray_img;
|
process_image_pipeline(gray_img;
|
||||||
otsu_scale=otsu_scale, noise_size_percent=noise_size_percent,
|
otsu_scale=otsu_scale, noise_size_percent=noise_size_percent,
|
||||||
hole_size_percent=hole_size_percent, smoothing=smoothing_level)
|
hole_size_percent=hole_size_percent, smoothing=smoothing_level)
|
||||||
|
|
||||||
@ -250,7 +248,6 @@ end
|
|||||||
is_browsing_slices = true
|
is_browsing_slices = true
|
||||||
is_editing_mask = false
|
is_editing_mask = false
|
||||||
folder_path = joinpath("public", selected_folder_main)
|
folder_path = joinpath("public", selected_folder_main)
|
||||||
println("Selected folder: $selected_folder_main")
|
|
||||||
if !isdir(folder_path)
|
if !isdir(folder_path)
|
||||||
imgInt = ""
|
imgInt = ""
|
||||||
msgimg = "Folder not found."
|
msgimg = "Folder not found."
|
||||||
@ -674,15 +671,17 @@ end
|
|||||||
# Update registry directly in the handler
|
# Update registry directly in the handler
|
||||||
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
|
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
|
||||||
registry = isfile(reg_path) ? JSON.parsefile(reg_path) : Dict{String, Any}()
|
registry = isfile(reg_path) ? JSON.parsefile(reg_path) : Dict{String, Any}()
|
||||||
|
|
||||||
|
full_mask_path = abspath(final_path)
|
||||||
|
|
||||||
# Update the registry entry
|
# Update the registry entry
|
||||||
if haskey(registry, selected_folder_main)
|
if haskey(registry, selected_folder_main)
|
||||||
registry[selected_folder_main]["mask_path"] = "/css/masks/$(final_mask_name)"
|
registry[selected_folder_main]["mask_path"] = full_mask_path
|
||||||
registry[selected_folder_main]["has_mask"] = true
|
registry[selected_folder_main]["has_mask"] = true
|
||||||
else
|
else
|
||||||
# Create a new entry if folder doesn't exist in registry
|
# Create a new entry if folder doesn't exist in registry
|
||||||
registry[selected_folder_main] = Dict(
|
registry[selected_folder_main] = Dict(
|
||||||
"mask_path" => "/css/masks/$(final_mask_name)",
|
"mask_path" => full_mask_path,
|
||||||
"has_mask" => true,
|
"has_mask" => true,
|
||||||
"is_imzML" => true,
|
"is_imzML" => true,
|
||||||
"processed_date" => string(Dates.now())
|
"processed_date" => string(Dates.now())
|
||||||
|
|||||||
164
mask.jl.html
164
mask.jl.html
@ -1,72 +1,91 @@
|
|||||||
<header id="header">
|
<header id="header">
|
||||||
<img src="/css/LABI_logo.png" alt="Labi Logo Icon" id="imgLogo">
|
<img src="/css/LABI_logo.png" alt="Labi Logo Icon" id="imgLogo">
|
||||||
<div>
|
<div>
|
||||||
<h4>JuliaMSI - Mask Editor </h4>
|
<h4>JuliaMSI - Mask Editor </h4>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="extDivStyle" class="row col-12 q-pa-xl">
|
<div id="extDivStyle" class="row col-12 q-pa-xl">
|
||||||
<div class="row col-4">
|
<div class="row col-4">
|
||||||
<!-- Left Panel: Controls -->
|
<!-- Left Panel: Controls -->
|
||||||
<div class="st-col col-12 st-module q-pa-md">
|
<div class="st-col col-12 st-module q-pa-md">
|
||||||
<div class="text-h6">Mask Generation Workflow</div>
|
<div class="text-h6">Mask Generation Workflow</div>
|
||||||
|
|
||||||
<!-- Step 1: Select Slice -->
|
|
||||||
<div class="text-subtitle1 q-mt-md">Step 1: Select Slice</div>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset" class="q-ma-sm col" standout="custom-standout" :disable="is_editing_mask"></q-select>
|
|
||||||
</div>
|
|
||||||
<p class="text-center" v-html="msgimg"></p>
|
|
||||||
|
|
||||||
<!-- Step 2: Provide Mask Input -->
|
<!-- Step 1: Select Slice -->
|
||||||
<div class="text-subtitle1 q-mt-md">Step 2: Create/Upload Mask</div>
|
<div class="text-subtitle1 q-mt-md">Step 1: Select Slice</div>
|
||||||
<div class="row justify-around">
|
<div class="row items-center">
|
||||||
<q-btn class="q-ma-sm btn-style" v-on:click="btn_use_slice_as_mask = true" label="Use Current Slice" :disable="!is_browsing_slices || !imgInt"></q-btn>
|
<q-select v-model="selected_folder_main" :options="image_available_folders" label="Select Dataset"
|
||||||
<q-btn class="q-ma-sm btn-style" v-on:click="btn_upload_mask = true" label="Upload to Create Mask" :disable="!is_browsing_slices || !imgInt"></q-btn>
|
class="q-ma-sm col" standout="custom-standout" :disable="is_editing_mask"></q-select>
|
||||||
<q-btn class="q-ma-sm btn-style" v-on:click="btn_change_slice = true" label="Change Mask Blueprint" :disable="!is_editing_mask"></q-btn>
|
</div>
|
||||||
</div>
|
<p class="text-center" v-html="msgimg"></p>
|
||||||
|
|
||||||
<!-- Step 3: Automated Processing & Overlay -->
|
<!-- Step 2: Provide Mask Input -->
|
||||||
<div class="text-subtitle1 q-mt-md">Step 3: Adjust & Verify</div>
|
<div class="text-subtitle1 q-mt-md">Step 2: Create/Upload Mask</div>
|
||||||
<div class="q-mt-sm">
|
<div class="row justify-around">
|
||||||
<p class="text-caption text-center">Adjust automated processing for the initial mask:</p>
|
<q-btn class="q-ma-sm btn-style" v-on:click="btn_use_slice_as_mask = true" label="Use Current Slice"
|
||||||
<q-slider color="primary" label-always v-model="otsu_scale" :label-value="'Otsu Scale: ' + otsu_scale.toFixed(2)" :step="0.01" :min="0.1" :max="2.0" :disable="!is_editing_mask"></q-slider>
|
:disable="!is_browsing_slices || !imgInt"></q-btn>
|
||||||
<q-slider color="primary" label-always v-model="noise_size_percent" :label-value="'Noise Size: ' + (noise_size_percent * 100).toFixed(3) + '%'" :step="0.001" :min="0.001" :max="0.5" :disable="!is_editing_mask"></q-slider>
|
<q-btn class="q-ma-sm btn-style" v-on:click="btn_upload_mask = true" label="Upload to Create Mask"
|
||||||
<q-slider color="primary" label-always v-model="hole_size_percent" :label-value="'Hole Size: ' + (hole_size_percent * 100).toFixed(4) + '%'" :step="0.0005" :min="0.0005" :max="0.5" :disable="!is_editing_mask"></q-slider>
|
:disable="!is_browsing_slices || !imgInt"></q-btn>
|
||||||
<q-slider color="primary" label-always v-model="smoothing_level" :label-value="'Smoothing: ' + smoothing_level + 'px'" :step="2" :min="1" :max="9" :disable="!is_editing_mask"></q-slider>
|
<q-btn class="q-ma-sm btn-style" v-on:click="btn_change_slice = true" label="Change Mask Blueprint"
|
||||||
</div>
|
:disable="!is_editing_mask"></q-btn>
|
||||||
<div class="q-mt-md">
|
</div>
|
||||||
<p class="text-caption text-center">Adjust slice overlay transparency:</p>
|
|
||||||
<q-slider color="black" v-model="imgTrans" :min="0.0" :max="1.0" :step="0.05" label-always :label-value="'Transparency: ' + imgTrans.toFixed(2)" :disable="!is_editing_mask" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="q-mt-md" :class="{'text-negative': mask_editor_warning}">{{ mask_editor_message }}</p>
|
<!-- Step 3: Automated Processing & Overlay -->
|
||||||
<div class="row">
|
<div class="text-subtitle1 q-mt-md">Step 3: Adjust & Verify</div>
|
||||||
<q-btn :loading="progress" class="q-ma-sm btn-style" :disable="!is_editing_mask" v-on:click="btn_save_final_mask = !btn_save_final_mask" padding="lg" icon="save" label="Save Final Mask"/>
|
<div class="q-mt-sm">
|
||||||
<q-btn class="q-ma-sm btn-style" icon="arrow_back" label="Return" href="/" ></q-btn>
|
<p class="text-caption text-center">Adjust automated processing for the initial mask:</p>
|
||||||
|
<q-slider color="primary" label-always v-model="otsu_scale"
|
||||||
|
:label-value="'Otsu Scale: ' + otsu_scale.toFixed(2)" :step="0.01" :min="0.1" :max="2.0"
|
||||||
|
:disable="!is_editing_mask"></q-slider>
|
||||||
|
<q-slider color="primary" label-always v-model="noise_size_percent"
|
||||||
|
:label-value="'Noise Size: ' + (noise_size_percent * 100).toFixed(3) + '%'" :step="0.001"
|
||||||
|
:min="0.001" :max="0.5" :disable="!is_editing_mask"></q-slider>
|
||||||
|
<q-slider color="primary" label-always v-model="hole_size_percent"
|
||||||
|
:label-value="'Hole Size: ' + (hole_size_percent * 100).toFixed(4) + '%'" :step="0.0005"
|
||||||
|
:min="0.0005" :max="0.5" :disable="!is_editing_mask"></q-slider>
|
||||||
|
<q-slider color="primary" label-always v-model="smoothing_level"
|
||||||
|
:label-value="'Smoothing: ' + smoothing_level + 'px'" :step="2" :min="1" :max="9"
|
||||||
|
:disable="!is_editing_mask"></q-slider>
|
||||||
|
</div>
|
||||||
|
<div class="q-mt-md">
|
||||||
|
<p class="text-caption text-center">Adjust slice overlay transparency:</p>
|
||||||
|
<q-slider color="black" v-model="imgTrans" :min="0.0" :max="1.0" :step="0.05" label-always
|
||||||
|
:label-value="'Transparency: ' + imgTrans.toFixed(2)" :disable="!is_editing_mask" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="q-mt-md" :class="{'text-negative': mask_editor_warning}">{{ mask_editor_message }}</p>
|
||||||
|
<div class="row">
|
||||||
|
<q-btn :loading="progress" class="q-ma-sm btn-style" :disable="!is_editing_mask"
|
||||||
|
v-on:click="btn_save_final_mask = !btn_save_final_mask" padding="lg" icon="save"
|
||||||
|
label="Save Final Mask" />
|
||||||
|
<q-btn class="q-ma-sm btn-style" icon="arrow_back" label="Return" href="/"></q-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row col-6">
|
<div class="row col-6">
|
||||||
<!-- Right Panel: Image Displays -->
|
<!-- Right Panel: Image Displays -->
|
||||||
<div id="intDivStyle-right" class="st-col col-12 st-module">
|
<div id="intDivStyle-right" class="st-col col-12 st-module">
|
||||||
<div class="text-h6">Mask Preview</div>
|
<div class="text-h6">Mask Preview</div>
|
||||||
<div class="row items-center">
|
<div class="row items-center">
|
||||||
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="btn_img_minus=true" :disable="is_editing_mask"></q-btn>
|
<q-btn icon="arrow_back" class="q-my-sm btn-style" v-on:click="btn_img_minus=true"
|
||||||
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="btn_img_plus=true" :disable="is_editing_mask"></q-btn>
|
:disable="is_editing_mask"></q-btn>
|
||||||
|
<q-btn icon="arrow_forward" class="q-my-sm on-right btn-style" v-on:click="btn_img_plus=true"
|
||||||
|
:disable="is_editing_mask"></q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!show_verification_plot" class="text-center text-grey q-pa-xl">
|
<div v-if="!show_verification_plot" class="text-center text-grey q-pa-xl">
|
||||||
<q-icon name="image" size="5em" class="q-mb-md" />
|
<q-icon name="image" size="5em" class="q-mb-md" />
|
||||||
<p>Please select a dataset and provide a mask input to begin.</p>
|
<p>Please select a dataset and provide a mask input to begin.</p>
|
||||||
<p class="text-caption">Use the controls on the left to load a slice or upload a mask image.</p>
|
<p class="text-caption">Use the controls on the left to load a slice or upload a mask image.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="show_verification_plot">
|
<div v-if="show_verification_plot">
|
||||||
<plotly :data="plotdata_verify" :layout="plotlayout_verify" class="q-pa-none q-ma-none pixelated-plot"></plotly>
|
<plotly :data="plotdata_verify" :layout="plotlayout_verify" class="q-pa-none q-ma-none pixelated-plot">
|
||||||
|
</plotly>
|
||||||
<div class="q-mt-md text-center">
|
<div class="q-mt-md text-center">
|
||||||
<q-btn color="primary" class="btn-style" label="Edit Manually" v-on:click="btn_edit_manually=true" />
|
<q-btn color="primary" class="btn-style" label="Edit Manually"
|
||||||
|
v-on:click="btn_edit_manually=true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center" v-html="msgimg"></p>
|
<p class="text-center" v-html="msgimg"></p>
|
||||||
@ -87,54 +106,59 @@
|
|||||||
<div class="q-pa-md q-gutter-sm row justify-center items-center">
|
<div class="q-pa-md q-gutter-sm row justify-center items-center">
|
||||||
<!-- Tool Selection -->
|
<!-- Tool Selection -->
|
||||||
<q-btn-group>
|
<q-btn-group>
|
||||||
<q-btn label="Brush" v-on:click="current_tool = 'brush'" :color="current_tool === 'brush' ? 'primary' : 'white'" text-color="black"/>
|
<q-btn label="Brush" v-on:click="current_tool = 'brush'"
|
||||||
<q-btn label="Eraser" v-on:click="current_tool = 'eraser'" :color="current_tool === 'eraser' ? 'primary' : 'white'" text-color="black"/>
|
:color="current_tool === 'brush' ? 'primary' : 'white'" text-color="black" />
|
||||||
<q-btn label="Bucket" v-on:click="current_tool = 'bucket'" :color="current_tool === 'bucket' ? 'primary' : 'white'" text-color="black"/>
|
<q-btn label="Eraser" v-on:click="current_tool = 'eraser'"
|
||||||
<q-btn label="Drag" v-on:click="current_tool = 'drag'" :color="current_tool === 'drag' ? 'primary' : 'white'" text-color="black"/>
|
:color="current_tool === 'eraser' ? 'primary' : 'white'" text-color="black" />
|
||||||
|
<q-btn label="Bucket" v-on:click="current_tool = 'bucket'"
|
||||||
|
:color="current_tool === 'bucket' ? 'primary' : 'white'" text-color="black" />
|
||||||
|
<q-btn label="Drag" v-on:click="current_tool = 'drag'"
|
||||||
|
:color="current_tool === 'drag' ? 'primary' : 'white'" text-color="black" />
|
||||||
</q-btn-group>
|
</q-btn-group>
|
||||||
|
|
||||||
<q-separator vertical inset />
|
<q-separator vertical inset />
|
||||||
|
|
||||||
<!-- Tool Settings -->
|
<!-- Tool Settings -->
|
||||||
<q-input dense filled label="Brush Size" type="number" v-model.number="brush_size" style="max-width: 120px" :min="1"/>
|
<q-input dense filled label="Brush Size" type="number" v-model.number="brush_size"
|
||||||
<q-input dense filled label="Zoom" type="number" step="0.1" v-model.number="editor_scale" style="max-width: 120px" :min="0.1"/>
|
style="max-width: 120px" :min="1" />
|
||||||
|
<q-input dense filled label="Zoom" type="number" step="0.1" v-model.number="editor_scale"
|
||||||
|
style="max-width: 120px" :min="0.1" />
|
||||||
|
|
||||||
<q-separator vertical inset />
|
<q-separator vertical inset />
|
||||||
|
|
||||||
<!-- Image Manipulation -->
|
<!-- Image Manipulation -->
|
||||||
<q-input dense filled label="Rotate (deg)" type="number" v-model.number="rotate_degrees" style="max-width: 150px" :step="90"/>
|
<q-input dense filled label="Rotate (deg)" type="number" v-model.number="rotate_degrees"
|
||||||
<q-select dense filled label="Flip" v-model="flip_direction" :options="['horizontal', 'vertical']" style="max-width: 150px"/>
|
style="max-width: 150px" :step="90" />
|
||||||
<q-btn label="Apply Flip" v-on:click="btn_flip_mask = !btn_flip_mask"/>
|
<q-select dense filled label="Flip" v-model="flip_direction" :options="['horizontal', 'vertical']"
|
||||||
|
style="max-width: 150px" />
|
||||||
|
<q-btn label="Apply Flip" v-on:click="btn_flip_mask = !btn_flip_mask" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Drag Tool Controls -->
|
<!-- Drag Tool Controls -->
|
||||||
<div v-if="current_tool === 'drag'" class="q-pa-sm q-gutter-sm row justify-center items-center" style="border: 1px solid #ccc; border-radius: 4px;">
|
<div v-if="current_tool === 'drag'" class="q-pa-sm q-gutter-sm row justify-center items-center"
|
||||||
<q-input dense filled label="Move Pixels" type="number" v-model.number="move_pixels" style="max-width: 120px" :min="1"/>
|
style="border: 1px solid #ccc; border-radius: 4px;">
|
||||||
|
<q-input dense filled label="Move Pixels" type="number" v-model.number="move_pixels"
|
||||||
|
style="max-width: 120px" :min="1" />
|
||||||
<q-btn icon="arrow_upward" v-on:click="move_mask_payload = { direction: 'up', t: Date.now() }" />
|
<q-btn icon="arrow_upward" v-on:click="move_mask_payload = { direction: 'up', t: Date.now() }" />
|
||||||
<q-btn icon="arrow_downward" v-on:click="move_mask_payload = { direction: 'down', t: Date.now() }" />
|
<q-btn icon="arrow_downward" v-on:click="move_mask_payload = { direction: 'down', t: Date.now() }" />
|
||||||
<q-btn icon="arrow_back" v-on:click="move_mask_payload = { direction: 'left', t: Date.now() }" />
|
<q-btn icon="arrow_back" v-on:click="move_mask_payload = { direction: 'left', t: Date.now() }" />
|
||||||
<q-btn icon="arrow_forward" v-on:click="move_mask_payload = { direction: 'right', t: Date.now() }" />
|
<q-btn icon="arrow_forward" v-on:click="move_mask_payload = { direction: 'right', t: Date.now() }" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Interactive Canvas -->
|
<!-- Interactive Canvas -->
|
||||||
<div class="row justify-center q-pa-md">
|
<div class="row justify-center q-pa-md">
|
||||||
<div style="position: relative; display: inline-block; overflow: auto; max-width: 100%;"> <!-- Added for scrolling very large images -->
|
<div style="position: relative; display: inline-block; overflow: auto; max-width: 100%;">
|
||||||
<canvas id="maskCanvas"
|
<!-- Added for scrolling very large images -->
|
||||||
:width="canvas_width"
|
<canvas id="maskCanvas" :width="canvas_width" :height="canvas_height" :style="{
|
||||||
:height="canvas_height"
|
|
||||||
:style="{
|
|
||||||
width: canvas_width * editor_scale + 'px',
|
width: canvas_width * editor_scale + 'px',
|
||||||
height: canvas_height * editor_scale + 'px',
|
height: canvas_height * editor_scale + 'px',
|
||||||
border: '2px solid #ccc',
|
border: '2px solid #ccc',
|
||||||
cursor: current_tool === 'drag' ? 'move' : 'crosshair',
|
cursor: current_tool === 'drag' ? 'move' : 'crosshair',
|
||||||
imageRendering: 'pixelated'
|
imageRendering: 'pixelated'
|
||||||
}"
|
}" v-on:mousedown.prevent="startDrawing" v-on:mousemove.prevent="draw"
|
||||||
v-on:mousedown.prevent="startDrawing"
|
v-on:mouseup.prevent="stopDrawing()" v-on:mouseleave.prevent="stopDrawing()">
|
||||||
v-on:mousemove.prevent="draw"
|
</canvas>
|
||||||
v-on:mouseup.prevent="stopDrawing()"
|
</div>
|
||||||
v-on:mouseleave.prevent="stopDrawing()">
|
|
||||||
</canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
@ -142,4 +166,4 @@
|
|||||||
<q-btn label="Cancel" color="negative" v-on:click="show_editor = false"/>
|
<q-btn label="Cancel" color="negative" v-on:click="show_editor = false"/>
|
||||||
</q-card-actions>-->
|
</q-card-actions>-->
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
@ -1,16 +1,80 @@
|
|||||||
module ImageProcessing
|
# src/ImageProcessing
|
||||||
|
|
||||||
using Images
|
using Images
|
||||||
using ImageBinarization
|
using ImageBinarization
|
||||||
using ImageMorphology
|
using ImageMorphology
|
||||||
using ImageComponentAnalysis
|
using ImageComponentAnalysis
|
||||||
|
using Colors # For converting to grayscale
|
||||||
|
|
||||||
export process_image_pipeline
|
export process_image_pipeline
|
||||||
|
export load_and_prepare_mask # Export the new function
|
||||||
|
|
||||||
|
"""
|
||||||
|
load_and_prepare_mask(mask_path::String, target_dims::Tuple{Int, Int})
|
||||||
|
|
||||||
|
Loads a PNG image mask, converts it to a binary (Boolean) matrix,
|
||||||
|
and resizes it to the specified `target_dims`. White pixels in the mask
|
||||||
|
are considered `true` (part of the ROI), and black pixels are `false`.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `mask_path`: Absolute path to the PNG mask file.
|
||||||
|
- `target_dims`: A tuple `(width, height)` representing the desired output dimensions.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A `BitMatrix` of `target_dims` where `true` indicates the ROI.
|
||||||
|
"""
|
||||||
|
function load_and_prepare_mask(mask_path::String, target_dims::Tuple{Int, Int})
|
||||||
|
if !isfile(mask_path)
|
||||||
|
error("Mask file not found: $(mask_path)")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load the image
|
||||||
|
img = Images.load(mask_path)
|
||||||
|
|
||||||
|
# Convert to grayscale if it's a color image
|
||||||
|
gray_img = Gray.(img)
|
||||||
|
|
||||||
|
# Binarize using Otsu's method - white regions become 'true'
|
||||||
|
binary_img = binarize(gray_img, Otsu())
|
||||||
|
|
||||||
|
# Resize to target dimensions
|
||||||
|
resized_img = imresize(binary_img, (target_dims[2], target_dims[1]))
|
||||||
|
|
||||||
|
# Ensure the output is a BitMatrix, as imresize can change the type
|
||||||
|
return resized_img .> 0.5
|
||||||
|
end
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# CORE PROCESSING PIPELINE
|
# CORE PROCESSING PIPELINE
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
|
"""
|
||||||
|
process_image_pipeline(gray_img; otsu_scale=1.0, noise_size_percent=0.1, hole_size_percent=0.05, smoothing=2)
|
||||||
|
|
||||||
|
Applies a multi-step image processing pipeline to a grayscale image to segment regions of interest.
|
||||||
|
|
||||||
|
The pipeline consists of:
|
||||||
|
1. **Binarization**: An adjusted Otsu's threshold is used to create a binary image.
|
||||||
|
2. **Noise Removal**: Small white regions (noise) are removed using an area opening operation.
|
||||||
|
3. **Hole Filling**: Small black regions (holes) within larger objects are filled.
|
||||||
|
4. **Edge Smoothing**: The edges of the final regions are smoothed using a morphological closing operation.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `gray_img`: The input grayscale image (`Matrix{<:Gray}`).
|
||||||
|
|
||||||
|
# Keyword Arguments
|
||||||
|
- `otsu_scale`: A factor to scale the automatically determined Otsu threshold. Values > 1.0 make the threshold stricter (less white), < 1.0 make it more lenient (more white). Default: `1.0`.
|
||||||
|
- `noise_size_percent`: The percentage of the total image area used as a threshold to remove small white noise components. Default: `0.1`.
|
||||||
|
- `hole_size_percent`: The percentage of the total image area used as a threshold to fill black holes in white components. Default: `0.05`.
|
||||||
|
- `smoothing`: The size of the kernel for the final edge smoothing (closing) operation. Default: `2`.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A tuple containing four images representing the intermediate steps of the pipeline:
|
||||||
|
1. `binary_img`: The result of the initial binarization.
|
||||||
|
2. `noise_removed_img`: The image after noise removal.
|
||||||
|
3. `holes_filled_img`: The image after filling holes.
|
||||||
|
4. `smoothed_img`: The final smoothed image.
|
||||||
|
"""
|
||||||
function process_image_pipeline(gray_img;
|
function process_image_pipeline(gray_img;
|
||||||
otsu_scale=1.0,
|
otsu_scale=1.0,
|
||||||
noise_size_percent=0.1,
|
noise_size_percent=0.1,
|
||||||
@ -43,5 +107,3 @@ function process_image_pipeline(gray_img;
|
|||||||
# --- Return all intermediate steps for visualization ---
|
# --- Return all intermediate steps for visualization ---
|
||||||
return binary_img, noise_removed_img, holes_filled_img, smoothed_img
|
return binary_img, noise_removed_img, holes_filled_img, smoothed_img
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
|
||||||
|
|||||||
122
src/MSIData.jl
122
src/MSIData.jl
@ -143,6 +143,45 @@ mutable struct MSIData
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
get_masked_spectrum_indices(data::MSIData, mask_matrix::BitMatrix) -> Set{Int}
|
||||||
|
|
||||||
|
Converts a 2D boolean mask matrix (e.g., loaded from a PNG) into a `Set` of linear
|
||||||
|
spectrum indices that fall within the `true` regions of the mask.
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
- `data`: The `MSIData` object containing the `coordinate_map`.
|
||||||
|
- `mask_matrix`: A `BitMatrix` where `true` indicates a pixel is part of the ROI.
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
- A `Set{Int}` containing the linear indices of spectra within the mask.
|
||||||
|
"""
|
||||||
|
function get_masked_spectrum_indices(data::MSIData, mask_matrix::BitMatrix)
|
||||||
|
if data.coordinate_map === nothing
|
||||||
|
error("Coordinate map not available. Cannot apply mask to non-imaging data.")
|
||||||
|
end
|
||||||
|
|
||||||
|
width, height = data.image_dims
|
||||||
|
mask_height, mask_width = size(mask_matrix)
|
||||||
|
|
||||||
|
if mask_width != width || mask_height != height
|
||||||
|
error("Mask dimensions ($(mask_width)x$(mask_height)) do not match image dimensions ($(width)x$(height)).")
|
||||||
|
end
|
||||||
|
|
||||||
|
masked_indices = Set{Int}()
|
||||||
|
for y in 1:height
|
||||||
|
for x in 1:width
|
||||||
|
if mask_matrix[y, x] # If this pixel is part of the mask
|
||||||
|
idx = data.coordinate_map[x, y]
|
||||||
|
if idx != 0 # Ensure there's an actual spectrum at this coordinate
|
||||||
|
push!(masked_indices, idx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return masked_indices
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
# --- Internal function for reading binary data --- #
|
# --- Internal function for reading binary data --- #
|
||||||
|
|
||||||
@ -513,7 +552,7 @@ It uses a fast, two-pass approach:
|
|||||||
This function is highly optimized for `.imzML` by leveraging direct binary
|
This function is highly optimized for `.imzML` by leveraging direct binary
|
||||||
reading and optimized binning logic. It is called by `get_total_spectrum`.
|
reading and optimized binning logic. It is called by `get_total_spectrum`.
|
||||||
"""
|
"""
|
||||||
function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000)
|
function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000, masked_indices::Union{Set{Int}, Nothing}=nothing)
|
||||||
println("Calculating total spectrum for imzML (2-pass method)...")
|
println("Calculating total spectrum for imzML (2-pass method)...")
|
||||||
total_start_time = time_ns()
|
total_start_time = time_ns()
|
||||||
|
|
||||||
@ -529,7 +568,7 @@ function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
println(" Pass 1: Finding global m/z range...")
|
println(" Pass 1: Finding global m/z range...")
|
||||||
g_min_mz = Inf
|
g_min_mz = Inf
|
||||||
g_max_mz = -Inf
|
g_max_mz = -Inf
|
||||||
_iterate_spectra_fast(msi_data) do idx, mz, _
|
_iterate_spectra_fast(msi_data, masked_indices === nothing ? nothing : collect(masked_indices)) do idx, mz, _
|
||||||
if !isempty(mz)
|
if !isempty(mz)
|
||||||
local_min, local_max = extrema(mz)
|
local_min, local_max = extrema(mz)
|
||||||
g_min_mz = min(g_min_mz, local_min)
|
g_min_mz = min(g_min_mz, local_min)
|
||||||
@ -539,7 +578,7 @@ function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
pass1_duration = (time_ns() - pass1_start_time) / 1e9
|
pass1_duration = (time_ns() - pass1_start_time) / 1e9
|
||||||
if !isfinite(g_min_mz)
|
if !isfinite(g_min_mz)
|
||||||
@warn "Could not determine a valid m/z range for imzML. All spectra might be empty."
|
@warn "Could not determine a valid m/z range for imzML. All spectra might be empty."
|
||||||
return (Float64[], Float64[])
|
return (Float64[], Float64[], 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use and cache the result
|
# Use and cache the result
|
||||||
@ -560,7 +599,9 @@ function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
# 3. Second Pass: Optimized binning
|
# 3. Second Pass: Optimized binning
|
||||||
pass2_start_time = time_ns()
|
pass2_start_time = time_ns()
|
||||||
println(" Pass 2: Summing intensities into $num_bins bins...")
|
println(" Pass 2: Summing intensities into $num_bins bins...")
|
||||||
_iterate_spectra_fast(msi_data) do idx, mz, intensity
|
num_spectra_processed = 0
|
||||||
|
_iterate_spectra_fast(msi_data, masked_indices === nothing ? nothing : collect(masked_indices)) do idx, mz, intensity
|
||||||
|
num_spectra_processed += 1
|
||||||
if isempty(mz)
|
if isempty(mz)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@ -594,7 +635,7 @@ function get_total_spectrum_imzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
println("----------------------------------------\n")
|
println("----------------------------------------\n")
|
||||||
|
|
||||||
println("Total spectrum calculation complete for imzML.")
|
println("Total spectrum calculation complete for imzML.")
|
||||||
return (collect(mz_bins), intensity_sum)
|
return (collect(mz_bins), intensity_sum, num_spectra_processed)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -608,7 +649,7 @@ It uses a two-pass approach analogous to the `imzML` implementation:
|
|||||||
|
|
||||||
This function is called by `get_total_spectrum`.
|
This function is called by `get_total_spectrum`.
|
||||||
"""
|
"""
|
||||||
function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000)
|
function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000, masked_indices::Union{Set{Int}, Nothing}=nothing)
|
||||||
println("Calculating total spectrum for mzML (2-pass method)...")
|
println("Calculating total spectrum for mzML (2-pass method)...")
|
||||||
total_start_time = time_ns()
|
total_start_time = time_ns()
|
||||||
|
|
||||||
@ -624,7 +665,7 @@ function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
println(" Pass 1: Finding global m/z range...")
|
println(" Pass 1: Finding global m/z range...")
|
||||||
g_min_mz = Inf
|
g_min_mz = Inf
|
||||||
g_max_mz = -Inf
|
g_max_mz = -Inf
|
||||||
_iterate_spectra_fast(msi_data) do idx, mz, intensity
|
_iterate_spectra_fast(msi_data, masked_indices === nothing ? nothing : collect(masked_indices)) do idx, mz, intensity
|
||||||
if !isempty(mz)
|
if !isempty(mz)
|
||||||
local_min, local_max = extrema(mz)
|
local_min, local_max = extrema(mz)
|
||||||
g_min_mz = min(g_min_mz, local_min)
|
g_min_mz = min(g_min_mz, local_min)
|
||||||
@ -635,7 +676,7 @@ function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
|
|
||||||
if !isfinite(g_min_mz)
|
if !isfinite(g_min_mz)
|
||||||
@warn "Could not determine a valid m/z range for mzML. All spectra might be empty."
|
@warn "Could not determine a valid m/z range for mzML. All spectra might be empty."
|
||||||
return (Float64[], Float64[])
|
return (Float64[], Float64[], 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use and cache the result
|
# Use and cache the result
|
||||||
@ -658,7 +699,9 @@ function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
inv_bin_step = 1.0 / bin_step # Precompute reciprocal
|
inv_bin_step = 1.0 / bin_step # Precompute reciprocal
|
||||||
min_mz = global_min_mz
|
min_mz = global_min_mz
|
||||||
|
|
||||||
_iterate_spectra_fast(msi_data) do idx, mz, intensity
|
num_spectra_processed = 0
|
||||||
|
_iterate_spectra_fast(msi_data, masked_indices === nothing ? nothing : collect(masked_indices)) do idx, mz, intensity
|
||||||
|
num_spectra_processed += 1
|
||||||
if isempty(mz)
|
if isempty(mz)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@ -687,7 +730,7 @@ function get_total_spectrum_mzml(msi_data::MSIData; num_bins::Int=2000)
|
|||||||
println("----------------------------------------\n")
|
println("----------------------------------------\n")
|
||||||
|
|
||||||
println("Total spectrum calculation complete for mzML.")
|
println("Total spectrum calculation complete for mzML.")
|
||||||
return (collect(mz_bins), intensity_sum)
|
return (collect(mz_bins), intensity_sum, num_spectra_processed)
|
||||||
end
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -699,11 +742,18 @@ This function dispatches to a specialized implementation based on the file type
|
|||||||
|
|
||||||
Returns a tuple containing two vectors: the binned m/z axis and the summed intensities.
|
Returns a tuple containing two vectors: the binned m/z axis and the summed intensities.
|
||||||
"""
|
"""
|
||||||
function get_total_spectrum(msi_data::MSIData; num_bins::Int=2000)
|
function get_total_spectrum(msi_data::MSIData; num_bins::Int=2000, mask_path::Union{String, Nothing}=nothing)::Tuple{Vector{Float64}, Vector{Float64}, Int}
|
||||||
|
local masked_indices::Union{Set{Int}, Nothing} = nothing
|
||||||
|
if mask_path !== nothing
|
||||||
|
mask_matrix = load_and_prepare_mask(mask_path, msi_data.image_dims)
|
||||||
|
masked_indices = get_masked_spectrum_indices(msi_data, mask_matrix)
|
||||||
|
println("Calculating total spectrum for masked region (mask from: $(mask_path))")
|
||||||
|
end
|
||||||
|
|
||||||
if msi_data.source isa ImzMLSource
|
if msi_data.source isa ImzMLSource
|
||||||
return get_total_spectrum_imzml(msi_data, num_bins=num_bins)
|
return get_total_spectrum_imzml(msi_data, num_bins=num_bins, masked_indices=masked_indices)
|
||||||
else # MzMLSource
|
else # MzMLSource
|
||||||
return get_total_spectrum_mzml(msi_data, num_bins=num_bins)
|
return get_total_spectrum_mzml(msi_data, num_bins=num_bins, masked_indices=masked_indices)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -715,14 +765,15 @@ This is effectively the total spectrum divided by the number of spectra.
|
|||||||
|
|
||||||
Returns a tuple containing two vectors: the binned m/z axis and the averaged intensities.
|
Returns a tuple containing two vectors: the binned m/z axis and the averaged intensities.
|
||||||
"""
|
"""
|
||||||
function get_average_spectrum(msi_data::MSIData; num_bins::Int=2000)
|
function get_average_spectrum(msi_data::MSIData; num_bins::Int=2000, mask_path::Union{String, Nothing}=nothing)::Tuple{Vector{Float64}, Vector{Float64}}
|
||||||
mz_bins, intensity_sum = get_total_spectrum(msi_data, num_bins=num_bins)
|
mz_bins, intensity_sum, num_spectra_processed = get_total_spectrum(msi_data, num_bins=num_bins, mask_path=mask_path)
|
||||||
|
|
||||||
if isempty(intensity_sum)
|
if isempty(intensity_sum) || num_spectra_processed == 0
|
||||||
return (mz_bins, intensity_sum)
|
@warn "Cannot calculate average spectrum: no spectra were processed."
|
||||||
|
return (mz_bins, Float64[])
|
||||||
end
|
end
|
||||||
num_spectra = length(msi_data.spectra_metadata)
|
|
||||||
average_intensity = intensity_sum ./ num_spectra
|
average_intensity = intensity_sum ./ num_spectra_processed
|
||||||
|
|
||||||
return (mz_bins, average_intensity)
|
return (mz_bins, average_intensity)
|
||||||
end
|
end
|
||||||
@ -855,13 +906,16 @@ is significantly faster for bulk processing than reading spectra one by one.
|
|||||||
- `data`: The `MSIData` object.
|
- `data`: The `MSIData` object.
|
||||||
- `source`: The `ImzMLSource`.
|
- `source`: The `ImzMLSource`.
|
||||||
"""
|
"""
|
||||||
function _iterate_uncompressed_fast(f::Function, data::MSIData, source::ImzMLSource)
|
function _iterate_uncompressed_fast(f::Function, data::MSIData, source::ImzMLSource, indices_to_iterate::Union{AbstractVector{Int}, Nothing})
|
||||||
# Optimized path for uncompressed data using buffer reuse
|
# Optimized path for uncompressed data using buffer reuse
|
||||||
max_points = maximum(meta -> meta.mz_asset.encoded_length, data.spectra_metadata)
|
max_points = maximum(meta -> meta.mz_asset.encoded_length, data.spectra_metadata)
|
||||||
mz_buffer = Vector{source.mz_format}(undef, max_points)
|
mz_buffer = Vector{source.mz_format}(undef, max_points)
|
||||||
int_buffer = Vector{source.intensity_format}(undef, max_points)
|
int_buffer = Vector{source.intensity_format}(undef, max_points)
|
||||||
|
|
||||||
for i in 1:length(data.spectra_metadata)
|
# Determine which indices to iterate over
|
||||||
|
spectrum_indices = (indices_to_iterate === nothing) ? (1:length(data.spectra_metadata)) : indices_to_iterate
|
||||||
|
|
||||||
|
for i in spectrum_indices
|
||||||
meta = data.spectra_metadata[i]
|
meta = data.spectra_metadata[i]
|
||||||
nPoints = meta.mz_asset.encoded_length
|
nPoints = meta.mz_asset.encoded_length
|
||||||
|
|
||||||
@ -904,10 +958,14 @@ and will be slower and allocate more memory than `_iterate_uncompressed_fast`.
|
|||||||
- `data`: The `MSIData` object.
|
- `data`: The `MSIData` object.
|
||||||
- `source`: The `ImzMLSource`.
|
- `source`: The `ImzMLSource`.
|
||||||
"""
|
"""
|
||||||
function _iterate_compressed_fast(f::Function, data::MSIData, source::ImzMLSource)
|
function _iterate_compressed_fast(f::Function, data::MSIData, source::ImzMLSource, indices_to_iterate::Union{AbstractVector{Int}, Nothing})
|
||||||
# Path for datasets containing at least one compressed spectrum.
|
# Path for datasets containing at least one compressed spectrum.
|
||||||
# This path reads and decompresses each spectrum individually.
|
# This path reads and decompresses each spectrum individually.
|
||||||
for i in 1:length(data.spectra_metadata)
|
|
||||||
|
# Determine which indices to iterate over
|
||||||
|
spectrum_indices = (indices_to_iterate === nothing) ? (1:length(data.spectra_metadata)) : indices_to_iterate
|
||||||
|
|
||||||
|
for i in spectrum_indices
|
||||||
meta = data.spectra_metadata[i]
|
meta = data.spectra_metadata[i]
|
||||||
|
|
||||||
if meta.mz_asset.encoded_length == 0 && meta.int_asset.encoded_length == 0
|
if meta.mz_asset.encoded_length == 0 && meta.int_asset.encoded_length == 0
|
||||||
@ -937,7 +995,7 @@ This function acts as a dispatcher:
|
|||||||
- If all spectra are uncompressed, it uses a highly optimized path that reuses pre-allocated
|
- If all spectra are uncompressed, it uses a highly optimized path that reuses pre-allocated
|
||||||
buffers to minimize memory allocations and overhead.
|
buffers to minimize memory allocations and overhead.
|
||||||
"""
|
"""
|
||||||
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::ImzMLSource)
|
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::ImzMLSource, indices_to_iterate::Union{AbstractVector{Int}, Nothing})
|
||||||
if isempty(data.spectra_metadata)
|
if isempty(data.spectra_metadata)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@ -947,9 +1005,9 @@ function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::ImzMLSou
|
|||||||
data.spectra_metadata)
|
data.spectra_metadata)
|
||||||
|
|
||||||
if any_compressed
|
if any_compressed
|
||||||
_iterate_compressed_fast(f, data, source)
|
_iterate_compressed_fast(f, data, source, indices_to_iterate)
|
||||||
else
|
else
|
||||||
_iterate_uncompressed_fast(f, data, source)
|
_iterate_uncompressed_fast(f, data, source, indices_to_iterate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -961,12 +1019,16 @@ through each spectrum, decodes the Base64 data on the fly, and calls the
|
|||||||
provided function. It bypasses the `GetSpectrum` cache to avoid storing
|
provided function. It bypasses the `GetSpectrum` cache to avoid storing
|
||||||
all decoded spectra in memory.
|
all decoded spectra in memory.
|
||||||
"""
|
"""
|
||||||
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSource)
|
function _iterate_spectra_fast_impl(f::Function, data::MSIData, source::MzMLSource, indices_to_iterate::Union{AbstractVector{Int}, Nothing})
|
||||||
# This implementation is for mzML. It iterates through each spectrum,
|
# This implementation is for mzML. It iterates through each spectrum,
|
||||||
# decodes it, and then calls the function `f`. It's less performant than
|
# decodes it, and then calls the function `f`. It's less performant than
|
||||||
# the imzML version because we cannot pre-allocate buffers of a known size,
|
# the imzML version because we cannot pre-allocate buffers of a known size,
|
||||||
# but it is correct and still faster than using GetSpectrum due to bypassing the cache.
|
# but it is correct and still faster than using GetSpectrum due to bypassing the cache.
|
||||||
for i in 1:length(data.spectra_metadata)
|
|
||||||
|
# Determine which indices to iterate over
|
||||||
|
spectrum_indices = (indices_to_iterate === nothing) ? (1:length(data.spectra_metadata)) : indices_to_iterate
|
||||||
|
|
||||||
|
for i in spectrum_indices
|
||||||
meta = data.spectra_metadata[i]
|
meta = data.spectra_metadata[i]
|
||||||
|
|
||||||
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
mz = read_binary_vector(source.file_handle, meta.mz_asset)
|
||||||
@ -986,7 +1048,7 @@ It dispatches to a specialized implementation based on the data source type
|
|||||||
- `f`: A function to call for each spectrum, with signature `f(index, mz, intensity)`.
|
- `f`: A function to call for each spectrum, with signature `f(index, mz, intensity)`.
|
||||||
- `data`: The `MSIData` object.
|
- `data`: The `MSIData` object.
|
||||||
"""
|
"""
|
||||||
function _iterate_spectra_fast(f::Function, data::MSIData)
|
function _iterate_spectra_fast(f::Function, data::MSIData, indices_to_iterate::Union{AbstractVector{Int}, Nothing}=nothing)
|
||||||
# Dispatch to the correct implementation based on the source type
|
# Dispatch to the correct implementation based on the source type
|
||||||
_iterate_spectra_fast_impl(f, data, data.source)
|
_iterate_spectra_fast_impl(f, data, data.source, indices_to_iterate)
|
||||||
end
|
end
|
||||||
@ -14,7 +14,10 @@ export OpenMSIData,
|
|||||||
get_average_spectrum,
|
get_average_spectrum,
|
||||||
LoadMzml,
|
LoadMzml,
|
||||||
precompute_analytics,
|
precompute_analytics,
|
||||||
process_spectrum
|
process_spectrum,
|
||||||
|
generate_colorbar_image,
|
||||||
|
process_image_pipeline,
|
||||||
|
load_and_prepare_mask
|
||||||
|
|
||||||
# Export the public Preprocessing API
|
# Export the public Preprocessing API
|
||||||
export FeatureMatrix,
|
export FeatureMatrix,
|
||||||
@ -43,6 +46,7 @@ include("mzML.jl")
|
|||||||
include("imzML.jl")
|
include("imzML.jl")
|
||||||
include("MzmlConverter.jl")
|
include("MzmlConverter.jl")
|
||||||
include("Preprocessing.jl")
|
include("Preprocessing.jl")
|
||||||
|
include("ImageProcessing.jl")
|
||||||
|
|
||||||
|
|
||||||
# --- Main Entry Point --- #
|
# --- Main Entry Point --- #
|
||||||
|
|||||||
221
src/imzML.jl
221
src/imzML.jl
@ -744,10 +744,17 @@ This is a performant function that iterates through spectra once.
|
|||||||
# Returns
|
# Returns
|
||||||
- A `Matrix{Float64}` representing the intensity slice.
|
- A `Matrix{Float64}` representing the intensity slice.
|
||||||
"""
|
"""
|
||||||
function get_mz_slice(data::MSIData, mass::Real, tolerance::Real)
|
function get_mz_slice(data::MSIData, mass::Real, tolerance::Real; mask_path::Union{String, Nothing}=nothing)
|
||||||
width, height = data.image_dims
|
width, height = data.image_dims
|
||||||
slice_matrix = zeros(Float64, height, width)
|
slice_matrix = zeros(Float64, height, width)
|
||||||
|
|
||||||
|
local masked_indices::Union{Set{Int}, Nothing} = nothing
|
||||||
|
if mask_path !== nothing
|
||||||
|
mask_matrix = load_and_prepare_mask(mask_path, (width, height))
|
||||||
|
masked_indices = get_masked_spectrum_indices(data, mask_matrix)
|
||||||
|
println("Applying mask from: $(mask_path), found $(length(masked_indices)) spectra in ROI.")
|
||||||
|
end
|
||||||
|
|
||||||
# INTELLIGENT LOADING: Ensure analytics are computed for filtering.
|
# INTELLIGENT LOADING: Ensure analytics are computed for filtering.
|
||||||
if data.spectrum_stats_df === nothing || !hasproperty(data.spectrum_stats_df, :MinMZ)
|
if data.spectrum_stats_df === nothing || !hasproperty(data.spectrum_stats_df, :MinMZ)
|
||||||
println("Per-spectrum metadata not found. Running one-time analytics computation...")
|
println("Per-spectrum metadata not found. Running one-time analytics computation...")
|
||||||
@ -761,7 +768,9 @@ function get_mz_slice(data::MSIData, mass::Real, tolerance::Real)
|
|||||||
|
|
||||||
# 1. Find all candidate spectra first for efficient filtering
|
# 1. Find all candidate spectra first for efficient filtering
|
||||||
candidate_indices = Set{Int}()
|
candidate_indices = Set{Int}()
|
||||||
for i in 1:length(data.spectra_metadata)
|
indices_to_check = masked_indices === nothing ? (1:length(data.spectra_metadata)) : masked_indices
|
||||||
|
|
||||||
|
for i in indices_to_check
|
||||||
spec_min_mz = stats_df.MinMZ[i]
|
spec_min_mz = stats_df.MinMZ[i]
|
||||||
spec_max_mz = stats_df.MaxMZ[i]
|
spec_max_mz = stats_df.MaxMZ[i]
|
||||||
if target_max >= spec_min_mz && target_min <= spec_max_mz
|
if target_max >= spec_min_mz && target_min <= spec_max_mz
|
||||||
@ -769,20 +778,17 @@ function get_mz_slice(data::MSIData, mass::Real, tolerance::Real)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
println("Found $(length(candidate_indices)) candidate spectra (filtered from $(length(data.spectra_metadata)))")
|
println("Found $(length(candidate_indices)) candidate spectra (filtered from $(length(indices_to_check)) initial spectra)")
|
||||||
|
|
||||||
# 2. Iterate using the optimized, low-allocation iterator
|
# 2. Iterate using the optimized, low-allocation iterator
|
||||||
results_count = 0
|
results_count = 0
|
||||||
_iterate_spectra_fast(data) do idx, mz_array, intensity_array
|
_iterate_spectra_fast(data, collect(candidate_indices)) do idx, mz_array, intensity_array
|
||||||
# Process only the spectra that are candidates
|
meta = data.spectra_metadata[idx]
|
||||||
if idx in candidate_indices
|
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
||||||
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
if intensity > 0.0
|
||||||
if intensity > 0.0
|
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
||||||
meta = data.spectra_metadata[idx]
|
slice_matrix[meta.y, meta.x] = intensity
|
||||||
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
results_count += 1
|
||||||
slice_matrix[meta.y, meta.x] = intensity
|
|
||||||
results_count += 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -802,7 +808,7 @@ This is a highly performant function that iterates through the full dataset only
|
|||||||
# Returns
|
# Returns
|
||||||
- A `Dict{Real, Matrix{Float64}}` mapping each mass to its intensity slice matrix.
|
- 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)
|
function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, tolerance::Real; mask_path::Union{String, Nothing}=nothing)
|
||||||
width, height = data.image_dims
|
width, height = data.image_dims
|
||||||
|
|
||||||
# 1. Initialize a dictionary to hold the output slice matrices
|
# 1. Initialize a dictionary to hold the output slice matrices
|
||||||
@ -811,6 +817,13 @@ function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, t
|
|||||||
slice_dict[mass] = zeros(Float64, height, width)
|
slice_dict[mass] = zeros(Float64, height, width)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local masked_indices::Union{Set{Int}, Nothing} = nothing
|
||||||
|
if mask_path !== nothing
|
||||||
|
mask_matrix = load_and_prepare_mask(mask_path, (width, height))
|
||||||
|
masked_indices = get_masked_spectrum_indices(data, mask_matrix)
|
||||||
|
println("Applying mask from: $(mask_path), found $(length(masked_indices)) spectra in ROI.")
|
||||||
|
end
|
||||||
|
|
||||||
# 2. Ensure analytics are computed for filtering.
|
# 2. Ensure analytics are computed for filtering.
|
||||||
if data.spectrum_stats_df === nothing || !hasproperty(data.spectrum_stats_df, :MinMZ)
|
if data.spectrum_stats_df === nothing || !hasproperty(data.spectrum_stats_df, :MinMZ)
|
||||||
println("Per-spectrum metadata not found. Running one-time analytics computation...")
|
println("Per-spectrum metadata not found. Running one-time analytics computation...")
|
||||||
@ -820,12 +833,13 @@ function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, t
|
|||||||
println("Filtering candidate spectra for $(length(masses)) m/z values...")
|
println("Filtering candidate spectra for $(length(masses)) m/z values...")
|
||||||
stats_df = data.spectrum_stats_df
|
stats_df = data.spectrum_stats_df
|
||||||
candidate_indices = Set{Int}()
|
candidate_indices = Set{Int}()
|
||||||
|
indices_to_check = masked_indices === nothing ? (1:length(data.spectra_metadata)) : masked_indices
|
||||||
|
|
||||||
# 3. Find all spectra that could contain *any* of the requested masses.
|
# 3. Find all spectra that could contain *any* of the requested masses.
|
||||||
for mass in masses
|
for mass in masses
|
||||||
target_min = mass - tolerance
|
target_min = mass - tolerance
|
||||||
target_max = mass + tolerance
|
target_max = mass + tolerance
|
||||||
for i in 1:length(data.spectra_metadata)
|
for i in indices_to_check
|
||||||
# If already a candidate, no need to check again
|
# If already a candidate, no need to check again
|
||||||
if i in candidate_indices
|
if i in candidate_indices
|
||||||
continue
|
continue
|
||||||
@ -841,20 +855,17 @@ function get_multiple_mz_slices(data::MSIData, masses::AbstractVector{<:Real}, t
|
|||||||
println("Found $(length(candidate_indices)) total candidate spectra.")
|
println("Found $(length(candidate_indices)) total candidate spectra.")
|
||||||
|
|
||||||
# 4. Iterate through the data a single time using the optimized iterator.
|
# 4. Iterate through the data a single time using the optimized iterator.
|
||||||
_iterate_spectra_fast(data) do idx, mz_array, intensity_array
|
_iterate_spectra_fast(data, collect(candidate_indices)) do idx, mz_array, intensity_array
|
||||||
# Process only the spectra that are candidates
|
meta = data.spectra_metadata[idx]
|
||||||
if idx in candidate_indices
|
# For this single spectrum, check all masses of interest
|
||||||
meta = data.spectra_metadata[idx]
|
for mass in masses
|
||||||
# For this single spectrum, check all masses of interest
|
# Check if this spectrum's range actually covers the current mass
|
||||||
for mass in masses
|
# This is a finer-grained check than the initial filtering
|
||||||
# Check if this spectrum's range actually covers the current mass
|
if !isempty(mz_array) && (mass + tolerance) >= first(mz_array) && (mass - tolerance) <= last(mz_array)
|
||||||
# This is a finer-grained check than the initial filtering
|
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
||||||
if !isempty(mz_array) && (mass + tolerance) >= first(mz_array) && (mass - tolerance) <= last(mz_array)
|
if intensity > 0.0
|
||||||
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
||||||
if intensity > 0.0
|
slice_dict[mass][meta.y, meta.x] = intensity
|
||||||
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
|
||||||
slice_dict[mass][meta.y, meta.x] = intensity
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -878,30 +889,15 @@ Generates and saves a plot of a single image slice for a given m/z value.
|
|||||||
This function closely imitates the logic of the original `GetSlice` but uses
|
This function closely imitates the logic of the original `GetSlice` but uses
|
||||||
the modern `MSIData` access patterns and robust peak finding.
|
the modern `MSIData` access patterns and robust peak finding.
|
||||||
"""
|
"""
|
||||||
function plot_slice(msi_data::MSIData, mass::Real, tolerance::Real, output_dir::String; stage_name="slice_mz_$(mass)", bins=256)
|
function plot_slice(msi_data::MSIData, mass::Real, tolerance::Real, output_dir::String;
|
||||||
|
stage_name="slice_mz_$(mass)", bins=256, mask_path::Union{String, Nothing}=nothing)
|
||||||
|
|
||||||
# 1. Create an empty image for the slice, with dimensions matching plotting expectations
|
# 1. Generate the slice, applying the mask if provided.
|
||||||
width, height = msi_data.image_dims
|
|
||||||
slice_matrix = zeros(Float64, height, width)
|
|
||||||
|
|
||||||
# 2. Iterate through each spectrum to build the slice
|
|
||||||
println("Generating slice for m/z $mass...")
|
println("Generating slice for m/z $mass...")
|
||||||
_iterate_spectra_fast(msi_data) do spec_idx, mz_array, intensity_array
|
slice_matrix = get_mz_slice(msi_data, mass, tolerance, mask_path=mask_path)
|
||||||
meta = msi_data.spectra_metadata[spec_idx]
|
|
||||||
|
|
||||||
# Find the peak intensity using the modern, robust find_mass
|
|
||||||
intensity = find_mass(mz_array, intensity_array, mass, tolerance)
|
|
||||||
|
|
||||||
if intensity > 0.0
|
|
||||||
# Populate the matrix using (y, x) indexing
|
|
||||||
if 1 <= meta.x <= width && 1 <= meta.y <= height
|
|
||||||
slice_matrix[meta.y, meta.x] = intensity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
println("Slice generation complete.")
|
println("Slice generation complete.")
|
||||||
|
|
||||||
# 3. Plot the resulting slice matrix using CairoMakie
|
# 2. Plot the resulting slice matrix using CairoMakie
|
||||||
println("Plotting slice...")
|
println("Plotting slice...")
|
||||||
|
|
||||||
fig = Figure(size = (600, 500))
|
fig = Figure(size = (600, 500))
|
||||||
@ -925,7 +921,7 @@ function plot_slice(msi_data::MSIData, mass::Real, tolerance::Real, output_dir::
|
|||||||
Colorbar(fig[1, 2], hm, label="Intensity")
|
Colorbar(fig[1, 2], hm, label="Intensity")
|
||||||
colgap!(fig.layout, 5)
|
colgap!(fig.layout, 5)
|
||||||
|
|
||||||
# 4. Save the plot
|
# 3. Save the plot
|
||||||
mkpath(output_dir)
|
mkpath(output_dir)
|
||||||
filename = "$(stage_name).png"
|
filename = "$(stage_name).png"
|
||||||
save_path = joinpath(output_dir, filename)
|
save_path = joinpath(output_dir, filename)
|
||||||
@ -957,9 +953,9 @@ is used to exclude outliers before normalization.
|
|||||||
# Returns
|
# Returns
|
||||||
- A tuple `(low, high)` representing the calculated lower and upper intensity bounds.
|
- A tuple `(low, high)` representing the calculated lower and upper intensity bounds.
|
||||||
"""
|
"""
|
||||||
function get_outlier_thres(img::AbstractMatrix{<:Real}, prob::Real=0.98)
|
function get_outlier_thres(img::AbstractVector{<:Real}, prob::Real=0.98)
|
||||||
# DO NOT filter zeros. Use all pixel values like R does.
|
# DO NOT filter zeros. Use all pixel values like R does.
|
||||||
int_values = vec(img)
|
int_values = img
|
||||||
low = minimum(int_values)
|
low = minimum(int_values)
|
||||||
upp = maximum(int_values)
|
upp = maximum(int_values)
|
||||||
|
|
||||||
@ -1001,6 +997,10 @@ function get_outlier_thres(img::AbstractMatrix{<:Real}, prob::Real=0.98)
|
|||||||
return (low, actual_threshold)
|
return (low, actual_threshold)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function get_outlier_thres(img::AbstractMatrix{<:Real}, prob::Real=0.98)
|
||||||
|
return get_outlier_thres(vec(img), prob)
|
||||||
|
end
|
||||||
|
|
||||||
"""
|
"""
|
||||||
set_pixel_depth(img, bounds, depth)
|
set_pixel_depth(img, bounds, depth)
|
||||||
|
|
||||||
@ -1063,11 +1063,26 @@ within these bounds.
|
|||||||
# Returns
|
# Returns
|
||||||
- A new image matrix with intensities quantized to the specified depth within the TrIQ bounds.
|
- A new image matrix with intensities quantized to the specified depth within the TrIQ bounds.
|
||||||
"""
|
"""
|
||||||
function TrIQ(pixMap::AbstractMatrix{<:Real}, depth::Integer, prob::Real=0.98)
|
function TrIQ(pixMap::AbstractMatrix{<:Real}, depth::Integer, prob::Real=0.98; mask_matrix::Union{BitMatrix, Nothing}=nothing)
|
||||||
# Compute new dynamic range
|
local values_for_thres
|
||||||
bounds = get_outlier_thres(pixMap, prob)
|
if mask_matrix !== nothing
|
||||||
|
# Extract values only from the masked region for threshold calculation
|
||||||
|
values_for_thres = pixMap[mask_matrix]
|
||||||
|
# Filter out only zeros created by the mask.
|
||||||
|
filter!(x -> x != 0, values_for_thres)
|
||||||
|
else
|
||||||
|
values_for_thres = pixMap
|
||||||
|
end
|
||||||
|
|
||||||
|
# If the relevant values are empty or all zero, return a zero matrix
|
||||||
|
if isempty(values_for_thres) || all(iszero, values_for_thres)
|
||||||
|
bounds = (0.0, 0.0)
|
||||||
|
else
|
||||||
|
# Compute new dynamic range from the (potentially filtered) values
|
||||||
|
bounds = get_outlier_thres(values_for_thres, prob)
|
||||||
|
end
|
||||||
|
|
||||||
# Set intensity dynamic range
|
# Set intensity dynamic range for the *entire* pixMap, using bounds derived from masked data
|
||||||
return set_pixel_depth(pixMap, bounds, depth)
|
return set_pixel_depth(pixMap, bounds, depth)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1107,8 +1122,17 @@ Linearly scales the intensity values in a slice to a specified number of levels.
|
|||||||
The output is an array of `UInt8` values.
|
The output is an array of `UInt8` values.
|
||||||
This is a modernized version of `IntQuantCl`.
|
This is a modernized version of `IntQuantCl`.
|
||||||
"""
|
"""
|
||||||
function quantize_intensity(slice::AbstractMatrix{<:Real}, levels::Integer=256)
|
function quantize_intensity(slice::AbstractMatrix{<:Real}, levels::Integer=256; mask_matrix::Union{BitMatrix, Nothing}=nothing)
|
||||||
max_val = maximum(slice)
|
local max_val
|
||||||
|
if mask_matrix !== nothing
|
||||||
|
masked_slice = slice[mask_matrix]
|
||||||
|
# Filter out zeros that might have been introduced by masking, if any
|
||||||
|
filter!(x -> x != 0, masked_slice)
|
||||||
|
max_val = isempty(masked_slice) ? 0.0 : maximum(masked_slice)
|
||||||
|
else
|
||||||
|
max_val = maximum(slice)
|
||||||
|
end
|
||||||
|
|
||||||
if max_val <= 0
|
if max_val <= 0
|
||||||
return zeros(UInt8, size(slice))
|
return zeros(UInt8, size(slice))
|
||||||
end
|
end
|
||||||
@ -1208,7 +1232,7 @@ function display_statistics(slices::AbstractArray{<:AbstractMatrix{<:Real}, 2},
|
|||||||
stats_to_calc = Dict{String, Function}("Mean" => mean)
|
stats_to_calc = Dict{String, Function}("Mean" => mean)
|
||||||
all_dfs = Dict{String, DataFrame}()
|
all_dfs = Dict{String, DataFrame}()
|
||||||
|
|
||||||
for (stat_name, stat_func) in stats_to_to_calc
|
for (stat_name, stat_func) in stats_to_calc
|
||||||
# Pre-allocate matrix for better performance
|
# Pre-allocate matrix for better performance
|
||||||
stat_matrix = zeros(Float64, n_files, n_masses)
|
stat_matrix = zeros(Float64, n_files, n_masses)
|
||||||
|
|
||||||
@ -1497,4 +1521,81 @@ function process_columns_first!(A::AbstractMatrix)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Viridis color palette (256 colors) - defined as constant
|
# Viridis color palette (256 colors) - defined as constant
|
||||||
const ViridisPalette = generate_palette(ColorSchemes.viridis)
|
const ViridisPalette = generate_palette(ColorSchemes.viridis)
|
||||||
|
|
||||||
|
function generate_colorbar_image(slice_data::AbstractMatrix, color_levels::Int, output_path::String; use_triq::Bool=false, triq_prob::Float64=0.98, mask_path::Union{String, Nothing}=nothing)
|
||||||
|
# 1. Determine bounds based on whether TrIQ is used and if a mask is applied
|
||||||
|
local data_for_bounds
|
||||||
|
if mask_path !== nothing
|
||||||
|
height, width = size(slice_data)
|
||||||
|
mask_matrix = load_and_prepare_mask(mask_path, (width, height))
|
||||||
|
# Extract only the non-zero values within the mask to accurately calculate the range
|
||||||
|
data_for_bounds = slice_data[mask_matrix]
|
||||||
|
filter!(x -> x > 0, data_for_bounds)
|
||||||
|
else
|
||||||
|
data_for_bounds = vec(slice_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle cases where data_for_bounds might be empty after filtering
|
||||||
|
if isempty(data_for_bounds)
|
||||||
|
min_val, max_val = 0.0, 1.0 # Default range if no data in ROI
|
||||||
|
else
|
||||||
|
min_val, max_val = if use_triq
|
||||||
|
get_outlier_thres(data_for_bounds, triq_prob)
|
||||||
|
else
|
||||||
|
extrema(data_for_bounds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. Replicate the tick calculation logic from plot_slices
|
||||||
|
bins = color_levels
|
||||||
|
levels = range(min_val, stop=max_val, length=bins + 1)
|
||||||
|
level_range = levels[end] - levels[1]
|
||||||
|
|
||||||
|
if level_range == 0
|
||||||
|
levels = range(min_val - 0.1, stop=max_val + 0.1, length=bins + 1)
|
||||||
|
level_range = 0.2
|
||||||
|
end
|
||||||
|
|
||||||
|
exponent = level_range > 0 ? floor(log10(level_range)) / 3 : 0
|
||||||
|
scale = 10^(3 * exponent)
|
||||||
|
scaled_levels = levels ./ scale
|
||||||
|
|
||||||
|
format_num = level_range > 0 ? floor(log10(level_range)) % 3 : 0
|
||||||
|
labels = if format_num == 0
|
||||||
|
[ @sprintf("%3.2f", lvl) for lvl in scaled_levels]
|
||||||
|
elseif format_num == 1
|
||||||
|
[ @sprintf("%3.2f", lvl) for lvl in scaled_levels]
|
||||||
|
else
|
||||||
|
[ @sprintf("%3.2f", lvl) for lvl in scaled_levels]
|
||||||
|
end
|
||||||
|
|
||||||
|
divisors = 2:7
|
||||||
|
remainders = (bins - 1) .% divisors
|
||||||
|
best_divisor = divisors[findlast(x -> x == minimum(remainders), remainders)]
|
||||||
|
tick_indices = round.(Int, range(1, stop=bins + 1, length=best_divisor + 1))
|
||||||
|
|
||||||
|
if !(1 in tick_indices)
|
||||||
|
pushfirst!(tick_indices, 1)
|
||||||
|
end
|
||||||
|
if !((bins + 1) in tick_indices)
|
||||||
|
push!(tick_indices, bins + 1)
|
||||||
|
end
|
||||||
|
unique!(sort!(tick_indices))
|
||||||
|
|
||||||
|
tick_positions = levels[tick_indices]
|
||||||
|
tick_labels = labels[tick_indices]
|
||||||
|
|
||||||
|
# 3. Create and save the colorbar image
|
||||||
|
fig = Figure(size=(150, 250))
|
||||||
|
Colorbar(fig[1, 1],
|
||||||
|
colormap=cgrad(:viridis, bins, categorical=true),
|
||||||
|
# limits=(min_val, max_val),
|
||||||
|
limits=(levels[1], levels[end]),
|
||||||
|
label=(scale == 1 ? "Intensity" : "Intensity ×10^$(round(Int, 3 * exponent))"),
|
||||||
|
ticks=(tick_positions, tick_labels),
|
||||||
|
labelsize=20,
|
||||||
|
ticklabelsize=16
|
||||||
|
)
|
||||||
|
save(output_path, fig)
|
||||||
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user