{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n\n# Fixing BEM and head surfaces\n\nSometimes when creating a BEM model the surfaces need manual correction because\nof a series of problems that can arise (e.g. intersection between surfaces).\nHere, we will see how this can be achieved by exporting the surfaces to the 3D\nmodeling program [Blender](https://blender.org), editing them, and\nre-importing them. We will also give a simple example of how to use\n`pymeshfix ` to fix topological problems.\n\nMuch of this tutorial is based on\nhttps://github.com/ezemikulan/blender_freesurfer by Ezequiel Mikulan.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Authors: Marijn van Vliet \n# Ezequiel Mikulan \n# Manorama Kadwani \n#\n# License: BSD-3-Clause\n# Copyright the MNE-Python contributors." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import os\nimport shutil\n\nimport mne\n\ndata_path = mne.datasets.sample.data_path()\nsubjects_dir = data_path / \"subjects\"\nbem_dir = subjects_dir / \"sample\" / \"bem\" / \"flash\"\nsurf_dir = subjects_dir / \"sample\" / \"surf\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exporting surfaces to Blender\n\nIn this tutorial, we are working with the MNE-Sample set, for which the\nsurfaces have no issues. To demonstrate how to fix problematic surfaces, we\nare going to manually place one of the inner-skull vertices outside the\nouter-skill mesh.\n\nWe then convert the surfaces to [.obj](https://en.wikipedia.org/wiki/Wavefront_.obj_file) files and create a new\nfolder called ``conv`` inside the FreeSurfer subject folder to keep them in.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Put the converted surfaces in a separate 'conv' folder\nconv_dir = subjects_dir / \"sample\" / \"conv\"\nos.makedirs(conv_dir, exist_ok=True)\n\n# Load the inner skull surface and create a problem\n# The metadata is empty in this example. In real study, we want to write the\n# original metadata to the fixed surface file. Set read_metadata=True to do so.\ncoords, faces = mne.read_surface(bem_dir / \"inner_skull.surf\")\ncoords[0] *= 1.1 # Move the first vertex outside the skull\n\n# Write the inner skull surface as an .obj file that can be imported by\n# Blender.\nmne.write_surface(conv_dir / \"inner_skull.obj\", coords, faces, overwrite=True)\n\n# Also convert the outer skull surface.\ncoords, faces = mne.read_surface(bem_dir / \"outer_skull.surf\")\nmne.write_surface(conv_dir / \"outer_skull.obj\", coords, faces, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Editing in Blender\n\nSee the following video tutorial for how to import, edit and export\nsurfaces in Blender (step-by-step instructions are also below):\n\n.. youtube:: JBIaX7VaTZk\n\nWe can now open Blender and import the surfaces. Go to *File > Import >\nWavefront (.obj)*. Navigate to the ``conv`` folder and select the file you\nwant to import. Make sure to select the *Keep Vert Order* option. You can\nalso select the *Y Forward* option to load the axes in the correct direction\n(RAS):\n\n\"Importing\n\nFor convenience, you can save these settings by pressing the ``+`` button\nnext to *Operator Presets*.\n\nRepeat the procedure for all surfaces you want to import (e.g. inner_skull\nand outer_skull).\n\nYou can now edit the surfaces any way you like. See the\n[Beginner Blender Tutorial Series](https://www.youtube.com/watch?v=nIoXOplUvAw&list=PLjEaoINr3zgFX8ZsChQVQsuDSjEqdWMAD&index=1&t=0s)\nto learn how to use Blender. Specifically, [part 2](https://youtu.be/imdYIdv8F4w?t=712) will teach you how to\nuse the basic editing tools you need to fix the surface.\n\n\"Editing\n\n## Using the fixed surfaces in MNE-Python\n\nIn Blender, you can export a surface as an .obj file by selecting it and go\nto *File > Export > Wavefront (.obj)*. You need to again select the *Y\nForward* option and check the *Keep Vertex Order* box.\n\n\"Exporting\n\n\nEach surface needs to be exported as a separate file. We recommend saving\nthem in the ``conv`` folder and ending the file name with ``_fixed.obj``,\nalthough this is not strictly necessary.\n\nIn order to be able to run this tutorial script top to bottom, we here\nsimulate the edits you did manually in Blender using Python code:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "coords, faces = mne.read_surface(conv_dir / \"inner_skull.obj\")\ncoords[0] /= 1.1 # Move the first vertex back inside the skull\nmne.write_surface(conv_dir / \"inner_skull_fixed.obj\", coords, faces, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Back in Python, you can read the fixed .obj files and save them as\nFreeSurfer .surf files. For the :func:`mne.make_bem_model` function to find\nthem, they need to be saved using their original names in the ``surf``\nfolder, e.g. ``bem/inner_skull.surf``. Be sure to first backup the original\nsurfaces in case you make a mistake!\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Read the fixed surface\ncoords, faces = mne.read_surface(conv_dir / \"inner_skull_fixed.obj\")\n\n# Backup the original surface\nshutil.copy(bem_dir / \"inner_skull.surf\", bem_dir / \"inner_skull_orig.surf\")\n\n# Overwrite the original surface with the fixed version\n# In real study you should provide the correct metadata using ``volume_info=``\n# This could be accomplished for example with:\n#\n# _, _, vol_info = mne.read_surface(bem_dir / 'inner_skull.surf',\n# read_metadata=True)\n# mne.write_surface(bem_dir / 'inner_skull.surf', coords, faces,\n# volume_info=vol_info, overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Editing the head surfaces\n\nSometimes the head surfaces are faulty and require manual editing. We use\n:func:`mne.write_head_bem` to convert the fixed surfaces to ``.fif`` files.\n\n### Low-resolution head\n\nFor EEG forward modeling, it is possible that ``outer_skin.surf`` would be\nmanually edited. In that case, remember to save the fixed version of\n``-head.fif`` from the edited surface file for coregistration.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Load the fixed surface\ncoords, faces = mne.read_surface(bem_dir / \"outer_skin.surf\")\n\n# Make sure we are in the correct directory\nhead_dir = bem_dir.parent\n\n# Remember to backup the original head file in advance!\n# Overwrite the original head file\n#\n# mne.write_head_bem(head_dir / 'sample-head.fif', coords, faces,\n# overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### High-resolution head\n\nWe use :func:`mne.read_bem_surfaces` to read the head surface files. After\nediting, we again output the head file with :func:`mne.write_head_bem`.\nHere we use ``-head.fif`` for speed.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# If ``-head-dense.fif`` does not exist, you need to run\n# ``mne make_scalp_surfaces`` first.\n# [0] because a list of surfaces is returned\nsurf = mne.read_bem_surfaces(head_dir / \"sample-head.fif\")[0]\n\n# For consistency only\ncoords = surf[\"rr\"]\nfaces = surf[\"tris\"]\n\n# Write the head as an .obj file for editing\nmne.write_surface(conv_dir / \"sample-head.obj\", coords, faces, overwrite=True)\n\n# Usually here you would go and edit your meshes.\n#\n# Here we just use the same surface as if it were fixed\n# Read in the .obj file\ncoords, faces = mne.read_surface(conv_dir / \"sample-head.obj\")\n\n# Remember to backup the original head file in advance!\n# Overwrite the original head file\n#\n# mne.write_head_bem(head_dir / 'sample-head.fif', coords, faces,\n# overwrite=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Note

See also `tut-fix-meshes-smoothing` for a possible alternative\n high-resolution head fix using FreeSurfer smoothing instead of\n blender.

\n\n### Blender editing tips\n\nA particularly useful operation is the *Shrinkwrap* functionality, that will\nrestrict one surface inside another surface (for example the Skull inside the\nOuter Skin). Here is how to use it:\n\n\u2460 Select surface\n Select the surface that is creating the problem.\n\u2461 Select vertices\n In *Edit Mode*, press :kbd:`C` to use the circle selection tool to\n select the vertices that are outside.\n\u2462-\u2464 Group vertices\n In the *Object Data Properties* tab use the ``+`` button\n to add a *Vertex Group* and click *Assign* to assign the current\n selection to the group.\n\u2465-\u2467 Shrinkwrap\n In the *Modifiers* tab go to *Add Modifier* add a\n *Shrinkwrap* modifier and set it to snap *Inside* with the outer surface\n as the *Target* and the *Group* that you created before as the\n *Vertex Group*. You can then use the *Offset* parameter to adjust the\n distance.\n\n

Note

If nothing happens, the normals of the inner skull surface\n may be inverted. To fix this select all the vertices or faces\n of the inner skull and go to Mesh>Normals>Flip in the toolbar.

\n\n\u2468 Apply\n In *Object Mode* click on the down-pointing arrow of the *Shrinkwrap*\n modifier and click on *Apply*.\n\n\"Shrinkwrap\n\nThat's it! You are ready to continue with your analysis pipeline (e.g.\nrunning :func:`mne.make_bem_model`).\n\n## What if you still get an error?\n\n### Blender operations\nWhen editing BEM surfaces/meshes in Blender, make sure to use\ntools that do not change the number or order of vertices, or the geometry\nof triangular faces. For example, avoid the extrusion tool, because it\nduplicates the extruded vertices.\n\n\n### Cleaning up a bad dense head surface by smoothing\nIf you get a rough head surfaces when using `mne make_scalp_surfaces`,\nconsider smoothing your T1 ahead of time with a Gaussian kernel with\nFreeSurfer using something like the following within the subject's ``mri``\ndirectory:\n\n```console\n$ mri_convert --fwhm 3 T1.mgz T1_smoothed_3.mgz\n```\nHere the ``--fwhm`` argument determines how much smoothing (in mm) to apply.\nThen delete ``SUBJECTS_DIR/SUBJECT/surf/lh.seghead``, and re-run\n`mne make_scalp_surfaces` with the additional arguments\n``--mri=\"T1_smoothed_3.mgz\" --overwrite``, and you should get cleaner\nsurfaces.\n\n### Topological errors\nMNE-Python requires that meshes satisfy some topological checks to ensure\nthat subsequent processing like BEM solving and electrode projection can\nwork properly.\n\nBelow are some examples of errors you might encounter when working with\nmeshes in MNE-Python, and the likely causes of those errors.\n\n1. Cannot decimate to requested ico grade\n This error is caused by having too few or too many vertices. The full\n error is something like:\n\n```text\nRuntimeError: Cannot decimate to requested ico grade 4. The provided\nBEM surface has 20516 triangles, which cannot be isomorphic with a\nsubdivided icosahedron. Consider manually decimating the surface to a\nsuitable density and then use ico=None in make_bem_model.\n```\n2. Surface inner skull has topological defects\n This error can occur when trying to match the original number of\n triangles by removing vertices. The full error looks like:\n\n```text\nRuntimeError: Surface inner skull has topological defects: 12 / 20484\nvertices have fewer than three neighboring triangles [733, 1014,\n 2068, 7732, 8435, 8489, 10181, 11120, 11121, 11122, 11304, 11788]\n```\n3. Surface inner skull is not complete\n This error (like the previous error) reflects a problem with the surface\n topology (i.e., the expected pattern of vertices/edges/faces is\n disrupted).\n\n```text\nRuntimeError: Surface inner skull is not complete (sum of solid\nangles yielded 0.999668, should be 1.)\n```\n4. Triangle ordering is wrong\n This error reflects a mismatch between how the surface is represented in\n memory (the order of the vertex/face definitions) and what is expected\n by MNE-Python. The full error is:\n\n```text\nRuntimeError: The source surface has a matching number of triangles\nbut ordering is wrong\n```\n#### Dealing with topology in blender\nFor any of these errors, it is usually easiest to start over with the\nunedited BEM surface and try again, making sure to only *move* vertices and\nfaces without *adding* or *deleting* any. For example,\nselect a circle of vertices, then press :kbd:`G` to drag them to the desired\nlocation. Smoothing a group of selected vertices in Blender (by\nright-clicking and selecting \"Smooth Vertices\") can also be helpful.\n\n\n#### Dealing with topology using pymeshfix\n[pymeshfix](https://pymeshfix.pyvista.org/)_ is a GPL-licensed Python\nmodule designed to produce water-tight meshes that satisfy the topological\nchecks listed above. For example, if your\n``'SUBJECTS_DIR/SUBJECT/surf/lh.seghead'`` has topological problems and\n`tut-fix-meshes-smoothing` does not work, you can try fixing the mesh\ninstead. After installing ``pymeshfix`` using conda or pip, from within the\n``'SUBJECTS_DIR/SUBJECT/surf'`` directory, you could try::\n\n >>> import pymeshfix\n >>> rr, tris = mne.read_surface('lh.seghead')\n >>> rr, tris = pymeshfix.clean_from_arrays(rr, tris)\n >>> mne.write_surface('lh.seghead', rr, tris)\n\nIn some cases this could fix the topology such that a subsequent call to\n`mne make_scalp_surfaces` will succeed without needing to use the\n``--force`` parameter.\n\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.2" } }, "nbformat": 4, "nbformat_minor": 0 }