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:
parent
39c92ba850
commit
e13b5ef0cc
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
||||
391
Manifest.toml
391
Manifest.toml
@ -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"
|
||||
|
||||
13
Project.toml
13
Project.toml
@ -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"
|
||||
|
||||
155
app.jl
155
app.jl
@ -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") : []
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -627,3 +627,150 @@ 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
897
mask.jl
Normal 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
145
mask.jl.html
Normal 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 </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>
|
||||
@ -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
3
routes.jl
Normal file
@ -0,0 +1,3 @@
|
||||
# Include all your app modules
|
||||
include("app.jl")
|
||||
include("mask.jl")
|
||||
47
src/ImageProcessing.jl
Normal file
47
src/ImageProcessing.jl
Normal 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
|
||||
@ -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 --- #
|
||||
|
||||
@ -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
979
src/Preprocessing.jl
Normal 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 Savitzky–Golay 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
|
||||
|
||||
@ -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
468
test/run_preprocessing.jl
Normal 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()
|
||||
@ -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.
|
||||
# ===================================================================
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user