.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/plot_on_surface.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_plot_on_surface.py: ================================== Plot sensors on realistic surfaces ================================== Sometimes you may want to plot sensor positions on a realistic surface, like an actual head. The ``fsaverage`` standard brain template shipped with the Freesurfer package provides surfaces of the head, so we are going to use them below. For more information, check out these MNE resources: - https://mne.tools/dev/auto_examples/visualization/eeg_on_scalp.html - https://mne.tools/dev/auto_examples/visualization/montage_sgskip.html .. currentmodule:: eeg_positions .. GENERATED FROM PYTHON SOURCE LINES 20-24 We start by importing what we need. This example furthermore assumes that you have installed eeg_positions via ``pip install eeg_positions[docs]``. .. GENERATED FROM PYTHON SOURCE LINES 24-30 .. code-block:: Python import matplotlib.pyplot as plt import mne import numpy as np from eeg_positions import get_elec_coords .. GENERATED FROM PYTHON SOURCE LINES 31-32 Get fsaverage data via MNE-Python, this will download some data. .. GENERATED FROM PYTHON SOURCE LINES 32-34 .. code-block:: Python subjects_dir = mne.datasets.fetch_fsaverage().parent .. rst-class:: sphx-glr-script-out .. code-block:: none Using default location ~/mne_data for montage coregistration... Attempting to create new mne-python configuration file: /home/docs/.mne/mne-python.json 0 files missing from root.txt in /home/docs/mne_data/MNE-fsaverage-data 0 files missing from bem.txt in /home/docs/mne_data/MNE-fsaverage-data/fsaverage .. GENERATED FROM PYTHON SOURCE LINES 35-37 Get idealized sensor positions that were computed on a sphere and export as :class:`mne.channels.DigMontage`. .. GENERATED FROM PYTHON SOURCE LINES 37-39 .. code-block:: Python montage = get_elec_coords(as_mne_montage=True) .. GENERATED FROM PYTHON SOURCE LINES 40-42 For the code below we need to provide an :class:`mne.Info` object. The sampling frequency does not matter so we are setting it to 1. .. GENERATED FROM PYTHON SOURCE LINES 42-45 .. code-block:: Python info = mne.create_info(ch_names=montage.ch_names, sfreq=1, ch_types="eeg") info.set_montage(montage) .. raw:: html
General
MNE object type Info
Measurement date Unknown
Participant Unknown
Experimenter Unknown
Acquisition
Sampling frequency 1.00 Hz
Channels
EEG
Head & sensor digitization 348 points
Filters
Highpass 0.00 Hz
Lowpass 0.50 Hz


