finished mask creator suite, a new route on the JuliaMSI which helps to create binary masks using either slices or added images, with drawing capability, which in turn will help in the future get more accurate results without extra ambiental noise, added preprocessing test pipeline, along with functions, fixed windows route error for batch processing

This commit is contained in:
Pixelguy14 2025-10-31 12:05:03 -06:00
parent 39c92ba850
commit e13b5ef0cc
18 changed files with 2851 additions and 456 deletions

3
.gitignore vendored
View File

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

View File

@ -2,7 +2,7 @@
julia_version = "1.11.7"
manifest_format = "2.0"
project_hash = "b3d73d786a430f468e0f2f1b3b05da6a3addc7fe"
project_hash = "9b36a3561cfc9927071202de68baf8c2b0f02a5c"
[[deps.ATK_jll]]
deps = ["Artifacts", "Glib_jll", "JLLWrappers", "Libdl"]
@ -26,6 +26,30 @@ git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177"
uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
version = "0.4.5"
[[deps.Accessors]]
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
git-tree-sha1 = "3b86719127f50670efe356bc11073d84b4ed7a5d"
uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
version = "0.1.42"
[deps.Accessors.extensions]
AxisKeysExt = "AxisKeys"
IntervalSetsExt = "IntervalSets"
LinearAlgebraExt = "LinearAlgebra"
StaticArraysExt = "StaticArrays"
StructArraysExt = "StructArrays"
TestExt = "Test"
UnitfulExt = "Unitful"
[deps.Accessors.weakdeps]
AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5"
IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
[[deps.Adapt]]
deps = ["LinearAlgebra", "Requires"]
git-tree-sha1 = "7e35fca2bdfba44d797c53dfe63a51fabf39bfc0"
@ -336,6 +360,15 @@ deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
version = "1.1.1+0"
[[deps.CompositionsBase]]
git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad"
uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b"
version = "0.1.2"
weakdeps = ["InverseFunctions"]
[deps.CompositionsBase.extensions]
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
[[deps.ComputationalResources]]
git-tree-sha1 = "52cb3ec90e8a8bea0e62e275ba577ad0f74821f7"
uuid = "ed09eef8-17a6-5b46-8889-db040fac31e3"
@ -347,12 +380,6 @@ git-tree-sha1 = "d9d26935a0bcffc87d2613ce14c527c99fc543fd"
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
version = "2.5.0"
[[deps.Conda]]
deps = ["Downloads", "JSON", "VersionParsing"]
git-tree-sha1 = "b19db3927f0db4151cb86d073689f2428e524576"
uuid = "8f4d0f93-b110-5947-807f-2305c1781a2d"
version = "1.10.2"
[[deps.Configurations]]
deps = ["ExproniconLite", "OrderedCollections", "TOML"]
git-tree-sha1 = "4358750bb58a3caefd5f37a4a0c5bfdbbf075252"
@ -375,6 +402,16 @@ git-tree-sha1 = "439e35b0b36e2e5881738abc8857bd92ad6ff9a8"
uuid = "d38c429a-6771-53c6-b99e-75d170b6e991"
version = "0.6.3"
[[deps.ConvexHulls2d]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "cd7759cfeaf855fed46ec26a26fb323ac3a5501e"
uuid = "438b5665-92b0-42e6-bc36-e4ac8449fa2d"
version = "0.1.1"
weakdeps = ["Makie"]
[deps.ConvexHulls2d.extensions]
ConvexHulls2dMakieExt = "Makie"
[[deps.CoordinateTransformations]]
deps = ["LinearAlgebra", "StaticArrays"]
git-tree-sha1 = "a692f5e257d332de1e554e4566a4e5a8a72de2b2"
@ -525,11 +562,6 @@ git-tree-sha1 = "7bb1361afdb33c7f2b085aa49ea8fe1b0fb14e58"
uuid = "2e619515-83b5-522b-bb60-26c02a35a201"
version = "2.7.1+0"
[[deps.ExpressionExplorer]]
git-tree-sha1 = "4a8c0a9eebf807ac42f0f6de758e60a20be25ffb"
uuid = "21656369-7473-754a-2065-74616d696c43"
version = "1.1.3"
[[deps.ExproniconLite]]
git-tree-sha1 = "c13f0b150373771b0fdc1713c97860f8df12e6c2"
uuid = "55351af7-c7e9-48d6-89ff-24e801d99491"
@ -546,12 +578,6 @@ git-tree-sha1 = "680a39c9aadce7c721b68d979e66dc65d2021aa6"
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
version = "1.2.2"
[[deps.FFMPEG]]
deps = ["FFMPEG_jll"]
git-tree-sha1 = "83dc665d0312b41367b7263e8a4d172eac1897f4"
uuid = "c87230d0-a227-11e9-1b43-d7ebe4e7570a"
version = "0.4.4"
[[deps.FFMPEG_jll]]
deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "PCRE2_jll", "Zlib_jll", "libaom_jll", "libass_jll", "libfdk_aac_jll", "libvorbis_jll", "x264_jll", "x265_jll"]
git-tree-sha1 = "eaa040768ea663ca695d442be1bc97edfe6824f2"
@ -578,9 +604,9 @@ version = "3.3.11+0"
[[deps.FileIO]]
deps = ["Pkg", "Requires", "UUIDs"]
git-tree-sha1 = "b66970a70db13f45b7e57fbda1736e1cf72174ea"
git-tree-sha1 = "d60eb76f37d7e5a40cc2e7c36974d864b82dc802"
uuid = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
version = "1.17.0"
version = "1.17.1"
weakdeps = ["HTTP"]
[deps.FileIO.extensions]
@ -665,29 +691,29 @@ deps = ["Random"]
uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820"
version = "1.11.0"
[[deps.GLFW]]
deps = ["GLFW_jll"]
git-tree-sha1 = "40412e58ec374029de3d4ad7c13e1a52aa1e149f"
uuid = "f7f18e0c-5ee9-5ccd-a5bf-e8befd85ed98"
version = "3.4.5"
[[deps.GLFW_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Libglvnd_jll", "Xorg_libXcursor_jll", "Xorg_libXi_jll", "Xorg_libXinerama_jll", "Xorg_libXrandr_jll", "libdecor_jll", "xkbcommon_jll"]
git-tree-sha1 = "fcb0584ff34e25155876418979d4c8971243bb89"
uuid = "0656b61e-2033-5cc2-a64a-77c0f6c09b89"
version = "3.4.0+2"
[[deps.GLMakie]]
deps = ["ColorTypes", "Colors", "FileIO", "FixedPointNumbers", "FreeTypeAbstraction", "GLFW", "GeometryBasics", "LinearAlgebra", "Makie", "Markdown", "MeshIO", "ModernGL", "Observables", "PrecompileTools", "Printf", "ShaderAbstractions", "StaticArrays"]
git-tree-sha1 = "487ffeede54553565023a107529434ff585060ae"
uuid = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
version = "0.11.2"
[[deps.GMP_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "781609d7-10c4-51f6-84f2-b8444358ff6d"
version = "6.3.0+0"
[[deps.GR]]
deps = ["Artifacts", "Base64", "DelimitedFiles", "Downloads", "GR_jll", "HTTP", "JSON", "Libdl", "LinearAlgebra", "Preferences", "Printf", "Qt6Wayland_jll", "Random", "Serialization", "Sockets", "TOML", "Tar", "Test", "p7zip_jll"]
git-tree-sha1 = "1828eb7275491981fa5f1752a5e126e8f26f8741"
uuid = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
version = "0.73.17"
[[deps.GR_jll]]
deps = ["Artifacts", "Bzip2_jll", "Cairo_jll", "FFMPEG_jll", "Fontconfig_jll", "FreeType2_jll", "GLFW_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pixman_jll", "Qt6Base_jll", "Zlib_jll", "libpng_jll"]
git-tree-sha1 = "27299071cc29e409488ada41ec7643e0ab19091f"
uuid = "d2c73de3-f751-5644-a686-071e5b155ba9"
version = "0.73.17+0"
[[deps.GTK3_jll]]
deps = ["ATK_jll", "Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "FriBidi_jll", "Glib_jll", "HarfBuzz_jll", "JLLWrappers", "Libdl", "Libepoxy_jll", "Pango_jll", "Pkg", "Wayland_jll", "Xorg_libX11_jll", "Xorg_libXcomposite_jll", "Xorg_libXcursor_jll", "Xorg_libXdamage_jll", "Xorg_libXext_jll", "Xorg_libXfixes_jll", "Xorg_libXi_jll", "Xorg_libXinerama_jll", "Xorg_libXrandr_jll", "Xorg_libXrender_jll", "at_spi2_atk_jll", "gdk_pixbuf_jll", "iso_codes_jll", "xkbcommon_jll"]
git-tree-sha1 = "b080a592525632d287aee4637a62682576b7f5e4"
@ -783,12 +809,6 @@ git-tree-sha1 = "97285bbd5230dd766e9ef6749b80fc617126d496"
uuid = "c27321d9-0574-5035-807b-f59d2c89b15c"
version = "1.3.1"
[[deps.GracefulPkg]]
deps = ["Compat", "Pkg", "TOML"]
git-tree-sha1 = "698050b04f3cc0906d0817329d6e96484bf238eb"
uuid = "828d9ff0-206c-6161-646e-6576656f7244"
version = "2.3.0"
[[deps.Graphics]]
deps = ["Colors", "LinearAlgebra", "NaNMath"]
git-tree-sha1 = "a641238db938fff9b2f60d08ed9030387daf428c"
@ -859,12 +879,6 @@ git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec"
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
version = "0.3.28"
[[deps.HypertextLiteral]]
deps = ["Tricks"]
git-tree-sha1 = "7134810b1afce04bbc1045ca1985fbe81ce17653"
uuid = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2"
version = "0.9.5"
[[deps.IfElse]]
git-tree-sha1 = "debdd00ffef04665ccbb3e150747a77560e8fad1"
uuid = "615f187c-cbe4-4ef1-ba3b-2fcf58d6d173"
@ -888,6 +902,12 @@ git-tree-sha1 = "33485b4e40d1df46c806498c73ea32dc17475c59"
uuid = "cbc4b850-ae4b-5111-9e64-df94c024a13d"
version = "0.3.1"
[[deps.ImageComponentAnalysis]]
deps = ["AbstractTrees", "ConvexHulls2d", "DataFrames", "DataStructures", "ImageFiltering", "LeftChildRightSiblingTrees", "LinearAlgebra", "OffsetArrays", "Parameters", "StaticArrays"]
git-tree-sha1 = "0f1e954d54bafec3df4aa9d1ce1f110e311a9043"
uuid = "d9b9e9a0-1569-11e9-2cb5-bbca914b0e89"
version = "0.2.2"
[[deps.ImageContrastAdjustment]]
deps = ["ImageBase", "ImageCore", "ImageTransformations", "Parameters"]
git-tree-sha1 = "eb3d4365a10e3f3ecb3b115e9d12db131d28a386"
@ -1121,12 +1141,6 @@ weakdeps = ["UnPack"]
[deps.JLD2.extensions]
UnPackExt = "UnPack"
[[deps.JLFzf]]
deps = ["REPL", "Random", "fzf_jll"]
git-tree-sha1 = "82f7acdc599b65e0f8ccd270ffa1467c21cb647b"
uuid = "1019f520-868f-41f5-a6de-eb00f4b6a39c"
version = "0.1.11"
[[deps.JLLWrappers]]
deps = ["Artifacts", "Preferences"]
git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2"
@ -1199,15 +1213,6 @@ git-tree-sha1 = "eb62a3deb62fc6d8822c0c4bef73e4412419c5d8"
uuid = "1d63c593-3942-5779-bab2-d838dc0a180e"
version = "18.1.8+0"
[[deps.LRUCache]]
git-tree-sha1 = "5519b95a490ff5fe629c4a7aa3b3dfc9160498b3"
uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
version = "1.6.2"
weakdeps = ["Serialization"]
[deps.LRUCache.extensions]
SerializationExt = ["Serialization"]
[[deps.LZO_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "1c602b1127f4751facb671441ca72715cc95938a"
@ -1219,35 +1224,12 @@ git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c"
uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
version = "1.4.0"
[[deps.Latexify]]
deps = ["Format", "Ghostscript_jll", "InteractiveUtils", "LaTeXStrings", "MacroTools", "Markdown", "OrderedCollections", "Requires"]
git-tree-sha1 = "44f93c47f9cd6c7e431f2f2091fcba8f01cd7e8f"
uuid = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
version = "0.16.10"
[deps.Latexify.extensions]
DataFramesExt = "DataFrames"
SparseArraysExt = "SparseArrays"
SymEngineExt = "SymEngine"
TectonicExt = "tectonic_jll"
[deps.Latexify.weakdeps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
SymEngine = "123dc426-2d89-5057-bbad-38513e3affd8"
tectonic_jll = "d7dd28d6-a5e6-559c-9131-7eb760cdacc5"
[[deps.LayoutPointers]]
deps = ["ArrayInterface", "LinearAlgebra", "ManualMemory", "SIMDTypes", "Static", "StaticArrayInterface"]
git-tree-sha1 = "a9eaadb366f5493a5654e843864c13d8b107548c"
uuid = "10f19ff3-798f-405d-979b-55457f8fc047"
version = "0.1.17"
[[deps.LazilyInitializedFields]]
git-tree-sha1 = "0f2da712350b020bc3957f269c9caad516383ee0"
uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf"
version = "1.3.0"
[[deps.LazyArtifacts]]
deps = ["Artifacts", "Pkg"]
uuid = "4af54fe1-eca0-43a8-85a7-787d91b784e3"
@ -1258,6 +1240,12 @@ git-tree-sha1 = "a560dd966b386ac9ae60bdd3a3d3a326062d3c3e"
uuid = "8cdb02fc-e678-4876-92c5-9defec4f444e"
version = "0.3.1"
[[deps.LeftChildRightSiblingTrees]]
deps = ["AbstractTrees"]
git-tree-sha1 = "95ba48564903b43b2462318aa243ee79d81135ff"
uuid = "1d6d02ad-be62-4b6b-8a6d-2f90e265016e"
version = "0.2.1"
[[deps.LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
@ -1346,6 +1334,12 @@ git-tree-sha1 = "8e6a74641caf3b84800f2ccd55dc7ab83893c10b"
uuid = "d3a379c0-f9a3-5b72-a4c0-6bf4d2e8af0f"
version = "2.17.0+0"
[[deps.Loess]]
deps = ["Distances", "LinearAlgebra", "Statistics", "StatsAPI"]
git-tree-sha1 = "f749e7351f120b3566e5923fefdf8e52ba5ec7f9"
uuid = "4345ca2d-374a-55d4-8d30-97f9976e7612"
version = "0.6.4"
[[deps.LogExpFunctions]]
deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"]
git-tree-sha1 = "13ca9e2586b89836fd20cccf56e57e2b9ae7f38f"
@ -1421,12 +1415,6 @@ git-tree-sha1 = "c731269d5a2c85ffdc689127a9ba6d73e978a4b1"
uuid = "20f20a25-4f0e-4fdf-b5d1-57303727442b"
version = "0.9.0"
[[deps.Malt]]
deps = ["Distributed", "Logging", "RelocatableFolders", "Serialization", "Sockets"]
git-tree-sha1 = "636abf4fb184be05888dacc0b636fe0911d6d746"
uuid = "36869731-bdee-424d-aa32-cab38c994e3b"
version = "1.2.0"
[[deps.ManualMemory]]
git-tree-sha1 = "bcaef4fc7a0cfe2cba636d84cda54b5e4e4ca3cd"
uuid = "d125e4d3-2237-4719-b19c-fa641b8a4667"
@ -1459,10 +1447,11 @@ deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
version = "2.28.6+0"
[[deps.Measures]]
git-tree-sha1 = "c13304c81eec1ed3af7fc20e75fb6b26092a1102"
uuid = "442fdcdd-2543-5da2-b0f3-8c86c306513e"
version = "0.3.2"
[[deps.MeshIO]]
deps = ["ColorTypes", "FileIO", "GeometryBasics", "Printf"]
git-tree-sha1 = "c009236e222df68e554c7ce5c720e4a33cc0c23f"
uuid = "7269a6da-0436-5bbc-96c2-40638cbb6118"
version = "0.5.3"
[[deps.MetaGraphs]]
deps = ["Graphs", "JLD2", "Random"]
@ -1491,6 +1480,12 @@ version = "0.1.2"
uuid = "a63ad114-7e13-5084-954f-fe012c677804"
version = "1.11.0"
[[deps.ModernGL]]
deps = ["Libdl"]
git-tree-sha1 = "ac6cb1d8807a05cf1acc9680e09d2294f9d33956"
uuid = "66fc600b-dfda-50eb-8b99-91cfa97b1301"
version = "1.1.8"
[[deps.MosaicViews]]
deps = ["MappedArrays", "OffsetArrays", "PaddedViews", "StackViews"]
git-tree-sha1 = "7b86a5d4d70a9f5cdf2dacb3cbe6d251d1a61dbe"
@ -1501,12 +1496,6 @@ version = "0.3.4"
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
version = "2023.12.12"
[[deps.MsgPack]]
deps = ["Serialization"]
git-tree-sha1 = "f5db02ae992c260e4826fe78c942954b48e1d9c2"
uuid = "99f44e22-a591-53d1-9472-aa23ef4bd671"
version = "1.2.1"
[[deps.NaNMath]]
deps = ["OpenLibm_jll"]
git-tree-sha1 = "9b8215b1ee9e78a293f99797cd31375471b2bcae"
@ -1720,12 +1709,6 @@ git-tree-sha1 = "f9501cc0430a26bc3d156ae1b5b0c1b47af4d6da"
uuid = "eebad327-c553-4316-9ea0-9fa01ccd7688"
version = "0.3.3"
[[deps.PlotThemes]]
deps = ["PlotUtils", "Statistics"]
git-tree-sha1 = "41031ef3a1be6f5bbbf3e8073f210556daeae5ca"
uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a"
version = "3.3.0"
[[deps.PlotUtils]]
deps = ["ColorSchemes", "Colors", "Dates", "PrecompileTools", "Printf", "Random", "Reexport", "StableRNGs", "Statistics"]
git-tree-sha1 = "3ca9a356cd2e113c420f2c13bea19f8d3fb1cb18"
@ -1738,38 +1721,6 @@ git-tree-sha1 = "56baf69781fc5e61607c3e46227ab17f7040ffa2"
uuid = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
version = "0.8.19"
[[deps.Plots]]
deps = ["Base64", "Contour", "Dates", "Downloads", "FFMPEG", "FixedPointNumbers", "GR", "JLFzf", "JSON", "LaTeXStrings", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "Pkg", "PlotThemes", "PlotUtils", "PrecompileTools", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "RelocatableFolders", "Requires", "Scratch", "Showoff", "SparseArrays", "Statistics", "StatsBase", "TOML", "UUIDs", "UnicodeFun", "Unzip"]
git-tree-sha1 = "12ce661880f8e309569074a61d3767e5756a199f"
uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
version = "1.41.1"
[deps.Plots.extensions]
FileIOExt = "FileIO"
GeometryBasicsExt = "GeometryBasics"
IJuliaExt = "IJulia"
ImageInTerminalExt = "ImageInTerminal"
UnitfulExt = "Unitful"
[deps.Plots.weakdeps]
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
IJulia = "7073ff75-c697-5162-941a-fcdaad2a7d2a"
ImageInTerminal = "d8c32880-2388-543b-8c61-d9f865259254"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
[[deps.Pluto]]
deps = ["Base64", "Configurations", "Dates", "Downloads", "ExpressionExplorer", "FileWatching", "GracefulPkg", "HTTP", "HypertextLiteral", "InteractiveUtils", "LRUCache", "Logging", "LoggingExtras", "MIMEs", "Malt", "Markdown", "MsgPack", "Pkg", "PlutoDependencyExplorer", "PrecompileSignatures", "PrecompileTools", "REPL", "RegistryInstances", "RelocatableFolders", "Scratch", "Sockets", "TOML", "Tables", "URIs", "UUIDs"]
git-tree-sha1 = "64ff678257a6e59364ac2c094642348104c17443"
uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781"
version = "0.20.18"
[[deps.PlutoDependencyExplorer]]
deps = ["ExpressionExplorer", "InteractiveUtils", "Markdown"]
git-tree-sha1 = "c3e5073a977b1c58b2d55c1ec187c3737e64e6af"
uuid = "72656b73-756c-7461-726b-72656b6b696b"
version = "1.2.2"
[[deps.PolyesterWeave]]
deps = ["BitTwiddlingConvenienceFunctions", "CPUSummary", "IfElse", "Static", "ThreadingUtilities"]
git-tree-sha1 = "645bed98cd47f72f67316fd42fc47dee771aefcd"
@ -1805,11 +1756,6 @@ git-tree-sha1 = "36d8b4b899628fb92c2749eb488d884a926614d3"
uuid = "2dfb63ee-cc39-5dd5-95bd-886bf059d720"
version = "1.4.3"
[[deps.PrecompileSignatures]]
git-tree-sha1 = "18ef344185f25ee9d51d80e179f8dad33dc48eb1"
uuid = "91cefc8d-f054-46dc-8f8c-26e11d7c5411"
version = "3.0.3"
[[deps.PrecompileTools]]
deps = ["Preferences"]
git-tree-sha1 = "5aa36f7049a63a1528fe8f7c3f2113413ffd4e1f"
@ -1844,48 +1790,12 @@ git-tree-sha1 = "1d36ef11a9aaf1e8b74dacc6a731dd1de8fd493d"
uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d"
version = "1.3.0"
[[deps.PyCall]]
deps = ["Conda", "Dates", "Libdl", "LinearAlgebra", "MacroTools", "Serialization", "VersionParsing"]
git-tree-sha1 = "9816a3826b0ebf49ab4926e2b18842ad8b5c8f04"
uuid = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
version = "1.96.4"
[[deps.PyPlot]]
deps = ["Colors", "LaTeXStrings", "PyCall", "Sockets", "Test", "VersionParsing"]
git-tree-sha1 = "d2c2b8627bbada1ba00af2951946fb8ce6012c05"
uuid = "d330b81b-6aea-500a-939a-2ce795aea3ee"
version = "2.11.6"
[[deps.QOI]]
deps = ["ColorTypes", "FileIO", "FixedPointNumbers"]
git-tree-sha1 = "8b3fc30bc0390abdce15f8822c889f669baed73d"
uuid = "4b34888f-f399-49d4-9bb3-47ed5cae4e65"
version = "1.0.1"
[[deps.Qt6Base_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Fontconfig_jll", "Glib_jll", "JLLWrappers", "Libdl", "Libglvnd_jll", "OpenSSL_jll", "Vulkan_Loader_jll", "Xorg_libSM_jll", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Xorg_libxcb_jll", "Xorg_xcb_util_cursor_jll", "Xorg_xcb_util_image_jll", "Xorg_xcb_util_keysyms_jll", "Xorg_xcb_util_renderutil_jll", "Xorg_xcb_util_wm_jll", "Zlib_jll", "libinput_jll", "xkbcommon_jll"]
git-tree-sha1 = "eb38d376097f47316fe089fc62cb7c6d85383a52"
uuid = "c0090381-4147-56d7-9ebc-da0b1113ec56"
version = "6.8.2+1"
[[deps.Qt6Declarative_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Base_jll", "Qt6ShaderTools_jll"]
git-tree-sha1 = "da7adf145cce0d44e892626e647f9dcbe9cb3e10"
uuid = "629bc702-f1f5-5709-abd5-49b8460ea067"
version = "6.8.2+1"
[[deps.Qt6ShaderTools_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Base_jll"]
git-tree-sha1 = "9eca9fc3fe515d619ce004c83c31ffd3f85c7ccf"
uuid = "ce943373-25bb-56aa-8eca-768745ed7b5a"
version = "6.8.2+1"
[[deps.Qt6Wayland_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Base_jll", "Qt6Declarative_jll"]
git-tree-sha1 = "e1d5e16d0f65762396f9ca4644a5f4ddab8d452b"
uuid = "e99dba38-086e-5de3-a5b1-6e4c66e897c3"
version = "6.8.2+1"
[[deps.QuadGK]]
deps = ["DataStructures", "LinearAlgebra"]
git-tree-sha1 = "9da16da70037ba9d701192e27befedefb91ec284"
@ -1941,12 +1851,6 @@ git-tree-sha1 = "5c3d09cc4f31f5fc6af001c250bf1278733100ff"
uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
version = "1.3.4"
[[deps.RecipesPipeline]]
deps = ["Dates", "NaNMath", "PlotUtils", "PrecompileTools", "RecipesBase"]
git-tree-sha1 = "45cf9fd0ca5839d06ef333c8201714e888486342"
uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c"
version = "0.6.12"
[[deps.Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
@ -1958,12 +1862,6 @@ git-tree-sha1 = "4618ed0da7a251c7f92e869ae1a19c74a7d2a7f9"
uuid = "dee08c22-ab7f-5625-9660-a9af2021b33f"
version = "0.3.2"
[[deps.RegistryInstances]]
deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"]
git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51"
uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3"
version = "0.1.0"
[[deps.RelocatableFolders]]
deps = ["SHA", "Scratch"]
git-tree-sha1 = "ffdaf70d81cf6ff22c2b6e733c900c3321cab864"
@ -2046,6 +1944,12 @@ git-tree-sha1 = "456f610ca2fbd1c14f5fcf31c6bfadc55e7d66e0"
uuid = "476501e8-09a2-5ece-8869-fb82de89a1fa"
version = "0.6.43"
[[deps.SavitzkyGolay]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "7bbc5949a42f53f4fca1a0157c72f9d3f78050d1"
uuid = "c4bf5708-b6a6-4fbe-bcd0-6850ed671584"
version = "0.9.1"
[[deps.SciMLPublic]]
git-tree-sha1 = "ed647f161e8b3f2973f24979ec074e8d084f1bee"
uuid = "431bcebd-1456-4ced-9d72-93c2757fff0b"
@ -2396,11 +2300,6 @@ git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742"
uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.11.3"
[[deps.Tricks]]
git-tree-sha1 = "372b90fe551c019541fafc6ff034199dc19c8436"
uuid = "410a4b4d-49e4-4fbc-ab6d-cb71b17b3775"
version = "0.1.12"
[[deps.TriplotBase]]
git-tree-sha1 = "4d4ed7f294cda19382ff7de4c137d24d16adc89b"
uuid = "981d1d27-644d-49a2-9326-4793e63143c3"
@ -2458,28 +2357,12 @@ version = "1.25.0"
Latexify = "23fbe1c1-3f47-55db-b15f-69d7ec21a316"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
[[deps.Unzip]]
git-tree-sha1 = "ca0969166a028236229f63514992fc073799bb78"
uuid = "41fe7b60-77ed-43a1-b4f0-825fd5a5650d"
version = "0.2.0"
[[deps.VectorizationBase]]
deps = ["ArrayInterface", "CPUSummary", "HostCPUFeatures", "IfElse", "LayoutPointers", "Libdl", "LinearAlgebra", "SIMDTypes", "Static", "StaticArrayInterface"]
git-tree-sha1 = "d1d9a935a26c475ebffd54e9c7ad11627c43ea85"
uuid = "3d5dd08c-fd9d-11e8-17fa-ed2836048c2f"
version = "0.21.72"
[[deps.VersionParsing]]
git-tree-sha1 = "58d6e80b4ee071f5efd07fda82cb9fbe17200868"
uuid = "81def892-9a0e-5fdd-b105-ffc91e053289"
version = "1.3.0"
[[deps.Vulkan_Loader_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Wayland_jll", "Xorg_libX11_jll", "Xorg_libXrandr_jll", "xkbcommon_jll"]
git-tree-sha1 = "2f0486047a07670caad3a81a075d2e518acc5c59"
uuid = "a44049a8-05dd-5a78-86c9-5fde0876e88c"
version = "1.3.243+0"
[[deps.Wayland_jll]]
deps = ["Artifacts", "EpollShim_jll", "Expat_jll", "JLLWrappers", "Libdl", "Libffi_jll"]
git-tree-sha1 = "96478df35bbc2f3e1e791bc7a3d0eeee559e60e9"
@ -2521,18 +2404,6 @@ git-tree-sha1 = "fee71455b0aaa3440dfdd54a9a36ccef829be7d4"
uuid = "ffd25f8a-64ca-5728-b0f7-c24cf3aae800"
version = "5.8.1+0"
[[deps.Xorg_libICE_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "a3ea76ee3f4facd7a64684f9af25310825ee3668"
uuid = "f67eecfb-183a-506d-b269-f58e52b52d7c"
version = "1.1.2+0"
[[deps.Xorg_libSM_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libICE_jll"]
git-tree-sha1 = "9c7ad99c629a44f81e7799eb05ec2746abb5d588"
uuid = "c834827a-8449-5923-a945-d239c165b7dd"
version = "1.2.6+0"
[[deps.Xorg_libX11_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll", "Xorg_xtrans_jll"]
git-tree-sha1 = "b5899b25d17bf1889d25906fb9deed5da0c15b3b"
@ -2623,42 +2494,6 @@ git-tree-sha1 = "e3150c7400c41e207012b41659591f083f3ef795"
uuid = "cc61e674-0454-545c-8b26-ed2c68acab7a"
version = "1.1.3+0"
[[deps.Xorg_xcb_util_cursor_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_image_jll", "Xorg_xcb_util_jll", "Xorg_xcb_util_renderutil_jll"]
git-tree-sha1 = "c5bf2dad6a03dfef57ea0a170a1fe493601603f2"
uuid = "e920d4aa-a673-5f3a-b3d7-f755a4d47c43"
version = "0.1.5+0"
[[deps.Xorg_xcb_util_image_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"]
git-tree-sha1 = "f4fc02e384b74418679983a97385644b67e1263b"
uuid = "12413925-8142-5f55-bb0e-6d7ca50bb09b"
version = "0.4.1+0"
[[deps.Xorg_xcb_util_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxcb_jll"]
git-tree-sha1 = "68da27247e7d8d8dafd1fcf0c3654ad6506f5f97"
uuid = "2def613f-5ad1-5310-b15b-b15d46f528f5"
version = "0.4.1+0"
[[deps.Xorg_xcb_util_keysyms_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"]
git-tree-sha1 = "44ec54b0e2acd408b0fb361e1e9244c60c9c3dd4"
uuid = "975044d2-76e6-5fbe-bf08-97ce7c6574c7"
version = "0.4.1+0"
[[deps.Xorg_xcb_util_renderutil_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"]
git-tree-sha1 = "5b0263b6d080716a02544c55fdff2c8d7f9a16a0"
uuid = "0d47668e-0667-5a69-a72c-f761630bfb7e"
version = "0.3.10+0"
[[deps.Xorg_xcb_util_wm_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_xcb_util_jll"]
git-tree-sha1 = "f233c83cad1fa0e70b7771e0e21b061a116f2763"
uuid = "c22f9ab0-d5fe-5066-847c-f4bb1cd4e361"
version = "0.4.2+0"
[[deps.Xorg_xkbcomp_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Xorg_libxkbfile_jll"]
git-tree-sha1 = "801a858fc9fb90c11ffddee1801bb06a738bda9b"
@ -2706,18 +2541,6 @@ git-tree-sha1 = "120f41bc9540b8f137e5e5dea65845ee4f089f9e"
uuid = "0fc3237b-ac94-5853-b45c-d43d59a06200"
version = "2.57.1+0"
[[deps.eudev_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "c3b0e6196d50eab0c5ed34021aaa0bb463489510"
uuid = "35ca27e7-8b34-5b7f-bca9-bdc33f59eb06"
version = "3.2.14+0"
[[deps.fzf_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "b6a34e0e0960190ac2a4363a1bd003504772d631"
uuid = "214eeab7-80f7-51ab-84ad-2988db7cef09"
version = "0.61.1+0"
[[deps.gdk_pixbuf_jll]]
deps = ["Artifacts", "Glib_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Xorg_libX11_jll", "libpng_jll"]
git-tree-sha1 = "895f21b699121d1a57ecac57e65a852caf569254"
@ -2736,14 +2559,6 @@ git-tree-sha1 = "51b5eeb3f98367157a7a12a1fb0aa5328946c03c"
uuid = "9a68df92-36a6-505f-a73e-abb412b6bfb4"
version = "0.2.3+0"
[[deps.julia_mzML_imzML]]
deps = ["Compat", "Libz", "Plots", "Pluto", "PyPlot"]
git-tree-sha1 = "e1aceca0a4b5fad50f5947d3dd5b22a759ff86f3"
repo-rev = "master"
repo-url = "https://github.com/CINVESTAV-LABI/julia_mzML_imzML"
uuid = "38eb50d3-2fb6-4afa-992a-964ed8562ed9"
version = "0.0.1-DEV"
[[deps.libaom_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "4bba74fa59ab0755167ad24f98800fe5d727175b"
@ -2767,24 +2582,12 @@ git-tree-sha1 = "9bf7903af251d2050b467f76bdbe57ce541f7f4f"
uuid = "1183f4f0-6f2a-5f1a-908b-139f9cdfea6f"
version = "0.2.2+0"
[[deps.libevdev_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "56d643b57b188d30cccc25e331d416d3d358e557"
uuid = "2db6ffa8-e38f-5e21-84af-90c45d0032cc"
version = "1.13.4+0"
[[deps.libfdk_aac_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "646634dd19587a56ee2f1199563ec056c5f228df"
uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280"
version = "2.0.4+0"
[[deps.libinput_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "eudev_jll", "libevdev_jll", "mtdev_jll"]
git-tree-sha1 = "91d05d7f4a9f67205bd6cf395e488009fe85b499"
uuid = "36db933b-70db-51c0-b978-0f229ee0e533"
version = "1.28.1+0"
[[deps.libpng_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Zlib_jll"]
git-tree-sha1 = "07b6a107d926093898e82b3b1db657ebe33134ec"
@ -2815,19 +2618,13 @@ git-tree-sha1 = "86addc139bca85fdf9e7741e10977c45785727b7"
uuid = "337d8026-41b4-5cde-a456-74a10e5b31d1"
version = "1.11.3+0"
[[deps.mtdev_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "b4d631fd51f2e9cdd93724ae25b2efc198b059b1"
uuid = "009596ad-96f7-51b1-9f1b-5ce2d5e8a71e"
version = "1.1.7+0"
[[deps.nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
version = "1.59.0+0"
[[deps.oneTBB_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
deps = ["Artifacts", "JLLWrappers", "LazyArtifacts", "Libdl"]
git-tree-sha1 = "d5a767a3bb77135a99e433afe0eb14cd7f6914c3"
uuid = "1317d2d5-d96f-522e-a858-c73665f53c3e"
version = "2022.0.0+0"

View File

@ -3,6 +3,7 @@ authors = ["JJSA"]
version = "0.1.0"
[deps]
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
@ -10,19 +11,29 @@ ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e"
GenieFramework = "a59fdf5c-6bf0-4f5d-949c-a137c9e2f353"
HistogramThresholding = "2c695a8d-9458-5d45-9878-1b8a99cf7853"
ImageBinarization = "cbc4b850-ae4b-5111-9e64-df94c024a13d"
ImageComponentAnalysis = "d9b9e9a0-1569-11e9-2cb5-bbca914b0e89"
ImageContrastAdjustment = "f332f351-ec65-5f6a-b3d1-319c6670881a"
ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5"
ImageMorphology = "787d08f9-d448-5407-9aad-5290dd7ab264"
ImageSegmentation = "80713f31-8817-5129-9cf8-209ff8fb23e1"
Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Libz = "2ec943e9-cfe8-584d-b93d-64dcb6d567b7"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612"
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
NativeFileDialog = "e1fe445b-aa65-4df4-81c1-2041507f0fd4"
NaturalSort = "c020b1a1-e9b0-503a-9c33-f039bfc54a85"
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
SavitzkyGolay = "c4bf5708-b6a6-4fbe-bcd0-6850ed671584"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
StipplePlotly = "ec984513-233d-481d-95b0-a3b58b97af2b"
julia_mzML_imzML = "38eb50d3-2fb6-4afa-992a-964ed8562ed9"

View File

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

155
app.jl
View File

@ -24,153 +24,6 @@ using .MSI_src: MSIData, OpenMSIData, #=GetSpectrum,=# process_spectrum, Iterate
include("./julia_imzML_visual.jl")
function load_registry(registry_path)
if isfile(registry_path)
try
return JSON.parsefile(registry_path, dicttype=Dict{String, Any})
catch e
@error "Failed to parse registry.json: $e"
return Dict{String, Any}()
end
end
return Dict{String, Any}()
end
function extract_metadata(msi_data::MSIData, source_path::String)
df = msi_data.spectrum_stats_df
if df === nothing
# This can happen if precompute_analytics hasn't been run
# We can still return basic info
return Dict(
"summary" => [
Dict("parameter" => "File Name", "value" => basename(source_path)),
Dict("parameter" => "Number of Spectra", "value" => length(msi_data.spectra_metadata)),
Dict("parameter" => "Image Dimensions", "value" => "$(msi_data.image_dims[1]) x $(msi_data.image_dims[2])"),
],
"global_min_mz" => nothing,
"global_max_mz" => nothing
)
end
summary_stats = [
Dict("parameter" => "File Name", "value" => basename(source_path)),
Dict("parameter" => "Number of Spectra", "value" => length(msi_data.spectra_metadata)),
Dict("parameter" => "Image Dimensions", "value" => "$(msi_data.image_dims[1]) x $(msi_data.image_dims[2])"),
Dict("parameter" => "Global Min m/z", "value" => @sprintf("%.4f", msi_data.global_min_mz)),
Dict("parameter" => "Global Max m/z", "value" => @sprintf("%.4f", msi_data.global_max_mz)),
Dict("parameter" => "Mean TIC", "value" => @sprintf("%.2e", mean(df.TIC))),
Dict("parameter" => "Mean BPI", "value" => @sprintf("%.2e", mean(df.BPI))),
Dict("parameter" => "Mean # Points", "value" => @sprintf("%.1f", mean(df.NumPoints))),
]
if hasproperty(df, :Mode)
centroid_count = count(==(MSI_src.CENTROID), df.Mode)
profile_count = count(==(MSI_src.PROFILE), df.Mode)
unknown_count = count(==(MSI_src.UNKNOWN), df.Mode)
push!(summary_stats, Dict("parameter" => "Centroid Spectra", "value" => string(centroid_count)))
push!(summary_stats, Dict("parameter" => "Profile Spectra", "value" => string(profile_count)))
if unknown_count > 0
push!(summary_stats, Dict("parameter" => "Unknown Mode Spectra", "value" => string(unknown_count)))
end
end
return Dict(
"summary" => summary_stats,
"global_min_mz" => msi_data.global_min_mz,
"global_max_mz" => msi_data.global_max_mz
)
end
function update_registry(registry_path, dataset_name, source_path, metadata=nothing, is_imzML=false)
registry = load_registry(registry_path)
entry = Dict{String, Any}( # Explicitly type the dictionary to allow mixed value types
"source_path" => source_path,
"processed_date" => string(now()),
"is_imzML" => is_imzML
)
if metadata !== nothing
entry["metadata"] = metadata
end
registry[dataset_name] = entry
try
open(registry_path, "w") do f
JSON.print(f, registry, 4)
end
catch e
@error "Failed to write to registry.json: $e"
end
end
function process_file_safely(file_path, masses, params, progress_message_ref, overall_progress_ref)
local_msi_data = nothing
dataset_name = replace(basename(file_path), r"\.imzML$"i => "")
output_dir = joinpath("public", dataset_name)
println("Processing: $dataset_name -> $output_dir")
try
# --- Load Data ---
progress_message_ref = "Loading: $(basename(file_path))"
local_msi_data = OpenMSIData(file_path)
if !(local_msi_data.source isa ImzMLSource)
@warn "Skipping non-imzML file: $(basename(file_path))"
return (false, "Skipped: Not an imzML file")
end
# --- Generate Slices (this will call precompute_analytics if needed) ---
progress_message_ref = "Generating $(length(masses)) slices for $(dataset_name)..."
slice_dict = get_multiple_mz_slices(local_msi_data, masses, params.tolerance)
# --- Extract metadata *after* it has been computed ---
metadata = extract_metadata(local_msi_data, file_path)
# --- Save Slices ---
mkpath(output_dir) # Ensure output directory exists
for (mass_idx, mass) in enumerate(masses)
progress_message_ref = "File $(params.fileIdx)/$(params.nFiles): Saving slice for m/z=$mass"
slice = slice_dict[mass]
text_nmass = replace(string(mass), "." => "_")
bitmap_filename = params.triqE ? "TrIQ_$(text_nmass).bmp" : "MSI_$(text_nmass).bmp"
colorbar_filename = params.triqE ? "colorbar_TrIQ_$(text_nmass).png" : "colorbar_MSI_$(text_nmass).png"
if all(iszero, slice)
sliceQuant = zeros(UInt8, size(slice))
@warn "No intensity data for m/z = $mass in $(dataset_name)"
else
sliceQuant = params.triqE ? TrIQ(slice, params.colorL, params.triqP) : quantize_intensity(slice, params.colorL)
if params.medianF
sliceQuant = round.(UInt8, median_filter(sliceQuant))
end
end
save_bitmap(joinpath(output_dir, bitmap_filename), sliceQuant, ViridisPalette)
if !all(iszero, slice)
generate_colorbar_image(slice, params.colorL, joinpath(output_dir, colorbar_filename); use_triq=params.triqE, triq_prob=params.triqP)
end
end
is_imzML = local_msi_data.source isa ImzMLSource
update_registry(params.registry, dataset_name, file_path, metadata, is_imzML)
return (true, "")
catch e
@error "File processing failed" file=file_path exception=(e, catch_backtrace())
return (false, "File: $(basename(file_path)) - $(sprint(showerror, e))")
finally
if local_msi_data !== nothing
# Cleanup
end
local_msi_data = nothing
GC.gc(true)
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0)
end
end
end
@genietools
# == Reactive code ==
@ -316,7 +169,7 @@ end
@in selected_files = String[]
@out available_folders = String[]
@out image_available_folders = String[]
@out registry_path = joinpath("public", "registry.json")
@out registry_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
# Progress reporting
@out overall_progress = 0.0
@out progress_message = ""
@ -502,7 +355,7 @@ end
# This handler correctly uses pick_file and loads the selected file
# as the active dataset for the UI.
@onbutton btnSearch @time begin
@onbutton btnSearch begin
picked_route = pick_file(; filterlist="imzML,imzml,mzML,mzml")
if isempty(picked_route)
return
@ -513,7 +366,7 @@ end
@async begin
try
dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML))$"i => "")
dataset_name = replace(basename(picked_route), r"(\.(imzML|imzml|mzML|mzml))$"i => "")
registry = load_registry(registry_path)
existing_entry = get(registry, dataset_name, nothing)
@ -1864,7 +1717,7 @@ end
sleep(1.0) # Give frontend time to initialize
try
println("Synchronizing registry with filesystem on backend init...")
reg_path = joinpath("public", "registry.json")
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
registry = isfile(reg_path) ? load_registry(reg_path) : Dict{String, Any}()
public_dirs = isdir("public") ? readdir("public") : []

View File

@ -138,6 +138,7 @@
label="Compare"></q-btn>
<!--<q-btn class="q-ma-sm btn-style" :disable="btnMetadataDisable"
v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>-->
<q-btn class="q-ma-sm btn-style" icon="edit" label="Mask Editor" href="/mask"></q-btn>
<q-btn class="q-ma-sm btn-style" v-on:click="showMetadataBtn=true" label="Show Metadata"></q-btn>
<div class="q-pa-md row items-center" v-show="progress">
<q-spinner color="primary" size="2em" class="q-mr-sm"></q-spinner>

View File

@ -626,4 +626,151 @@ function warmup_init()
println("Pre-compilation finished.")
end
end
function load_registry(registry_path)
if isfile(registry_path)
try
return JSON.parsefile(registry_path, dicttype=Dict{String, Any})
catch e
@error "Failed to parse registry.json: $e"
return Dict{String, Any}()
end
end
return Dict{String, Any}()
end
function extract_metadata(msi_data::MSIData, source_path::String)
df = msi_data.spectrum_stats_df
if df === nothing
# This can happen if precompute_analytics hasn't been run
# We can still return basic info
return Dict(
"summary" => [
Dict("parameter" => "File Name", "value" => basename(source_path)),
Dict("parameter" => "Number of Spectra", "value" => length(msi_data.spectra_metadata)),
Dict("parameter" => "Image Dimensions", "value" => "$(msi_data.image_dims[1]) x $(msi_data.image_dims[2])"),
],
"global_min_mz" => nothing,
"global_max_mz" => nothing
)
end
summary_stats = [
Dict("parameter" => "File Name", "value" => basename(source_path)),
Dict("parameter" => "Number of Spectra", "value" => length(msi_data.spectra_metadata)),
Dict("parameter" => "Image Dimensions", "value" => "$(msi_data.image_dims[1]) x $(msi_data.image_dims[2])"),
Dict("parameter" => "Global Min m/z", "value" => @sprintf("%.4f", msi_data.global_min_mz)),
Dict("parameter" => "Global Max m/z", "value" => @sprintf("%.4f", msi_data.global_max_mz)),
Dict("parameter" => "Mean TIC", "value" => @sprintf("%.2e", mean(df.TIC))),
Dict("parameter" => "Mean BPI", "value" => @sprintf("%.2e", mean(df.BPI))),
Dict("parameter" => "Mean # Points", "value" => @sprintf("%.1f", mean(df.NumPoints))),
]
if hasproperty(df, :Mode)
centroid_count = count(==(MSI_src.CENTROID), df.Mode)
profile_count = count(==(MSI_src.PROFILE), df.Mode)
unknown_count = count(==(MSI_src.UNKNOWN), df.Mode)
push!(summary_stats, Dict("parameter" => "Centroid Spectra", "value" => string(centroid_count)))
push!(summary_stats, Dict("parameter" => "Profile Spectra", "value" => string(profile_count)))
if unknown_count > 0
push!(summary_stats, Dict("parameter" => "Unknown Mode Spectra", "value" => string(unknown_count)))
end
end
return Dict(
"summary" => summary_stats,
"global_min_mz" => msi_data.global_min_mz,
"global_max_mz" => msi_data.global_max_mz
)
end
function update_registry(registry_path, dataset_name, source_path, metadata=nothing, is_imzML=false)
registry = load_registry(registry_path)
entry = Dict{String, Any}( # Explicitly type the dictionary to allow mixed value types
"source_path" => source_path,
"processed_date" => string(now()),
"is_imzML" => is_imzML
)
if metadata !== nothing
entry["metadata"] = metadata
end
registry[dataset_name] = entry
try
open(registry_path, "w") do f
JSON.print(f, registry, 4)
end
catch e
@error "Failed to write to registry.json: $e"
end
end
function process_file_safely(file_path, masses, params, progress_message_ref, overall_progress_ref)
local_msi_data = nothing
dataset_name = replace(basename(file_path), r"\.imzML$"i => "")
output_dir = joinpath("public", dataset_name)
println("Processing: $dataset_name -> $output_dir")
try
# --- Load Data ---
progress_message_ref = "Loading: $(basename(file_path))"
local_msi_data = OpenMSIData(file_path)
if !(local_msi_data.source isa ImzMLSource)
@warn "Skipping non-imzML file: $(basename(file_path))"
return (false, "Skipped: Not an imzML file")
end
# --- Generate Slices (this will call precompute_analytics if needed) ---
progress_message_ref = "Generating $(length(masses)) slices for $(dataset_name)..."
slice_dict = get_multiple_mz_slices(local_msi_data, masses, params.tolerance)
# --- Extract metadata *after* it has been computed ---
metadata = extract_metadata(local_msi_data, file_path)
# --- Save Slices ---
mkpath(output_dir) # Ensure output directory exists
for (mass_idx, mass) in enumerate(masses)
progress_message_ref = "File $(params.fileIdx)/$(params.nFiles): Saving slice for m/z=$mass"
slice = slice_dict[mass]
text_nmass = replace(string(mass), "." => "_")
bitmap_filename = params.triqE ? "TrIQ_$(text_nmass).bmp" : "MSI_$(text_nmass).bmp"
colorbar_filename = params.triqE ? "colorbar_TrIQ_$(text_nmass).png" : "colorbar_MSI_$(text_nmass).png"
if all(iszero, slice)
sliceQuant = zeros(UInt8, size(slice))
@warn "No intensity data for m/z = $mass in $(dataset_name)"
else
sliceQuant = params.triqE ? TrIQ(slice, params.colorL, params.triqP) : quantize_intensity(slice, params.colorL)
if params.medianF
sliceQuant = round.(UInt8, median_filter(sliceQuant))
end
end
save_bitmap(joinpath(output_dir, bitmap_filename), sliceQuant, ViridisPalette)
if !all(iszero, slice)
generate_colorbar_image(slice, params.colorL, joinpath(output_dir, colorbar_filename); use_triq=params.triqE, triq_prob=params.triqP)
end
end
is_imzML = local_msi_data.source isa ImzMLSource
update_registry(params.registry, dataset_name, file_path, metadata, is_imzML)
return (true, "")
catch e
@error "File processing failed" file=file_path exception=(e, catch_backtrace())
return (false, "File: $(basename(file_path)) - $(sprint(showerror, e))")
finally
if local_msi_data !== nothing
# Cleanup
end
local_msi_data = nothing
GC.gc(true)
if Sys.islinux()
ccall(:malloc_trim, Int32, (Int32,), 0)
end
end
end

897
mask.jl Normal file
View File

@ -0,0 +1,897 @@
module MaskApp
# --- Dependencies ---
using GenieFramework
using Images, ImageBinarization, ImageMorphology, ImageComponentAnalysis
using NativeFileDialog, FileIO, ImageCore, Printf, Dates, JSON
using Pkg, Libz, PlotlyBase, CairoMakie, Colors, Base64
using Statistics, NaturalSort, LinearAlgebra, StipplePlotly
using Base.Filesystem: mv
using MSI_src
using .MSI_src: MSIData
# Plot Handling
include("./julia_imzML_visual.jl")
# Image Processing Pipeline
include("src/ImageProcessing.jl")
using .ImageProcessing
using ImageBinarization
function load_and_binarize_mask(path)
img = load(path)
gray_img = ensure_grayscale(img)
# Binarize to ensure only pure black and white values
return binarize(gray_img, Otsu())
end
function ensure_grayscale(img)
if eltype(img) <: AbstractRGB; return Gray.(img); end
if eltype(img) <: AbstractRGBA; return Gray.(RGB.(img)); end
if eltype(img) <: Color; return Gray.(img); end
return img
end
function alter_image(img_path, otsu_scale, noise_size_percent, hole_size_percent, smoothing_level)
try
# Strip query parameters from img_path
clean_img_path = replace(img_path, r"\?.*" => "")
full_image_path = joinpath("public", lstrip(clean_img_path, '/'))
if !isfile(full_image_path)
return (success=false, message="Image file not found: $(full_image_path)", path="")
end
original_img = load(full_image_path)
gray_img = Float32.(ensure_grayscale(original_img))
binary, noise_removed, holes_filled, smoothed =
ImageProcessing.process_image_pipeline(gray_img;
otsu_scale=otsu_scale, noise_size_percent=noise_size_percent,
hole_size_percent=hole_size_percent, smoothing=smoothing_level)
output_dir = joinpath("public", "css", "masks")
mkpath(output_dir)
path_smooth = "/css/masks/smoothed.png"
save(joinpath(output_dir, basename(path_smooth)), smoothed)
return (success=true, message="Image processed successfully!", path=path_smooth)
catch e
@error "Mask editor processing failed" exception=(e, catch_backtrace())
return (success=false, message="Error processing image: $(sprint(showerror, e))", path="")
end
end
# New helper function to display a single slice
function display_slice(slice_path)
if isempty(slice_path)
return [PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())], PlotlyBase.Layout(margin=attr(l=0,r=0,t=0,b=0,pad=0)), false
end
full_path = joinpath("public", lstrip(slice_path, '/'))
if !isfile(full_path)
return [PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())], PlotlyBase.Layout(margin=attr(l=0,r=0,t=0,b=0,pad=0)), false
end
plotdata, plotlayout, _, _ = loadImgPlot(slice_path, "", 0.0) # No mask overlay
return plotdata, plotlayout, true
end
function get_timestamped_path(base_path)
clean_path = replace(base_path, r"\?.*" => "")
return clean_path * "?t=" * string(time_ns())
end
function refresh_editor_preview(imgInt, smoothed_mask_path, imgTrans)
if !isempty(smoothed_mask_path)
timestamped_path = get_timestamped_path(smoothed_mask_path)
plotdata, plotlayout, _, _ = loadImgPlot(imgInt, timestamped_path, imgTrans)
return plotdata, plotlayout
else
return [PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())], PlotlyBase.Layout(margin=attr(l=0,r=0,t=0,b=0,pad=0))
end
end
function flood_fill!(img::AbstractMatrix, x::Int, y::Int, fill_color)
h, w = size(img)
if !(1 <= x <= h && 1 <= y <= w); return; end
target_color = img[x, y]
if target_color == fill_color; return; end
q = [(x, y)]
img[x, y] = fill_color
while !isempty(q)
cx, cy = popfirst!(q)
for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)]
nx, ny = cx + dx, cy + dy
if 1 <= nx <= h && 1 <= ny <= w && img[nx, ny] == target_color
img[nx, ny] = fill_color
push!(q, (nx, ny))
end
end
end
end
function update_main_plot(imgInt::String, smoothed_mask_path::String, imgTrans::Float64)
try
if !isempty(imgInt) && !isempty(smoothed_mask_path)
# Both slice and mask available - show overlay
plotdata, plotlayout, _, _ = loadImgPlot(imgInt, smoothed_mask_path, imgTrans)
return plotdata, plotlayout, true
elseif !isempty(imgInt)
# Only slice available - show slice alone
plotdata, plotlayout, show = display_slice(imgInt)
return plotdata, plotlayout, show
elseif !isempty(smoothed_mask_path)
# Only mask available - show mask alone
plotdata, plotlayout, _, _ = loadImgPlot(smoothed_mask_path, "", 0.0)
return plotdata, plotlayout, true
else
# Nothing to show
return [PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())],
PlotlyBase.Layout(margin=attr(l=0,r=0,t=0,b=0,pad=0)),
false
end
catch e
@error "Failed to update main plot" exception=(e, catch_backtrace())
return [PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())],
PlotlyBase.Layout(margin=attr(l=0,r=0,t=0,b=0,pad=0)),
false
end
end
@genietools
@app begin
# --- State for Slice Selection (from app.jl) ---
@in selected_folder_main = ""
@out available_folders = String[]
@out image_available_folders = String[]
@private registry_init_done = false
@out imgInt = "" # Path to current slice
@out current_msi = ""
@out msgimg = "Please select a dataset."
@in imgTrans = 0.5 # Default transparency
# --- State for Mask Workflow ---
@in otsu_scale = 1.0
@in noise_size_percent = 0.01
@in hole_size_percent = 0.005
@in smoothing_level = 3
@in is_editing_mask = false
@in is_browsing_slices = true
@in mask_input_path = "" # Reactive trigger for processing
@out smoothed_mask_path = "" # Path to the final smoothed.png
@out plotdata_verify = [PlotlyBase.heatmap(x=Vector{Float64}(), y=Vector{Float64}())]
@out plotlayout_verify = PlotlyBase.Layout(margin=attr(l=0,r=0,t=0,b=0,pad=0))
@out show_verification_plot = false
@out show_editor = false # To launch the custom editor
# --- Messages and Warnings ---
@out mask_editor_message = ""
@out mask_editor_warning = false
@out progress = false
# --- Buttons ---
@in btn_use_slice_as_mask = false
@in btn_upload_mask = false
@in btn_edit_manually = false
@in btn_img_plus = false
@in btn_img_minus = false
@in btn_change_slice = false # New button
@in btn_flip_mask = false
@in btn_save_final_mask = false
@in btn_move_mask = false
# --- Manual Editor State ---
@in brush_size = 10
@in brush_color = "#ff0000" # Red brush
@in current_tool = "brush" # "brush", "eraser", "bucket", "drag"
@in editor_scale = 1.0
@in bucket_fill_trigger = Dict()
# Canvas dimensions matching the smoothed mask
@out canvas_width = 512
@out canvas_height = 512
# Mouse coordinates for drawing
@in mouse_x = 0
@in mouse_y = 0
@in is_drawing = false
# Editor operations
@in rotate_degrees = 0
@in flip_direction = "horizontal"
@in move_direction = "right"
@in move_pixels = 10
@in move_mask_payload = Dict()
# Final save
@in final_mask_name = ""
@in updated_mask_data = "" # New property for canvas data
@in canvas_refresh_trigger = 0 # New property to trigger canvas refresh
# --- Handlers ---
@onchange updated_mask_data begin
if !isempty(updated_mask_data)
try
# Data URL format: data:image/png;base64,iVBORw0KGgo...
# Extract base64 part
base64_data = split(updated_mask_data, ",")[2]
decoded_img_bytes = base64decode(base64_data)
# Load image from bytes, convert to binary, and re-save
img_from_canvas = load(IOBuffer(decoded_img_bytes))
binary_mask = binarize(ensure_grayscale(img_from_canvas), Otsu())
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
save(mask_path, binary_mask)
smoothed_mask_path = get_timestamped_path(smoothed_mask_path)
@info "Mask updated from canvas data and binarized."
catch e
@error "Failed to update mask from canvas data" exception=(e, catch_backtrace())
end
end
end
@onchange selected_folder_main begin
if !isempty(selected_folder_main)
is_browsing_slices = true
is_editing_mask = false
folder_path = joinpath("public", selected_folder_main)
println("Selected folder: $selected_folder_main")
if !isdir(folder_path)
imgInt = ""
msgimg = "Folder not found."
show_verification_plot = false
return
end
msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural)
if !isempty(msi_bmp)
current_msi = first(msi_bmp)
imgInt = "/$(selected_folder_main)/$(current_msi)"
text_nmass = replace(current_msi, r"MSI_|.bmp" => "")
msgimg = "<i>m/z</i>: $(replace(text_nmass, "_" => "."))"
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
else
imgInt = ""
msgimg = "No MSI images found in this dataset."
show_verification_plot = false
end
end
end
@onbutton btn_img_plus begin
if isempty(selected_folder_main) || is_editing_mask return end
folder_path = joinpath("public", selected_folder_main)
if !isdir(folder_path) return end
msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural)
new_msi = increment_image(current_msi, msi_bmp)
if new_msi !== nothing
current_msi = new_msi
imgInt = "/$(selected_folder_main)/$(current_msi)"
text_nmass = replace(current_msi, r"MSI_|.bmp" => "")
msgimg = "<i>m/z</i>: $(replace(text_nmass, "_" => "."))"
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
end
end
@onbutton btn_img_minus begin
if isempty(selected_folder_main) || is_editing_mask return end
folder_path = joinpath("public", selected_folder_main)
if !isdir(folder_path) return end
msi_bmp = sort(filter(f -> startswith(f, "MSI_") && endswith(f, ".bmp"), readdir(folder_path)), lt=natural)
new_msi = decrement_image(current_msi, msi_bmp)
if new_msi !== nothing
current_msi = new_msi
imgInt = "/$(selected_folder_main)/$(current_msi)"
text_nmass = replace(current_msi, r"MSI_|.bmp" => "")
msgimg = "<i>m/z</i>: $(replace(text_nmass, "_" => "."))"
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
end
end
@onbutton btn_use_slice_as_mask begin
if !isempty(imgInt)
is_browsing_slices = false
is_editing_mask = true
mask_input_path = imgInt
mask_editor_message = "Using current slice as mask input..."
mask_editor_warning = false
# Trigger processing which will update the plot via mask_input_path handler
else
mask_editor_message = "No slice selected. Please select a dataset and slice first."
mask_editor_warning = true
end
end
@onbutton btn_upload_mask begin
picked_path = pick_file(; filterlist="png,bmp,jpg,jpeg")
if !isempty(picked_path)
target_dir = joinpath("public", "css", "masks")
mkpath(target_dir)
uploaded_filename = "uploaded_mask_input.png"
destination_path = joinpath(target_dir, uploaded_filename)
cp(picked_path, destination_path; force=true)
is_browsing_slices = false
is_editing_mask = true
mask_input_path = get_timestamped_path("/css/masks/$uploaded_filename")
mask_editor_message = "Uploaded mask image for processing..."
mask_editor_warning = false
# Plot will be updated via mask_input_path handler after processing
else
mask_editor_message = "No file selected for upload."
mask_editor_warning = true
end
end
@onbutton btn_change_slice begin
is_browsing_slices = true
is_editing_mask = false
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, "", imgTrans)
smoothed_mask_path = "" # Clear the mask path
mask_editor_message = "Slice browsing re-enabled."
end
@onchange mask_input_path begin
if isempty(mask_input_path) return end
result = alter_image(mask_input_path, otsu_scale, noise_size_percent, hole_size_percent, smoothing_level)
if result.success
local_smoothed_path = joinpath("public", lstrip(result.path, '/'))
slice_path_cleaned = replace(imgInt, r"\?.*" => "")
slice_full_path = joinpath("public", lstrip(slice_path_cleaned, '/'))
if isfile(slice_full_path) && isfile(local_smoothed_path)
slice_img = load(slice_full_path)
mask_img = load(local_smoothed_path)
if size(slice_img) != size(mask_img)
@info "Resizing mask to match slice dimensions."
resized_mask = imresize(mask_img, size(slice_img))
save(local_smoothed_path, resized_mask)
end
smoothed_mask_path = get_timestamped_path(result.path)
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
mask_editor_message = result.message
mask_editor_warning = false
elseif isfile(local_smoothed_path)
smoothed_mask_path = get_timestamped_path(result.path)
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot("", smoothed_mask_path, imgTrans)
mask_editor_message = "Displaying generated mask. Select a dataset to overlay a slice."
mask_editor_warning = false
else
mask_editor_message = "Slice or mask image not found for verification plot."
mask_editor_warning = true
end
else
mask_editor_message = result.message
mask_editor_warning = true
end
end
@onchange otsu_scale, noise_size_percent, hole_size_percent, smoothing_level begin
if is_editing_mask
if isempty(mask_input_path) return end
result = alter_image(mask_input_path, otsu_scale, noise_size_percent, hole_size_percent, smoothing_level)
if result.success
local_smoothed_path = joinpath("public", lstrip(result.path, '/'))
slice_path_cleaned = replace(imgInt, r"\?.*" => "")
slice_full_path = joinpath("public", lstrip(slice_path_cleaned, '/'))
if isfile(slice_full_path) && isfile(local_smoothed_path)
slice_img = load(slice_full_path)
mask_img = load(local_smoothed_path)
if size(slice_img) != size(mask_img)
@info "Resizing mask to match slice dimensions."
resized_mask = imresize(mask_img, size(slice_img))
save(local_smoothed_path, resized_mask)
end
smoothed_mask_path = get_timestamped_path(result.path)
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
mask_editor_message = "Mask updated."
mask_editor_warning = false
elseif isfile(local_smoothed_path)
smoothed_mask_path = get_timestamped_path(result.path)
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot("", smoothed_mask_path, imgTrans)
mask_editor_message = "Displaying generated mask. Select a dataset to overlay a slice."
mask_editor_warning = false
else
mask_editor_message = "Slice or mask image not found for verification plot."
mask_editor_warning = true
end
else
mask_editor_message = result.message
mask_editor_warning = true
end
end
end
@onchange imgTrans begin
if is_editing_mask
# Use centralized plot update
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
end
end
@onbutton btn_edit_manually begin
if !isempty(smoothed_mask_path)
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
if isfile(mask_path)
mask_img = load(mask_path)
h, w = size(mask_img)
canvas_width = w
canvas_height = h
show_editor = true
end
else
mask_editor_message = "No mask available for editing. Process a mask first."
mask_editor_warning = true
end
end
@onchange show_editor begin
if !show_editor && is_editing_mask
# When the editor dialog is closed, refresh the main plot using centralized function
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
is_drawing = false # Reset drawing state
end
end
@onchange mouse_x, mouse_y, is_drawing begin
if show_editor && is_drawing && mouse_x > 0 && mouse_y > 0
try
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
if !isfile(mask_path) return end
img = load(mask_path)
actual_x = round(Int, mouse_x)
actual_y = round(Int, mouse_y)
if current_tool == "brush" || current_tool == "eraser"
if brush_size <= 0 return end
radius = brush_size / 2 # Treat as diameter
for i in max(1, floor(Int, actual_y-radius)):min(size(img, 1), ceil(Int, actual_y+radius))
for j in max(1, floor(Int, actual_x-radius)):min(size(img, 2), ceil(Int, actual_x+radius))
if (i - actual_y)^2 + (j - actual_x)^2 <= radius^2
color = (current_tool == "brush") ? RGB(1, 1, 1) : RGB(0, 0, 0)
img[i, j] = color
end
end
end
end
save(mask_path, img)
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
smoothed_mask_path = get_timestamped_path(smoothed_mask_path)
catch e
@error "Editor action failed" exception=(e, catch_backtrace())
end
end
end
@onchange bucket_fill_trigger begin
if show_editor && !isempty(bucket_fill_trigger)
try
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
if !isfile(mask_path) return end
img = load_and_binarize_mask(mask_path)
actual_x = round(Int, bucket_fill_trigger["x"])
actual_y = round(Int, bucket_fill_trigger["y"])
if 1 <= actual_y <= size(img, 1) && 1 <= actual_x <= size(img, 2)
fill_color = Gray(1) # White
flood_fill!(img, actual_y, actual_x, fill_color)
save(mask_path, img)
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
smoothed_mask_path = get_timestamped_path(smoothed_mask_path)
canvas_refresh_trigger = canvas_refresh_trigger[] + 1
end
catch e
@error "Bucket fill failed" exception=(e, catch_backtrace())
end
end
end
@onchange rotate_degrees begin
if show_editor && rotate_degrees != 0
try
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
if !isfile(mask_path) return end
img = load_and_binarize_mask(mask_path)
original_size = size(img)
# Normalize angle to 0, 90, 180, 270
angle = mod(round(Int, rotate_degrees), 360)
if angle == 0; rotate_degrees = 0; return; end
rotated_img = if angle == 90
rot_r90(img)
elseif angle == 180
rot180(img)
elseif angle == 270
rot_l90(img)
else
img # No change for other angles
end
if rotated_img !== img
# Create a new image with the original dimensions, filled with black
new_img = similar(img, original_size)
fill!(new_img, eltype(img)(0))
# Calculate padding/offset to center the rotated image
rotated_size = size(rotated_img)
offset_h = (original_size[1] - rotated_size[1]) ÷ 2
offset_w = (original_size[2] - rotated_size[2]) ÷ 2
# Define the region in the new image where the rotated image will be placed
dest_region_h = (1:rotated_size[1]) .+ offset_h
dest_region_w = (1:rotated_size[2]) .+ offset_w
# Ensure the destination region is within the bounds of the new image
clamped_dest_h = max(1, dest_region_h.start):min(original_size[1], dest_region_h.stop)
clamped_dest_w = max(1, dest_region_w.start):min(original_size[2], dest_region_w.stop)
# Define the source region from the rotated image
src_h = (1:length(clamped_dest_h))
src_w = (1:length(clamped_dest_w))
if !isempty(clamped_dest_h) && !isempty(clamped_dest_w)
view(new_img, clamped_dest_h, clamped_dest_w) .= view(rotated_img, src_h, src_w)
end
save(mask_path, new_img)
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
end
# Reset the slider and update the path
smoothed_mask_path = get_timestamped_path(smoothed_mask_path)
rotate_degrees = 0
canvas_refresh_trigger = canvas_refresh_trigger[] + 1
catch e
@error "Rotation failed" exception=(e, catch_backtrace())
end
end
end
@onbutton btn_flip_mask begin
if show_editor
try
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
if !isfile(mask_path) return end
img = load_and_binarize_mask(mask_path)
flipped = (flip_direction == "horizontal") ? reverse(img, dims=2) : reverse(img, dims=1)
save(mask_path, flipped)
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
smoothed_mask_path = get_timestamped_path(smoothed_mask_path)
canvas_refresh_trigger = canvas_refresh_trigger[] + 1
catch e
@error "Flip failed" exception=(e, catch_backtrace())
end
end
end
@onchange move_mask_payload begin
if show_editor && move_pixels > 0 && !isempty(move_mask_payload)
try
mask_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
if !isfile(mask_path) return end
img = load_and_binarize_mask(mask_path)
h, w = size(img)
new_img = similar(img)
fill!(new_img, eltype(img)(0))
dx, dy = 0, 0
direction = move_mask_payload["direction"]
if direction == "right"; dx = move_pixels; end
if direction == "left"; dx = -move_pixels; end
if direction == "down"; dy = move_pixels; end
if direction == "up"; dy = -move_pixels; end
src_y_range = max(1, 1-dy):min(h, h-dy)
src_x_range = max(1, 1-dx):min(w, w-dx)
dest_y_range = max(1, 1+dy):min(h, h+dy)
dest_x_range = max(1, 1+dx):min(w, w+dx)
if !isempty(src_y_range) && !isempty(src_x_range) && !isempty(dest_y_range) && !isempty(dest_x_range)
view(new_img, dest_y_range, dest_x_range) .= view(img, src_y_range, src_x_range)
end
save(mask_path, new_img)
plotdata_verify, plotlayout_verify, show_verification_plot = update_main_plot(imgInt, smoothed_mask_path, imgTrans)
smoothed_mask_path = get_timestamped_path(smoothed_mask_path)
canvas_refresh_trigger = canvas_refresh_trigger[] + 1
catch e
@error "Move failed" exception=(e, catch_backtrace())
end
end
end
@onbutton btn_save_final_mask begin
try
parent_folder = split(selected_folder_main, '/')[end]
final_mask_name = "$(parent_folder).png"
source_path = joinpath("public", lstrip(replace(smoothed_mask_path, r"\?.*" => ""), '/'))
target_dir = joinpath("public", "css", "masks")
mkpath(target_dir)
final_path = joinpath(target_dir, final_mask_name)
if isfile(source_path)
# Copy the mask to final location
cp(source_path, final_path; force=true)
# Update registry directly in the handler
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
registry = isfile(reg_path) ? JSON.parsefile(reg_path) : Dict{String, Any}()
# Update the registry entry
if haskey(registry, selected_folder_main)
registry[selected_folder_main]["mask_path"] = "/css/masks/$(final_mask_name)"
registry[selected_folder_main]["has_mask"] = true
else
# Create a new entry if folder doesn't exist in registry
registry[selected_folder_main] = Dict(
"mask_path" => "/css/masks/$(final_mask_name)",
"has_mask" => true,
"is_imzML" => true,
"processed_date" => string(Dates.now())
)
end
# Save the updated registry
open(reg_path, "w") do f
JSON.print(f, registry, 4)
end
@info "Registry updated with mask: $(final_mask_name)"
# Update UI state
mask_editor_message = "Final mask saved as $(final_mask_name)"
mask_editor_warning = false
show_editor = false
else
mask_editor_message = "Source mask file not found: $(source_path)"
mask_editor_warning = true
end
catch e
@error "Save final mask failed" exception=(e, catch_backtrace())
mask_editor_message = "Error saving final mask: $(sprint(showerror, e))"
mask_editor_warning = true
end
end
@onchange isready begin
if isready && !registry_init_done
@async begin # Run asynchronously to not block startup
sleep(1.0) # Give frontend time to initialize
try
println("Synchronizing registry for mask editor...")
reg_path = abspath(joinpath(@__DIR__, "public", "registry.json"))
# Assuming load_registry is available from MSI_src or a similar utility file
# For now, handle its absence gracefully if it's not explicitly defined here.
registry = isfile(reg_path) ? load_registry(reg_path) : Dict{String, Any}()
public_dirs = isdir("public") ? readdir("public") : []
ignored_dirs = ["css", "masks"]
dataset_dirs = filter(d -> isdir(joinpath("public", d)) && !(d in ignored_dirs), public_dirs)
registry_keys = Set(keys(registry))
folder_set = Set(dataset_dirs)
new_folders = setdiff(folder_set, registry_keys)
for folder in new_folders
println("Found new folder: $folder")
registry[folder] = Dict(
"source_path" => "unknown (manually added)",
"processed_date" => "unknown",
"metadata" => Dict(),
"is_imzML" => true # Assume folder contains images if found this way
)
end
removed_folders = setdiff(registry_keys, folder_set)
for folder in removed_folders
delete!(registry, folder)
end
if !isempty(new_folders) || !isempty(removed_folders)
println("Registry changed, saving...")
open(reg_path, "w") do f
JSON.print(f, registry, 4)
end
end
all_folders = sort(collect(keys(registry)), lt=natural)
img_folders = filter(folder -> get(get(registry, folder, Dict()), "is_imzML", false), all_folders)
available_folders = deepcopy(all_folders)
image_available_folders = deepcopy(img_folders)
println("Mask editor UI lists updated. All: $(length(available_folders)), Images: $(length(image_available_folders))")
catch e
@warn "Mask editor registry synchronization failed: $e"
available_folders = []
image_available_folders = []
finally
registry_init_done = true
end
end
end
end
@methods """
// Close the default methods object and define our own component structure
},
created() {
// Initialize non-reactive data properties here
this.canvas = null;
this.ctx = null;
this.isDrawingOnCanvas = false;
this.lastX = 0;
this.lastY = 0;
this.imgObj = null;
},
watch: {
show_editor(newValue) {
if (newValue) {
// Wait for the dialog to render before initializing canvas
setTimeout(() => { this.initCanvas() }, 100);
}
},
canvas_refresh_trigger(newValue) {
if (newValue > 0) {
this.initCanvas();
}
}
},
methods: {
// Re-opened methods object for all our functions
initCanvas() {
this.canvas = document.getElementById('maskCanvas');
if (!this.canvas) {
console.error("Canvas element not found!");
return;
}
this.ctx = this.canvas.getContext('2d');
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.imgObj = new Image();
this.imgObj.onload = () => {
this.ctx.drawImage(this.imgObj, 0, 0, this.canvas.width, this.canvas.height);
};
this.imgObj.src = this.smoothed_mask_path.split('?')[0] + '?t=' + Date.now();
},
startDrawing(event) {
if (this.current_tool === 'drag' || !this.ctx) return;
this.isDrawingOnCanvas = true;
const { x, y } = this.getCanvasMousePosition(event);
this.lastX = x;
this.lastY = y;
if (this.current_tool === 'bucket') {
this.bucket_fill_trigger = { x: x, y: y, t: Date.now() };
this.isDrawingOnCanvas = false;
return;
}
this.ctx.beginPath();
this.ctx.moveTo(this.lastX, this.lastY);
},
draw(event) {
if (!this.isDrawingOnCanvas || this.current_tool === 'drag' || this.current_tool === 'bucket' || !this.ctx) return;
const { x, y } = this.getCanvasMousePosition(event);
this.ctx.lineWidth = this.brush_size;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
if (this.current_tool === 'brush') {
this.ctx.strokeStyle = 'white';
this.ctx.globalCompositeOperation = 'source-over';
} else if (this.current_tool === 'eraser') {
this.ctx.strokeStyle = 'black';
this.ctx.globalCompositeOperation = 'source-over';
}
this.ctx.lineTo(x, y);
this.ctx.stroke();
},
stopDrawing() {
if (this.isDrawingOnCanvas) {
this.isDrawingOnCanvas = false;
this.ctx.closePath();
this.syncMaskToServer();
}
},
getCanvasMousePosition(event) {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const x = (event.clientX - rect.left) * scaleX;
const y = (event.clientY - rect.top) * scaleY;
return { x, y };
},
syncMaskToServer() {
if (!this.canvas) return;
const dataURL = this.canvas.toDataURL('image/png');
this.updated_mask_data = dataURL;
},
// Kept for compatibility with bucket tool logic which uses original image coordinates
updateMousePosition(event) {
const rect = event.target.getBoundingClientRect();
const scale = this.editor_scale || 1;
this.mouse_x = (event.clientX - rect.left) / scale;
this.mouse_y = (event.clientY - rect.top) / scale;
}
// The closing brace for methods is intentionally omitted, as Genie/Stipple will add it.
"""
end
@page("/mask", "mask.jl.html")
end # module

145
mask.jl.html Normal file
View File

@ -0,0 +1,145 @@
<header id="header">
<img src="/css/LABI_logo.png" alt="Labi Logo Icon" id="imgLogo">
<div>
<h4>JuliaMSI - Mask Editor&nbsp;</h4>
</div>
</header>
<div id="extDivStyle" class="row col-12 q-pa-xl">
<div class="row col-4">
<!-- Left Panel: Controls -->
<div class="st-col col-12 st-module q-pa-md">
<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 -->
<div class="text-subtitle1 q-mt-md">Step 2: Create/Upload Mask</div>
<div class="row justify-around">
<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-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>
<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>
<!-- Step 3: Automated Processing & Overlay -->
<div class="text-subtitle1 q-mt-md">Step 3: Adjust & Verify</div>
<div class="q-mt-sm">
<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 class="row col-6">
<!-- Right Panel: Image Displays -->
<div id="intDivStyle-right" class="st-col col-12 st-module">
<div class="text-h6">Mask Preview</div>
<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_forward" class="q-my-sm on-right btn-style" v-on:click="btn_img_plus=true" :disable="is_editing_mask"></q-btn>
</div>
<div v-if="!show_verification_plot" class="text-center text-grey q-pa-xl">
<q-icon name="image" size="5em" class="q-mb-md" />
<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>
</div>
<div v-if="show_verification_plot">
<plotly :data="plotdata_verify" :layout="plotlayout_verify" class="q-pa-none q-ma-none pixelated-plot"></plotly>
<div class="q-mt-md text-center">
<q-btn color="primary" class="btn-style" label="Edit Manually" v-on:click="btn_edit_manually=true" />
</div>
</div>
<p class="text-center" v-html="msgimg"></p>
</div>
</div>
</div>
<q-dialog v-model="show_editor">
<q-card style="min-width: 80vw; min-height: 80vh;">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">Manual Mask Editor</div>
<q-space />
<q-btn icon="close" flat round dense v-on:click="syncMaskToServer()" v-close-popup />
</q-card-section>
<q-card-section class="scroll" style="max-height: 70vh;">
<!-- Editor Tools -->
<div class="q-pa-md q-gutter-sm row justify-center items-center">
<!-- Tool Selection -->
<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="Eraser" v-on:click="current_tool = 'eraser'" :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-separator vertical inset />
<!-- 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="Zoom" type="number" step="0.1" v-model.number="editor_scale" style="max-width: 120px" :min="0.1"/>
<q-separator vertical inset />
<!-- Image Manipulation -->
<q-input dense filled label="Rotate (deg)" type="number" v-model.number="rotate_degrees" style="max-width: 150px" :step="90"/>
<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>
<!-- 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;">
<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_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_forward" v-on:click="move_mask_payload = { direction: 'right', t: Date.now() }" />
</div>
<!-- Interactive Canvas -->
<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 -->
<canvas id="maskCanvas"
:width="canvas_width"
:height="canvas_height"
:style="{
width: canvas_width * editor_scale + 'px',
height: canvas_height * editor_scale + 'px',
border: '2px solid #ccc',
cursor: current_tool === 'drag' ? 'move' : 'crosshair',
imageRendering: 'pixelated'
}"
v-on:mousedown.prevent="startDrawing"
v-on:mousemove.prevent="draw"
v-on:mouseup.prevent="stopDrawing()"
v-on:mouseleave.prevent="stopDrawing()">
</canvas>
</div>
</div>
</q-card-section>
<!--<q-card-actions align="right" class="q-pa-md">
<q-btn label="Cancel" color="negative" v-on:click="show_editor = false"/>
</q-card-actions>-->
</q-card>
</q-dialog>

View File

@ -90,6 +90,12 @@
overflow-y: auto; /* Add scroll if content overflows */
}
.pixelated-plot svg image {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: crisp-edges;
}
*{
font-family: 'Roboto', 'Lato', sans-serif;
}

3
routes.jl Normal file
View File

@ -0,0 +1,3 @@
# Include all your app modules
include("app.jl")
include("mask.jl")

47
src/ImageProcessing.jl Normal file
View File

@ -0,0 +1,47 @@
module ImageProcessing
using Images
using ImageBinarization
using ImageMorphology
using ImageComponentAnalysis
export process_image_pipeline
# ===================================================================
# CORE PROCESSING PIPELINE
# ===================================================================
function process_image_pipeline(gray_img;
otsu_scale=1.0,
noise_size_percent=0.1,
hole_size_percent=0.05,
smoothing=2)
# --- Step 1: Otsu Binarization ---
otsu_threshold = find_threshold(gray_img, Otsu())
adjusted_threshold = otsu_threshold * otsu_scale
binary_img = gray_img .>= adjusted_threshold
# --- Smart Parameter Scaling ---
image_area = length(gray_img)
noise_size_pixels = round(Int, image_area * noise_size_percent)
hole_size_pixels = round(Int, image_area * hole_size_percent)
# --- Step 2: Remove Small White Regions (Noise) ---
# area_opening is the correct morphological operation for this.
noise_removed_img = area_opening(binary_img, min_area=noise_size_pixels)
# --- Step 3: Fill Small Black Holes ---
# area_closing is the dual of area_opening and fills holes.
holes_filled_img = area_closing(noise_removed_img, min_area=hole_size_pixels)
# --- Step 4: Smooth Edges ---
# A morphological closing with a small disk smooths outlines.
smoothing_kernel = ones(Bool, (smoothing, smoothing))
smoothed_img = closing(holes_filled_img, smoothing_kernel)
# --- Return all intermediate steps for visualization ---
return binary_img, noise_removed_img, holes_filled_img, smoothed_img
end
end

View File

@ -1,7 +1,40 @@
# MSI_src.jl
module MSI_src
# Export the public API
export OpenMSIData, GetSpectrum, IterateSpectra, ImportMzmlFile, load_slices, plot_slices, plot_slice, get_total_spectrum, get_average_spectrum, LoadMzml, LoadSpectra, precompute_analytics
# Export the public MSI API
export OpenMSIData,
GetSpectrum,
IterateSpectra,
ImportMzmlFile,
load_slices,
plot_slices,
plot_slice,
get_total_spectrum,
get_average_spectrum,
LoadMzml,
precompute_analytics,
process_spectrum
# Export the public Preprocessing API
export FeatureMatrix,
run_preprocessing_pipeline,
qc_is_empty,
qc_is_regular,
transform_intensity,
smooth_spectrum,
snip_baseline,
tic_normalize,
pqn_normalize,
detect_peaks_profile,
align_peaks_lowess,
bin_peaks,
plot_stage_spectrum,
calculate_ppm_error,
calculate_resolution_fwhm,
analyze_mass_accuracy,
generate_qc_report,
get_common_calibration_standards
# Include all source files directly into the main module
include("MSIData.jl")
@ -9,6 +42,7 @@ include("ParserHelpers.jl")
include("mzML.jl")
include("imzML.jl")
include("MzmlConverter.jl")
include("Preprocessing.jl")
# --- Main Entry Point --- #

View File

@ -1,3 +1,4 @@
# src/MzmlConverter.jl
"""
This file contains the workflow for converting .mzML files (with one spectrum per pixel)
into a proper .imzML/.ibd file pair, using a separate synchronization file.

979
src/Preprocessing.jl Normal file
View File

@ -0,0 +1,979 @@
# src/Preprocessing.jl
"""
This module provides a comprehensive workflow for mass spectrometry imaging (MSI) data
preprocessing, inspired by the functionality of the R package MALDIquant. It includes
functions for quality control, intensity transformation, smoothing, baseline correction,
normalization, peak picking, alignment, and feature matrix generation.
"""
# =============================================================================
# Dependencies
# =============================================================================
using Statistics # For mean, median
using StatsBase # For mad (Median Absolute Deviation)
using SavitzkyGolay # For SavitzkyGolay filtering
using Dates # For now()
using CSV # For writing CSV files
using DataFrames # For creating dataframes
# =============================================================================
# Data Structures
# =============================================================================
"""
FeatureMatrix
Structure to hold the final feature matrix, including m/z bin boundaries and sample indices.
# Fields
- `matrix`: The numerical matrix where rows are samples and columns are features (m/z bins).
- `mz_bins`: A vector of tuples `(low_mz, high_mz)` for each feature column.
- `sample_ids`: A vector of indices corresponding to the original spectra.
"""
struct FeatureMatrix
matrix::Array{Float64,2} # samples × features
mz_bins::Vector{Tuple{Float64,Float64}} # [(low, high), ...]
sample_ids::Vector{Int}
end
# =============================================================================
# 0) Quality Control (QC)
# =============================================================================
"""
qc_is_empty(mz, intensity) -> Bool
Returns `true` if the spectrum is empty or contains no finite intensity data.
"""
qc_is_empty(mz::AbstractVector, intensity::AbstractVector)::Bool =
isempty(mz) || isempty(intensity) || all(!isfinite, intensity)
"""
qc_is_regular(mz) -> Bool
Checks that the m/z axis is monotonically non-decreasing, as expected in a profile spectrum.
"""
function qc_is_regular(mz::AbstractVector)
n = length(mz)
n < 2 && return true
@inbounds for i in 2:n
if mz[i] < mz[i-1]
return false
end
end
return true
end
# =============================================================================
# 1) Intensity Transformation & Smoothing
# =============================================================================
"""
transform_intensity(intensity; method=:sqrt) -> Vector
Applies a variance-stabilizing transformation to the intensity vector.
Supported methods: `:sqrt` (default) and `:log1p`.
"""
function transform_intensity(intensity::AbstractVector{<:Real}; method::Symbol=:sqrt)
if method === :sqrt
return sqrt.(max.(zero(eltype(intensity)), intensity))
elseif method === :log1p
return log1p.(max.(zero(eltype(intensity)), intensity))
else
return collect(float.(intensity))
end
end
"""
smooth_spectrum(y; window=21, order=2) -> Vector
Applies a SavitzkyGolay filter if `SavitzkyGolay.jl` is available.
Otherwise, falls back to a simple (non-phase-correct) moving average.
"""
function smooth_spectrum(y::AbstractVector{<:Real}; window::Int=9, order::Int=2)
win = isodd(window) ? window : window + 1
res = SavitzkyGolay.savitzky_golay(collect(float.(y)), win, order)
return res.y
end
# =============================================================================
# 2) Baseline Correction
# =============================================================================
"""
snip_baseline(y, iterations=100) -> Vector
Estimates the baseline using a simple 1D SNIP (Statistics-sensitive Non-linear
Iterative Peak-clipping) algorithm. `iterations` controls the aggressiveness.
"""
function snip_baseline(y::AbstractVector{<:Real}, iterations::Int=100)
n = length(y)
b = collect(float.(y)) # work copy
buf = similar(b)
for k in 1:iterations
copyto!(buf, b)
@inbounds for i in 2:n-1
buf[i] = min(b[i], 0.5 * (b[i-1] + b[i+1]))
end
# Handle endpoints
buf[1] = min(b[1], b[2])
buf[end] = min(b[end], b[end-1])
b, buf = buf, b # Swap buffers
end
return b
end
# =============================================================================
# 3) Intensity Normalization
# =============================================================================
"""
tic_normalize(y) -> Vector
Normalizes intensities to the Total Ion Current (TIC). If the sum is zero, returns a copy.
"""
function tic_normalize(y::AbstractVector{<:Real})
s = sum(y)
return s <= 0 ? collect(float.(y)) : collect(float.(y)) ./ s
end
"""
pqn_normalize(M) -> Matrix
Performs Probabilistic Quotient Normalization on a matrix `M` where columns are spectra.
"""
function pqn_normalize(M::AbstractMatrix{<:Real})
M_float = collect(float.(M))
# Calculate reference spectrum (median across all spectra)
ref = mapslices(median, M_float; dims=2)[:,1]
# Calculate quotients for each spectrum relative to the reference
Q = similar(M_float)
@inbounds for j in axes(M_float, 2)
Q[:, j] = M_float[:, j] ./ (ref .+ eps(eltype(M_float)))
end
# Find the median quotient for each spectrum (scaling factor)
s = [median( @view Q[:, j]) for j in axes(Q, 2)]
# Normalize the original matrix
@inbounds for j in axes(M_float, 2)
M_float[:, j] ./= (s[j] + eps(eltype(M_float)))
end
return M_float
end
# =============================================================================
# 4) Peak Detection (for Profile Data)
# =============================================================================
"""
detect_peaks_profile(mz, y; half_window=10, snr_threshold=2.0)
Detects local maxima with a signal-to-noise threshold (using MAD for noise estimation).
Assumes profile-mode data and a monotonic m/z axis.
"""
function detect_peaks_profile(mz::AbstractVector{<:Real},
y::AbstractVector{<:Real};
half_window::Int=10,
snr_threshold::Float64=2.0)
n = length(y)
n < 3 && return (Float64[], Float64[])
# Noise estimation using Median Absolute Deviation (robust to peaks)
noise_level = mad(y, normalize=true) + eps(Float64)
# Smooth the spectrum to make peak detection more robust
ys = smooth_spectrum(y; window=max(5, 2*half_window+1), order=2)
peak_idx = Int[]
@inbounds for i in 2:n-1
left = max(1, i - half_window)
right = min(n, i + half_window)
local_max = ys[i]
# A point is a peak if it's the maximum in its neighborhood and above the SNR threshold
if local_max >= maximum( @view ys[left:right]) && (local_max > snr_threshold * noise_level)
# Ensure we only record one point for flat-topped peaks
if isempty(peak_idx) || (i - last(peak_idx) > half_window)
push!(peak_idx, i)
end
end
end
# Return original intensities at peak locations
pk_mz = [float(mz[i]) for i in peak_idx]
pk_int = [float(y[i]) for i in peak_idx]
return (pk_mz, pk_int)
end
"""
detect_peaks_centroid(mz, y; intensity_threshold=0.0)
Filters centroided data based on a minimum intensity threshold.
"""
function detect_peaks_centroid(mz::AbstractVector{<:Real},
y::AbstractVector{<:Real};
intensity_threshold::Float64=0.0)
keep_indices = findall(y .>= intensity_threshold)
return (mz[keep_indices], y[keep_indices])
end
# =============================================================================
# 5) Peak Alignment
# =============================================================================
"""
align_peaks_lowess(ref_mz, tgt_mz; tolerance=0.002) -> warp::Function
Generates a warping function `warp(x)` to map target m/z values to reference m/z values.
Uses a lightweight LOWESS-like approach with linear interpolation.
"""
function align_peaks_lowess(ref_mz::Vector{<:Real},
tgt_mz::Vector{<:Real};
tolerance::Float64=0.002)
# Efficiently match peaks between sorted lists
pairs = Tuple{Float64,Float64}[] # (target_mz, reference_mz)
i = 1; j = 1
while i <= length(tgt_mz) && j <= length(ref_mz)
dt = tgt_mz[i] - ref_mz[j]
if abs(dt) <= tolerance
push!(pairs, (float(tgt_mz[i]), float(ref_mz[j])))
i += 1; j += 1
elseif dt < 0
i += 1
else
j += 1
end
end
if length(pairs) < 3
@warn "Too few matching peaks for alignment. Returning identity function."
return x -> float.(x)
end
t = [p[1] for p in pairs]
r = [p[2] for p in pairs]
# Lightly smooth the mapping to reduce noise
t_s = smooth_spectrum(t; window=5, order=2)
r_s = smooth_spectrum(r; window=5, order=2)
# Return a function that performs linear interpolation for warping
function warp(x::AbstractVector{<:Real})
out = similar(collect(float.(x)))
for (k, xv) in enumerate(x)
if xv <= t_s[1]
# Linear extrapolation at the start
m = (r_s[2]-r_s[1]) / (t_s[2]-t_s[1] + eps())
out[k] = r_s[1] + m*(xv - t_s[1])
elseif xv >= t_s[end]
# Linear extrapolation at the end
m = (r_s[end]-r_s[end-1]) / (t_s[end]-t_s[end-1] + eps())
out[k] = r_s[end-1] + m*(xv - t_s[end-1])
else
# Linear interpolation for points in the middle
lo = searchsortedlast(t_s, xv)
hi = lo + 1
α = (xv - t_s[lo]) / (t_s[hi] - t_s[lo] + eps())
out[k] = (1-α)*r_s[lo] + α*r_s[hi]
end
end
return out
end
return warp
end
# =============================================================================
# 6) Peak Binning & Feature Matrix Generation
# =============================================================================
"""
_find_bin_index(x, bins) -> Int
Efficiently finds the index of the bin `(low, high)` that contains `x` using binary search.
Returns 0 if not found.
"""
function _find_bin_index(x::Float64, bins::Vector{Tuple{Float64,Float64}})
lo, hi = 1, length(bins)
while lo <= hi
mid = (lo + hi) >>> 1
b = bins[mid]
if x < b[1]
hi = mid - 1
elseif x > b[2]
lo = mid + 1
else
return mid
end
end
return 0
end
"""
bin_peaks(all_pk_mz, all_pk_int, tolerance; frequency_threshold=0.25)
Groups peaks from all spectra into consensus m/z bins and creates a feature matrix.
Filters out features that do not appear in a minimum fraction of spectra.
"""
function bin_peaks(all_pk_mz::Vector{<:AbstractVector{<:Real}},
all_pk_int::Vector{<:AbstractVector{<:Real}},
tolerance::Float64; frequency_threshold::Float64=0.25)
ns = length(all_pk_mz)
ns == 0 && return (zeros(0,0), Tuple{Float64,Float64}[])
# 1) Collect all unique peak m/z values and sort them
flat_mz = Float64[]
for v in all_pk_mz
append!(flat_mz, float.(v))
end
sort!(flat_mz)
isempty(flat_mz) && return (zeros(ns, 0), Tuple{Float64,Float64}[])
# 2) Create contiguous m/z bins based on tolerance
bins = Tuple{Float64,Float64}[]
cur_lo = flat_mz[1]
cur_hi = flat_mz[1]
for x in @view flat_mz[2:end]
if x - cur_hi <= tolerance
cur_hi = x # Extend the current bin
else
push!(bins, (cur_lo, cur_hi)) # Finalize old bin
cur_lo = x; cur_hi = x # Start a new one
end
end
push!(bins, (cur_lo, cur_hi))
# 3) Create the feature matrix (samples x features) using max intensity per bin
X = zeros(Float64, ns, length(bins))
for i in 1:ns
for (mzv, iv) in zip(all_pk_mz[i], all_pk_int[i])
bidx = _find_bin_index(float(mzv), bins)
if bidx > 0
X[i, bidx] = max(X[i, bidx], float(iv))
end
end
end
# 4) Filter features by minimum frequency
if frequency_threshold > 0
present_count = vec(sum(X .> 0, dims=1))
min_count = ceil(Int, frequency_threshold * ns)
keep_mask = findall(present_count .>= min_count)
X = X[:, keep_mask]
bins = bins[keep_mask]
end
return (X, bins)
end
# =============================================================================
# 7) Plotting Helper
# =============================================================================
"""
plot_stage_spectrum(mz, intensity; title, ...)
Returns a `CairoMakie.Figure` for a single spectrum trace. The caller is responsible for saving.
"""
function plot_stage_spectrum(mz::AbstractVector, intensity::AbstractVector;
title::AbstractString, xlabel::AbstractString="m/z",
ylabel::AbstractString="Intensity")
# This dynamic import is for script-like use; in a package, Makie would be a full dependency.
@eval begin
import CairoMakie
using CairoMakie
end
fig = CairoMakie.Figure(size = (1400, 500))
ax = CairoMakie.Axis(fig[1, 1], title=title, xlabel=xlabel, ylabel=ylabel)
CairoMakie.lines!(ax, mz, intensity)
return fig
end
# =============================================================================
# 8) Pipeline Orchestrator
# =============================================================================
"""
run_preprocessing_pipeline(spectra; steps, params, on_stage)
Executes a flexible preprocessing pipeline on a vector of spectra.
# Arguments
- `spectra`: A vector of `(mz, intensity)` tuples.
- `steps`: A vector of symbols defining the pipeline order (e.g., `[:qc, :smooth, :baseline, :peaks, :bin]`).
- `params`: A dictionary of parameters for each step.
- `on_stage`: An optional callback function `on_stage(stage_symbol; idx, mz, intensity)` executed after each step for logging or visualization.
# Returns
- A `FeatureMatrix` if `:bin` is in the steps, otherwise the vector of processed spectra.
"""
function run_preprocessing_pipeline(spectra::Vector;
steps::Vector{Symbol},
params::Dict=Dict(),
on_stage::Function=(;kwargs...)->nothing)
processed = deepcopy(spectra) # Don't mutate the original input
reference_peaks = nothing # For alignment
_emit(stage::Symbol, idx::Int, mz, y) = on_stage(stage; idx=idx, mz=mz, intensity=y)
for step in steps
@info "Running step: $step"
if step === :qc
for (i, (mz, y)) in enumerate(processed)
(isempty(mz) || isempty(y)) && continue
_emit(:qc_raw, i, mz, y)
qc_is_empty(mz, y) && @warn "Spectrum at index $i is empty."
!qc_is_regular(mz) && @warn "m/z axis at index $i is not monotonic."
end
elseif step === :transform
meth = get(params, :transform_method, :sqrt)
for i in eachindex(processed)
mz, y = processed[i]
y_new = transform_intensity(y; method=meth)
processed[i] = (mz, y_new)
_emit(:transform, i, mz, y_new)
end
elseif step === :smooth
win = get(params, :sg_window, 21)
ord = get(params, :sg_order, 2)
for i in eachindex(processed)
mz, y = processed[i]
y_smooth = smooth_spectrum(y; window=win, order=ord)
processed[i] = (mz, y_smooth)
_emit(:smooth, i, mz, y_smooth)
end
elseif step === :baseline
iters = get(params, :snip_iterations, 100)
for i in eachindex(processed)
mz, y = processed[i]
baseline = snip_baseline(y, iters)
y_corrected = max.(0.0, y .- baseline)
processed[i] = (mz, y_corrected)
_emit(:baseline, i, mz, y_corrected)
end
elseif step === :normalize
mode = get(params, :normalize_method, :tic)
if mode === :tic
for i in eachindex(processed)
mz, y = processed[i]
y_norm = tic_normalize(y)
processed[i] = (mz, y_norm)
_emit(:normalize, i, mz, y_norm)
end
elseif mode === :pqn
# Note: PQN assumes spectra are on a common m/z grid.
matrix = hcat([float.(p[2]) for p in processed]...)
matrix_norm = pqn_normalize(matrix)
for i in eachindex(processed)
mz, _ = processed[i]
processed[i] = (mz, view(matrix_norm, :, i))
_emit(:normalize, i, mz, processed[i][2])
end
end
elseif step === :peaks
peak_results = Vector{Tuple{Vector{Float64},Vector{Float64}}}(undef, length(processed))
hw = get(params, :peak_half_window, 10)
snr = get(params, :peak_snr, 2.0)
for (i, (mz, y)) in enumerate(processed)
pk_mz, pk_int = detect_peaks_profile(mz, y; half_window=hw, snr_threshold=snr)
peak_results[i] = (pk_mz, pk_int)
# Emit with original mz axis but maybe stem plot of peaks?
_emit(:peaks, i, pk_mz, pk_int)
end
processed = peak_results
# Set reference for alignment
reference_peaks = isempty(processed) ? nothing : processed[1][1]
elseif step === :align
reference_peaks === nothing && (@error "Alignment requires a :peaks step first."; continue)
tol = get(params, :align_tolerance, 0.002)
for i in 2:length(processed)
tgt_peaks, intens = processed[i]
warp_func = align_peaks_lowess(reference_peaks, tgt_peaks; tolerance=tol)
processed[i] = (warp_func(tgt_peaks), intens)
_emit(:align, i, processed[i][1], processed[i][2])
end
elseif step === :bin
all_pks = [s[1] for s in processed]
all_ints = [s[2] for s in processed]
tol = get(params, :bin_tolerance, 0.002)
freq = get(params, :bin_min_frequency, 0.25)
mat, mz_bins = bin_peaks(all_pks, all_ints, tol; frequency_threshold=freq)
return FeatureMatrix(mat, mz_bins, collect(1:length(processed)))
end
end
@warn "Pipeline finished without a :bin step; returning processed spectra."
return processed
end
"""
run_preprocessing_pipeline(msi_data::MSIData, indices::Vector{Int}; steps, params, on_stage)
Executes a flexible preprocessing pipeline on a subset of spectra from an MSIData object,
with mode-aware logic for centroid and profile data.
"""
function run_preprocessing_pipeline(msi_data::MSIData, indices::Vector{Int};
steps::Vector{Symbol},
params::Dict=Dict(),
on_stage::Function=(;kwargs...)->nothing)
# This version of the pipeline is mode-aware.
# It processes spectra directly from the MSIData object.
processed_spectra = Vector{Tuple}(undef, length(indices))
# First, load all spectra and apply initial steps that run on individual spectra
for (i, spec_idx) in enumerate(indices)
mz, intensity = GetSpectrum(msi_data, spec_idx)
mode = msi_data.spectra_metadata[spec_idx].mode
on_stage(:qc_raw; idx=spec_idx, mz=mz, intensity=intensity)
for step in steps
if step === :transform
meth = get(params, :transform_method, :sqrt)
intensity = transform_intensity(intensity; method=meth)
on_stage(:transform; idx=spec_idx, mz=mz, intensity=intensity)
elseif step === :smooth && mode == PROFILE
win = get(params, :sg_window, 21)
ord = get(params, :sg_order, 2)
intensity = smooth_spectrum(intensity; window=win, order=ord)
on_stage(:smooth; idx=spec_idx, mz=mz, intensity=intensity)
elseif step === :baseline && mode == PROFILE
iters = get(params, :snip_iterations, 100)
baseline = snip_baseline(intensity, iters)
intensity = max.(0.0, intensity .- baseline)
on_stage(:baseline; idx=spec_idx, mz=mz, intensity=intensity)
elseif step === :normalize
norm_mode = get(params, :normalize_method, :tic)
if norm_mode === :tic
intensity = tic_normalize(intensity)
on_stage(:normalize; idx=spec_idx, mz=mz, intensity=intensity)
end
end
end
processed_spectra[i] = (mz, intensity, mode) # Store mode for peak detection
end
# Now, handle steps that require all spectra (like PQN) or are the final steps
final_result = nothing
for step in steps
if step === :normalize && get(params, :normalize_method, :tic) === :pqn
# Note: PQN assumes spectra are on a common m/z grid.
matrix = hcat([float.(p[2]) for p in processed_spectra]...)
matrix_norm = pqn_normalize(matrix)
for i in eachindex(processed_spectra)
mz, _, mode = processed_spectra[i]
processed_spectra[i] = (mz, view(matrix_norm, :, i), mode)
on_stage(:normalize; idx=indices[i], mz=mz, intensity=processed_spectra[i][2])
end
elseif step === :peaks
peak_results = Vector{Tuple{Vector{Float64},Vector{Float64}}}(undef, length(processed_spectra))
hw = get(params, :peak_half_window, 10)
snr = get(params, :peak_snr, 2.0)
intensity_thresh = get(params, :peak_intensity_threshold, 0.0)
for (i, (mz, y, mode)) in enumerate(processed_spectra)
spec_idx = indices[i]
if mode == PROFILE
pk_mz, pk_int = detect_peaks_profile(mz, y; half_window=hw, snr_threshold=snr)
else # CENTROID
pk_mz, pk_int = detect_peaks_centroid(mz, y; intensity_threshold=intensity_thresh)
end
peak_results[i] = (pk_mz, pk_int)
on_stage(:peaks; idx=spec_idx, mz=pk_mz, intensity=pk_int)
end
processed_spectra = peak_results # Now contains peak lists
elseif step === :align
# Alignment requires a reference peak list, typically from the first spectrum
reference_peaks = isempty(processed_spectra) ? nothing : processed_spectra[1][1]
if reference_peaks === nothing
@error "Alignment requires a :peaks step first."; continue
end
tol = get(params, :align_tolerance, 0.002)
for i in 2:length(processed_spectra)
tgt_peaks, intens = processed_spectra[i]
warp_func = align_peaks_lowess(reference_peaks, tgt_peaks; tolerance=tol)
processed_spectra[i] = (warp_func(tgt_peaks), intens)
on_stage(:align; idx=indices[i], mz=processed_spectra[i][1], intensity=processed_spectra[i][2])
end
elseif step === :bin
all_pks = [s[1] for s in processed_spectra]
all_ints = [s[2] for s in processed_spectra]
tol = get(params, :bin_tolerance, 0.002)
freq = get(params, :bin_min_frequency, 0.25)
mat, mz_bins = bin_peaks(all_pks, all_ints, tol; frequency_threshold=freq)
final_result = FeatureMatrix(mat, mz_bins, indices)
break # Binning is the last step
end
end
if final_result !== nothing
return final_result
else
@warn "Pipeline finished without a :bin step; returning processed spectra."
return processed_spectra
end
end
# =============================================================================
# 9) Quality Control Metrics
# =============================================================================
"""
calculate_ppm_error(measured_mz::Float64, theoretical_mz::Float64) -> Float64
Calculates mass accuracy in parts-per-million (PPM).
# Formula
PPM = 10 × |measured_mz - theoretical_mz| / theoretical_mz
"""
function calculate_ppm_error(measured_mz::Real, theoretical_mz::Real)
if theoretical_mz == 0
return Inf
end
return 1e6 * abs(Float64(measured_mz) - Float64(theoretical_mz)) / Float64(theoretical_mz)
end
"""
calculate_ppm_error_bulk(measured_mz::Vector{Float64}, theoretical_mz::Vector{Float64}) -> Vector{Float64}
Calculates PPM errors for multiple mass values.
"""
function calculate_ppm_error_bulk(measured_mz::Vector{Real}, theoretical_mz::Vector{Real})
return [calculate_ppm_error(m, t) for (m, t) in zip(measured_mz, theoretical_mz)]
end
"""
calculate_resolution_fwhm(mz::Float64, profile_mz::Vector{Float64},
profile_intensity::Vector{Float64}) -> Float64
Calculates mass resolution using Full Width at Half Maximum (FWHM).
# Formula
Resolution = m / Δm, where Δm is FWHM
# Arguments
- `mz`: Peak centroid m/z
- `profile_mz`: Full m/z array from profile data
- `profile_intensity`: Full intensity array from profile data
# Returns
Resolution or NaN if cannot be calculated
"""
function calculate_resolution_fwhm(mz::Real, profile_mz::AbstractVector{<:Real},
profile_intensity::AbstractVector{<:Real})
# Find peak center index
peak_idx = argmin(abs.(profile_mz .- mz))
peak_height = Float64(profile_intensity[peak_idx])
half_max = peak_height / 2
# Find left half-maximum point (interpolate for accuracy)
left_idx = find_last_below(profile_intensity[1:peak_idx], half_max)
if left_idx == 0 || left_idx == length(profile_intensity[1:peak_idx])
return NaN
end
# Linear interpolation for left FWHM
x1, x2 = Float64(profile_mz[left_idx]), Float64(profile_mz[left_idx+1])
y1, y2 = Float64(profile_intensity[left_idx]), Float64(profile_intensity[left_idx+1])
left_fwhm = x1 + (x2 - x1) * (half_max - y1) / (y2 - y1)
# Find right half-maximum point
right_slice = profile_intensity[peak_idx:end]
right_offset = find_first_below(right_slice, half_max)
if right_offset == 0 || right_offset == length(right_slice)
return NaN
end
right_idx = peak_idx + right_offset - 1
x1, x2 = Float64(profile_mz[right_idx-1]), Float64(profile_mz[right_idx])
y1, y2 = Float64(profile_intensity[right_idx-1]), Float64(profile_intensity[right_idx])
right_fwhm = x1 + (x2 - x1) * (half_max - y1) / (y2 - y1)
fwhm = right_fwhm - left_fwhm
return fwhm > 0 ? Float64(mz) / fwhm : NaN
end
# Helper functions for FWHM calculation
function find_last_below(v::AbstractVector{<:Real}, threshold::Real)
for i in length(v):-1:2
if v[i] >= threshold && v[i-1] < threshold
return i-1
end
end
return 0
end
function find_first_below(v::AbstractVector{<:Real}, threshold::Real)
for i in 1:(length(v)-1)
if v[i] >= threshold && v[i+1] < threshold
return i+1
end
end
return 0
end
"""
analyze_mass_accuracy(msi_data, reference_peaks; ppm_tolerance=20.0)
Analyzes mass accuracy across the dataset using known reference peaks.
# Arguments
- `msi_data`: Your MSI dataset
- `reference_peaks`: Dict of theoretical m/z values -> compound names
- `ppm_tolerance`: Initial tolerance for peak matching
# Returns
Comprehensive mass accuracy report
"""
function analyze_mass_accuracy(msi_data, reference_peaks::Dict{Float64,String};
ppm_tolerance::Float64=5.0, sample_spectra=100)
println("\n[ MASS ACCURACY ANALYSIS ]")
println("PPM tolerance: ", ppm_tolerance, " ppm")
println("Spectra to sample: ", sample_spectra)
theoretical_mz = sort(collect(keys(reference_peaks)))
ppm_errors = Float64[]
matched_peaks = Tuple{Float64,Float64,String}[] # (theoretical, measured, compound)
# Sample spectra across the dataset
if sample_spectra >= length(msi_data.spectra_metadata)
spectrum_indices = 1:length(msi_data.spectra_metadata)
else
spectrum_indices = round.(Int, range(1, length(msi_data.spectra_metadata), length=sample_spectra))
end
for idx in spectrum_indices
mz, intensity = GetSpectrum(msi_data, idx)
# Detect peaks in this spectrum
detected_peaks, _ = detect_peaks_profile(mz, intensity, snr_threshold=3.0)
# Match detected peaks to reference peaks
for (i, theoretical) in enumerate(theoretical_mz)
# Find closest detected peak within tolerance
distances = abs.(detected_peaks .- theoretical)
if !isempty(distances)
min_idx = argmin(distances)
min_distance = distances[min_idx]
ppm_error = calculate_ppm_error(detected_peaks[min_idx], theoretical)
if ppm_error <= ppm_tolerance
push!(ppm_errors, ppm_error)
push!(matched_peaks, (theoretical, detected_peaks[min_idx], reference_peaks[theoretical]))
end
end
end
end
if isempty(ppm_errors)
@warn "No peaks matched within $ppm_tolerance ppm tolerance"
return (mean_ppm=NaN, std_ppm=NaN, min_ppm=NaN, max_ppm=NaN, optimal_ppm=NaN, n_matches=0, matched_peaks=[], all_ppm_errors=[])
end
# Calculate statistics
mean_ppm = mean(ppm_errors)
std_ppm = std(ppm_errors)
min_ppm = minimum(ppm_errors)
max_ppm = maximum(ppm_errors)
# Determine optimal ppm tolerance (mean + 3σ covers ~99.7% of peaks for normal distribution)
optimal_ppm = mean_ppm + 3 * std_ppm
return (
mean_ppm = mean_ppm,
std_ppm = std_ppm,
min_ppm = min_ppm,
max_ppm = max_ppm,
optimal_ppm = optimal_ppm,
n_matches = length(ppm_errors),
matched_peaks = matched_peaks,
all_ppm_errors = ppm_errors
)
end
"""
get_common_calibration_standards(standard_type::Symbol)
Returns common calibration masses for different instrument types.
# Supported standards
- `:maldi_pos`: Common MALDI-TOF positive mode calibrants
- `:maldi_neg`: Common MALDI-TOF negative mode calibrants
- `:esi_pos`: ESI positive mode calibrants
- `:lcms`: LC-MS commonly used standards
"""
function get_common_calibration_standards(standard_type::Symbol=:maldi_pos)
standards = Dict{Float64,String}()
if standard_type == :maldi_pos
standards = Dict(
104.10754 => "C5H4N2 (Imidazole)",
175.11995 => "C6H15O4P (Glycerophosphocholine fragment)",
226.15687 => "C10H20NO4P (Phosphocholine)",
322.04810 => "[Glu1]-Fibrinopeptide B fragment",
379.09247 => "C12H22O11 (Sucrose)",
515.32539 => "C26H52NO7P (PC(16:0/0:0))",
622.02896 => "C20H12O5S2 (1-Hydroxypyrene-3,6,8-trisulfate)",
757.39917 => "C37H74NO8P (PC(34:1))",
1046.54198 => "Angiotensin I",
1296.68477 => "ACTH clip 1-17",
1570.67744 => "ACTH clip 18-39",
2465.19829 => "ACTH clip 7-38"
)
elseif standard_type == :maldi_neg
standards = Dict(
112.98563 => "C2F3O2 (Trifluoroacetate)",
152.99568 => "C2F6S (Perfluoroethylsulfonate)",
214.00166 => "C4F7O2 (Heptafluorobutyrate)",
264.93278 => "C6F6 (Hexafluorobenzene)",
362.96198 => "C8F15O2 (Perfluorooctanoate)",
466.96714 => "C10F17O2S (Perfluorooctanesulfonate)"
)
elseif standard_type == :esi_pos
standards = Dict(
118.08626 => "C5H12NO2 (Valine)",
175.11900 => "C6H15O4P (Phosphocholine fragment)",
524.26496 => "C23H48NO7P (LysoPC(16:0))",
622.02896 => "C20H12O5S2 (Standard)",
922.00980 => "C18H18O6N3S3 (Ultramark 1621)"
)
end
return standards
end
"""
generate_qc_report(msi_data; reference_peaks, output_dir)
Generates a comprehensive QC report including mass accuracy and resolution.
"""
function generate_qc_report(msi_data, filename::String; reference_peaks=nothing, output_dir="qc_results", sample_spectra=100)
println("\n[ QC REPORT GENERATION ]")
println("Input file: ", filename)
println("Output directory: ", output_dir)
mkpath(output_dir)
# Use default calibrants if none provided
if reference_peaks === nothing
reference_peaks = get_common_calibration_standards(:maldi_pos)
end
println("Generating QC Report...")
println("Using $(length(reference_peaks)) reference masses")
# 1. Analyze mass accuracy
accuracy_report = analyze_mass_accuracy(msi_data, reference_peaks, sample_spectra=sample_spectra)
println("\n" * "="^50)
println("MASS ACCURACY REPORT")
println("="^50)
println("Mean PPM error: $(round(accuracy_report.mean_ppm, digits=2)) ppm")
println("Std PPM error: $(round(accuracy_report.std_ppm, digits=2)) ppm")
println("Min PPM error: $(round(accuracy_report.min_ppm, digits=2)) ppm")
println("Max PPM error: $(round(accuracy_report.max_ppm, digits=2)) ppm")
if haskey(accuracy_report, :optimal_ppm)
println("Optimal PPM tolerance: $(round(accuracy_report.optimal_ppm, digits=2)) ppm")
else
println("Optimal PPM tolerance: Not available")
end
println("Number of matches: $(accuracy_report.n_matches)")
# 2. Calculate resolution for a few representative peaks
println("\n" * "="^50)
println("RESOLUTION ANALYSIS")
println("="^50)
resolution_results = []
sample_spectra = min(10, length(msi_data.spectra_metadata))
for (i, idx) in enumerate(round.(Int, range(1, length(msi_data.spectra_metadata), length=sample_spectra)))
process_spectrum(msi_data, idx) do mz, intensity
if !qc_is_empty(mz, intensity)
# Test resolution on the most intense peak
max_intensity_idx = argmax(intensity)
test_mz = mz[max_intensity_idx]
resolution = calculate_resolution_fwhm(test_mz, mz, intensity)
if !isnan(resolution)
push!(resolution_results, resolution)
println("Spectrum $idx: Resolution = $(round(resolution))")
end
end
end
end
if !isempty(resolution_results)
avg_resolution = mean(resolution_results)
println("\nAverage resolution: $(round(avg_resolution))")
println("Resolution range: $(round(minimum(resolution_results))) - $(round(maximum(resolution_results)))")
end
# 3. Save detailed results
# Save PPM error distribution
ppm_df = DataFrame(
theoretical_mz = [p[1] for p in accuracy_report.matched_peaks],
measured_mz = [p[2] for p in accuracy_report.matched_peaks],
compound = [p[3] for p in accuracy_report.matched_peaks],
ppm_error = accuracy_report.all_ppm_errors
)
CSV.write(joinpath(output_dir, "mass_accuracy_results.csv"), ppm_df)
# Save resolution results
if !isempty(resolution_results)
res_df = DataFrame(resolution = resolution_results)
CSV.write(joinpath(output_dir, "resolution_results.csv"), res_df)
end
# 4. Create summary
summary = """
QC REPORT SUMMARY
=================
Date: $(now())
File: $(filename)
Spectra analyzed: $(length(msi_data.spectra_metadata))
MASS ACCURACY:
- Mean PPM: $(round(accuracy_report.mean_ppm, digits=2)) ppm
- Std PPM: $(round(accuracy_report.std_ppm, digits=2)) ppm
- Recommended tolerance: $(round(accuracy_report.optimal_ppm, digits=2)) ppm (mean + 3 * std)
RESOLUTION:
- Average: $(isempty(resolution_results) ? "N/A" : string(round(mean(resolution_results))))
- Range: $(isempty(resolution_results) ? "N/A" : "$(round(minimum(resolution_results))) - $(round(maximum(resolution_results)))")
RECOMMENDATIONS:
- Use $(round(accuracy_report.optimal_ppm, digits=2)) ppm for peak matching
- Instrument performance: $(accuracy_report.mean_ppm < 5 ? "Excellent" : accuracy_report.mean_ppm < 10 ? "Good" : "Needs calibration")
"""
open(joinpath(output_dir, "qc_summary.txt"), "w") do f
write(f, summary)
end
println("\nQC report saved to: $output_dir")
return accuracy_report, resolution_results
end

View File

@ -37,21 +37,24 @@ Before running the tests, you must edit the `test/run_tests.jl` file to point to
cd /path/to/your/JuliaMSI
```
2. **Execute the Test Script**:
2. **Execute the Different Test Scripts**:
Run the following command from the project's root directory. This will install the necessary dependencies and run the tests.
```bash
julia --project=. test/run_tests.jl
```
```bash
julia --project=. test/run_preprocessing.jl
```
3. **Check the Results**:
The script will print its progress to the console. Any generated images (plots and image slices) will be saved in the `test/results/` directory.
## Test Case Configuration
You can customize the test run by editing the variables in `test/run_tests.jl`.
You can customize the test run by editing the variables in `test/run_tests.jl` or `test/run_preprocessing.jl` respectively.
### Enabling and Disabling Test Cases
You can run or skip specific test cases by setting the corresponding boolean variables to `true` or `false`.
You can run or skip specific test cases of run_tests by setting the corresponding boolean variables to `true` or `false`.
```julia
test1 = true # Runs Test Case 1

468
test/run_preprocessing.jl Normal file
View File

@ -0,0 +1,468 @@
# test/run_preprocessing.jl
# ===================================================================
# Test Environment for the Preprocessing.jl Module
# ===================================================================
# This script tests the full preprocessing pipeline on single spectra
# and total spectra from both .mzML and .imzML files.
# It generates an overlay plot showing all preprocessing stages and
# saves the resulting feature matrix to a CSV file.
#
# Instructions:
# 1. Ensure the file paths in the "CONFIG" section are correct.
# 2. Run the script from the project's root directory:
# julia test/run_preprocessing.jl
# 3. Check the `test/results/` folder for output plots and CSVs.
# ===================================================================
using Printf
using CairoMakie
import Pkg
using DataFrames # For saving FeatureMatrix to CSV
using CSV # For saving FeatureMatrix to CSV
using Statistics # For mean()
# --- Load Modules ---
# Activate the project environment to access dependencies
Pkg.activate(joinpath(@__DIR__, ".."))
using MSI_src # This brings in Preprocessing.jl functions via export
# ===================================================================
# CONFIG: Test files and parameters
# ===================================================================
# --- Test Files ---
# An mzML file for testing spectrum-based processing
# const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/CE4_BF_R1/CE4_BF_R1.mzML"
# const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/set de datos MS/Leaf_profile_LD_LTP_MS.mzML"
const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/set de datos MS/Escopolamina_tuneo_fraq_20ev.mzML"
#const TEST_MZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/set de datos MS/Atropina_tuneo_fraq_20ev.mzML"
const MZML_SPECTRUM_ID = 1
# An imzML file for testing
# const TEST_IMZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/CE4_BF_R1/CE4_BF_R1.imzML"
# const IMZML_COORDS = (50, 50)
const TEST_IMZML_FILE = "/home/pixel/Documents/Cinvestav_2025/Analisis/salida/Stomach_DHB_uncompressed.imzML"
const IMZML_COORDS = (1997, 639)
# --- Output Directory ---
const RESULTS_DIR = "test/results"
# ===================================================================
# HELPER FUNCTIONS FOR PLOTTING
# ===================================================================
"""
plot_overlay_stages(collected_data, output_path, title)
Creates a single plot overlaying spectra from different preprocessing stages.
"""
function plot_overlay_stages(collected_data, output_path, title)
fig = Figure(size = (1400, 800))
ax = Axis(fig[1, 1], title=title, xlabel="m/z", ylabel="Intensity")
colors = Makie.wong_colors() # A good set of distinct colors
for (i, (stage, mz, intensity)) in enumerate(collected_data)
color = colors[mod1(i, length(colors))] # Cycle through colors
# Plot the spectrum as a line
lines!(ax, mz, intensity, color=color, label=string(stage))
# If it's the peaks stage, also mark the peak tops
if stage == :peaks
scatter!(ax, mz, intensity, color=color, marker=:circle, markersize=8, label="$(string(stage)) (tops)")
end
end
axislegend(ax, position=:rt) # Right top position
save(output_path, fig)
println("SUCCESS: Overlay plot saved to $output_path")
end
# ===================================================================
# TEST DEFINITIONS
# ===================================================================
"""
test_full_pipeline(msi_data, spectrum_id; output_dir, file_type_prefix)
Tests the full preprocessing pipeline on a single spectrum and saves a plot
for each intermediate step using the `on_stage` callback.
`spectrum_id` can be an `Int` (for mzML) or a `Tuple{Int, Int}` (for imzML).
"""
function test_full_pipeline(msi_data, spectrum_id; output_dir, file_type_prefix, mz_tolerance=0.002)
println("\n--- Testing Full Preprocessing Pipeline on Spectrum: $spectrum_id (File Type: $file_type_prefix) ---")
# 1. Determine the spectrum index
local spec_idx
if spectrum_id isa Int
spec_idx = spectrum_id
else # Tuple for imzML
spec_idx = msi_data.coordinate_map[spectrum_id...]
end
if spec_idx == 0
println("SKIPPED: No spectrum found at coordinates $spectrum_id.")
return
end
# 2. Define the pipeline steps in the desired order
pipeline_steps = [
:qc,
:transform,
:smooth,
:baseline,
:normalize,
:peaks,
:align, # Align requires multiple spectra, but we'll run it on a single one for now (will warn)
:bin
]
# Define parameters for each step
params = Dict(
:transform_method => :sqrt,
:sg_window => 15,
:sg_order => 2,
:snip_iterations => 100,
:normalize_method => :tic,
:peak_half_window => 10,
:peak_snr => 3.0,
:peak_intensity_threshold => 0.0, # For centroid peak detection
:align_tolerance => mz_tolerance,
:bin_tolerance => mz_tolerance,
:bin_min_frequency => 0.0 # Keep all bins for a single spectrum
)
# 3. Define the on_stage callback to collect data for overlay plot and save separate plots
collected_stage_data = []
stage_counter = Ref(0) # Initialize counter for sequential naming
normalized_spectrum = nothing # Variable to hold the normalized spectrum
function stage_callback(stage; idx, mz, intensity)
stage_counter[] += 1 # Increment counter
println(" -> Generating plot for stage: $stage")
local fig # Make fig available in the whole function scope
if stage == :normalize
normalized_spectrum = (mz, intensity)
fig = plot_stage_spectrum(mz, intensity, title="Stage: $stage (Spectrum $spectrum_id)")
elseif stage == :peaks && normalized_spectrum !== nothing
# For the peaks stage, plot the normalized spectrum as a base layer
fig = Figure(size = (1400, 500))
ax = Axis(fig[1, 1], title="Stage: Peaks (Spectrum $spectrum_id)", xlabel="m/z", ylabel="Intensity")
lines!(ax, normalized_spectrum[1], normalized_spectrum[2], color=:gray, label="Normalized Spectrum")
scatter!(ax, mz, intensity, color=:red, marker=:circle, markersize=8, label="Detected Peaks")
axislegend(ax)
else
# Default plotting for all other stages
fig = plot_stage_spectrum(mz, intensity, title="Stage: $stage (Spectrum $spectrum_id)")
end
# Save the figure
stage_output_path = joinpath(output_dir, "$(file_type_prefix)_$(spectrum_id)_$(stage_counter[])_$(stage).png")
save(stage_output_path, fig)
# Collect data for overlay plot
push!(collected_stage_data, (stage, mz, intensity))
end
# 4. Run the pipeline on the single spectrum
println("Running pipeline with steps: $pipeline_steps")
processed_result = run_preprocessing_pipeline(
msi_data,
[spec_idx], # The pipeline expects a vector of indices
steps=pipeline_steps,
params=params,
on_stage=stage_callback
)
# 5. Generate and save the overlay plot
overlay_output_path = joinpath(output_dir, "$(file_type_prefix)_$(spectrum_id)_all_stages_overlay.png")
plot_overlay_stages(collected_stage_data, overlay_output_path, "Preprocessing Stages Overlay (Spectrum $spectrum_id)")
# 6. Save feature matrix if generated
if processed_result isa FeatureMatrix
feature_matrix_output_path = joinpath(output_dir, "$(file_type_prefix)_$(spectrum_id)_feature_matrix.csv")
# Convert mz_bins to a more readable format for CSV
mz_labels = ["$(round(b[1], digits=4))_$(round(b[2], digits=4))" for b in processed_result.mz_bins]
df = DataFrame(processed_result.matrix, Symbol.(mz_labels))
CSV.write(feature_matrix_output_path, df)
println("SUCCESS: Feature matrix saved to $feature_matrix_output_path")
else
@warn "Pipeline did not return a FeatureMatrix for Spectrum $spectrum_id."
processed_result
end
println("--- Pipeline test finished for Spectrum: $spectrum_id (File Type: $file_type_prefix) ---")
println("Check the '$(output_dir)' directory for output plots and CSVs.")
end
"""
test_full_pipeline_on_total_spectrum(msi_data; output_dir, file_type_prefix)
Tests the full preprocessing pipeline on the *total spectrum* (sum of all spectra)
and saves a plot for each intermediate step.
"""
function test_full_pipeline_on_total_spectrum(msi_data; output_dir, file_type_prefix, mz_tolerance=0.002)
println("\n--- Testing Full Preprocessing Pipeline on TOTAL Spectrum (File Type: $file_type_prefix) ---")
# 1. Get the total spectrum
total_mz, total_intensity = get_total_spectrum(msi_data)
total_spectrum = (total_mz, total_intensity)
if qc_is_empty(total_mz, total_intensity)
println("SKIPPED: Total spectrum is empty.")
return
end
# 2. Define the pipeline steps and parameters (same as for single spectrum)
pipeline_steps = [
:qc,
:transform,
:smooth,
:baseline,
:normalize,
:peaks,
:align, # Align requires multiple spectra, but we'll run it on a single one for now (will warn)
:bin
]
params = Dict(
:transform_method => :sqrt,
:sg_window => 15,
:sg_order => 2,
:snip_iterations => 100,
:normalize_method => :tic,
:peak_half_window => 10,
:peak_snr => 3.0,
:align_tolerance => mz_tolerance,
:bin_tolerance => mz_tolerance,
:bin_min_frequency => 0.0 # Keep all bins for a single spectrum
)
# 3. Define the on_stage callback
collected_stage_data = []
stage_counter = Ref(0) # Initialize counter for sequential naming
normalized_spectrum_total = nothing # Variable to hold the normalized spectrum
function stage_callback_total(stage; idx, mz, intensity)
stage_counter[] += 1 # Increment counter
println(" -> Generating plot for stage: $stage (Total Spectrum)")
local fig
if stage == :normalize
normalized_spectrum_total = (mz, intensity)
fig = plot_stage_spectrum(mz, intensity, title="Stage: $stage (Total Spectrum)")
elseif stage == :peaks && normalized_spectrum_total !== nothing
fig = Figure(size = (1400, 500))
ax = Axis(fig[1, 1], title="Stage: Peaks (Total Spectrum)", xlabel="m/z", ylabel="Intensity")
lines!(ax, normalized_spectrum_total[1], normalized_spectrum_total[2], color=:gray, label="Normalized Spectrum")
scatter!(ax, mz, intensity, color=:red, marker=:circle, markersize=8, label="Detected Peaks")
axislegend(ax)
else
fig = plot_stage_spectrum(mz, intensity, title="Stage: $stage (Total Spectrum)")
end
# Save separate plot with sequential name
stage_output_path = joinpath(output_dir, "$(file_type_prefix)_total_$(stage_counter[])_$(stage).png")
save(stage_output_path, fig)
# Collect data for overlay plot
push!(collected_stage_data, (stage, mz, intensity))
end
# 4. Run the pipeline on the single total spectrum
println("Running pipeline with steps: $pipeline_steps")
processed_result = run_preprocessing_pipeline(
[total_spectrum], # Pass the total spectrum as a vector of one spectrum
steps=pipeline_steps,
params=params,
on_stage=stage_callback_total
)
# 5. Generate and save the overlay plot
overlay_output_path = joinpath(output_dir, "$(file_type_prefix)_total_all_stages_overlay.png")
plot_overlay_stages(collected_stage_data, overlay_output_path, "Preprocessing Stages Overlay (Total Spectrum)")
# 6. Save feature matrix if generated
if processed_result isa FeatureMatrix
feature_matrix_output_path = joinpath(output_dir, "$(file_type_prefix)_total_feature_matrix.csv")
# Convert mz_bins to a more readable format for CSV
mz_labels = ["$(round(b[1], digits=4))_$(round(b[2], digits=4))" for b in processed_result.mz_bins]
df = DataFrame(processed_result.matrix, Symbol.(mz_labels))
CSV.write(feature_matrix_output_path, df)
println("SUCCESS: Feature matrix saved to $feature_matrix_output_path")
else
@warn "Pipeline did not return a FeatureMatrix for Total Spectrum."
processed_result
end
println("--- Pipeline test finished for TOTAL Spectrum (File Type: $file_type_prefix) ---")
println("Check the '$(output_dir)' directory for output plots and CSVs.")
end
# ===================================================================
# TEST RUNNER
# ===================================================================
function run_preprocessing_tests()
println("="^80)
println("STARTING PREPROCESSING TEST SUITE")
println("="^80)
# --- Test Case 1: Run full pipeline on a single mzML spectrum ---
println("\n" * "="^20 * " Test Case 1: Full Pipeline on .mzML Spectrum " * "="^20)
println("FILE: ", TEST_MZML_FILE)
if isfile(TEST_MZML_FILE)
try
msi_data_mzml = OpenMSIData(TEST_MZML_FILE)
# Dynamically determine tolerance
println("\n--- Calculating optimal tolerance for .mzML data ---")
report_mzml = analyze_mass_accuracy(msi_data_mzml, get_common_calibration_standards(:maldi_pos))
mz_tolerance_mzml = 0.002 # Default
if haskey(report_mzml, :optimal_ppm) && !isnan(report_mzml.optimal_ppm) && !isempty(report_mzml.matched_peaks)
avg_mz = mean([p[1] for p in report_mzml.matched_peaks])
mz_tolerance_mzml = avg_mz * report_mzml.optimal_ppm / 1e6
println("Optimal PPM: $(round(report_mzml.optimal_ppm, digits=2)), Average m/z: $(round(avg_mz, digits=2))")
println("Calculated m/z tolerance: $(round(mz_tolerance_mzml, digits=5))")
else
println("Could not determine optimal tolerance, using default: $mz_tolerance_mzml")
end
# Create a dedicated subdirectory for the output plots
mzml_output_dir = joinpath(RESULTS_DIR, "mzml_pipeline_stages")
mkpath(mzml_output_dir)
test_full_pipeline(msi_data_mzml, MZML_SPECTRUM_ID, output_dir=mzml_output_dir, file_type_prefix="mzml", mz_tolerance=mz_tolerance_mzml)
test_full_pipeline_on_total_spectrum(msi_data_mzml, output_dir=mzml_output_dir, file_type_prefix="mzml", mz_tolerance=mz_tolerance_mzml)
catch e
println("ERROR in .mzML pipeline test: $e")
showerror(stdout, e, catch_backtrace())
end
else
println("SKIPPED: File not found: $TEST_MZML_FILE")
end
# --- Test Case 2: Run full pipeline on a single imzML spectrum ---
println("\n" * "="^20 * " Test Case 2: Full Pipeline on .imzML Spectrum " * "="^20)
println("FILE: ", TEST_IMZML_FILE)
if isfile(TEST_IMZML_FILE)
try
msi_data_imzml = OpenMSIData(TEST_IMZML_FILE)
# Dynamically determine tolerance
println("\n--- Calculating optimal tolerance for .imzML data ---")
report_imzml = analyze_mass_accuracy(msi_data_imzml, get_common_calibration_standards(:maldi_pos))
mz_tolerance_imzml = 0.002 # Default
if haskey(report_imzml, :optimal_ppm) && !isnan(report_imzml.optimal_ppm) && !isempty(report_imzml.matched_peaks)
avg_mz = mean([p[1] for p in report_imzml.matched_peaks])
mz_tolerance_imzml = avg_mz * report_imzml.optimal_ppm / 1e6
println("Optimal PPM: $(round(report_imzml.optimal_ppm, digits=2)), Average m/z: $(round(avg_mz, digits=2))")
println("Calculated m/z tolerance: $(round(mz_tolerance_imzml, digits=5))")
else
println("Could not determine optimal tolerance, using default: $mz_tolerance_imzml")
end
# Create a dedicated subdirectory for the output plots
imzml_output_dir = joinpath(RESULTS_DIR, "imzml_pipeline_stages")
mkpath(imzml_output_dir)
test_full_pipeline(msi_data_imzml, IMZML_COORDS, output_dir=imzml_output_dir, file_type_prefix="imzml", mz_tolerance=mz_tolerance_imzml)
test_full_pipeline_on_total_spectrum(msi_data_imzml, output_dir=imzml_output_dir, file_type_prefix="imzml", mz_tolerance=mz_tolerance_imzml)
# generate_qc_report(msi_data_imzml, TEST_IMZML_FILE, output_dir=imzml_output_dir)
custom_reference_peaks = Dict(
31.974 => "Red Phosphorus",
432.6584 => "P13",
464.6059 => "P15",
526.5534 => "P17",
650.4485 => "P21",
774.3435 => "P25",
898.2385 => "P29",
950.1861 => "P31",
1022.1336 => "P33",
1146.0286 => "P37",
1593.8187 => "P45",
772.433 => "Unknown 1",
772.5253 => "Unknown 2"
)
n_samples = length(msi_data_imzml.spectra_metadata)
generate_qc_report(msi_data_imzml, TEST_IMZML_FILE, reference_peaks=custom_reference_peaks, output_dir=imzml_output_dir, sample_spectra=n_samples)
catch e
println("ERROR in .imzML pipeline test: $e")
showerror(stdout, e, catch_backtrace())
end
else
println("SKIPPED: File not found: $TEST_IMZML_FILE")
end
# --- Test Case 3: Generate QC Report for .imzML data ---
println("\n" * "="^20 * " Test Case 3: QC Report Generation for .imzML " * "="^20)
println("FILE: ", TEST_IMZML_FILE)
if isfile(TEST_IMZML_FILE)
try
msi_data_imzml = OpenMSIData(TEST_IMZML_FILE)
# Create a dedicated subdirectory for the QC report
qc_output_dir = joinpath(RESULTS_DIR, "qc_report")
mkpath(qc_output_dir)
println("\n--- Generating comprehensive QC report ---")
custom_reference_peaks = Dict(
31.974 => "Red Phosphorus",
432.6584 => "P13",
464.6059 => "P15",
526.5534 => "P17",
650.4485 => "P21",
774.3435 => "P25",
898.2385 => "P29",
950.1861 => "P31",
1022.1336 => "P33",
1146.0286 => "P37",
1593.8187 => "P45",
772.433 => "Unknown 1",
772.5253 => "Unknown 2"
)
# You can control the number of spectra sampled for the QC report.
# For the most accurate results, you can sample all spectra, but it will take longer.
# To sample all, use: n_samples = length(msi_data_imzml.spectra_metadata)
n_samples = length(msi_data_imzml.spectra_metadata)
#generate_qc_report(msi_data_imzml, TEST_IMZML_FILE, output_dir=qc_output_dir)
generate_qc_report(msi_data_imzml, TEST_IMZML_FILE, reference_peaks=custom_reference_peaks, output_dir=qc_output_dir, sample_spectra=n_samples)
println("\n--- Analyzing specific reference peaks ---")
#=
reference_peaks = Dict(
104.10754 => "Imidazole",
175.11995 => "GPC fragment",
226.15687 => "Phosphocholine"
)
=#
report = analyze_mass_accuracy(msi_data_imzml, custom_reference_peaks)
if haskey(report, :optimal_ppm)
println("Optimal PPM tolerance with specific peaks: $(round(report.optimal_ppm, digits=2)) ppm")
else
println("Could not determine optimal PPM with specific peaks.")
end
catch e
println("ERROR in QC report generation test: $e")
showerror(stdout, e, catch_backtrace())
end
else
println("SKIPPED: File not found: $TEST_IMZML_FILE")
end
println("\nPreprocessing tests finished.")
end
# --- Execute ---
# Ensure the results directory exists
mkpath(RESULTS_DIR)
@time run_preprocessing_tests()

View File

@ -10,7 +10,7 @@
# Instructions:
# 1. Fill in the placeholder paths in the "CONFIG" section below.
# 2. Run the script from the project's root directory:
# julia test/run_tests.jl
# julia --project=. test/run_tests.jl
# 3. Check the `test/results/` folder for the output images.
# ===================================================================