Skip to content

Commit

Permalink
Merge pull request #60 from unfoldtoolbox/positions
Browse files Browse the repository at this point in the history
3D to 2D locations + pymne extension
  • Loading branch information
behinger authored Sep 11, 2023
2 parents 22f784e + 959de26 commit 72bdfee
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name = "UnfoldMakie"
uuid = "69a5ce3b-64fb-4f22-ae69-36dd4416af2a"

authors = ["Benedikt Ehinger","Vladimir Mikheev", "Daniel Baumgartner", "Sören Döring", "Niklas Gärtner"]
version = "0.3.4"

Expand All @@ -9,6 +10,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"
Expand All @@ -17,10 +19,17 @@ 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"

[weakdeps]
PyMNE = "6c5003b2-cbe8-491c-a0d1-70088e6a0fd6"

[extensions]
UnfoldMakiePyMNEExt = "PyMNE"

[compat]
AlgebraOfGraphics = "0.6"
CategoricalArrays = "0.10"
Expand All @@ -33,6 +42,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"
Expand Down
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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.md"
]
],
)

Expand Down
30 changes: 30 additions & 0 deletions docs/src/literate/reference/positions.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using UnfoldMakie
using CairoMakie
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)

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)
19 changes: 19 additions & 0 deletions ext/UnfoldMakiePyMNEExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module UnfoldMakiePyMNEExt

using GeometryBasics
using PyMNE
using UnfoldMakie
"""
to_positions(raw::PyMNE.Py;kwargs...)
calls MNE-pythons make_eeg_layout (with optional kwargs)
Returns an array of Points
"""
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]

points = map(GeometryBasics.Point{2,Float64},eachrow(positions))
return points
end
end
6 changes: 5 additions & 1 deletion src/UnfoldMakie.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ using DataStructures
using DataFrames
using SparseArrays
using CategoricalArrays # for cut for TopoPlotSeries
using StaticArrays

using CoordinateTransformations # for 3D positions to 2D

import Makie.hidedecorations!
import Makie.hidespines!
Expand Down Expand Up @@ -63,5 +66,6 @@ export plot_circulareegtopoplot!
export plot_topoplotseries
export plot_topoplotseries!

export nonnumeric
export to_positions
export nonnumeric # reexport from AoG
end
55 changes: 55 additions & 0 deletions src/eeg_positions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -363,4 +363,59 @@ 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


"""
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
"""
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
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

0 comments on commit 72bdfee

Please sign in to comment.