# run_preprocessing.jl using Printf import Pkg using CairoMakie using DataFrames # For creating dataframes using CSV # --- Load the MSI_src Module --- Pkg.activate(joinpath(@__DIR__, "..")) using MSI_src # =================================================================== # CONFIG: PLEASE FILL IN YOUR FILE PATHS HERE # =================================================================== const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/Thricoderma_etc/Imaging_interaccion_trichoderma_vs_streptomyces.imzML" # const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/set de datos MS/Atropina_tuneo_fraq_20ev.mzML" # const TEST_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/salida/Stomach_DHB_uncompressed.imzML" # const MASK_ROUTE = "/home/pixel/Documents/Cinvestav_2025/JuliaMSI/public/css/masks/Stomach_DHB_uncompressed.png" const MASK_ROUTE = "" const OUTPUT_DIR = "./test/results/preprocessing_results" # Reference peaks for calibration and alignment const reference_peaks = Dict( 137.0244 => "DHB_fragment", 155.0349 => "DHB_M+H", 177.0168 => "DHB_M+Na", 496.3398 => "PC_16:0_16:0", 520.3398 => "PC_16:0_18:1", 760.5851 => "PC_16:0_18:1_Na", 391.2843 => "PDMS", 413.2662 => "PDMS_Na", 842.5092 => "Protein_standard", 1045.532 => "Protein_standard", 290.1747 => "Atropine [M+H]+", 304.1903 => "Scopolamine [M+H]+", 124.0393 => "Tropine [M+H]+", ) # =================================================================== # HELPER FUNCTIONS # =================================================================== function ensure_output_dir() if !isdir(OUTPUT_DIR) mkdir(OUTPUT_DIR) println("Created output directory: $OUTPUT_DIR") end end function plot_spectrum_step(mz, intensity, step_name; peaks=nothing, spectrum_index=1) fig = Figure(size=(1200, 600)) ax = Axis(fig[1, 1], xlabel="m/z", ylabel="Intensity", title="Step: $step_name (Spectrum $spectrum_index)") lines!(ax, mz, intensity, color=:blue, linewidth=1) if peaks !== nothing && !isempty(peaks) peak_mz = [p.mz for p in peaks] peak_intensity = [p.intensity for p in peaks] scatter!(ax, peak_mz, peak_intensity, color=:red, markersize=5) end filename = joinpath(OUTPUT_DIR, "spectrum_$(step_name)_index_$spectrum_index.png") save(filename, fig) println(" - Saved plot: $(basename(filename))") end function print_step_header(step_name::String) println("\n" * ">"^50) println("APPLYING STEP: $step_name") println(">"^50) end # =================================================================== # PREPROCESSING PIPELINE FUNCTIONS (IN-PLACE) # =================================================================== function apply_baseline_correction(spectra::Vector{MutableSpectrum}, params::Dict, msi_data::MSIData) print_step_header("Baseline Correction") method = get(params, :method, :snip) iterations = get(params, :iterations, 100) window = get(params, :window, 20) println(" Method: $method, Iterations: $iterations, Window: $window") # Safely plot the first spectrum if !isempty(spectra) s = spectra[1] if validate_spectrum(s.mz, s.intensity) baseline = MSI_src.apply_baseline_correction(s.intensity; method=method, iterations=iterations, window=window) corrected_intensity = max.(0.0, s.intensity .- baseline) plot_spectrum_step(s.mz, corrected_intensity, "baseline_correction") end end Threads.@threads for s in spectra if validate_spectrum(s.mz, s.intensity) baseline = MSI_src.apply_baseline_correction(s.intensity; method=method, iterations=iterations, window=window) s.intensity = max.(0.0, s.intensity .- baseline) end end end function apply_smoothing(spectra::Vector{MutableSpectrum}, params::Dict) print_step_header("Smoothing") method = get(params, :method, :savitzky_golay) window = get(params, :window, 9) order = get(params, :order, 2) println(" Method: $method, Window: $window, Order: $order") # Safely plot the first spectrum if !isempty(spectra) s = spectra[1] if validate_spectrum(s.mz, s.intensity) smoothed_intensity = max.(0.0, smooth_spectrum(s.intensity; method=method, window=window, order=order)) plot_spectrum_step(s.mz, smoothed_intensity, "smoothing", spectrum_index=s.id) end end Threads.@threads for s in spectra if validate_spectrum(s.mz, s.intensity) smoothed_intensity = max.(0.0, smooth_spectrum(s.intensity; method=method, window=window, order=order)) s.intensity = smoothed_intensity end end end function apply_peak_picking(spectra::Vector{MutableSpectrum}, params::Dict) print_step_header("Peak Picking") method = get(params, :method, :profile) snr_threshold = get(params, :snr_threshold, 3.0) half_window = get(params, :half_window, 10) min_peak_prominence = get(params, :min_peak_prominence, 0.1) merge_peaks_tolerance = get(params, :merge_peaks_tolerance, 0.002) println(" Method: $method, SNR: $snr_threshold, Half Window: $half_window") found_invalid_spectrum = Threads.Atomic{Bool}(false) Threads.@threads for s in spectra is_valid = validate_spectrum(s.mz, s.intensity) if !is_valid && !found_invalid_spectrum[] if Threads.atomic_xchg!(found_invalid_spectrum, true) == false # Debug printing for the first invalid spectrum found end end if is_valid if method == :profile s.peaks = detect_peaks_profile(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window, min_peak_prominence=min_peak_prominence, merge_peaks_tolerance=merge_peaks_tolerance) elseif method == :wavelet s.peaks = detect_peaks_wavelet(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window) elseif method == :centroid s.peaks = detect_peaks_centroid(s.mz, s.intensity; snr_threshold=snr_threshold) else s.peaks = detect_peaks_profile(s.mz, s.intensity; snr_threshold=snr_threshold, half_window=half_window) end else s.peaks = [] end end # Safely plot the first spectrum if !isempty(spectra) s1 = findfirst(s -> s.id == 1, spectra) if s1 !== nothing plot_spectrum_step(spectra[s1].mz, spectra[s1].intensity, "peak_picking", peaks=spectra[s1].peaks) println(" - Detected $(length(spectra[s1].peaks)) peaks in spectrum 1") end end end function apply_calibration(spectra::Vector{MutableSpectrum}, params::Dict, reference_peaks::Dict) print_step_header("Calibration") method = get(params, :method, :none) ppm_tolerance = get(params, :ppm_tolerance, 20.0) println(" Method: $method, PPM Tolerance: $ppm_tolerance") if method == :none || isempty(reference_peaks) println(" - Skipping calibration (no method or reference peaks)") return end reference_masses = collect(keys(reference_peaks)) calibration_info = Vector{String}(undef, length(spectra)) Threads.@threads for i in 1:length(spectra) s = spectra[i] info_message = "" if validate_spectrum(s.mz, s.intensity) matched_peaks = find_calibration_peaks(s.mz, s.intensity, reference_masses; ppm_tolerance=ppm_tolerance) if length(matched_peaks) >= 2 measured = sort(collect(values(matched_peaks))) theoretical = sort(collect(keys(matched_peaks))) itp = linear_interpolation(measured, theoretical, extrapolation_bc=Line()) s.mz = itp(s.mz) # Modify mz-axis in-place info_message = " - Spectrum $(s.id): calibrated using $(length(matched_peaks)) reference peaks" else info_message = " - Spectrum $(s.id): insufficient reference peaks ($(length(matched_peaks)) found), skipping" end end calibration_info[i] = info_message end # Print summary of results printed_count = 0 for info in calibration_info if !isempty(info) && printed_count < 5 println(info) printed_count += 1 end end end function apply_peak_alignment(spectra::Vector{MutableSpectrum}, params::Dict) print_step_header("Peak Alignment") method = get(params, :method, :none) tolerance = get(params, :tolerance, 0.002) tolerance_unit = get(params, :tolerance_unit, :mz) println(" Method: $method, Tolerance: $tolerance $tolerance_unit") if method == :none println(" - Skipping peak alignment") return end ref_find_idx = findfirst(s -> !isempty(s.peaks), spectra) if ref_find_idx === nothing println(" - Insufficient spectra with peaks for alignment. Skipping.") return end ref_spectrum = spectra[ref_find_idx] ref_peaks_mz = [p.mz for p in ref_spectrum.peaks] println(" - Using spectrum $(ref_spectrum.id) as reference with $(length(ref_peaks_mz)) peaks") Threads.@threads for s in spectra if s.id == ref_spectrum.id || isempty(s.peaks) continue end current_peaks_mz = [p.mz for p in s.peaks] alignment_func = align_peaks_lowess(ref_peaks_mz, current_peaks_mz; method=method, tolerance=tolerance, tolerance_unit=tolerance_unit) s.mz = alignment_func.(s.mz) # Update m/z axis # Update peak m/z values for i in 1:length(s.peaks) old_peak = s.peaks[i] aligned_peak_mz = alignment_func(old_peak.mz) s.peaks[i] = (mz=aligned_peak_mz, intensity=old_peak.intensity, fwhm=old_peak.fwhm, shape_r2=old_peak.shape_r2, snr=old_peak.snr, prominence=old_peak.prominence) end end end function apply_normalization(spectra::Vector{MutableSpectrum}, params::Dict) print_step_header("Normalization") method = get(params, :method, :tic) println(" Method: $method") # Safely plot the first spectrum if !isempty(spectra) s1 = findfirst(s -> s.id == 1, spectra) if s1 !== nothing s = spectra[s1] if validate_spectrum(s.mz, s.intensity) normalized_intensity = MSI_src.apply_normalization(s.intensity; method=method) plot_spectrum_step(s.mz, normalized_intensity, "normalization") end end end Threads.@threads for s in spectra if validate_spectrum(s.mz, s.intensity) s.intensity = MSI_src.apply_normalization(s.intensity; method=method) end end end function apply_peak_binning(spectra::Vector{MutableSpectrum}, params::Dict) print_step_header("Peak Binning") method = get(params, :method, :adaptive) tolerance = get(params, :tolerance, 20.0) tolerance_unit = get(params, :tolerance_unit, :ppm) min_peak_per_bin = get(params, :min_peak_per_bin, 3) max_bin_width_ppm = get(params, :max_bin_width_ppm, 150.0) intensity_weighted_centers = get(params, :intensity_weighted_centers, true) println(" Method: $method, Tolerance: $tolerance $tolerance_unit") if isempty(spectra) || all(s -> isempty(s.peaks), spectra) @warn "No peaks found for binning. Returning empty feature matrix." return nothing, nothing end println(" - Binning peaks from $(length(spectra)) spectra") binning_params = PeakBinningParams( method=method, tolerance=tolerance, tolerance_unit=tolerance_unit, min_peak_per_bin=min_peak_per_bin, max_bin_width_ppm=max_bin_width_ppm, intensity_weighted_centers=intensity_weighted_centers ) feature_matrix, bin_definitions = bin_peaks(spectra, binning_params) if feature_matrix !== nothing && bin_definitions !== nothing println(" - Generated feature matrix: $(size(feature_matrix.matrix))") println(" - Number of bins: $(length(bin_definitions))") end return feature_matrix, bin_definitions end function save_feature_matrix(feature_matrix, bin_definitions) print_step_header("Saving Results") # Save feature matrix as CSV csv_path = joinpath(OUTPUT_DIR, "feature_matrix.csv") # Create column headers bin_headers = ["bin_$(i)_$(round(def[1], digits=4))-$(round(def[2], digits=4))" for (i, def) in enumerate(bin_definitions)] # Open file for writing open(csv_path, "w") do io # Write header row write(io, "spectrum_index," * join(bin_headers, ",") * "\n") # Write data rows for r_idx in 1:size(feature_matrix.matrix, 1) write(io, "$(feature_matrix.sample_ids[r_idx]),") for c_idx in 1:size(feature_matrix.matrix, 2) write(io, "$(feature_matrix.matrix[r_idx, c_idx])") if c_idx < size(feature_matrix.matrix, 2) write(io, ",") end end write(io, "\n") end end println(" - Saved feature matrix: $csv_path") # Save bin definitions (still using DataFrame as it's small) bins_path = joinpath(OUTPUT_DIR, "bin_definitions.csv") bins_df = DataFrame( bin_index = 1:length(bin_definitions), mz_start = [def[1] for def in bin_definitions], mz_end = [def[2] for def in bin_definitions], mz_center = [(def[1] + def[2])/2 for def in bin_definitions] ) CSV.write(bins_path, bins_df) println(" - Saved bin definitions: $bins_path") return csv_path, bins_path end # =================================================================== # USER OVERRIDES: Manually specify parameters here # =================================================================== # This dictionary allows you to override any auto-detected parameters. # The structure should match the output of `main_precalculation`. # Example: Force a less aggressive peak prominence threshold. const USER_OVERRIDES = Dict( :PeakPicking => Dict( #:snr_threshold => 1.5, :half_window => 5, :snr_threshold => 15.0, #:merge_peaks_tolerance => 2.5, #:half_window => 2 ), # :Smoothing => Dict( # :window => 11 # ) #:PeakAlignment => Dict( # :tolerance => 0.01 #) :PeakBinningParams => Dict( :tolerance => 30.0 ) ) # =================================================================== # MAIN PREPROCESSING PIPELINE # =================================================================== function run_preprocessing_pipeline() println("="^80) println("MSI PREPROCESSING PIPELINE") println("="^80) # Ensure output directory exists ensure_output_dir() # Load data println("\nLoading data: $(basename(TEST_FILE))") msi_data = OpenMSIData(TEST_FILE) # Precompute analytics println("\nPrecomputing analytics...") precompute_analytics(msi_data) # Run precalculation to get automatic parameters println("\nRunning precalculation for automatic parameter determination...") auto_params = main_precalculation(msi_data, reference_peaks=reference_peaks, mask_path=MASK_ROUTE) # --- Apply User Overrides --- if !isempty(USER_OVERRIDES) println("\nApplying user overrides...") for (step, params) in USER_OVERRIDES if haskey(auto_params, step) for (param, value) in params @info "OVERRIDE: For step `:$step`, setting `:$param` to `$value` (was `$(get(auto_params[step], param, "not set"))`)" end merge!(auto_params[step], params) else @warn "User override for non-existent step `:$step` ignored." end end end # --- Final Parameter Summary --- println("\n" * "-"^60) println("FINAL PARAMETERS FOR PIPELINE RUN") println("-"^60) for (step, params) in sort(collect(pairs(auto_params)), by=p -> p.first) println(" - Step: $step") if isempty(params) println(" (No parameters)") continue end for (param, value) in sort(collect(pairs(params)), by=p -> p.first) println(" - $(rpad(param, 25)): $value") end end # Define pipeline steps pipeline_steps = [ "baseline_correction", "smoothing", "peak_picking", "calibration", "peak_alignment", "normalization", "peak_binning" ] println("\nPipeline steps: $(join(pipeline_steps, " -> "))") # Initialize `current_spectra` as a Vector of mutable structs for in-place modification println("\nInitializing spectra data structure...") num_spectra = length(msi_data.spectra_metadata) current_spectra = Vector{MutableSpectrum}(undef, num_spectra) _iterate_spectra_fast(msi_data) do idx, mz, intensity current_spectra[idx] = MutableSpectrum(idx, mz, intensity, []) # Plot raw spectrum without masks if idx == 1 plot_spectrum_step(mz, intensity, "raw_unmasked_spectrum") end end # These variables will be populated by the pipeline steps feature_matrix = nothing bin_definitions = nothing # Apply pipeline steps, modifying `current_spectra` in-place for step in pipeline_steps println("\n" * "-"^60) println("PROCESSING STEP: $step") println("-"^60) if step == "baseline_correction" @time apply_baseline_correction(current_spectra, auto_params[:BaselineCorrection], msi_data) elseif step == "smoothing" apply_smoothing(current_spectra, auto_params[:Smoothing]) elseif step == "peak_picking" @time apply_peak_picking(current_spectra, auto_params[:PeakPicking]) elseif step == "calibration" @time apply_calibration(current_spectra, auto_params[:Calibration], reference_peaks) elseif step == "peak_alignment" @time apply_peak_alignment(current_spectra, auto_params[:PeakAlignment]) elseif step == "normalization" @time apply_normalization(current_spectra, auto_params[:Normalization]) elseif step == "peak_binning" # This step is different as it generates the final matrix, not modifying spectra in-place feature_matrix, bin_definitions = @time apply_peak_binning(current_spectra, auto_params[:PeakBinningParams]) if feature_matrix !== nothing @time save_feature_matrix(feature_matrix, bin_definitions) end else @warn "Unknown step: $step, skipping" end # Print progress println("✓ Completed step: $step") end # Clean up close(msi_data) GC.gc() println("\n" * "="^80) println("PREPROCESSING PIPELINE COMPLETED SUCCESSFULLY!") println("Results saved to: $OUTPUT_DIR") println("="^80) end # =================================================================== # EXECUTE PIPELINE # =================================================================== if abspath(PROGRAM_FILE) == @__FILE__ @time run_preprocessing_pipeline() end