def mkoutersurf(image, radius, outfile): #radius information is currently ignored #it is a little tougher to deal with the morphology in python fill = nib.load( image ) filld = fill.get_data() filld[filld==1] = 255 gaussian = np.ones((2,2))*.25 image_f = np.zeros((256,256,256)) for slice in xrange(256): temp = filld[:,:,slice] image_f[:,:,slice] = convolve(temp, gaussian, 'same') image2 = np.zeros((256,256,256)) image2[np.where(image_f <= 25)] = 0 image2[np.where(image_f > 25)] = 255 strel15 = generate_binary_structure(3, 1) BW2 = grey_closing(image2, structure=strel15) thresh = np.max(BW2)/2 BW2[np.where(BW2 <= thresh)] = 0 BW2[np.where(BW2 > thresh)] = 255 v, f = marching_cubes(BW2, 100) v2 = np.transpose( np.vstack( ( 128 - v[:,0], v[:,2] - 128, 128 - v[:,1], ))) write_surface(outfile, v2, f)
def test_io_surface(): """Test reading and writing of Freesurfer surface mesh files """ fname = op.join(data_path, 'subjects', 'fsaverage', 'surf', 'lh.inflated') pts, tri = read_surface(fname) write_surface(op.join(tempdir, 'tmp'), pts, tri) c_pts, c_tri = read_surface(op.join(tempdir, 'tmp')) assert_array_equal(pts, c_pts) assert_array_equal(tri, c_tri)
def test_io_surface(): """Test reading and writing of Freesurfer surface mesh files """ tempdir = _TempDir() fname_quad = op.join(data_path, 'subjects', 'bert', 'surf', 'lh.inflated.nofix') fname_tri = op.join(data_path, 'subjects', 'fsaverage', 'surf', 'lh.inflated') for fname in (fname_quad, fname_tri): pts, tri = read_surface(fname) write_surface(op.join(tempdir, 'tmp'), pts, tri) c_pts, c_tri = read_surface(op.join(tempdir, 'tmp')) assert_array_equal(pts, c_pts) assert_array_equal(tri, c_tri)
def test_io_surface(): """Test reading and writing of Freesurfer surface mesh files """ tempdir = _TempDir() fname_quad = op.join(data_path, 'subjects', 'bert', 'surf', 'lh.inflated.nofix') fname_tri = op.join(data_path, 'subjects', 'fsaverage', 'surf', 'lh.inflated') for fname in (fname_quad, fname_tri): pts, tri, vol_info = read_surface(fname, read_metadata=True) write_surface(op.join(tempdir, 'tmp'), pts, tri, volume_info=vol_info) c_pts, c_tri, c_vol_info = read_surface(op.join(tempdir, 'tmp'), read_metadata=True) assert_array_equal(pts, c_pts) assert_array_equal(tri, c_tri) assert_true(_is_equal_dict([vol_info, c_vol_info]))
def test_io_surface(): """Test reading and writing of Freesurfer surface mesh files.""" tempdir = _TempDir() fname_quad = op.join(data_path, 'subjects', 'bert', 'surf', 'lh.inflated.nofix') fname_tri = op.join(data_path, 'subjects', 'fsaverage', 'surf', 'lh.inflated') for fname in (fname_quad, fname_tri): with pytest.warns(None): # no volume info pts, tri, vol_info = read_surface(fname, read_metadata=True) write_surface(op.join(tempdir, 'tmp'), pts, tri, volume_info=vol_info) with pytest.warns(None): # no volume info c_pts, c_tri, c_vol_info = read_surface(op.join(tempdir, 'tmp'), read_metadata=True) assert_array_equal(pts, c_pts) assert_array_equal(tri, c_tri) assert_equal(object_diff(vol_info, c_vol_info), '')
def test_io_surface(): """Test reading and writing of Freesurfer surface mesh files.""" tempdir = _TempDir() fname_quad = op.join(data_path, 'subjects', 'bert', 'surf', 'lh.inflated.nofix') fname_tri = op.join(data_path, 'subjects', 'fsaverage', 'surf', 'lh.inflated') for fname in (fname_quad, fname_tri): with warnings.catch_warnings(record=True) as w: pts, tri, vol_info = read_surface(fname, read_metadata=True) assert_true(all('No volume info' in str(ww.message) for ww in w)) write_surface(op.join(tempdir, 'tmp'), pts, tri, volume_info=vol_info) with warnings.catch_warnings(record=True) as w: # No vol info c_pts, c_tri, c_vol_info = read_surface(op.join(tempdir, 'tmp'), read_metadata=True) assert_array_equal(pts, c_pts) assert_array_equal(tri, c_tri) assert_equal(object_diff(vol_info, c_vol_info), '')
def test_bem_model_topology(tmpdir): """Test BEM model topological checks.""" # bad topology (not enough neighboring tris) tempdir = str(tmpdir) makedirs(op.join(tempdir, 'foo', 'bem')) for fname in ('inner_skull', 'outer_skull', 'outer_skin'): fname += '.surf' copy(op.join(subjects_dir, 'sample', 'bem', fname), op.join(tempdir, 'foo', 'bem', fname)) outer_fname = op.join(tempdir, 'foo', 'bem', 'outer_skull.surf') rr, tris = read_surface(outer_fname) tris = tris[:-1] write_surface(outer_fname, rr, tris[:-1]) with pytest.raises(RuntimeError, match='Surface outer skull is not compl'): make_bem_model('foo', None, subjects_dir=tempdir) # Now get past this error to reach gh-6127 (not enough neighbor tris) rr_bad = np.concatenate([rr, np.mean(rr, axis=0, keepdims=True)], axis=0) write_surface(outer_fname, rr_bad, tris) with pytest.raises(RuntimeError, match='Surface outer skull.*triangles'): make_bem_model('foo', None, subjects_dir=tempdir)
# folder called ``conv`` inside the FreeSurfer subject folder to keep them in. # Put the converted surfaces in a separate 'conv' folder conv_dir = op.join(subjects_dir, 'sample', 'conv') os.makedirs(conv_dir, exist_ok=True) # Load the inner skull surface and create a problem # The metadata is empty in this example. In real study, we want to write the # original metadata to the fixed surface file. Set read_metadata=True to do so. coords, faces = mne.read_surface(op.join(bem_dir, 'inner_skull.surf')) coords[0] *= 1.1 # Move the first vertex outside the skull # Write the inner skull surface as an .obj file that can be imported by # Blender. mne.write_surface(op.join(conv_dir, 'inner_skull.obj'), coords, faces, overwrite=True) # Also convert the outer skull surface. coords, faces = mne.read_surface(op.join(bem_dir, 'outer_skull.surf')) mne.write_surface(op.join(conv_dir, 'outer_skull.obj'), coords, faces, overwrite=True) ############################################################################### # Editing in Blender # ^^^^^^^^^^^^^^^^^^ # # We can now open Blender and import the surfaces. Go to *File > Import > # Wavefront (.obj)*. Navigate to the ``conv`` folder and select the file you
surf = check_seghead() if surf is None: print('mkheadsurf did not produce the standard output file.') sys.exit(1) fif = '{0}/{1}/bem/{1}-head-dense.fif'.format(subj_dir, subject) print('2. Creating %s ...' % fif) cmd = 'mne_surf2bem --surf %s --id 4 %s --fif %s' % (surf, force, fif) my_run_cmd(cmd, 'Failed to create %s, see above' % fif) levels = 'medium', 'sparse' for ii, (n_tri, level) in enumerate(zip([30000, 2500], levels), 3): my_surf = mne.read_bem_surfaces(fif)[0] print('%i. Creating medium grade tessellation...' % ii) print('%i.1 Decimating the dense tessellation...' % ii) points, tris = mne.decimate_surface(points=my_surf['rr'], triangles=my_surf['tris'], n_triangles=n_tri) out_fif = fif.replace('dense', level) print('%i.2 Creating %s' % (ii, out_fif)) surf_fname = '/tmp/tmp-surf.surf' # convert points to meters, make mne_analyze happy mne.write_surface(surf_fname, points * 1e3, tris) # XXX for some reason --check does not work here. cmd = 'mne_surf2bem --surf %s --id 4 --force --fif %s' cmd %= (surf_fname, out_fif) my_run_cmd(cmd, 'Failed to create %s, see above' % out_fif) os.remove(surf_fname) sys.exit(0)
surf = check_seghead() if surf is None: print 'mkheadsurf did not produce the standard output file.' sys.exit(1) fif = '{0}/{1}/bem/{1}-head-dense.fif'.format(subj_dir, subject) print '2. Creating %s ...' % fif cmd = 'mne_surf2bem --surf %s --id 4 %s --fif %s' % (surf, force, fif) my_run_cmd(cmd, 'Failed to create %s, see above' % fif) levels = 'medium', 'sparse' for ii, (n_tri, level) in enumerate(zip([30000, 2500], levels), 3): my_surf = mne.read_bem_surfaces(fif)[0] print '%i. Creating medium grade tessellation...' % ii print '%i.1 Decimating the dense tessellation...' % ii points, tris = mne.decimate_surface(points=my_surf['rr'], triangles=my_surf['tris'], n_triangles=n_tri) out_fif = fif.replace('dense', level) print '%i.2 Creating %s' % (ii, out_fif) surf_fname = '/tmp/tmp-surf.surf' # convert points to meters, make mne_analyze happy mne.write_surface(surf_fname, points * 1e3, tris) # XXX for some reason --check does not work here. cmd = 'mne_surf2bem --surf %s --id 4 --force --fif %s' cmd %= (surf_fname, out_fif) my_run_cmd(cmd, 'Failed to create %s, see above' % out_fif) os.remove(surf_fname) sys.exit(0)
def _run(subjects_dir, subject, force, overwrite, verbose=None): this_env = copy.copy(os.environ) this_env['SUBJECTS_DIR'] = subjects_dir this_env['SUBJECT'] = subject if 'SUBJECTS_DIR' not in this_env: raise RuntimeError('The environment variable SUBJECTS_DIR should ' 'be set') if not op.isdir(subjects_dir): raise RuntimeError('subjects directory %s not found, specify using ' 'the environment variable SUBJECTS_DIR or ' 'the command line option --subjects-dir') if 'MNE_ROOT' not in this_env: raise RuntimeError('MNE_ROOT environment variable is not set') if 'FREESURFER_HOME' not in this_env: raise RuntimeError('The FreeSurfer environment needs to be set up ' 'for this script') force = '--force' if force else '--check' subj_path = op.join(subjects_dir, subject) if not op.exists(subj_path): raise RuntimeError('%s does not exits. Please check your subject ' 'directory path.' % subj_path) if op.exists(op.join(subj_path, 'mri', 'T1.mgz')): mri = 'T1.mgz' else: mri = 'T1' logger.info('1. Creating a dense scalp tessellation with mkheadsurf...') def check_seghead(surf_path=op.join(subj_path, 'surf')): for k in ['/lh.seghead', '/lh.smseghead']: surf = surf_path + k if op.exists(surf_path + k) else None if surf is not None: break return surf my_seghead = check_seghead() if my_seghead is None: run_subprocess(['mkheadsurf', '-subjid', subject, '-srcvol', mri], env=this_env) surf = check_seghead() if surf is None: raise RuntimeError('mkheadsurf did not produce the standard output ' 'file.') dense_fname = '{0}/{1}/bem/{1}-head-dense.fif'.format(subjects_dir, subject) logger.info('2. Creating %s ...' % dense_fname) _check_file(dense_fname, overwrite) run_subprocess(['mne_surf2bem', '--surf', surf, '--id', '4', force, '--fif', dense_fname], env=this_env) levels = 'medium', 'sparse' my_surf = mne.read_bem_surfaces(dense_fname)[0] tris = [30000, 2500] if os.getenv('_MNE_TESTING_SCALP', 'false') == 'true': tris = [len(my_surf['tris'])] # don't actually decimate for ii, (n_tri, level) in enumerate(zip(tris, levels), 3): logger.info('%i. Creating %s tessellation...' % (ii, level)) logger.info('%i.1 Decimating the dense tessellation...' % ii) points, tris = mne.decimate_surface(points=my_surf['rr'], triangles=my_surf['tris'], n_triangles=n_tri) other_fname = dense_fname.replace('dense', level) logger.info('%i.2 Creating %s' % (ii, other_fname)) _check_file(other_fname, overwrite) tempdir = _TempDir() surf_fname = tempdir + '/tmp-surf.surf' # convert points to meters, make mne_analyze happy mne.write_surface(surf_fname, points * 1e3, tris) # XXX for some reason --check does not work here. try: run_subprocess(['mne_surf2bem', '--surf', surf_fname, '--id', '4', '--force', '--fif', other_fname], env=this_env) finally: del tempdir
def save_surface(self, filename): """ save the surface """ mne.write_surface(filename, self.pos, self.triangles, create_stamp=self.subject)
def make_mne_anatomy(subject, subjects_dir, recordings_path=None, hcp_path=op.curdir, outputs=('label', 'mri', 'surf')): """Extract relevant anatomy and create MNE friendly directory layout The function will create the following outputs by default: $subjects_dir/$subject/bem/inner_skull.surf $subjects_dir/$subject/label/* $subjects_dir/$subject/mri/* $subjects_dir/$subject/surf/* $recordings_path/$subject/$subject-head_mri-trans.fif These can then be set as $SUBJECTS_DIR and as MEG directory, consistent with MNE examples. Parameters ---------- subject : str The subject name. subjects_dir : str The path corresponding to MNE/freesurfer SUBJECTS_DIR (to be created) hcp_path : str The path where the HCP files can be found. outputs : {'label', 'mri', 'stats', 'surf', 'touch'} The outputs of the freesrufer pipeline shipped by HCP. Defaults to ('mri', 'surf'), the minimum needed to extract MNE-friendly anatomy files and data. """ if hcp_path == op.curdir: hcp_path = op.realpath(hcp_path) if not op.isabs(subjects_dir): subjects_dir = op.realpath(subjects_dir) this_subjects_dir = op.join(subjects_dir, subject) if not op.isabs(recordings_path): recordings_path = op.realpath(recordings_path) this_recordings_path = op.join(recordings_path, subject) if not op.exists(this_recordings_path): os.makedirs(this_recordings_path) for output in outputs: if not op.exists(op.join(this_subjects_dir, output)): os.makedirs(op.join(this_subjects_dir, output)) if output == 'mri': for suboutput in ['orig', 'transforms']: if not op.exists(op.join(this_subjects_dir, output, suboutput)): os.makedirs(op.join(this_subjects_dir, output, suboutput)) files = get_file_paths(subject=subject, data_type='freesurfer', output=output, hcp_path=hcp_path) for source in files: match = [match for match in re.finditer(subject, source)][-1] split_path = source[:match.span()[1] + 1] target = op.join(this_subjects_dir, source.split(split_path)[-1]) if (not op.isfile(target) and not op.islink(target) and op.exists(source)): # don't link if it's not there. if sys.platform != 'win32': os.symlink(source, target) else: shutil.copyfile(source, target) logger.info('reading extended structural processing ...') # Step 1 ################################################################# # transform head models to expected coordinate system # make hcp trans transforms_fname = get_file_paths(subject=subject, data_type='meg_anatomy', output='transforms', hcp_path=hcp_path) transforms_fname = [ k for k in transforms_fname if k.endswith('transform.txt') ][0] hcp_trans = _read_trans_hcp(fname=transforms_fname, convert_to_meter=False) # get RAS freesurfer trans c_ras_trans_fname = get_file_paths(subject=subject, data_type='freesurfer', output='mri', hcp_path=hcp_path) c_ras_trans_fname = [ k for k in c_ras_trans_fname if k.endswith('c_ras.mat') ][0] logger.info('reading RAS freesurfer transform') # ceci n'est pas un .mat file ... with open(op.join(subjects_dir, c_ras_trans_fname)) as fid: ras_trans = np.array([r.split() for r in fid.read().split('\n') if r], dtype=np.float64) logger.info('Combining RAS transform and coregistration') ras_trans_m = linalg.inv(ras_trans) # and the inversion logger.info('extracting head model') head_model_fname = get_file_paths(subject=subject, data_type='meg_anatomy', output='head_model', hcp_path=hcp_path)[0] pnts, faces = _get_head_model(head_model_fname=head_model_fname) logger.info('coregistring head model to MNE-HCP coordinates') pnts = apply_trans(ras_trans_m.dot(hcp_trans['bti2spm']), pnts) tri_fname = op.join(this_subjects_dir, 'bem', 'inner_skull.surf') if not op.exists(op.dirname(tri_fname)): os.makedirs(op.dirname(tri_fname)) write_surface(tri_fname, pnts, faces) # Step 2 ################################################################# # write corresponding device to MRI transform logger.info('extracting coregistration') # now convert to everything meter too here ras_trans_m[:3, 3] *= 1e-3 bti2spm = hcp_trans['bti2spm'] bti2spm[:3, 3] *= 1e-3 head_mri_t = Transform( # we're lying here for a good purpose 'head', 'mri', np.dot(ras_trans_m, bti2spm)) # it should be 'ctf_head' write_trans( op.join(this_recordings_path, '%s-head_mri-trans.fif' % subject), head_mri_t)
def make_mne_anatomy(subject, subjects_dir, recordings_path=None, hcp_path=op.curdir, outputs=('label', 'mri', 'surf')): """Extract relevant anatomy and create MNE friendly directory layout The function will create the following outputs by default: $subjects_dir/$subject/bem/inner_skull.surf $subjects_dir/$subject/label/* $subjects_dir/$subject/mri/* $subjects_dir/$subject/surf/* $recordings_path/$subject/$subject-head_mri-trans.fif These can then be set as $SUBJECTS_DIR and as MEG directory, consistent with MNE examples. Parameters ---------- subject : str The subject name. subjects_dir : str The path corresponding to MNE/freesurfer SUBJECTS_DIR (to be created) hcp_path : str The path where the HCP files can be found. outputs : {'label', 'mri', 'stats', 'surf', 'touch'} The outputs of the freesrufer pipeline shipped by HCP. Defaults to ('mri', 'surf'), the minimum needed to extract MNE-friendly anatomy files and data. """ if hcp_path == op.curdir: hcp_path = op.realpath(hcp_path) if not op.isabs(subjects_dir): subjects_dir = op.realpath(subjects_dir) this_subjects_dir = op.join(subjects_dir, subject) if not op.isabs(recordings_path): recordings_path = op.realpath(recordings_path) this_recordings_path = op.join(recordings_path, subject) if not op.exists(this_recordings_path): os.makedirs(this_recordings_path) for output in outputs: if not op.exists(op.join(this_subjects_dir, output)): os.makedirs(op.join(this_subjects_dir, output)) if output == 'mri': for suboutput in ['orig', 'transforms']: if not op.exists( op.join(this_subjects_dir, output, suboutput)): os.makedirs(op.join(this_subjects_dir, output, suboutput)) files = get_file_paths( subject=subject, data_type='freesurfer', output=output, hcp_path=hcp_path) for source in files: match = [match for match in re.finditer(subject, source)][-1] split_path = source[:match.span()[1] + 1] target = op.join(this_subjects_dir, source.split(split_path)[-1]) if (not op.isfile(target) and not op.islink(target) and op.exists(source)): # don't link if it's not there. if sys.platform != 'win32': os.symlink(source, target) else: shutil.copyfile(source, target) logger.info('reading extended structural processing ...') # Step 1 ################################################################# # transform head models to expected coordinate system # make hcp trans transforms_fname = get_file_paths( subject=subject, data_type='meg_anatomy', output='transforms', hcp_path=hcp_path) transforms_fname = [k for k in transforms_fname if k.endswith('transform.txt')][0] hcp_trans = _read_trans_hcp(fname=transforms_fname, convert_to_meter=False) # get RAS freesurfer trans c_ras_trans_fname = get_file_paths( subject=subject, data_type='freesurfer', output='mri', hcp_path=hcp_path) c_ras_trans_fname = [k for k in c_ras_trans_fname if k.endswith('c_ras.mat')][0] logger.info('reading RAS freesurfer transform') # ceci n'est pas un .mat file ... with open(op.join(subjects_dir, c_ras_trans_fname)) as fid: ras_trans = np.array([ r.split() for r in fid.read().split('\n') if r], dtype=np.float64) logger.info('Combining RAS transform and coregistration') ras_trans_m = linalg.inv(ras_trans) # and the inversion logger.info('extracting head model') head_model_fname = get_file_paths( subject=subject, data_type='meg_anatomy', output='head_model', hcp_path=hcp_path)[0] pnts, faces = _get_head_model(head_model_fname=head_model_fname) logger.info('coregistring head model to MNE-HCP coordinates') pnts = apply_trans(ras_trans_m.dot(hcp_trans['bti2spm']), pnts) tri_fname = op.join(this_subjects_dir, 'bem', 'inner_skull.surf') if not op.exists(op.dirname(tri_fname)): os.makedirs(op.dirname(tri_fname)) write_surface(tri_fname, pnts, faces) # Step 2 ################################################################# # write corresponding device to MRI transform logger.info('extracting coregistration') # now convert to everything meter too here ras_trans_m[:3, 3] *= 1e-3 bti2spm = hcp_trans['bti2spm'] bti2spm[:3, 3] *= 1e-3 head_mri_t = Transform( # we're lying here for a good purpose 'head', 'mri', np.dot(ras_trans_m, bti2spm)) # it should be 'ctf_head' write_trans(op.join(this_recordings_path, '%s-head_mri-trans.fif' % subject), head_mri_t)
def make_outer_surf( orig_pial: Union[str, Path], image: Union[str, Path], output_fpath: Union[str, Path], outer_surface_sphere: float = 15, ): """Create outer surface of a pial volume. Make outer surface based on a pial volume and radius, write to surface in outfile. Parameters ---------- orig_pial : str | pathlib.Path Pial surface (e.g. lh.pial) image : str | pathlib.Path Filled lh or rh pial image (e.g. lh.pial.filled.mgz) output_fpath : str | pathlib.Path surface file to write data to outer_surface_sphere : float | None radius for smoothing in mm (default=15). diameter of the sphere used by make_outer_surface to close the sulci using morphological operations. Ignored currently. Corresponds to ``se=strel('sphere',se_diameter);`` in Matlab. See [1]. References ---------- .. [1] See FieldTrip Toolbox ``make_outer_surface`` function inside ``prepare_mesh_cortexhull.m`` file. .. [2] https://github.com/aestrivex/ielu """ from scipy.signal import convolve from scipy.ndimage.morphology import grey_closing, generate_binary_structure from mne import write_surface from mcubes import marching_cubes # radius information is currently ignored # it is a little tougher to deal with the morphology in python # load original pial surface to get the volume information pial_surf = nb.freesurfer.read_geometry(orig_pial, read_metadata=True) volume_info = pial_surf[2] # load filled pial image fill = nb.load(image) filld = fill.get_data() filld[filld == 1] = 255 # apply a very soft Gaussian filter with sigma = 1mm to # facilitate the closing gaussian = np.ones((2, 2)) * 0.25 # initialize image cube array image_f = np.zeros((256, 256, 256)) # initialize a thresholded image image2 = np.zeros((256, 256, 256)) # for each slice, convolve the Gaussian filter on the # filled image for slice in range(256): temp = filld[:, :, slice] image_f[:, :, slice] = convolve(temp, gaussian, "same") # thresholded image based on value of 25 image2[np.where(image_f <= 25)] = 0 image2[np.where(image_f > 25)] = 255 strel15 = generate_binary_structure(3, 1) # run multi-dimensional grayscale closing of the image BW2 = grey_closing(image2, structure=strel15) thresh = np.max(BW2) / 2 BW2[np.where(BW2 <= thresh)] = 0 BW2[np.where(BW2 > thresh)] = 255 # apply marching cubes algorithm to get # vertices and faces v, f = marching_cubes(BW2, 100) # in order to cope with the different orientation v2 = np.transpose( np.vstack(( 128 - v[:, 0], v[:, 2] - 128, 128 - v[:, 1], ))) write_surface(output_fpath, v2, f, volume_info=volume_info)
# <https://en.wikipedia.org/wiki/Wavefront_.obj_file>`_ files and create a new # folder called ``conv`` inside the FreeSurfer subject folder to keep them in. # Put the converted surfaces in a separate 'conv' folder conv_dir = subjects_dir / 'sample' / 'conv' os.makedirs(conv_dir, exist_ok=True) # Load the inner skull surface and create a problem # The metadata is empty in this example. In real study, we want to write the # original metadata to the fixed surface file. Set read_metadata=True to do so. coords, faces = mne.read_surface(bem_dir / 'inner_skull.surf') coords[0] *= 1.1 # Move the first vertex outside the skull # Write the inner skull surface as an .obj file that can be imported by # Blender. mne.write_surface(conv_dir / 'inner_skull.obj', coords, faces, overwrite=True) # Also convert the outer skull surface. coords, faces = mne.read_surface(bem_dir / 'outer_skull.surf') mne.write_surface(conv_dir / 'outer_skull.obj', coords, faces, overwrite=True) # %% # Editing in Blender # ^^^^^^^^^^^^^^^^^^ # # See the following video tutorial for how to import, edit and export # surfaces in Blender (step-by-step instructions are also below): # # .. youtube:: JBIaX7VaTZk # # We can now open Blender and import the surfaces. Go to *File > Import >