20 """Python module for reconstruction of neonatal cortex 22 This module implements the deformable surfaces method for the reconstruction of 23 the neonatal cortex as detailed in the conference paper submission to ISBI 2017. 24 The recon-neonatal-cortex script provides a command-line tool for executing 25 the functions of this module to obtain explicit surface representations 26 of the inner and outer cortical surfaces given a Draw-EM segmentation 27 and the bias corrected T2-weighted (and T1-weighted if available) image(s). 29 Note: The merge-surfaces tool failed when MIRTK was built with VTK 6.2. 30 Use a more recent version of VTK if you encounter such issue. 46 from contextlib
import contextmanager
49 from contextlib
import ExitStack
51 from contextlib2
import ExitStack
67 _cortex_mask_array =
'CortexMask' 68 _region_id_array =
'RegionId' 69 _collision_mask_array =
'CollisionMask' 70 _collision_type_array =
'CollisionType' 78 """Enumeration of brain hemispheres""" 86 """Convert Hemisphere enumeration value to string""" 87 if hemi == Hemisphere.Right:
return 'rh' 88 elif hemi == Hemisphere.Left:
return 'lh' 97 """Split file name into name and extension.""" 98 (base, ext) = os.path.splitext(name)
99 if ext.lower() ==
'.gz':
100 (base, fext) = os.path.splitext(base)
106 """Split temporary output file name into base name, incremental output ID, and extension.""" 107 (base, ext) = splitext(name)
108 m = re.match(
'^(.*)-([0-9]+)$', base)
111 num = int(m.group(2))
114 return (base, num, ext)
117 def joinname(base, num, ext):
118 """Join temporary file name parts.""" 119 return '{}-{}{}'.format(base, num, ext)
122 def nextname(name, temp=None):
123 """Get next incremental output file path.""" 124 (base, num, ext) = splitname(name)
126 base = os.path.join(temp, os.path.basename(base))
127 return joinname(base, num+1, ext)
131 """Make directories for output file if not existent.""" 132 path = os.path.dirname(name)
133 if not os.path.isdir(path):
137 def rename(src, dst):
143 def try_remove(name):
144 """Try to remove file, do not throw exception if it fails.""" 157 def iterable_or_int_to_list(arg):
158 """Given either an int or an iterable, returns list of items.""" 159 if isinstance(arg, int):
165 def run(tool, args=[], opts={}):
166 """Run MIRTK command with global `showcmd` flag and maximum allowed number of `threads`.""" 167 _run(tool, args=args, opts=opts, showcmd=showcmd, threads=threads)
171 def output(name_or_func, delete=False):
172 """Context with (temporary) output file path. 174 This context is used to ensure that a temporary file is deleted on 175 exit or when an exception occurs. Moreover, it is used to guarantee that an 176 output file which is the result of a number of intermediate processes which 177 write to the same file corresponds to the final output. When an error occurs 178 before the final output was written to the specified file, the output file 179 is removed when this context is exited to ensure that every output file has 180 a known final state. Alternatively, use different unique output file name 181 for each intermediate file and remove these intermediate files on exit. 185 name_or_func : str, def 186 File path or function which creates the output file and returns its path. 188 Whether the output file is temporary and should be deleted when done. 193 Absolute path of output file. 196 if isinstance(name_or_func, str): path = name_or_func
197 else: path = name_or_func()
200 yield os.path.abspath(path)
201 except BaseException
as e:
205 if debug == 0
and path
and delete:
208 raise Exception(
"Invalid file path")
211 def push_output(stack, name_or_func, delete=True):
212 """Perform interim processing step with auto-file removal upon exit.""" 213 return stack.enter_context(output(name_or_func, delete=delete))
216 def get_voxel_size(image):
217 """Get voxel size of image file.""" 218 info = check_output([
'info', image])
219 match = re.search(
'Spacing:\s+([0-9][0-9.]*)\s*x\s*([0-9][0-9.]*)\s*x\s*([0-9][0-9.]*)', info)
221 dx = float(match.group(1))
222 dy = float(match.group(2))
223 dz = float(match.group(3))
225 raise Exception(
"Failed to determine voxel size of image: {}".format(image))
229 def get_surface_property(name_or_info, props, mesh=False, topology=False, opts={}):
230 """Get surface property values""" 232 if os.path.splitext(name_or_info)[1]
in [
'.vtp',
'.vtk',
'.stl',
'.obj']:
233 info = evaluate_surface(name_or_info, mesh=mesh, topology=topology, opts=opts)
236 if isinstance(props, str):
239 regex = re.compile(
'^\s*' + re.escape(prop) +
r'\s*=\s*(.+)\s*$', re.MULTILINE)
240 match = regex.search(info)
243 sys.stderr.write(info)
244 raise Exception(
"Missing surface property: " + prop)
245 values.append(match.group(1))
251 def get_num_components(name_or_info):
252 """Get number of connected surface components""" 253 return int(get_surface_property(name_or_info,
'No. of components', mesh=
True))
256 def get_num_boundaries(name_or_info):
257 """Get number of surface boundaries""" 258 return int(get_surface_property(name_or_info,
'No. of boundary segments', mesh=
True))
261 def get_euler_characteristic(name_or_info):
262 """Get Euler characteristic of surface mesh""" 263 return int(get_surface_property(name_or_info,
'Euler characteristic', topology=
True))
266 def get_genus(name_or_info):
267 """Get Genus of surface mesh""" 268 return float(get_surface_property(name_or_info,
'Genus', topology=
True))
275 def invert_mask(iname, oname=None):
276 """Invert a binary mask.""" 280 oname = nextname(iname)
281 run(
'calculate-element-wise', args=[iname], opts=[(
'binarize', [0, 0]), (
'out', oname,
'binary')])
285 def close_image(iname, oname=None, iterations=1, connectivity=18):
286 """Close (binary) image using morphological dilation followed by erosion.""" 290 oname = nextname(iname)
291 run(
'close-image', args=[iname, oname], opts={
'iterations': iterations,
'connectivity': connectivity})
295 def del_mesh_attr(iname, oname=None, **opts):
296 """Delete surface mesh attributes.""" 299 run(
'delete-pointset-attributes', args=[iname, oname], opts=opts)
302 def evaluate_surface(name, oname=None, mesh=False, topology=False, intersections=False, collisions=0, opts={}):
303 """Evaluate properties of surface mesh""" 304 argv = [
'evaluate-surface-mesh', name]
306 argv.extend([oname,
'-v'])
307 argv.extend([
'-threads', str(threads)])
308 if mesh: argv.append(
'-attr')
309 if topology: argv.append(
'-topology')
310 if collisions > 0
or intersections:
311 argv.extend([
'-collisions', collisions])
313 if isinstance(opts, list):
315 if isinstance(item, (tuple, list)):
317 arg = flatten(item[1:])
321 if not opt.startswith(
'-'):
325 argv.extend(flatten(arg))
327 for opt, arg
in opts.items():
328 if not opt.startswith(
'-'):
332 argv.extend(flatten(arg))
333 return check_output(argv, verbose=showcmd)
336 def smooth_surface(iname, oname=None, iterations=1, lambda_value=1, mu=None, mask=None, weighting='combinatorial', excl_node=False):
337 if not oname: oname = nextname(iname)
338 argv = [
'smooth-surface', iname, oname,
'-threads', str(threads),
'-iterations', iterations,
'-' + weighting]
339 if mask: argv.extend([
'-mask', mask])
340 if excl_node: argv.append(
'-exclnode')
341 else: argv.append(
'-inclnode')
342 argv.extend([
'-lambda', lambda_value])
343 if mu: argv.extend([
'-mu', mu])
344 check_call(argv, verbose=showcmd)
348 def check_intersections(iname, oname=None):
349 """Check surface mesh for triangle/triangle intersections.""" 350 argv = [
'evaluate-surface-mesh', iname]
352 argv.extend([oname,
'-v'])
353 argv.extend([
'-threads', threads,
'-collisions', 0])
354 info = check_output(argv, verbose=showcmd)
355 match = re.search(
'No\. of self-intersections\s*=\s*(\d+)', info)
356 return int(match.group(1))
359 def remove_intersections(iname, oname=None, mask=None, max_attempt=10, smooth_iter=5, smooth_lambda=1):
360 """Remove intersections of surface mesh triangles. 362 .. seealso:: MRISremoveIntersections function of FreeSurfer (dev/utils/mrisurf.c) 365 oname = nextname(iname)
368 cur = check_intersections(iname, oname)
371 if itr == 1
and verbose > 0:
372 print((
"Trying to resolve {} remaining self-intersections of {}".format(cur, iname)))
373 if itr > max_attempt:
374 raise Exception(
"Failed to resolve self-intersections of {}".format(iname))
375 run(
'dilate-scalars', args=[oname, oname], opts={
'array': _collision_mask_array,
'iterations': nbr})
377 run(
'calculate-element-wise', args=[oname], opts=[(
'scalars', _collision_mask_array), (
'mul', mask), (
'out', oname)])
378 smooth_surface(oname, oname, iterations=smooth_iter, lambda_value=smooth_lambda,
379 weighting=
'combinatorial', mask=_collision_mask_array, excl_node=
False)
381 cur = check_intersections(oname, oname)
382 if cur >= pre: nbr += 1
383 del_mesh_attr(oname, oname, pointdata=_collision_mask_array, celldata=_collision_type_array)
387 def remove_white_pial_intersections(iname, oname=None, mask=None, max_attempt=10, smooth_iter=1, smooth_lambda=1, region_id_array=_region_id_array):
388 """Smooth white and pial surfaces in an attempt to remove intersections between triangles of white and pial surface.""" 389 iname = os.path.abspath(iname)
391 oname = os.path.abspath(oname)
393 oname = nextname(iname)
396 weighting =
'combinatorial' 397 smooth_mask_array =
'SmoothMask' 398 curvature_array =
'Curvature' 400 cur = check_intersections(iname, oname)
403 run(
'calculate-surface-attributes', args=[oname, oname], opts={
'H': curvature_array,
'vtk-curvatures':
None})
404 run(
'copy-pointset-attributes', args=[oname, oname], opts={
'celldata-as-pointdata': region_id_array,
'unanimous':
None})
408 if itr == 1
and verbose > 0:
409 print((
"Trying to resolve {} remaining self-intersections of {}".format(cur, iname)))
410 if itr > max_attempt:
411 raise Exception(
"Failed to resolve self-intersections of {}".format(iname))
413 run(
'calculate-element-wise', args=[oname], opts=[(
'point-data', region_id_array), (
'label', 3, 4),
414 (
'mask', _collision_mask_array), (
'set', 1), (
'pad', 0),
415 (
'mul', curvature_array), (
'threshold-ge', 0), (
'pad', 0),
416 (
'out', oname,
'binary', smooth_mask_array)])
417 run(
'dilate-scalars', args=[oname, oname], opts={
'array': smooth_mask_array,
'iterations': nbr})
419 run(
'calculate-element-wise', args=[oname], opts=[(
'scalars', smooth_mask_array), (
'mul', mask), (
'out', oname)])
420 smooth_surface(oname, oname, iterations=smooth_iter, lambda_value=smooth_lambda,
421 weighting=weighting, mask=smooth_mask_array, excl_node=
False)
422 if check_intersections(oname, oname) > 0:
424 run(
'calculate-element-wise', args=[oname], opts=[(
'point-data', region_id_array), (
'label', 1, 2),
425 (
'mask', _collision_mask_array), (
'set', 1), (
'pad', 0),
426 (
'mul', curvature_array), (
'threshold-le', 0), (
'pad', 0),
427 (
'out', oname,
'binary', smooth_mask_array)])
428 run(
'dilate-scalars', args=[oname, oname], opts={
'array': smooth_mask_array,
'iterations': nbr})
430 run(
'calculate-element-wise', args=[oname], opts=[(
'scalars', smooth_mask_array), (
'mul', mask), (
'out', oname)])
431 smooth_surface(oname, oname, iterations=smooth_iter, lambda_value=smooth_lambda,
432 weighting=weighting, mask=smooth_mask_array, excl_node=
False)
435 cur = check_intersections(oname, oname)
436 if cur >= pre: nbr += 1
438 del_mesh_attr(oname, pointdata=[region_id_array, curvature_array, smooth_mask_array, _collision_mask_array], celldata=_collision_type_array)
442 def project_mask(iname, oname, mask, name, dilation=0, invert=False, fill=False):
443 """Project binary mask onto surface.""" 445 run(
'project-onto-surface', args=[iname, oname],
446 opts={
'labels': mask,
'fill': fill,
'dilation-radius': dilation,
'name': name})
448 run(
'calculate-element-wise', args=[oname],
449 opts=[(
'scalars', name), (
'threshold-inside', [0, 0]), (
'set', 0), (
'pad', 1), (
'out', oname)])
452 def calculate_distance_map(iname, oname=None, temp=None, distance='euclidean', isotropic=1):
453 """Calculate signed distance map from binary object mask.""" 456 temp = os.path.dirname(iname)
457 oname = splitname(os.path.basename(iname))[0]
458 if oname.endswith(
'-mask'):
460 oname = os.path.join(temp,
'{}-dmap.nii.gz'.format(oname))
462 run(
'calculate-distance-map', args=[iname, oname], opts={
'distance': distance,
'isotropic': isotropic})
466 def extract_isosurface(iname, oname=None, temp=None, isoval=0, blur=0, close=True):
467 """Extract iso-surface from image.""" 470 temp = os.path.dirname(iname)
471 oname = splitname(os.path.basename(iname))[0]
472 if oname.endswith(
'-dmap')
or oname.endswith(
'-mask'):
474 oname =
'{}-iso.vtp'.format(oname)
475 oname = os.path.join(temp, oname)
477 run(
'extract-surface', args=[iname, oname], opts={
'isovalue': isoval,
'blur': blur,
'close': close})
481 def remesh_surface(iname, oname=None, temp=None, edge_length=0, min_edge_length=0, max_edge_length=10):
482 """Remesh surface.""" 485 temp = os.path.dirname(iname)
486 oname = os.path.join(temp, nextname(os.path.basename(iname)))
489 min_edge_length = edge_length
490 max_edge_length = edge_length
491 run(
'remesh-surface', args=[iname, oname], opts={
'min-edge-length': min_edge_length,
'max-edge-length': max_edge_length})
495 def get_convex_hull(iname, oname=None, temp=None, remeshing=10, edge_length=1, smoothing=100):
496 """Get convex hull of surface mesh.""" 498 oname = splitname(iname)[0]
499 if oname.endswith(
'-iso'):
501 oname =
'{}-hull.vtp'.format(oname)
504 if not os.path.isfile(oname):
505 with ExitStack()
as stack:
507 hull = push_output(stack, nextname(oname, temp=temp))
508 run(
'extract-pointset-surface', opts={
'input': iname,
'output': hull,
'hull':
None})
511 for i
in range(0, remeshing):
512 name = push_output(stack, nextname(hull))
513 run(
'remesh-surface', args=[hull, name], opts={
'edge-length': edge_length})
514 run(
'smooth-surface', args=[name, name],
515 opts={
'combinatorial':
None,
'exclnode':
None,
516 'iterations': smoothing,
'lambda': .33,
'mu': -.34})
524 def extract_convex_hull(iname, oname=None, temp=None, isoval=0, blur=0):
525 """Calculate convex hull of implicit surface.""" 527 temp = os.path.dirname(iname)
528 base = splitname(os.path.basename(iname))[0]
529 if base.endswith(
'-dmap')
or base.endswith(
'-mask'):
532 oname = os.path.join(temp,
'{}-hull.vtp'.format(base))
535 if not os.path.isfile(oname):
537 with output(extract_isosurface(iname, temp=temp, isoval=isoval, blur=blur), delete=
True)
as iso:
538 get_convex_hull(iso, oname, temp=temp)
542 def add_corpus_callosum_mask(iname, mask, oname=None):
543 """Add ImplicitSurfaceFillMask point data array to surface file. 545 This mask is considered by the ImplicitSurfaceDistance force when deforming 546 the convex hull towards the WM segmentation boundary. Distance values of points 547 with a zero mask value are excluded from the clustering based hole filling. 548 This is to allow the surface to deform into the sulcus next to corpus callosum. 551 oname = nextname(iname)
554 if not os.path.isfile(oname):
557 project_mask(iname, oname, mask, name=
'ImplicitSurfaceFillMask', dilation=20, invert=
True)
561 def del_corpus_callosum_mask(iname, oname=None):
562 """Remove ImplicitSurfaceFillMask again.""" 564 oname = nextname(iname)
567 if not os.path.isfile(oname):
569 del_mesh_attr(iname, oname, pointdata=
'ImplicitSurfaceFillMask')
574 def add_cortex_mask(iname, mask, name=_cortex_mask_array, region_id_array=_region_id_array, oname=None):
575 """Add a CortexMask cell data array to the surface file.""" 577 oname = nextname(iname)
579 assert os.path.realpath(iname) != os.path.realpath(oname),
"iname != oname" 580 if force
or not os.path.isfile(oname):
583 run(
'project-onto-surface', args=[iname, oname],
584 opts={
'labels': mask,
'name': name,
'dilation-radius': .5,
585 'point-data':
False,
'cell-data':
True,
'fill':
True,
586 'max-hole-size': 100,
'min-size': 1000,
'smooth': 2})
587 run(
'erode-scalars', args=[oname, oname], opts={
'cell-data': name,
'iterations': 2})
588 run(
'calculate-element-wise', args=[oname],
589 opts=[(
'cell-data', region_id_array), (
'label', 7), (
'set', 0), (
'pad', 1),
590 'reset-mask', (
'mul', name), (
'out', oname,
'binary', name)])
591 run(
'calculate-surface-attributes', args=[oname, oname],
592 opts={
'cell-labels': region_id_array,
'border-mask':
'RegionBorder'})
593 run(
'calculate-element-wise', args=[oname],
594 opts=[(
'cell-data',
'RegionBorder'),
595 (
'threshold', 1), (
'set', 0), (
'pad', 1),
'reset-mask',
596 (
'mul', region_id_array), (
'mul', name), (
'binarize', 1),
597 (
'out', oname,
'char', name)])
598 run(
'evaluate-surface-mesh', args=[oname, oname], opts={
'where': name,
'gt': 0})
599 run(
'calculate-element-wise', args=[oname],
600 opts=[(
'cell-data',
'ComponentId'), (
'binarize', 1, 2),
601 (
'mul', name), (
'out', oname,
'binary', name)])
602 run(
'calculate-element-wise', args=[oname],
603 opts=[(
'cell-data', region_id_array),
604 (
'label', 1), (
'mul', name), (
'threshold-gt', 0), (
'add', 5),
606 (
'label', 2), (
'mul', name), (
'threshold-gt', 0), (
'add', 6),
608 del_mesh_attr(oname, oname, name=[
'BoundaryMask',
'ComponentId',
'RegionBorder',
'DuplicatedMask'])
612 def append_surfaces(name, surfaces, merge=True, tol=1e-6):
613 """Append surface meshes into single mesh file. 618 File path of output mesh. 620 List of file paths of surface meshes to combine. 622 Whether to merge points of surface meshes. 624 Maximum distance between points to be merged. 629 Absolute file path of output mesh. 632 name = os.path.abspath(name)
637 if merge: opts[
'merge'] = tol
638 run(
'convert-pointset', args=args, opts=opts)
642 def white_refinement_mask(name, subcortex_mask):
643 """Create image foreground mask used for reconstruction of WM/cGM surface.""" 644 with output(invert_mask(subcortex_mask, nextname(name)), delete=
True)
as mask:
645 close_image(mask, name, iterations=2, connectivity=18)
649 def deform_mesh(iname, oname=None, temp=None, opts={}):
650 """Deform mesh with the specified options.""" 652 temp = os.path.dirname(iname)
654 oname = os.path.join(temp, os.path.basename(nextname(iname)))
657 if not os.path.isfile(oname):
658 base = splitext(os.path.basename(oname))[0]
659 debug_prefix = os.path.join(temp, base +
'-')
661 opts[
'debug'] = (debug-2
if debug > 3
else 1)
662 opts[
'debug-interval'] = (1
if debug > 2
else 10)
663 opts[
'debug-prefix'] = debug_prefix
664 opts[
'level-prefix'] =
False 665 fname_prefix = debug_prefix +
'output_' 666 for fname
in os.listdir(temp):
667 if fname.startswith(fname_prefix):
668 try_remove(os.path.join(temp, fname))
670 run(
'deform-mesh', args=[iname, oname], opts=opts)
674 def extract_surface(iname, oname, labels, array=_region_id_array):
675 """Extract sub-surface from combined surface mesh.""" 677 opts = [(
'normals',
True), (
'where', array),
'or']
678 opts.extend([(
'eq', label)
for label
in labels])
679 run(
'extract-pointset-cells', args=[iname, oname], opts=opts)
686 def binarize(name, segmentation, labels=[], image=None, interp='linear', threshold=.5, temp=None):
687 """Make binary mask from label image. 692 Path of output image file. 694 Path of segmentation label image file. 696 List of segmentation labels. 697 image : str, optional 698 Path of reference image file. When specified, the binary image is 699 resampled on this reference image grid using the specified 700 interpolation followed by thresholding using the given `threshold` 701 value unless `interp='nn'`. 703 Threshold used to binarize a non-nearest neighbor interpolated 704 resampled binary image when reference `image` is given. 709 Absolute path of output image file. 713 assert name,
"Invalid 'name' argument" 714 assert segmentation,
"Invalid 'segmentation' argument" 715 name = os.path.abspath(name)
717 temp = os.path.abspath(temp)
719 temp = os.path.dirname(name)
720 with ExitStack()
as stack:
722 mask = push_output(stack, nextname(name))
727 if not isinstance(labels, int)
and len(labels) == 0:
730 opts=[(
'label', labels)]
731 opts.extend([(
'set', 1), (
'pad', 0), (
'out', mask,
'binary')])
732 run(
'calculate-element-wise', args=[segmentation], opts=opts)
736 run(
'transform-image', args=[mask, name], opts={
'target': image,
'dofin':
'Id',
'interp': interp,
'type':
'binary'})
738 resampled_mask = push_output(stack, nextname(mask))
739 run(
'transform-image', args=[mask, resampled_mask], opts={
'target': image,
'dofin':
'Id',
'interp': interp,
'type':
'float'})
740 run(
'calculate-element-wise', args=[resampled_mask], opts=[(
'threshold-le', threshold), (
'set', 1), (
'out', [name,
'binary'])])
744 def binarize_cortex(regions, name=None, temp=None):
745 """Make binary cortex mask from regions label image.""" 747 assert regions,
"Invalid 'regions' argument" 749 name =
'cortex-mask.nii.gz' 751 temp = os.path.dirname(regions)
752 name = os.path.join(temp, name)
753 return binarize(name=name, segmentation=regions, labels=1)
756 def binarize_white_matter(regions, hemisphere=Hemisphere.Unspecified, name=None, temp=None):
757 """Make binary white matter mask from regions label image. 762 Relative or absolute path of output mask. 764 File path of regions image with right and left cerebrum labelled. 765 hemisphere : Hemisphere 766 Which hemisphere of the cerebral cortex to binarize. 771 Absolute path of binary white matter mask. 775 assert regions,
"Invalid 'regions' argument" 777 suffix = hemi2str(hemisphere)
779 suffix =
'-' + suffix
780 name =
'white{}-mask.nii.gz'.format(suffix)
782 temp = os.path.dirname(regions)
783 name = os.path.join(temp, name)
784 if hemisphere == Hemisphere.Right: labels = 2
785 elif hemisphere == Hemisphere.Left: labels = 3
786 else: labels = [2, 3]
787 return binarize(name=name, segmentation=regions, labels=labels)
790 def binarize_brainstem_plus_cerebellum(regions, name=None, temp=None):
791 """Make binary brainstem plus cerebellum mask from regions label image.""" 793 assert regions,
"Invalid 'regions' argument" 795 name =
'brainstem+cerebellum-mask.nii.gz' 797 temp = os.path.dirname(regions)
798 name = os.path.join(temp, name)
800 return binarize(name=name, segmentation=regions, labels=[4, 5])
803 def subdivide_brain(name, segmentation, white_labels, cortex_labels, right_labels, left_labels,
804 subcortex_labels=[], subcortex_closing=5,
805 brainstem_labels=[], brainstem_closing=5,
806 cerebellum_labels=[], cerebellum_closing=5,
807 tissues=None, brain_mask=None, merge_bs_cb=True,
808 cortical_hull_dmap=None, temp=None):
809 """Subdivide brain into major building blocks, nicely cut disjoint regions. 814 Path of output brain regions image file. 816 Path of brain segmentation label image file. May be a structural or tissue 817 segmentation. If a separate tissue segmentation label image should be used 818 for delineating the white matter and gray matter, provide also a `tissues` image. 819 white_labels : int, str, list 820 Label(s) of `segmentation` corresponding to interior of white matter surface. 821 When a `tissues` image is specified, these labels are considered to 822 be corresponding to the white matter tissue label instead. 823 cortex_labels : int, str, list 824 Label(s) of `segmentation` corresponding to cortical gray matter. 825 Can contain strings of label ranges such as '1..4' instead of int's only. 826 When a `tissues` image is specified, these labels are considered to 827 be corresponding to the gray matter tissue label instead. 828 right_labels : int, str, list 829 Label(s) of `segmentation` corresponding to structures of right hemisphere. 830 left_labels : int, str, list 831 Label(s) of `segmentation` corresponding to structures of left hemisphere. 832 subcortex_labels : int, str, list, optional 833 Label(s) of `segmentation` corresponding to subcortical structures. 834 subcortex_closing : int 835 Number of iterations of morphological closing applied to subcortical structures mask. 836 brainstem_labels : int, str, list, optional 837 Label(s) of `segmentation` corresponding to brainstem. 838 brainstem_closing : int 839 Number of iterations of morphological closing applied to brainstem mask. 840 cerebellum_labels : int, str, list, optional 841 Label(s) of `segmentation` corresponding to cerebellum. 842 cerebellum_closing : int 843 Number of iterations of morphological closing applied to cerebellum mask. 845 Path of tissue segmentation used for white and gray matter 846 instead of the (structural) brain `segmentation`. 848 Path of brain extraction mask used to ensure that the resampled 849 image grid contains the whole of the brain. 851 Whether to merge brainstem and cerebellum labels into a single output region. 852 cortical_hull_dmap : str, optional 853 Path of output cortical hull distance map image file. 855 Path of temporary directory for segmentation masks of white or gray 856 matter tissue, respectively, when `tissues` segmentation provided. 859 name = os.path.abspath(name)
861 temp = os.path.abspath(temp)
863 temp = os.path.dirname(name)
864 white_labels = iterable_or_int_to_list(white_labels)
865 cortex_labels = iterable_or_int_to_list(cortex_labels)
866 right_labels = iterable_or_int_to_list(right_labels)
867 left_labels = iterable_or_int_to_list(left_labels)
868 subcortex_labels = iterable_or_int_to_list(subcortex_labels)
869 brainstem_labels = iterable_or_int_to_list(brainstem_labels)
870 cerebellum_labels = iterable_or_int_to_list(cerebellum_labels)
871 with ExitStack()
as stack:
873 white_mask = os.path.join(temp,
'inner-surface-mask.nii.gz')
874 cortex_mask = os.path.join(temp,
'outer-surface-mask.nii.gz')
875 white_labels = push_output(stack, binarize(name=white_mask, segmentation=tissues, labels=white_labels))
876 cortex_labels = push_output(stack, binarize(name=cortex_mask, segmentation=tissues, labels=cortex_labels))
878 'rh': right_labels,
'lh': left_labels,
879 'wm': white_labels,
'gm': cortex_labels,
880 'sb': subcortex_labels,
881 'bs': brainstem_labels,
882 'cb': cerebellum_labels,
883 'bs+cb': merge_bs_cb,
884 'subcortical-closing': subcortex_closing,
885 'brainstem-closing': brainstem_closing,
886 'cerebellum-closing': cerebellum_closing
889 opts[
'fg'] = os.path.abspath(brain_mask)
890 if len(subcortex_labels) > 0:
891 opts[
'sb'] = subcortex_labels
892 if len(brainstem_labels) > 0:
893 opts[
'bs'] = brainstem_labels
894 if len(cerebellum_labels) > 0:
895 opts[
'cb'] = cerebellum_labels
896 if cortical_hull_dmap:
897 cortical_hull_dmap = os.path.abspath(cortical_hull_dmap)
898 opts[
'output-inner-cortical-distance'] = cortical_hull_dmap
899 makedirs(cortical_hull_dmap)
901 run(
'subdivide-brain-image', args=[segmentation, name], opts=opts)
909 def recon_boundary(name, mask, blur=1, edge_length=1, temp=None):
910 """Reconstruct surface of mask.""" 912 assert name,
"Invalid 'name' argument" 913 assert mask,
"Invalid 'mask' argument" 915 with ExitStack()
as stack:
916 dmap = push_output(stack, calculate_distance_map(mask, temp=temp))
917 surf = push_output(stack, extract_isosurface(dmap, blur=blur))
918 return remesh_surface(surf, oname=name, edge_length=edge_length)
921 def recon_brain_surface(name, mask, temp=None):
922 """Reconstruct surface of brain mask.""" 923 return recon_boundary(name, mask, blur=2, temp=temp)
926 def recon_brainstem_plus_cerebellum_surface(name, mask=None, regions=None,
927 region_id_array=_region_id_array,
928 cortex_mask_array=_cortex_mask_array,
930 """Reconstruct surface of merged brainstem plus cerebellum region.""" 932 assert mask
or regions,
"Either 'regions' or 'mask' argument required" 933 name = os.path.abspath(name)
935 temp = os.path.abspath(temp)
937 temp = os.path.dirname(name)
938 with ExitStack()
as stack:
940 mask = push_output(stack, binarize_brainstem_plus_cerebellum(regions, temp=temp))
941 mesh = push_output(stack, nextname(name, temp=temp))
942 recon_boundary(mesh, mask, blur=1, temp=temp)
944 run(
'project-onto-surface', args=[mesh, mesh],
945 opts={
'constant': 7,
'name': region_id_array,
'type':
'short',
946 'point-data':
False,
'cell-data':
True})
947 if cortex_mask_array:
948 run(
'project-onto-surface', args=[mesh, mesh],
949 opts={
'constant': 0,
'name': cortex_mask_array,
'type':
'uchar',
950 'point-data':
False,
'cell-data':
True})
955 def recon_cortical_surface(name, mask=None, regions=None,
956 hemisphere=Hemisphere.Unspecified,
957 corpus_callosum_mask=None, temp=None):
958 """Reconstruct initial surface of right and/or left cerebrum. 960 The input is either a regions label image generated by `subdivide-brain-image` 961 or a custom binary mask created from a given brain segmentation. The boundary of 962 the mask must be sufficiently close to the WM/cGM interface. 964 Attention: Order of arguments may differ from the order of parameter help below! 965 Pass parameter values as keyword argument, e.g., regions='regions.nii.gz'. 970 File path of output surface file. 972 File path of binary mask to use instead of `regions` segments. 973 When this argument is given, both `regions` and `hemisphere` are ignored. 975 File path of regions image with right and left cerebrum labelled. 976 Ignored when a custom `mask` image is given instead. 977 hemisphere : Hemisphere 978 Which hemisphere of the cerebral cortex to reconstruct. If Hemisphere.Both, 979 the surfaces of both hemispheres are reconstructed and the two file paths 980 returned as a 2-tuple. When Hemisphere.Unspecified, the hemisphere is 981 determined from the output file `name` if possible. 982 corpus_callosum_mask : str, optional 983 File path of binary mask of segmented corpus callosum. 984 Used to disable clustering based hole filling of implicit 985 surface distance for points nearby the corpus callosum. 987 Path of temporary working directory. Intermediate files are written to 988 this directory and deleted on exit unless the global `debug` flag is set. 989 When not specified, intermediate files are written to the same directory 990 as the output mesh file, i.e., the directory of `name`. 995 Absolute path of output surface mesh file. 996 A 2-tuple of (right, left) surface mesh file paths is returned when 997 no `mask` but a `regions` image was given with `hemisphere=Hemisphere.Both`. 1001 assert name,
"Invalid 'name' argument" 1002 assert mask
or regions,
"Either 'regions' or 'mask' argument required" 1003 name = os.path.abspath(name)
1004 (base, ext) = splitext(name)
1009 temp = os.path.abspath(temp)
1011 temp = os.path.dirname(name)
1012 if base.endswith(
'-lh')
or base.endswith(
'.lh')
or base.startswith(
'lh.'):
1013 if base.startswith(
'lh.'):
1017 if hemisphere == Hemisphere.Right:
1018 sys.stderr.write(
"Warning: Cortical surface file name suggests left hemisphere, but right hemisphere is being reconstructed!\n")
1019 elif hemisphere == Hemisphere.Unspecified:
1020 hemisphere = Hemisphere.Left
1021 elif base.endswith(
'-rh')
or base.endswith(
'.rh')
or base.startswith(
'rh.'):
1022 if base.startswith(
'rh.'):
1026 if hemisphere == Hemisphere.Left:
1027 sys.stderr.write(
"Warning: Cortical surface file name suggests right hemisphere, but left hemisphere is being reconstructed!\n")
1028 elif hemisphere == Hemisphere.Unspecified:
1029 hemisphere = Hemisphere.Right
1030 if not mask
and hemisphere == Hemisphere.Both:
1031 rname = recon_cortical_surface(
'{}-{}{}'.format(base, hemi2str(Hemisphere.Right), ext),
1032 regions=regions, hemisphere=Hemisphere.Right,
1033 corpus_callosum_mask=corpus_callosum_mask, temp=temp)
1034 lname = recon_cortical_surface(
'{}-{}{}'.format(base, hemi2str(Hemisphere.Left), ext),
1035 regions=regions, hemisphere=Hemisphere.Left,
1036 corpus_callosum_mask=corpus_callosum_mask, temp=temp)
1037 return (rname, lname)
1038 base = os.path.basename(base)
1039 hemi = hemi2str(hemisphere)
1040 with ExitStack()
as stack:
1042 mask =
'{}-{}-mask.nii.gz'.format(base, hemi)
1043 mask = push_output(stack, binarize_white_matter(regions, name=mask, hemisphere=hemisphere, temp=temp))
1044 dmap = push_output(stack, calculate_distance_map(mask, temp=temp))
1045 hull = push_output(stack, extract_convex_hull(dmap, temp=temp))
1048 'implicit-surface': dmap,
1050 'distance-measure':
'normal',
1051 'distance-threshold': 2,
1052 'distance-max-depth': 5,
1053 'distance-hole-filling':
True,
1055 'gauss-curvature': .4,
1056 'gauss-curvature-outside': 1,
1057 'gauss-curvature-minimum': .1,
1058 'gauss-curvature-maximum': .5,
1060 'repulsion-distance': .5,
1061 'repulsion-width': 1,
1062 'optimizer':
'EulerMethod',
1064 'steps': [300, 200],
1066 'extrinsic-energy':
True,
1071 'fast-collision-test':
True,
1072 'non-self-intersection':
True,
1074 'min-edge-length': .5,
1075 'max-edge-length': 1,
1076 'triangle-inversion':
True,
1077 'reset-status':
True 1080 if corpus_callosum_mask:
1081 out1 = nextname(name, temp=temp)
1082 out1 = push_output(stack, add_corpus_callosum_mask(hull, mask=corpus_callosum_mask, oname=out1))
1083 out2 = nextname(out1)
1086 out2 = nextname(name, temp=temp)
1087 out2 = push_output(stack, deform_mesh(out1, out2, opts=opts))
1088 if corpus_callosum_mask:
1089 out3 = push_output(stack, del_corpus_callosum_mask(out2))
1092 remove_intersections(out3, oname=name, max_attempt=10)
1096 def join_cortical_surfaces(name, regions, right_mesh, left_mesh, bs_cb_mesh=None,
1097 region_id_array=_region_id_array, cortex_mask_array=_cortex_mask_array,
1098 internal_mesh=None, temp=None, check=True):
1099 """Join cortical surfaces of right and left hemisphere at medial cut. 1101 Optionally, the brainstem plus cerebellum surface can be joined with the 1102 resulting surface mesh as well at the brainstem cut. This, however, currently 1103 fails for a significant number of cases to find a cutting plane which intersects 1104 the joined surface mesh with one closed curve near the superior brainstem cut 1105 and is therefore not recommended. 1107 Attention: Order of arguments may differ from the order of parameter help below! 1108 Pass parameter values as keyword argument, e.g., regions='regions.nii.gz'. 1113 Path of output surface mesh file. 1115 Path of regions image file with right and left cerebrum labelled. 1116 The boundary between right and left cerebrum segments defines the cut 1117 where the two surfaces are merged. Similarly for the brainstem cut 1118 when `bs_cb_mesh` surface given. 1120 Path of right surface mesh file. 1122 Path of left surface mesh file. 1123 bs_cb_mesh : str, optional 1124 Path of brainstem plus cerebellum surface mesh file. 1125 region_id_array : str, optional 1126 Name of cell data array with labels corresponding to the volumetric `regions` 1127 label of which the surface is the boundary of. Required for `cortex_mask_array`. 1128 Note: Required by subsequent cortical surface reconstruction steps. 1129 cortex_mask_array : str, optional 1130 Name of cortex mask cell data array. If None or an empty string, 1131 no cortex mask data array is added to the output surface file. 1132 Note: Required by subsequent cortical surface reconstruction steps. 1133 internal_mesh : str, optional 1134 Path of output surface mesh file with cutting plane cross sections 1135 that divide the interior of the merged surfaces into right, left, 1136 and brainstem plus cerebellum (when `bs_cb_mesh` surface given). 1138 Path of temporary working directory. Intermediate files are written to 1139 this directory and deleted on exit unless the global `debug` flag is set. 1140 When not specified, intermediate files are written to the same directory 1141 as the output mesh file, i.e., the directory of `name`. 1143 Check topology and consistency of output surface mesh. 1148 Absolute path of output surface mesh file. 1153 assert name,
"Output file 'name' required" 1154 assert regions,
"Input 'regions' label image required" 1155 assert right_mesh,
"Input 'right_mesh' required" 1156 assert left_mesh,
"Input 'left_mesh' required" 1160 name = os.path.abspath(name)
1162 temp = os.path.dirname(name)
1164 temp = os.path.abspath(temp)
1167 region_id_array_name = region_id_array
1169 region_id_array_name = _region_id_array
1171 with ExitStack()
as stack:
1173 joined = push_output(stack, nextname(name, temp=temp))
1174 if force
or not os.path.isfile(joined):
1175 surfaces = [right_mesh, left_mesh]
1176 if bs_cb_mesh
and join_bs_cb:
1177 surfaces.append(bs_cb_mesh)
1178 run(
'merge-surfaces',
1179 opts={
'input': surfaces,
'output': joined,
'labels': regions,
'source-array': region_id_array_name,
1180 'tolerance': 1,
'largest':
True,
'dividers': (internal_mesh !=
None),
'snap-tolerance': .1,
1181 'smoothing-iterations': 100,
'smoothing-lambda': 1})
1182 if bs_cb_mesh
and join_bs_cb:
1183 run(
'calculate-element-wise', args=[joined], opts=[(
'cell-data', region_id_array_name),
1184 (
'map', (-1, -3), (-2, -1), (-3, -2), (3, 7)),
1186 del_mesh_attr(joined, pointdata=region_id_array_name)
1189 info = evaluate_surface(joined, mesh=
True, topology=
True)
1190 num_boundaries = get_num_boundaries(info)
1191 if num_boundaries != 0:
1192 raise Exception(
'Merged surface is non-closed, no. of boundaries: {}'.format(num_boundaries))
1193 euler = get_euler_characteristic(info)
1195 if bs_cb_mesh: expect_euler = 4
1196 else: expect_euler = 3
1197 else: expect_euler = 2
1198 if euler != expect_euler:
1199 raise Exception(
'Merged surface with dividers has unexpected Euler characteristic: {} (expected {})'.format(euler, expect_euler))
1201 checked = push_output(stack, nextname(joined))
1202 if force
or not os.path.isfile(checked):
1203 remove_intersections(joined, checked, max_attempt=10, smooth_iter=10)
1208 if bs_cb_mesh
and not join_bs_cb:
1209 joined_with_bscb = push_output(stack, nextname(joined))
1210 run(
'merge-surfaces',
1211 opts={
'input': [joined, bs_cb_mesh],
'output': joined_with_bscb,
'labels': regions,
1212 'source-array': region_id_array_name,
'tolerance': .5,
'join':
False})
1213 run(
'calculate-element-wise', args=[joined_with_bscb], opts=[(
'cell-data', region_id_array_name), (
'map', (3, 7)), (
'out', joined_with_bscb)])
1214 modified_bscb = push_output(stack, nextname(joined_with_bscb))
1215 check_intersections(joined_with_bscb, modified_bscb)
1216 run(
'calculate-element-wise', args=[modified_bscb],
1217 opts=[(
'cell-data', region_id_array_name), (
'label', 7), (
'set', 1), (
'pad', 0),
1218 (
'mul',
'CollisionType'), (
'binarize', 0, 0), (
'out', modified_bscb,
'binary',
'SelectionMask')])
1219 run(
'extract-pointset-cells', args=[modified_bscb, modified_bscb], opts=[(
'where',
'SelectionMask'), (
'eq', 0)])
1220 del_mesh_attr(modified_bscb, pointdata=
'CollisionMask', celldata=[
'SelectionMask',
'CollisionType'])
1221 joined_with_modified_bscb = push_output(stack, nextname(modified_bscb))
1222 append_surfaces(joined_with_modified_bscb, surfaces=[joined, modified_bscb], merge=
True, tol=0)
1223 joined = joined_with_modified_bscb
1226 if cortex_mask_array:
1227 with output(binarize_cortex(regions, temp=temp), delete=
True)
as mask:
1228 joined = push_output(stack, add_cortex_mask(joined, mask, name=cortex_mask_array, region_id_array=region_id_array_name))
1230 info = evaluate_surface(joined, mesh=
True, opts=[(
'where', cortex_mask_array), (
'gt', 0)])
1231 num_components = get_num_components(info)
1232 if num_components != 2:
1233 raise Exception(
"No. of cortex mask components: {} (expected 2)".format(num_components))
1238 joined_without_dividers = push_output(stack, nextname(joined))
1239 internal_mesh = os.path.abspath(internal_mesh)
1240 run(
'extract-pointset-cells', args=[joined, internal_mesh], opts=[(
'where', region_id_array_name), (
'lt', 0)])
1241 run(
'extract-pointset-cells', args=[joined, joined_without_dividers], opts=[(
'where', region_id_array_name), (
'gt', 0)])
1242 joined = joined_without_dividers
1244 if not region_id_array:
1246 del_mesh_attr(internal_mesh, celldata=region_id_array_name)
1247 del_mesh_attr(joined, celldata=region_id_array_name)
1248 rename(joined, name)
1250 return (name, internal_mesh)
1254 def recon_white_surface(name, t2w_image, wm_mask, gm_mask, cortex_mesh, bs_cb_mesh=None,
1255 cortex_mask_array=_cortex_mask_array, region_id_array=_region_id_array,
1256 t1w_image=None, cortical_hull_dmap=None, segmentation=None,
1257 subcortex_labels =[], subcortex_mask =None,
1258 ventricles_labels=[], ventricles_mask=None, ventricles_dmap=None,
1259 cerebellum_labels=[], cerebellum_mask=None, cerebellum_dmap=None,
1260 temp=None, check=True):
1261 """Reconstruct white surface based on WM/cGM image edge distance forces. 1263 This step refines the initial surface mesh. In areas where the image 1264 edges are weak, no change of position should be enforced. Otherwise, move 1265 the surface points a limited distance from their initial position to a 1266 location along the normal direction where the image gradient is stronger. 1267 The resulting surface should delinate the WM/cGM interface at least as 1268 good as the initial surface mesh, but likely better in many places. 1270 Attention: Order of arguments may differ from the order of parameter help below! 1271 Pass parameter values as keyword argument, e.g., wm_mask='wm.nii.gz'. 1276 Path of output surface mesh file. 1278 Path of initial cortical surface mesh file. This surface is reconstructed 1279 from a given brain segmentation with possible errors such as in particular 1280 CSF mislabelled as either WM or GM. 1281 bs_cb_mesh : str, optional 1282 Path of brainstem plus cerebellum mesh file. 1283 cortex_mask_array : str 1284 Name of cortex mask cell data array. Only nodes adjacent to only cortical 1285 faces are deformed. Non-cortical surface nodes remain unchanged. 1287 t1w_image : str, optional 1288 Path of T1-weighted intensity image file. 1290 Path of T2-weighted intensity image file. 1292 Path of binary WM segmentation image file. 1294 Path of binary cGM segmentation image file. 1296 segmentation : str, optional 1297 Structural brain segmentation label image used to extract subcortical 1298 structures, ventricles, and cerebellum when structure labels instead 1299 of a pre-computed binary mask or structure distance image, respectively, 1300 is given. Otherwise, this image path is unused. 1301 subcortex_labels : int, list, optional 1302 Labels of `segmentation` labels corresponding to subcortical structures. 1303 Used to create a binary mask image when no `subcortex_mask` is given. 1304 subcortex_mask : str, optional 1305 Path of subcortical structures mask file. These structures are excluded 1306 from the image foreground such that the image-based edge distance 1307 force is not mislead by a WM/dGM edge of a subcortical structure. 1308 ventricles_labels : int, list, optional 1309 Labels of `segmentation` labels corresponding to lateral ventricles. 1310 Used to create a binary mask image when no `ventricles_mask` is given. 1311 ventricles_mask : str, optional 1312 Path of binary mask image file of lateral ventricles. 1313 ventricles_dmap : str, optional 1314 Path of distance map computed from lateral ventricles segment. 1315 cerebellum_labels : int, list, optional 1316 Labels of `segmentation` labels corresponding to cerebellum. 1317 Used to create a binary mask image when no `cerebellum_mask` is given. 1318 cerebellum_mask : str, optional 1319 Path of binary mask image file of cerebellum. 1320 cerebellum_dmap : str, optional 1321 Path of distance map computed from cerebellum segment. 1322 cortical_hull_dmap : str, optional 1323 Path of distance image from each voxel to the cortical hull with 1324 positive values inside the cortical hull. This image is an optional 1325 output of the `subdivide-brain-image` tool. 1328 Path of temporary working directory. Intermediate files are written to 1329 this directory and deleted on exit unless the global `debug` flag is set. 1330 When not specified, intermediate files are written to the same directory 1331 as the output mesh file, i.e., the directory of `name`. 1333 Check final surface mesh for self-intersections and try to remove these. 1338 Absolute path of output surface mesh file. 1343 assert name,
"Output file 'name' required" 1344 assert t2w_image,
"T2-weighted intensity image required" 1345 assert wm_mask,
"White matter segmentation mask required" 1346 assert gm_mask,
"Gray matter segmentation mask required" 1347 assert cortex_mesh,
"Initial cortical surface mesh required" 1349 name = os.path.abspath(name)
1350 (base, ext) = splitext(name)
1355 temp = os.path.dirname(name)
1357 temp = os.path.abspath(temp)
1359 with ExitStack()
as stack:
1361 init_mesh = push_output(stack, nextname(name, temp=temp))
1365 append_surfaces(init_mesh, surfaces=[cortex_mesh, bs_cb_mesh], merge=
False)
1366 cortex_mesh = init_mesh
1369 status_array =
'InitialStatus' 1370 run(
'copy-pointset-attributes', args=[cortex_mesh, cortex_mesh, init_mesh],
1371 opts={
'celldata-as-pointdata': [cortex_mask_array, status_array,
'other',
'binary'],
'unanimous':
None})
1372 run(
'erode-scalars', args=[init_mesh, init_mesh], opts={
'array': status_array,
'iterations': 8})
1375 opts={
'image': t2w_image,
1379 'edge-distance-type':
'Neonatal T2-w WM/cGM',
1380 'edge-distance-max-depth': 5,
1381 'edge-distance-median': 1,
1382 'edge-distance-smoothing': 1,
1383 'edge-distance-averaging': [4, 2, 1],
1385 'gauss-curvature': .5,
1386 'gauss-curvature-outside': .5,
1387 'gauss-curvature-minimum': .1,
1388 'gauss-curvature-maximum': .2,
1389 'optimizer':
'EulerMethod',
1392 'extrinsic-energy':
True,
1396 'reset-status':
True,
1397 'non-self-intersection':
True,
1398 'fast-collision-test':
True,
1401 'repulsion-distance': .5,
1402 'repulsion-width': 1,
1404 'min-edge-length': .5,
1405 'max-edge-length': 1}
1407 opts[
't1w-image'] = t1w_image
1408 if cortical_hull_dmap:
1409 opts[
'inner-cortical-distance-image'] = cortical_hull_dmap
1410 if (segmentation
and ventricles_labels)
or ventricles_mask
or ventricles_dmap:
1411 if not ventricles_dmap:
1412 if not ventricles_mask:
1413 ventricles_mask = os.path.join(temp,
'ventricles-mask.nii.gz')
1414 ventricles_mask = push_output(stack, binarize(name=ventricles_mask, segmentation=segmentation, labels=ventricles_labels))
1415 ventricles_dmap = push_output(stack, calculate_distance_map(ventricles_mask, temp=temp))
1416 opts[
'ventricles-distance-image'] = ventricles_dmap
1417 if (segmentation
and cerebellum_labels)
or cerebellum_mask
or cerebellum_dmap:
1418 if not cerebellum_dmap:
1419 if not cerebellum_mask:
1420 cerebellum_mask = os.path.join(temp,
'cerebellum-mask.nii.gz')
1421 cerebellum_mask = push_output(stack, binarize(name=cerebellum_mask, segmentation=segmentation, labels=cerebellum_labels))
1422 cerebellum_dmap = push_output(stack, calculate_distance_map(cerebellum_mask, temp=temp))
1423 opts[
'cerebellum-distance-image'] = cerebellum_dmap
1424 if (segmentation
and subcortex_labels)
or subcortex_mask:
1425 if not subcortex_mask:
1426 subcortex_mask = os.path.join(temp,
'subcortex-mask.nii.gz')
1427 subcortex_mask = push_output(stack, binarize(name=subcortex_mask, segmentation=segmentation, labels=subcortex_labels))
1428 mask = os.path.join(temp, os.path.basename(base) +
'-foreground.nii.gz')
1429 opts[
'mask'] = push_output(stack, white_refinement_mask(mask, subcortex_mask))
1430 mesh = push_output(stack, deform_mesh(init_mesh, opts=opts))
1432 run(
'extract-pointset-cells', args=[mesh, mesh], opts=[(
'where', region_id_array), (
'ne', 7)])
1435 smooth = push_output(stack, smooth_surface(mesh, iterations=100, lambda_value=.33, mu=-.34, weighting=
'combinatorial', excl_node=
True, mask=status_array))
1439 remove_intersections(smooth, oname=name, mask=status_array)
1442 del_mesh_attr(smooth, pointdata=status_array)
1443 rename(smooth, name)
1448 def recon_pial_surface(name, t2w_image, wm_mask, gm_mask, white_mesh,
1449 bs_cb_mesh=None, brain_mask=None, remesh=0, outside_white_mesh=True,
1450 region_id_array=_region_id_array, cortex_mask_array=_cortex_mask_array,
1451 temp=None, check=True):
1452 """Reconstruct pial surface based on cGM/CSF image edge distance forces. 1454 When the pial surface is not allowed to intersect the white surface mesh 1455 (i.e., `outside_white_surface=True`), the following steps are performed: 1456 1. Deform cortical nodes outside up to a maximum distance from the white surface 1457 - Hard non-self-intersection constraint disabled 1458 2. Blend between initial and outside white surface node position 1459 3. Merge white and pial surfaces into single connected surface mesh 1460 - If white and pial surfaces intersect afterwards, try to 1461 remove those by smoothing the surface near the intersections 1462 4. Deform pial surface nodes towards cGM/CSF image edges 1463 - Hard non-self-intersection constraint enabled 1464 5. Remove white surface mesh from output mesh file 1466 Otherwise, only step 4 is performed to deform the input white surface mesh 1467 towards the outer cortical surface based on image edge forces. In this case, 1468 the resulting pial surface mesh may potentially intersect the `white_mesh`. 1470 Attention: Order of arguments may differ from the order of parameter help below! 1471 Pass parameter values as keyword argument, e.g., wm_mask='wm.nii.gz'. 1476 Path of output surface mesh file. 1478 Path of T2-weighted intensity image file. 1480 Path of binary WM segmentation image file. 1482 Path of binary cGM segmentation image file. 1484 Path of white surface mesh file. 1485 bs_cb_mesh : str, optional 1486 Path of brainstem plus cerebellum surface mesh file. If specified, 1487 this surface mesh is added to the initial pial surface mesh for step 4 1488 of the pial surface reconstruction such that the pial surface does 1489 not deform into the brainstem plus cerebellum region. 1491 Number of Euler steps between local remeshing is performed. 1492 Generally, it is better to not remesh (`remesh=0`) such that ID based 1493 one-to-one vertex correspondences between white and pial surface meshes 1494 are preserved. These vertex correspondences simplify further analysis. 1495 outside_white_mesh : bool 1496 Whether pial surface mesh is required to be strictly outside the white 1497 surface mesh, i.e., the pial surface may not intersect the `white_mesh`. 1498 This is recommended, but may currently fail for some cases when the 1499 blended offset surface intersects the white surface mesh after step 3. 1500 brain_mask : str, optional 1501 Path of brain extraction mask file. 1502 temp : str, optional 1503 Path of temporary working directory. Intermediate files are written to 1504 this directory and deleted on exit unless the global `debug` flag is set. 1505 When not specified, intermediate files are written to the same directory 1506 as the output mesh file, i.e., the directory of `name`. 1508 Check topology and consistency of intermediate surface meshes. 1513 Absolute path of output surface mesh file. 1518 assert name,
"Output file 'name' required" 1519 assert t2w_image,
"T2-weighted intensity image required" 1520 assert wm_mask,
"White matter segmentation mask required" 1521 assert gm_mask,
"Gray matter segmentation mask required" 1522 assert white_mesh,
"White surface mesh required" 1524 name = os.path.abspath(name)
1525 (base, ext) = splitext(name)
1530 temp = os.path.dirname(name)
1532 temp = os.path.abspath(temp)
1534 with ExitStack()
as stack:
1537 mask = push_output(stack, os.path.join(temp, os.path.basename(base) +
'-foreground.nii.gz'))
1538 run(
'extract-pointset-surface', args=[], opts={
'input': white_mesh,
'mask': mask,
'reference': t2w_image,
'outside':
True})
1540 run(
'calculate-element-wise', args=[mask], opts=[(
'mul', brain_mask), (
'out', mask)])
1543 init_mesh = push_output(stack, nextname(name, temp=temp))
1544 status_array =
'Status' 1545 if outside_white_mesh:
1548 run(
'copy-pointset-attributes', args=[white_mesh, white_mesh, init_mesh], opts={
'celldata-as-pointdata': [region_id_array, status_array],
'unanimous':
None})
1549 run(
'calculate-element-wise', args=[init_mesh], opts=[(
'point-data', status_array), (
'label', 7), (
'set', 0), (
'pad', 1), (
'out', init_mesh,
'binary')])
1552 opts={
'normal-force': 1,
1554 'optimizer':
'EulerMethod',
1557 'max-displacement': max(get_voxel_size(t2w_image)),
1558 'non-self-intersection':
True,
1559 'fast-collision-test':
True,
1561 'min-active':
'10%',
1563 offset_mesh = push_output(stack, deform_mesh(init_mesh, opts=opts))
1564 if debug == 0: try_remove(init_mesh)
1567 blended_mesh = push_output(stack, nextname(offset_mesh))
1568 run(
'copy-pointset-attributes', args=[offset_mesh, offset_mesh, blended_mesh], opts={
'celldata-as-pointdata': cortex_mask_array,
'unanimous':
None})
1569 run(
'blend-surface', args=[white_mesh, blended_mesh, blended_mesh], opts={
'where': cortex_mask_array,
'gt': 0,
'smooth-iterations': 3})
1570 run(
'calculate-element-wise', args=[blended_mesh], opts=[(
'cell-data', region_id_array), (
'map', (1, 3), (2, 4)), (
'out', blended_mesh)])
1571 if debug == 0: try_remove(offset_mesh)
1574 cortex_mesh = push_output(stack, nextname(blended_mesh))
1575 append_surfaces(cortex_mesh, surfaces=[white_mesh, blended_mesh], merge=
True, tol=0)
1576 info = evaluate_surface(cortex_mesh, cortex_mesh, mesh=
True, opts=[(
'where', cortex_mask_array), (
'gt', 0)])
1577 run(
'calculate-element-wise', args=[cortex_mesh], opts=[(
'cell-data', cortex_mask_array), (
'mask',
'BoundaryMask'), (
'set', 0), (
'out', cortex_mesh)])
1578 run(
'calculate-element-wise', args=[cortex_mesh], opts=[(
'cell-data', region_id_array), (
'mask',
'BoundaryMask'), (
'add', 4), (
'out', cortex_mesh)])
1579 num_components = get_num_components(info)
1580 if num_components < 2:
1581 raise Exception(
"No. of cortex mask components: {} (expected 2)".format(num_components))
1582 elif num_components > 2:
1583 run(
'calculate-element-wise', args=[cortex_mesh],
1584 opts=[(
'cell-data',
'ComponentId'), (
'threshold-gt', 2), (
'pad', 0), (
'mul', cortex_mask_array),
1585 (
'clamp-above', 1), (
'out', cortex_mesh,
'binary', cortex_mask_array)])
1586 del_mesh_attr(cortex_mesh, name=[
'ComponentId',
'BoundaryMask'], celldata=
'DuplicatedMask')
1587 if debug == 0: try_remove(blended_mesh)
1590 init_mesh = push_output(stack, nextname(cortex_mesh))
1591 remove_white_pial_intersections(cortex_mesh, init_mesh, region_id_array=region_id_array)
1595 if debug == 0: try_remove(cortex_mesh)
1597 run(
'calculate-element-wise', args=[white_mesh], opts=[(
'cell-data', region_id_array), (
'map', (1, 3), (2, 4)), (
'out', init_mesh)])
1601 append_surfaces(init_mesh, surfaces=[init_mesh, bs_cb_mesh], merge=
False)
1604 run(
'copy-pointset-attributes', args=[init_mesh, init_mesh], opts={
'celldata-as-pointdata': [region_id_array, status_array],
'unanimous':
None})
1605 run(
'calculate-element-wise', args=[init_mesh], opts=[(
'point-data', status_array), (
'label', 3, 4), (
'set', 1), (
'pad', 0), (
'out', init_mesh,
'binary')])
1608 opts={
'image': t2w_image,
1613 'edge-distance-type':
'Neonatal T2-w cGM/CSF',
1614 'edge-distance-max-depth': 5,
1615 'edge-distance-median': 1,
1616 'edge-distance-smoothing': 1,
1617 'edge-distance-averaging': [4, 2, 1],
1619 'gauss-curvature': .8,
1620 'gauss-curvature-inside': 2,
1621 'gauss-curvature-minimum': .1,
1622 'gauss-curvature-maximum': .4,
1623 'negative-gauss-curvature-action':
'inflate',
1625 'repulsion-distance': .5,
1626 'repulsion-width': 1,
1627 'optimizer':
'EulerMethod',
1629 'steps': [25, 50, 100],
1633 'reset-status':
True,
1634 'non-self-intersection':
True,
1635 'fast-collision-test':
True,
1638 'min-edge-length': .3,
1639 'max-edge-length': 2,
1644 'triangle-inversion':
False}
1645 mesh = push_output(stack, deform_mesh(init_mesh, opts=opts))
1646 extract_surface(mesh, name, array=region_id_array, labels=[-1, -2, -3, 3, 4, 5, 6])
1651 def split_cortical_surfaces(joined_mesh, right_name=None, left_name=None, internal_mesh=None, temp=None):
1652 """Save cortical surfaces of right and left hemispheres as separate meshes.""" 1660 assert joined_mesh,
"Joined cortical surface mesh required" 1661 assert right_name
or left_name,
"At least either 'right_name' or 'left_name' required" 1663 temp = os.path.dirname(joined_mesh)
1664 with ExitStack()
as stack:
1666 name = os.path.join(temp, os.path.splitext(joined_mesh)[0] +
'+internal.vtp')
1667 joined_mesh = push_output(stack, append_surfaces(name, surfaces=[joined_mesh, internal_mesh], merge=
True, tol=0))
1668 if right_name: extract_surface(joined_mesh, right_name, labels=[-1, -3, 1, 3, 5])
1669 if left_name: extract_surface(joined_mesh, left_name, labels=[-1, -2, 2, 4, 6])