.. GENERATED FROM PYTHON SOURCE LINES 46-49 As a sanity check, let's create a spherical head model first. We expect a perfect fit with our idealized electrode positions, because they were also computed on a sphere. .. GENERATED FROM PYTHON SOURCE LINES 49-70 .. code-block:: Python sphere = mne.make_sphere_model(r0="auto", head_radius="auto", info=info) # We need a transform from the coordinates of our montage to the surface. # Given that both are spheres, we do not need to actually transform, and # can just use the identity transform. trans = mne.Transform("head", "mri", trans=np.eye(4)) fig = mne.viz.plot_alignment( info=info, trans=trans, bem=sphere, # our spherical head model surfaces={"head": 0.8}, # alpha value coord_frame="head", eeg=["original", "projected"], dig="fiducials", show_axes=True, mri_fiducials=False, ) mne.viz.set_3d_view(figure=fig, azimuth=65, elevation=75) .. image-sg:: /auto_examples/images/sphx_glr_plot_on_surface_001.png :alt: plot on surface :srcset: /auto_examples/images/sphx_glr_plot_on_surface_001.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Fitted sphere radius: 95.0 mm Origin head coordinates: 0.0 0.0 0.0 mm Origin device coordinates: 0.0 0.0 0.0 mm Equiv. model fitting -> RV = 0.00349096 %% mu1 = 0.944696 lambda1 = 0.1372 mu2 = 0.667439 lambda2 = 0.683766 mu3 = -0.265718 lambda3 = -0.0105969 Set up EEG sphere model with scalp radius 95.0 mm Using pyvistaqt 3d backend. Channel types:: eeg: 345 Projecting sensors to the head surface .. GENERATED FROM PYTHON SOURCE LINES 71-77 As can be seen above, the fit of our idealized sensor positions with a spherical head model is perfect. Now let's proceed to plot our positions on the fsaverage head. We expect this to be potentially problematic, because we computed our positions on a sphere ... and a sphere is usually a poor approximation of a human head. .. GENERATED FROM PYTHON SOURCE LINES 77-98 .. code-block:: Python # instead of the identity transform as above, we are using the inbuilt transformation # from MNE-Python that can transform a Montage to fit fsaverage. # Note that this only works for "fsaverage" and not other surfaces. trans = "fsaverage" fig = mne.viz.plot_alignment( info=info, trans=trans, subject="fsaverage", # this is fsaverage subjects_dir=subjects_dir, # directory of fsaverage surfaces={"head": 0.8}, # alpha value coord_frame="head", eeg=["original", "projected"], dig="fiducials", show_axes=True, mri_fiducials=True, ) mne.viz.set_3d_view(figure=fig, azimuth=135, elevation=80) .. image-sg:: /auto_examples/images/sphx_glr_plot_on_surface_002.png :alt: plot on surface :srcset: /auto_examples/images/sphx_glr_plot_on_surface_002.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Using outer_skin.surf for head surface. Channel types:: eeg: 345 Projecting sensors to the head surface .. GENERATED FROM PYTHON SOURCE LINES 99-110 As expected, the fit above is very poor. The electrode positions that were computed on a sphere are not easily projected to the fsaverage head. However, we can try to "scale" fsaverage in three dimensions to make the fit slightly better. Below we will try to scale fsaverage to minimize the distance of sensors to the scalp surface, while still trying to have the landmarks (LPA, RPA, NAS) aligned between MRI and montage. .. GENERATED FROM PYTHON SOURCE LINES 110-147 .. code-block:: Python fiducials = "estimated" # taken from fsaverage subject = "fsaverage" coreg = mne.coreg.Coregistration(info, subject, subjects_dir, fiducials=fiducials) coreg.set_scale_mode("3-axis") # can also be "uniform", but the fit would be worse coreg.fit_fiducials(verbose=True) coreg.fit_icp( n_iterations=40, lpa_weight=1.0, nasion_weight=1.0, rpa_weight=1.0, hsp_weight=0, eeg_weight=1.0, hpi_weight=0, verbose=True, ) trans = coreg.trans fig = mne.viz.plot_alignment( info=info, trans=trans, # this is the optimized transform based on scaling fsaverage subject="fsaverage", subjects_dir=subjects_dir, surfaces={"head": 0.8}, # alpha value coord_frame="head", eeg=["original", "projected"], dig="fiducials", show_axes=True, mri_fiducials=True, ) mne.viz.set_3d_view(figure=fig, azimuth=135, elevation=80) .. image-sg:: /auto_examples/images/sphx_glr_plot_on_surface_003.png :alt: plot on surface :srcset: /auto_examples/images/sphx_glr_plot_on_surface_003.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Using high resolution head model in /home/docs/mne_data/MNE-fsaverage-data/fsaverage/bem/fsaverage-head-dense.fif Triangle neighbors and vertex normals... Estimating fiducials from fsaverage. Aligning using fiducials Start median distance: 8.99 mm Enforcing 1 scaling parameter for fit with fiducials. End median distance: 12.84 mm Aligning using ICP Start median distance: 12.84 mm ICP 1 median distance: 9.54 mm ICP 2 median distance: 6.57 mm ICP 3 median distance: 4.17 mm ICP 4 median distance: 3.34 mm ICP 5 median distance: 2.87 mm ICP 6 median distance: 2.69 mm ICP 7 median distance: 2.54 mm ICP 8 median distance: 2.54 mm ICP 9 median distance: 2.47 mm ICP 10 median distance: 2.36 mm ICP 11 median distance: 2.20 mm ICP 12 median distance: 2.15 mm ICP 13 median distance: 2.09 mm ICP 14 median distance: 2.05 mm ICP 15 median distance: 1.98 mm ICP 16 median distance: 1.93 mm ICP 17 median distance: 1.89 mm ICP 18 median distance: 1.80 mm ICP 19 median distance: 1.78 mm ICP 20 median distance: 1.79 mm ICP 21 median distance: 1.75 mm ICP 22 median distance: 1.75 mm ICP 23 median distance: 1.78 mm ICP 24 median distance: 1.79 mm ICP 25 median distance: 1.79 mm ICP 26 median distance: 1.80 mm ICP 27 median distance: 1.78 mm ICP 28 median distance: 1.80 mm ICP 29 median distance: 1.79 mm ICP 30 median distance: 1.75 mm ICP 31 median distance: 1.73 mm ICP 32 median distance: 1.71 mm ICP 33 median distance: 1.73 mm ICP 34 median distance: 1.74 mm ICP 35 median distance: 1.75 mm ICP 36 median distance: 1.75 mm ICP 37 median distance: 1.76 mm ICP 38 median distance: 1.77 mm ICP 39 median distance: 1.73 mm ICP 40 median distance: 1.71 mm End median distance: 1.71 mm Using outer_skin.surf for head surface. Channel types:: eeg: 345 Projecting sensors to the head surface .. GENERATED FROM PYTHON SOURCE LINES 149-160 Luckily, the fit is now slightly better. It is still not perfect, so for actual source reconstruction, you should use sensor positions that are measured (digitized), rather than computed on a sphere (idealized). The 1005 system eeg electrode positions shipped with MNE-Python happen to not be calculated on a sphrere (idealized), so they should be a better fit on a realistic surface. Let's have a look, first in 2D, then in 3D .. GENERATED FROM PYTHON SOURCE LINES 160-176 .. code-block:: Python # Getting the MNE-Python inbuilt 1005 system positions montage_mne = mne.channels.make_standard_montage(kind="standard_1005") info_mne = mne.create_info(ch_names=montage_mne.ch_names, sfreq=1, ch_types="eeg") info_mne.set_montage(montage_mne) # Plot in 2D versus the eeg_positions montage fig, axs = plt.subplots(1, 2) fig.set_layout_engine("constrained") for i, this_info in enumerate([info, info_mne]): ax = axs.flat[i] this_info.plot_sensors(axes=ax, show=False) ax.set_title(["eeg_positions", "mne inbuilt"][i]) fig .. image-sg:: /auto_examples/images/sphx_glr_plot_on_surface_004.png :alt: eeg_positions, mne inbuilt :srcset: /auto_examples/images/sphx_glr_plot_on_surface_004.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none
.. GENERATED FROM PYTHON SOURCE LINES 177-184 As is maybe apparent from the plot above, the inbuilt mne 1005 montage does not look as clear when projected to a spherical 2D head model. The inbuilt ``eeg_positions`` montage, on the other hand, can really shine in this situation (because that is what it was designed for). Fortuntely, the inbuilt mne montage will give us a much better fit on a realistic surface, see below, and compare to the two plots above. .. GENERATED FROM PYTHON SOURCE LINES 184-202 .. code-block:: Python trans = "fsaverage" fig = mne.viz.plot_alignment( info=info_mne, trans=trans, subject="fsaverage", # this is fsaverage subjects_dir=subjects_dir, # directory of fsaverage surfaces={"head": 0.8}, # alpha value coord_frame="head", eeg=["original", "projected"], dig="fiducials", show_axes=True, mri_fiducials=True, ) mne.viz.set_3d_view(figure=fig, azimuth=135, elevation=80) .. image-sg:: /auto_examples/images/sphx_glr_plot_on_surface_005.png :alt: plot on surface :srcset: /auto_examples/images/sphx_glr_plot_on_surface_005.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Using outer_skin.surf for head surface. Channel types:: eeg: 343 Projecting sensors to the head surface .. rst-class:: sphx-glr-timing **Total running time of the script:** (2 minutes 23.987 seconds) .. _sphx_glr_download_auto_examples_plot_on_surface.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_on_surface.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_on_surface.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_on_surface.zip `