From bd6b0d73948e8052e3589717b9093c0655541350 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Wed, 9 Aug 2023 20:42:43 +0000 Subject: [PATCH 1/4] initial 3d-2d transformations, docs missings --- Project.toml | 10 ++++++- ext/UnfoldMakiePyMNEExt.jl | 19 ++++++++++++++ src/UnfoldMakie.jl | 2 ++ src/eeg_positions.jl | 53 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 ext/UnfoldMakiePyMNEExt.jl diff --git a/Project.toml b/Project.toml index b6b26bdd0..2ca236b90 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "UnfoldMakie" uuid = "69a5ce3b-64fb-4f22-ae69-36dd4416af2a" -authors = ["Benedikt Ehinger","Vladimir Mikheev", "Daniel Baumgartner", "Sören Döring", "Niklas Gärtner"] +authors = ["Benedikt Ehinger", "Vladimir Mikheev", "Daniel Baumgartner", "Sören Döring", "Niklas Gärtner"] version = "0.3.3" [deps] @@ -9,6 +9,7 @@ CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" @@ -21,6 +22,12 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TopoPlots = "2bdbdf9c-dbd8-403f-947b-1a4e0dd41a7a" Unfold = "181c99d8-e21b-4ff3-b70b-c233eddec679" +[weakdeps] +PyMNE = "6c5003b2-cbe8-491c-a0d1-70088e6a0fd6" + +[extensions] +UnfoldMakiePyMNEExt = "PyMNE" + [compat] AlgebraOfGraphics = "0.6" CategoricalArrays = "0.10" @@ -33,6 +40,7 @@ GeometryBasics = "0.4" GridLayoutBase = "0.9" ImageFiltering = "0.7" Makie = "0.17,0.18,0.19" +PyMNE = "0.2" TopoPlots = "0.1" Unfold = "0.3, 0.4, 0.5, 0.6" julia = "1" diff --git a/ext/UnfoldMakiePyMNEExt.jl b/ext/UnfoldMakiePyMNEExt.jl new file mode 100644 index 000000000..9126bdb93 --- /dev/null +++ b/ext/UnfoldMakiePyMNEExt.jl @@ -0,0 +1,19 @@ +module UnfoldMakiePyMNEExt + +using GeometryBasics +using PyMNE + +""" +toPositions(raw::PyMNE.Py;kwargs...) + +calls MNE-pythons make_eeg_layout (with optional kwargs) +Returns an array of Points + """ +function toPositions(raw::PyMNE.Py;kwargs...) +layout_from_raw = PyMNE.channels.make_eeg_layout(raw.info;kwargs...).pos +positions = pyconvert(Array,layout_from_raw)[:,1:2] + +points = map(GeometryBasics.Point{2,Float64},eachrow(positions)) +return points +end +end \ No newline at end of file diff --git a/src/UnfoldMakie.jl b/src/UnfoldMakie.jl index c933390c2..5e0579fe2 100644 --- a/src/UnfoldMakie.jl +++ b/src/UnfoldMakie.jl @@ -20,6 +20,8 @@ using DataFrames using SparseArrays using CategoricalArrays # for cut for TopoPlotSeries +using CoordinateTransformations # for 3D positions to 2D + import Makie.hidedecorations! import Makie.hidespines! import AlgebraOfGraphics.hidedecorations! diff --git a/src/eeg_positions.jl b/src/eeg_positions.jl index e81421eb8..56848c370 100644 --- a/src/eeg_positions.jl +++ b/src/eeg_positions.jl @@ -363,4 +363,57 @@ label_in_channel_order = ["FP1", "F3", "F7", "FC3", "C3", "C5", "P3", "P7", "P9" function channelToLabel(channel) return label_in_channel_order[channel] +end + + + + +""" + convert x/y/z electrode montage positions to spherical coordinate representation. output is a matrix +""" +function cart3d_to_spherical(x,y,z) + sph = SphericalFromCartesian().(SVector.(x,y,z)) + sph = [vcat(s.r,s.θ,π/2 - s.ϕ) for s in sph] + sph = hcat(sph...)' + return sph +end + + +""" +toPositions(x,y,z;sphere=[0,0,0.]) +toPositions(pos::AbstractMatrix;sphere=[0,0,0.]) +Projects 3D electrode positions to a 2D layout. + +The matrix case, assumes `size(pos) = (3,nChannels)` +Re-implementation of the MNE algorithm. +""" +toPositions(pos::AbstractMatrix;kwargs...) = toPositions(pos[1,:],pos[2,:],pos[3,:];kwargs) +function toPositions(x,y,z;sphere=[0,0,0.]) + #cart3d_to_spherical(x,y,z) + +# translate to sphere origin + x .-= sphere[1] + y .-= sphere[2] + z .-= sphere[3] + + # convert to spherical coordinates + sph = cart3d_to_spherical(x,y,z) + + # get rid of of the radius for now + pol_a = sph[:,3] + pol_b = sph[:,2] + + # use only theta & phi, convert back to cartesian coordinates + p_x = pol_a .* cos.(pol_b) + p_y = pol_a .* sin.(pol_b) + + # scale by the radius + p_x .*= sph[:,1] ./(π/2) + p_y .*= sph[:,1] ./(π/2) + + # move back by the sphere coordinates + p_x .+= sphere[1] + p_y .+= sphere[2] + + return Point2f.(p_x,p_y) end \ No newline at end of file From 3e2427a0a4c63fb5a7cdf6a0daa275c85f84f7c3 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 11 Sep 2023 17:18:31 +0000 Subject: [PATCH 2/4] added docs, fix bugs --- Project.toml | 1 + docs/Project.toml | 1 + docs/src/literate/reference/positions.jl | 23 +++++++++++++++++++++++ ext/UnfoldMakiePyMNEExt.jl | 6 +++--- src/UnfoldMakie.jl | 4 +++- src/eeg_positions.jl | 10 ++++++---- 6 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 docs/src/literate/reference/positions.jl diff --git a/Project.toml b/Project.toml index 2ca236b90..e5a52f03f 100644 --- a/Project.toml +++ b/Project.toml @@ -18,6 +18,7 @@ ImageFiltering = "6a3955dd-da59-5b1f-98d4-e7296123deb5" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TopoPlots = "2bdbdf9c-dbd8-403f-947b-1a4e0dd41a7a" Unfold = "181c99d8-e21b-4ff3-b70b-c233eddec679" diff --git a/docs/Project.toml b/docs/Project.toml index b8d8e1cfc..eb3932853 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -11,6 +11,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" MakieThemes = "e296ed71-da82-5faf-88ab-0034a9761098" +PyMNE = "6c5003b2-cbe8-491c-a0d1-70088e6a0fd6" StatsModels = "3eaba693-59b7-5ba5-a881-562e759f1c8d" TopoPlots = "2bdbdf9c-dbd8-403f-947b-1a4e0dd41a7a" Unfold = "181c99d8-e21b-4ff3-b70b-c233eddec679" diff --git a/docs/src/literate/reference/positions.jl b/docs/src/literate/reference/positions.jl new file mode 100644 index 000000000..bc3c9a9fa --- /dev/null +++ b/docs/src/literate/reference/positions.jl @@ -0,0 +1,23 @@ +using UnfoldMakie +using TopoPlots +using PyMNE + + +# # get MNE-positions +# Generate a fake MNE structure +# [taken from mne documentation](https://mne.tools/0.24/auto_examples/visualization/eeglab_head_sphere.html) + +biosemi_montage = PyMNE.channels.make_standard_montage("biosemi64") +n_channels = length(biosemi_montage.ch_names) +fake_info = PyMNE.create_info(ch_names=biosemi_montage.ch_names, sfreq=250., + ch_types="eeg") +data = rand(n_channels,1) * 1e-6 +fake_evoked = PyMNE.EvokedArray(data, fake_info) +fake_evoked.set_montage(biosemi_montage) + +pos = UnfoldMakie.toPositions(fake_evoked) + +# # project from 3D electrode locations to 2D +pos3d = hcat(values(pyconvert(Dict,biosemi_montage.get_positions()["ch_pos"]))...) + +pos2 = to_positions(pos3d) \ No newline at end of file diff --git a/ext/UnfoldMakiePyMNEExt.jl b/ext/UnfoldMakiePyMNEExt.jl index 9126bdb93..7f4f4d40a 100644 --- a/ext/UnfoldMakiePyMNEExt.jl +++ b/ext/UnfoldMakiePyMNEExt.jl @@ -2,14 +2,14 @@ module UnfoldMakiePyMNEExt using GeometryBasics using PyMNE - +using UnfoldMakie """ -toPositions(raw::PyMNE.Py;kwargs...) +to_positions(raw::PyMNE.Py;kwargs...) calls MNE-pythons make_eeg_layout (with optional kwargs) Returns an array of Points """ -function toPositions(raw::PyMNE.Py;kwargs...) +function UnfoldMakie.to_positions(raw::PyMNE.Py;kwargs...) layout_from_raw = PyMNE.channels.make_eeg_layout(raw.info;kwargs...).pos positions = pyconvert(Array,layout_from_raw)[:,1:2] diff --git a/src/UnfoldMakie.jl b/src/UnfoldMakie.jl index 5e0579fe2..2f3bd8acf 100644 --- a/src/UnfoldMakie.jl +++ b/src/UnfoldMakie.jl @@ -19,6 +19,7 @@ using DataStructures using DataFrames using SparseArrays using CategoricalArrays # for cut for TopoPlotSeries +using StaticArrays using CoordinateTransformations # for 3D positions to 2D @@ -65,5 +66,6 @@ export plot_circulareegtopoplot! export plot_topoplotseries export plot_topoplotseries! -export nonnumeric +export to_positions +export nonnumeric # reexport from AoG end diff --git a/src/eeg_positions.jl b/src/eeg_positions.jl index 56848c370..811b2ae3b 100644 --- a/src/eeg_positions.jl +++ b/src/eeg_positions.jl @@ -380,15 +380,17 @@ end """ -toPositions(x,y,z;sphere=[0,0,0.]) -toPositions(pos::AbstractMatrix;sphere=[0,0,0.]) +to_positions(x,y,z;sphere=[0,0,0.]) +to_positions(pos::AbstractMatrix;sphere=[0,0,0.]) Projects 3D electrode positions to a 2D layout. The matrix case, assumes `size(pos) = (3,nChannels)` Re-implementation of the MNE algorithm. + +Tipp: You can directly get positions from an MNE object after loading PyMNE and thus activating the UnfoldMakie PyMNE extension """ -toPositions(pos::AbstractMatrix;kwargs...) = toPositions(pos[1,:],pos[2,:],pos[3,:];kwargs) -function toPositions(x,y,z;sphere=[0,0,0.]) +to_positions(pos::AbstractMatrix;kwargs...) = to_positions(pos[1,:],pos[2,:],pos[3,:];kwargs...) +function to_positions(x,y,z;sphere=[0,0,0.]) #cart3d_to_spherical(x,y,z) # translate to sphere origin From be89609ce9b39f915f32c88d11dec28a5c2b4a20 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 11 Sep 2023 17:32:28 +0000 Subject: [PATCH 3/4] added reference tutorial --- docs/make.jl | 5 ++++- docs/src/literate/reference/positions.jl | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index ae105a963..fc4912fb5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -12,7 +12,7 @@ using Literate using Glob GENERATED = joinpath(@__DIR__, "src", "literate") -for subfolder ∈ ["explanations","HowTo","tutorials"] +for subfolder ∈ ["explanations","HowTo","tutorials","reference"] local SOURCE_FILES = Glob.glob(subfolder*"/*.jl", GENERATED) foreach(fn -> Literate.markdown(fn, GENERATED*"/"*subfolder), SOURCE_FILES) @@ -61,6 +61,9 @@ makedocs(; "Include multiple Visualizations in one Figure" => "how_to/mult_vis_in_fig.md", "Show out of Bounds Label" => "how_to/show_oob_labels.md", ], + "Reference" => [ + "Convert 3D positions / montages to 2D layouts" => "literate/reference/positions.jl" + ] ], ) diff --git a/docs/src/literate/reference/positions.jl b/docs/src/literate/reference/positions.jl index bc3c9a9fa..78ccf2d79 100644 --- a/docs/src/literate/reference/positions.jl +++ b/docs/src/literate/reference/positions.jl @@ -1,4 +1,5 @@ using UnfoldMakie +using CairoMakie using TopoPlots using PyMNE @@ -20,4 +21,10 @@ pos = UnfoldMakie.toPositions(fake_evoked) # # project from 3D electrode locations to 2D pos3d = hcat(values(pyconvert(Dict,biosemi_montage.get_positions()["ch_pos"]))...) -pos2 = to_positions(pos3d) \ No newline at end of file +pos2 = to_positions(pos3d) + +f = Figure(resolution=(600,300)) +scatter(f[1,1],pos3d[1:2,:]) +scatter(f[1,2],pos2) +f +# as one can see, the "naive" transform of just dropping the third dimension doesnt really work (left). We rather have to project the chanels to a sphere and unfold it (right) \ No newline at end of file From 437be60daf4d2257899953c485189f3224bd76f7 Mon Sep 17 00:00:00 2001 From: "behinger (s-ccs 001)" Date: Mon, 11 Sep 2023 17:52:02 +0000 Subject: [PATCH 4/4] typo --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index fc4912fb5..21222bbc0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -62,7 +62,7 @@ makedocs(; "Show out of Bounds Label" => "how_to/show_oob_labels.md", ], "Reference" => [ - "Convert 3D positions / montages to 2D layouts" => "literate/reference/positions.jl" + "Convert 3D positions / montages to 2D layouts" => "literate/reference/positions.md" ] ], )