Convert electrode positions form 3D to 2D

Sometimes you have 3D montage but you need 2D montage. How to convert one to another? The function to_positions should help.

using UnfoldMakie
using CairoMakie
using TopoPlots
using PyMNE;
Precompiling PyMNE...
Info Given PyMNE was explicitly requested, output will be shown live 
    CondaPkg Found dependencies: /home/runner/.julia/packages/PyMNE/HGgbW/CondaPkg.toml
    CondaPkg Found dependencies: /home/runner/.julia/packages/PythonCall/WMWY0/CondaPkg.toml
    CondaPkg Found dependencies: /home/runner/.julia/packages/PythonPlot/oS8x4/CondaPkg.toml
    CondaPkg Initialising pixi
             │ /home/runner/.julia/artifacts/cefba4912c2b400756d043a2563ef77a0088866b/bin/pixi
             │ init
             │ --format pixi
             └ /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg
✔ Created /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg/pixi.toml
    CondaPkg Wrote /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg/pixi.toml
             │ [dependencies]
             │ openssl = ">=3, <3.1"
             │ uv = ">=0.4"
             │ libstdcxx-ng = ">=3.4,<13.0"
             │ matplotlib = ">=1"
             │
             │     [dependencies.python]
             │     channel = "anaconda"
             │     version = ">=3.8,<4, >=3.4,<4"
             │
             │ [project]
             │ name = ".CondaPkg"
             │ platforms = ["linux-64"]
             │ channels = ["anaconda", "conda-forge"]
             │ channel-priority = "disabled"
             │ description = "automatically generated by CondaPkg.jl"
             │
             │ [pypi-dependencies]
             └ mne = ">=1.4"
    CondaPkg Installing packages
             │ /home/runner/.julia/artifacts/cefba4912c2b400756d043a2563ef77a0088866b/bin/pixi
             │ install
             └ --manifest-path /home/runner/work/UnfoldMakie.jl/UnfoldMakie.jl/docs/.CondaPkg/pixi.toml
✔ The default environment has been installed.
   5763.0 ms  ✓ PyMNE
  1 dependency successfully precompiled in 6 seconds. 51 already precompiled.
  1 dependency had output during precompilation:
┌ PyMNE
│  [Output was shown above]
└
Precompiling UnfoldMakiePyMNEExt...
   1509.5 ms  ✓ PyMNE
Info Given UnfoldMakiePyMNEExt was explicitly requested, output will be shown live 
┌ Warning: Module PyMNE with build ID fafbfcfd-8fff-79e9-0000-00cf2c81edd6 is missing from the cache.
│ This may mean PyMNE [6c5003b2-cbe8-491c-a0d1-70088e6a0fd6] does not support precompilation but is imported by a module that does.
└ @ Base loading.jl:2541
   1189.9 ms  ? UnfoldMakie → UnfoldMakiePyMNEExt
  1 dependency successfully precompiled in 3 seconds. 391 already precompiled.
  1 dependency precompiled but a different version is currently loaded. Restart julia to access the new version. Otherwise, loading dependents of this package may trigger further precompilation to work with the unexpected version.
  1 dependencies failed but may be precompilable after restarting julia
  1 dependency had output during precompilation:
┌ UnfoldMakie → UnfoldMakiePyMNEExt
│  [Output was shown above]
└
┌ Warning: Module PyMNE with build ID fafbfcfd-8fff-79e9-0000-00cf2c81edd6 is missing from the cache.
│ This may mean PyMNE [6c5003b2-cbe8-491c-a0d1-70088e6a0fd6] does not support precompilation but is imported by a module that does.
└ @ Base loading.jl:2541

Get positions from MNE

Generate an MNE structure taken from mne documentation

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.0, ch_types = "eeg")
data = rand(n_channels, 1) * 1e-6
fake_evoked = PyMNE.EvokedArray(data, fake_info)
fake_evoked.set_montage(biosemi_montage)

pos = to_positions(fake_evoked)
64-element Vector{Point{2, Float64}}:
 [0.34935274876763384, 0.8914697618166454]
 [0.2285454515991697, 0.8299153694246765]
 [0.3239207434789678, 0.8210416675361911]
 [0.38009721906796695, 0.7346772379558053]
 [0.28116929732806945, 0.7288908776450373]
 [0.19598811803244962, 0.729048111114921]
 [0.13267223773863893, 0.7340421555641459]
 [0.07111784534666996, 0.6132348583956815]
 [0.1381378125628734, 0.6118022611559795]
 [0.2400936018380034, 0.6086174147856195]
 ⋮
 [0.612772564740614, 0.34981504242270145]
 [0.5864401194985318, 0.2239606378043266]
 [0.6853680412384293, 0.2297469981150946]
 [0.7705492205340492, 0.22958976464521094]
 [0.8338651008278599, 0.22459572019598625]
 [0.8710458357539724, 0.19758233504967118]
 [0.7379918869673291, 0.12872250633545543]
 [0.6426165950875308, 0.13759620822394072]
 [0.6171845897988648, 0.06716811394348651]

Projecting from 3D montage to 2D

pos3d = hcat(values(pyconvert(Dict, biosemi_montage.get_positions()["ch_pos"]))...)

pos2 = to_positions(pos3d)

f = Figure(size = (600, 300))
scatter(f[1, 1], pos3d[1:2, :], axis = (title = "Dropping third dimension",))
scatter(f[1, 2], pos2, axis = (title = "Projection form 3D to 2D",))
f
Example block output

As you can see, the "naive" transformation of simply dropping the third dimension does not really work (left). Instead, we have to project the channels onto a sphere and unfold it (right).


This page was generated using Literate.jl.