--- a +++ b/docs/mesh/meshes.html @@ -0,0 +1,4444 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> +<meta name="generator" content="pdoc 0.10.0" /> +<title>pymskt.mesh.meshes API documentation</title> +<meta name="description" content="" /> +<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/sanitize.min.css" integrity="sha256-PK9q560IAAa6WVRRh76LtCaI8pjTJ2z11v0miyNNjrs=" crossorigin> +<link rel="preload stylesheet" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/11.0.1/typography.min.css" integrity="sha256-7l/o7C8jubJiy74VsKTidCy1yBkRtiUGbVkYBylBqUg=" crossorigin> +<link rel="stylesheet preload" as="style" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/github.min.css" crossorigin> +<style>:root{--highlight-color:#fe9}.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target{background:var(--highlight-color);padding:.2em 0}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}dt:target .name{background:var(--highlight-color)}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}td{padding:0 .5em}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style> +<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style> +<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style> +<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js" integrity="sha256-Uv3H6lx7dJmRfRvH8TH6kJD1TSK1aFcwgx+mdg3epi8=" crossorigin></script> +<script>window.addEventListener('DOMContentLoaded', () => hljs.initHighlighting())</script> +</head> +<body> +<main> +<article id="content"> +<header> +<h1 class="title">Module <code>pymskt.mesh.meshes</code></h1> +</header> +<section id="section-intro"> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">from logging import error +from posixpath import supports_unicode_filenames +import warnings +import numpy as np +import vtk +from pymskt.image.main import apply_transform_retain_array +from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy +import pyacvd +import pyvista as pv +import SimpleITK as sitk +import os +import random +import string +# import pyfocusr # MAKE THIS AN OPTIONAL IMPORT? + +import pymskt +from pymskt.mesh import createMesh +from pymskt.utils import safely_delete_tmp_file, copy_image_transform_to_mesh +from pymskt.image import read_nrrd, crop_bone_based_on_width +from pymskt.mesh.utils import vtk_deep_copy +from pymskt.mesh.meshTools import (gaussian_smooth_surface_scalars, + get_mesh_physical_point_coords, + get_cartilage_properties_at_points, + smooth_scalars_from_second_mesh_onto_base, + transfer_mesh_scalars_get_weighted_average_n_closest, + resample_surface + ) +from pymskt.mesh.createMesh import create_surface_mesh +from pymskt.mesh.meshTransform import (SitkVtkTransformer, + get_versor_from_transform, + break_versor_into_center_rotate_translate_transforms, + apply_transform) +from pymskt.mesh.meshRegistration import non_rigidly_register, get_icp_transform +import pymskt.mesh.io as io + +class Mesh: + """ + An object to contain surface meshes for musculoskeletal anatomy. Includes helper + functions to build surface meshes, to process them, and to save them. + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + + + Attributes + ---------- + _mesh : vtk.vtkPolyData + Item passed from __init__, or created during life of class. + This is the main surface mesh of this class. + _seg_image : SimpleITK.Image + Segmentation image that can be used to create mesh. This is optional. + path_seg_image : str + Path to medical image (.nrrd) that can be loaded to create `_seg_image` + and then creat surface mesh `_mesh` + label_idx : int + Integer of anatomy to create surface mesh from `_seg_image` + min_n_pixels : int + Minimum number of pixels for an isolated island of a segmentation to be + retained + list_applied_transforms : list + A list of transformations applied to a surface mesh. + This list allows for undoing of most recent transform, or undoing + all of them by iterating over the list in reverse. + + Methods + ---------- + + """ + def __init__(self, + mesh=None, + seg_image=None, + path_seg_image=None, + label_idx=None, + min_n_pixels=5000 + ): + """ + Initialize Mesh class + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + """ + if type(mesh) in (str,): #accept path like objects? + print('mesh string passed, loading mesh from disk') + self._mesh = io.read_vtk(mesh) + else: + self._mesh = mesh + self._seg_image = seg_image + self._path_seg_image = path_seg_image + self._label_idx = label_idx + self._min_n_pixels = min_n_pixels + + self._list_applied_transforms = [] + + def read_seg_image(self, + path_seg_image=None): + """ + Read segmentation image from disk. Must be a single file (e.g., nrrd, 3D dicom) + + Parameters + ---------- + path_seg_image : str, optional + Path to the medical image file to be loaded in, by default None + + Raises + ------ + Exception + If path_seg_image does not exist, exception is raised. + """ + # If passing new location/seg image name, then update variables. + if path_seg_image is not None: + self._path_seg_image = path_seg_image + + # If seg image location / name exist, then load image else raise exception + if (self._path_seg_image is not None): + self._seg_image = sitk.ReadImage(self._path_seg_image) + else: + raise Exception('No file path (self._path_seg_image) provided.') + + def create_mesh(self, + smooth_image=True, + smooth_image_var=0.3125 / 2, + marching_cubes_threshold=0.5, + label_idx=None, + min_n_pixels=None): + """ + Create a surface mesh from the classes `_seg_image`. If `_seg_image` + does not exist, then read it in using `read_seg_image`. + + Parameters + ---------- + smooth_image : bool, optional + Should the `_seg_image` be gaussian filtered, by default True + smooth_image_var : float, optional + Variance of gaussian filter to apply to `_seg_image`, by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold contour level to create surface mesh on, by default 0.5 + label_idx : int, optional + Label value / index to create mesh from, by default None + min_n_pixels : int, optional + Minimum number of continuous pixels to include segmentation island + in the surface mesh creation, by default None + + Raises + ------ + Exception + If the total number of pixels segmentated (`n_pixels_labelled`) is + < `min_n_pixels` then there is no object in the image. + Exception + If no `_seg_image` and no `label_idx` then we don't know what tissue to create the + surface mesh from. + Exception + If no `_seg_image` or `path_seg_image` then we have no image to create mesh from. + """ + # allow assigning label idx during mesh creation step. + if label_idx is not None: + self._label_idx = label_idx + + if self._seg_image is None: + self.read_seg_image() + + # Ensure the image has a certain number of pixels with the label of interest, otherwise there might be an issue. + if min_n_pixels is None: min_n_pixels = self._min_n_pixels + seg_view = sitk.GetArrayViewFromImage(self._seg_image) + n_pixels_labelled = sum(seg_view[seg_view == self._label_idx]) + + if n_pixels_labelled < min_n_pixels: + raise Exception('The mesh does not exist in this segmentation!, only {} pixels detected, threshold # is {}'.format(n_pixels_labelled, + marching_cubes_threshold)) + tmp_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + '.nrrd' + self._mesh = create_surface_mesh(self._seg_image, + self._label_idx, + smooth_image_var, + loc_tmp_save='/tmp', + tmp_filename=tmp_filename, + mc_threshold=marching_cubes_threshold, + filter_binary_image=smooth_image + ) + safely_delete_tmp_file('/tmp', + tmp_filename) + + def save_mesh(self, + filepath): + """ + Save the surface mesh from this class to disk. + + Parameters + ---------- + filepath : str + Location & filename to save the surface mesh (vtk.vtkPolyData) to. + """ + io.write_vtk(self._mesh, filepath) + + def resample_surface(self, + subdivisions=2, + clusters=10000 + ): + """ + Resample a surface mesh using the ACVD algorithm: + Version used: + - https://github.com/pyvista/pyacvd + Original version w/ more references: + - https://github.com/valette/ACVD + + Parameters + ---------- + subdivisions : int, optional + Subdivide the mesh to have more points before clustering, by default 2 + Probably not necessary for very dense meshes. + clusters : int, optional + The number of clusters (points/vertices) to create during resampling + surafce, by default 10000 + - This is not exact, might have slight differences. + """ + self._mesh = resample_surface(self._mesh, subdivisions=subdivisions, clusters=clusters) + + def apply_transform_to_mesh(self, + transform=None, + transformer=None, + save_transform=True): + """ + Apply a transformation to the surface mesh. + + Parameters + ---------- + transform : vtk.vtkTransform, optional + Transformation to apply to mesh, by default None + transformer : vtk.vtkTransformFilter, optional + Can supply transformFilter directly, by default None + save_transform : bool, optional + Should transform be saved to list of applied transforms, by default True + + Raises + ------ + Exception + No `transform` or `transformer` supplied - have not transformation + to apply. + """ + if (transform is not None) & (transformer is None): + transformer = vtk.vtkTransformPolyDataFilter() + transformer.SetTransform(transform) + + elif (transform is None) & (transformer is not None): + transform = transformer.GetTransform() + + if transformer is not None: + transformer.SetInputData(self._mesh) + transformer.Update() + self._mesh = vtk_deep_copy(transformer.GetOutput()) + + if save_transform is True: + self._list_applied_transforms.append(transform) + + else: + raise Exception('No transform or transformer provided') + + def reverse_most_recent_transform(self): + """ + Function to undo the most recent transformation stored in self._list_applied_transforms + """ + transform = self._list_applied_transforms.pop() + transform.Inverse() + self.apply_transform_to_mesh(transform=transform, save_transform=False) + + def reverse_all_transforms(self): + """ + Function to iterate over all of the self._list_applied_transforms (in reverse order) and undo them. + """ + while len(self._list_applied_transforms) > 0: + self.reverse_most_recent_transform() + + def non_rigidly_register( + self, + other_mesh, + as_source=True, + apply_transform_to_mesh=True, + return_transformed_mesh=False, + **kwargs + ): + """ + Function to perform non rigid registration between this mesh and another mesh. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Other mesh to use in registration process + as_source : bool, optional + Should the current mesh (in this object) be the source or the target, by default True + apply_transform_to_mesh : bool, optional + If as_source is True should we apply transformation to internal mesh, by default True + return_transformed_mesh : bool, optional + Should we return the registered mesh, by default False + + Returns + ------- + _type_ + _description_ + """ + # Setup the source & target meshes based on `as_source`` + if as_source is True: + source = self._mesh + target = other_mesh + elif as_source is False: + source = other_mesh + target = self._mesh + + # Get registered mesh (source to target) + source_transformed_to_target = non_rigidly_register( + target_mesh=target, + source_mesh=source, + **kwargs + ) + + # If current mesh is source & apply_transform_to_mesh is true then replace current mesh. + if (as_source is True) & (apply_transform_to_mesh is True): + self._mesh = source_transformed_to_target + + # curent mesh is target, or is source & want to return mesh, then return it. + if (as_source is False) or ((as_source is True) & (return_transformed_mesh is True)): + return source_transformed_to_target + + def rigidly_register( + self, + other_mesh, + as_source=True, + apply_transform_to_mesh=True, + return_transformed_mesh=False, + return_transform=False, + max_n_iter=100, + n_landmarks=1000, + reg_mode='similarity' + + ): + """ + Function to perform rigid registration between this mesh and another mesh. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Other mesh to use in registration process + as_source : bool, optional + Should the current mesh (in this object) be the source or the target, by default True + apply_transform_to_mesh : bool, optional + If as_source is True should we apply transformation to internal mesh, by default True + return_transformed_mesh : bool, optional + Should we return the registered mesh, by default False + max_n_iter : int, optional + Maximum number of iterations to perform, by default 100 + n_landmarks : int, optional + Number of landmarks to use in registration, by default 1000 + reg_mode : str, optional + Mode of registration to use, by default 'similarity' (similarity, rigid, or affine) + + Returns + ------- + _type_ + _description_ + """ + + if (return_transform is True) & (return_transformed_mesh is True): + raise Exception('Cannot return both transformed mesh and transform') + + if type(other_mesh) in (pymskt.mesh.meshes.BoneMesh, pymskt.mesh.meshes.Mesh): + other_mesh = other_mesh.mesh + + # Setup the source & target meshes based on `as_source`` + if as_source is True: + source = self._mesh + target = other_mesh + elif as_source is False: + source = other_mesh + target = self._mesh + + icp_transform = get_icp_transform( + source=source, + target=target, + max_n_iter=max_n_iter, + n_landmarks=n_landmarks, + reg_mode=reg_mode + ) + + # If current mesh is source & apply_transform_to_mesh is true then replace current mesh. + if (as_source is True) & (apply_transform_to_mesh is True): + self.apply_transform_to_mesh(transform=icp_transform) + + if return_transformed_mesh is True: + return self._mesh + + elif return_transform is True: + return icp_transform + + # curent mesh is target, or is source & want to return mesh, then return it. + elif (as_source is False) & (return_transformed_mesh is True): + return apply_transform(source=source, transform=icp_transform) + + else: + raise Exception('Nothing to return from rigid registration.') + + def copy_scalars_from_other_mesh_to_currect( + self, + other_mesh, + new_scalars_name='scalars_from_other_mesh', + weighted_avg=True, # Use weighted average, otherwise guassian smooth transfer + n_closest=3, + sigma=1., + idx_coords_to_smooth_base=None, + idx_coords_to_smooth_other=None, + set_non_smoothed_scalars_to_zero=True, + ): + """ + Convenience function to enable easy transfer scalars from another mesh to the current. + Can use either a gaussian smoothing function, or transfer using nearest neighbours. + + ** This function requires that the `other_mesh` is non-rigidly registered to the surface + of the mesh inside of this class. Or rigidly registered but using the same anatomy that + VERY closely matches. Otherwise, the transfered scalars will be bad. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Mesh we want to copy + new_scalars_name : str, optional + What to name the scalars being transfered to this current mesh, by default 'scalars_from_other_mesh' + weighted_avg : bool, optional + Should we use `weighted average` or `gaussian smooth` methods for transfer, by default True + n_closest : int, optional + If `weighted_avg` True, the number of nearest neighbours to use, by default 3 + sigma : float, optional + If `weighted_avg` False, the standard deviation of gaussian kernel, by default 1. + idx_coords_to_smooth_base : list, optional + If `weighted_avg` False, list of indices from current mesh to use in transfer, by default None + idx_coords_to_smooth_other : list, optional + If `weighted_avg` False, list of indices from `other_mesh` to use in transfer, by default None + set_non_smoothed_scalars_to_zero : bool, optional + Should all other indices (not included in idx_coords_to_smooth_other) be set to 0, by default True + """ + if type(other_mesh) is Mesh: + other_mesh = other_mesh.mesh + elif type(other_mesh) is vtk.vtkPolyData: + pass + else: + raise TypeError(f'other_mesh must be type `pymskt.mesh.Mesh` or `vtk.vtkPolyData` and received: {type(other_mesh)}') + + if weighted_avg is True: + transferred_scalars = transfer_mesh_scalars_get_weighted_average_n_closest( + self._mesh, + other_mesh, + n=n_closest + ) + else: + transferred_scalars = smooth_scalars_from_second_mesh_onto_base( + self._mesh, + other_mesh, + sigma=sigma, + idx_coords_to_smooth_base=idx_coords_to_smooth_base, + idx_coords_to_smooth_second=idx_coords_to_smooth_other, + set_non_smoothed_scalars_to_zero=set_non_smoothed_scalars_to_zero + ) + if (new_scalars_name is None) & (weighted_avg is True): + if transferred_scalars.shape[1] > 1: + n_arrays = other_mesh.GetPointData().GetNumberOfArrays() + array_names = [other_mesh.GetPointData().GetArray(array_idx).GetName() for array_idx in range(n_arrays)] + for idx, array_name in enumerate(array_names): + vtk_transferred_scalars = numpy_to_vtk(transferred_scalars[:,idx]) + vtk_transferred_scalars.SetName(array_name) + self._mesh.GetPointData().AddArray(vtk_transferred_scalars) + return + + vtk_transferred_scalars = numpy_to_vtk(transferred_scalars) + vtk_transferred_scalars.SetName(new_scalars_name) + self._mesh.GetPointData().AddArray(vtk_transferred_scalars) + + @property + def seg_image(self): + """ + Return the `_seg_image` object + + Returns + ------- + SimpleITK.Image + Segmentation image used to build the surface mesh + """ + return self._seg_image + + @seg_image.setter + def seg_image(self, new_seg_image): + """ + Set the `_seg_image` of the class to be the inputted `new_seg_image`. + + Parameters + ---------- + new_seg_image : SimpleITK.Image + New image to use for creating surface mesh. This can be used to provide image to + class if it was not provided during `__init__` + """ + self._seg_image = new_seg_image + + @property + def mesh(self): + """ + Return the `_mesh` object + + Returns + ------- + vtk.vtkPolyData + The main mesh of this class. + """ + return self._mesh + + @mesh.setter + def mesh(self, new_mesh): + """ + Set the `_mesh` of the class to be the inputted `new_mesh` + + Parameters + ---------- + new_mesh : vtk.vtkPolyData + New mesh for this class - or a method to provide a mesh to the class + after `__init__` has already been run. + """ + self._mesh = new_mesh + + @property + def point_coords(self): + """ + Convenience function to return the vertices (point coordinates) for the surface mesh. + + Returns + ------- + numpy.ndarray + Mx3 numpy array containing the x/y/z position of each vertex of the mesh. + """ + return get_mesh_physical_point_coords(self._mesh) + + @point_coords.setter + def point_coords(self, new_point_coords): + """ + Convenience function to change/update the vertices/points locations + + Parameters + ---------- + new_point_coords : numpy.ndarray + n_points X 3 numpy array to replace exisiting point coordinate locations + This can be used to easily/quickly update the x/y/z position of a set of points on a surface mesh. + The `new_point_coords` must include the same number of points as the mesh contains. + """ + orig_point_coords = get_mesh_physical_point_coords(self._mesh) + if new_point_coords.shape == orig_point_coords.shape: + self._mesh.GetPoints().SetData(numpy_to_vtk(new_point_coords)) + + + @property + def path_seg_image(self): + """ + Convenience function to get the `path_seg_image` + + Returns + ------- + str + Path to the segmentation image + """ + return self._path_seg_image + + @path_seg_image.setter + def path_seg_image(self, new_path_seg_image): + """ + Convenience function to set the `path_seg_image` + + Parameters + ---------- + new_path_seg_image : str + String to where segmentation image that should be loaded is. + """ + self._path_seg_image = new_path_seg_image + + @property + def label_idx(self): + """ + Convenience function to get `label_idx` + + Returns + ------- + int + Integer indeicating the index/value of the tissues in `seg_image` associated with this mesh. + """ + return self._label_idx + + @label_idx.setter + def label_idx(self, new_label_idx): + """ + Convenience function to set `label_idx` + + Parameters + ---------- + new_label_idx : int + Integer indeicating the index/value of the tissues in `seg_image` associated with this mesh. + """ + self._label_idx = new_label_idx + + @property + def min_n_pixels(self): + """ + Convenience function to get the minimum number of pixels for a segmentation region to be created as a mesh. + + Returns + ------- + int + Minimum number of pixels needed to create a mesh. Less than this and it will be skipped / error raised. + """ + return self._min_n_pixels + + @min_n_pixels.setter + def min_n_pixels(self, new_min_n_pixels): + """ + Convenience function to set the minimum number of pixels for a segmentation region to be created as a mesh. + + Parameters + ---------- + new_min_n_pixels : int + Minimum number of pixels needed to create a mesh. Less than this and it will be skipped / error raised. + """ + self._min_n_pixels = new_min_n_pixels + + @property + def list_applied_transforms(self): + """ + Convenience function to get the list of transformations that have been applied to this mesh. + + Returns + ------- + list + List of vtk.vtkTransform objects that have been applied to the current mesh. + """ + return self._list_applied_transforms + + +class CartilageMesh(Mesh): + """ + Class to create, store, and process cartilage meshes + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + + + Attributes + ---------- + _mesh : vtk.vtkPolyData + Item passed from __init__, or created during life of class. + This is the main surface mesh of this class. + _seg_image : SimpleITK.Image + Segmentation image that can be used to create mesh. This is optional. + path_seg_image : str + Path to medical image (.nrrd) that can be loaded to create `_seg_image` + and then creat surface mesh `_mesh` + label_idx : int + Integer of anatomy to create surface mesh from `_seg_image` + min_n_pixels : int + Minimum number of pixels for an isolated island of a segmentation to be + retained + list_applied_transforms : list + A list of transformations applied to a surface mesh. + This list allows for undoing of most recent transform, or undoing + all of them by iterating over the list in reverse. + + Methods + ---------- + + """ + + def __init__(self, + mesh=None, + seg_image=None, + path_seg_image=None, + label_idx=None, + min_n_pixels=1000 + ): + super().__init__(mesh=mesh, + seg_image=seg_image, + path_seg_image=path_seg_image, + label_idx=label_idx, + min_n_pixels=min_n_pixels) + + +class BoneMesh(Mesh): + """ + Class to create, store, and process bone meshes + + Intention is that this class includes functions to process other data & assign it to the bone surface. + It might be possible that instead this class & a cartilage class or, this class and image data etc. are + provided to another function or class that does those analyses. + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + list_cartilage_meshes : list, optional + List object which contains 1+ `CartilageMesh` objects that wrap + a vtk.vtkPolyData surface mesh of cartilage, by default None + list_cartilage_labels : list, optional + List of `int` values that represent the different cartilage + regions of interest appropriate for a single bone, by default None + crop_percent : float, optional + Proportion value to crop long-axis of bone so it is proportional + to the width of the bone for standardization purposes, by default 1.0 + bone : str, optional + String indicating what bone is being analyzed so that cropping + can be applied appropriatey. {'femur', 'tibia'}, by default 'femur'. + Patella is not an option because we do not need to crop for the patella. + + + Attributes + ---------- + _mesh : vtk.vtkPolyData + Item passed from __init__, or created during life of class. + This is the main surface mesh of this class. + _seg_image : SimpleITK.Image + Segmentation image that can be used to create mesh. This is optional. + path_seg_image : str + Path to medical image (.nrrd) that can be loaded to create `_seg_image` + and then creat surface mesh `_mesh` + label_idx : int + Integer of anatomy to create surface mesh from `_seg_image` + min_n_pixels : int + Minimum number of pixels for an isolated island of a segmentation to be + retained + list_applied_transforms : list + A list of transformations applied to a surface mesh. + This list allows for undoing of most recent transform, or undoing + all of them by iterating over the list in reverse. + crop_percent : float + Percent of width to crop along long-axis of bone + bone : str + A string indicating what bone is being represented by this class. + list_cartilage_meshes : list + List of cartialge meshes assigned to this bone. + list_cartilage_labels : list + List of cartilage labels for the `_seg_image` that are associated + with this bone. + + Methods + ---------- + + """ + + def __init__(self, + mesh=None, + seg_image=None, + path_seg_image=None, + label_idx=None, + min_n_pixels=5000, + list_cartilage_meshes=None, + list_cartilage_labels=None, + crop_percent=None, + bone='femur', + ): + """ + Class initialization + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + list_cartilage_meshes : list, optional + List object which contains 1+ `CartilageMesh` objects that wrap + a vtk.vtkPolyData surface mesh of cartilage, by default None + list_cartilage_labels : list, optional + List of `int` values that represent the different cartilage + regions of interest appropriate for a single bone, by default None + crop_percent : float, optional + Proportion value to crop long-axis of bone so it is proportional + to the width of the bone for standardization purposes, by default 1.0 + bone : str, optional + String indicating what bone is being analyzed so that cropping + can be applied appropriatey. {'femur', 'tibia'}, by default 'femur'. + Patella is not an option because we do not need to crop for the patella. + """ + self._crop_percent = crop_percent + self._bone = bone + self._list_cartilage_meshes = list_cartilage_meshes + self._list_cartilage_labels = list_cartilage_labels + + super().__init__(mesh=mesh, + seg_image=seg_image, + path_seg_image=path_seg_image, + label_idx=label_idx, + min_n_pixels=min_n_pixels) + + + def create_mesh(self, + smooth_image=True, + smooth_image_var=0.3125 / 2, + marching_cubes_threshold=0.5, + label_idx=None, + min_n_pixels=None, + crop_percent=None + ): + """ + This is an extension of `Mesh.create_mesh` that enables cropping of bones. + Bones might need to be cropped (this isnt necessary for cartilage) + So, adding this functionality to the processing steps before the bone mesh is created. + + All functionality, except for that relevant to `crop_percent` is the same as: + `Mesh.create_mesh`. + + Create a surface mesh from the classes `_seg_image`. If `_seg_image` + does not exist, then read it in using `read_seg_image`. + + Parameters + ---------- + smooth_image : bool, optional + Should the `_seg_image` be gaussian filtered, by default True + smooth_image_var : float, optional + Variance of gaussian filter to apply to `_seg_image`, by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold contour level to create surface mesh on, by default 0.5 + label_idx : int, optional + Label value / index to create mesh from, by default None + min_n_pixels : int, optional + Minimum number of continuous pixels to include segmentation island + in the surface mesh creation, by default None + crop_percent : [type], optional + [description], by default None + + Raises + ------ + Exception + If cropping & bone is not femur or tibia, then raise an error. + Exception + If the total number of pixels segmentated (`n_pixels_labelled`) is + < `min_n_pixels` then there is no object in the image. + Exception + If no `_seg_image` and no `label_idx` then we don't know what tissue to create the + surface mesh from. + Exception + If no `_seg_image` or `path_seg_image` then we have no image to create mesh from. + """ + + if self._seg_image is None: + self.read_seg_image() + + # Bones might need to be cropped (this isnt necessary for cartilage) + # So, adding this functionality to the processing steps before the bone mesh is created + if crop_percent is not None: + self._crop_percent = crop_percent + if (self._crop_percent is not None) and (('femur' in self._bone) or ('tibia' in self._bone)): + if 'femur' in self._bone: + bone_crop_distal = True + elif 'tibia' in self._bone: + bone_crop_distal = False + else: + raise Exception('var bone should be "femur" or "tiba" got: {} instead'.format(self._bone)) + + self._seg_image = crop_bone_based_on_width(self._seg_image, + self._label_idx, + percent_width_to_crop_height=self._crop_percent, + bone_crop_distal=bone_crop_distal) + elif self._crop_percent is not None: + warnings.warn(f'Trying to crop bone, but {self._bone} specified and only bones `femur`', + 'or `tibia` currently supported for cropping. If using another bone, consider', + 'making a pull request. If cropping not desired, set `crop_percent=None`.' + ) + super().create_mesh(smooth_image=smooth_image, smooth_image_var=smooth_image_var, marching_cubes_threshold=marching_cubes_threshold, label_idx=label_idx, min_n_pixels=min_n_pixels) + + def create_cartilage_meshes(self, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5): + """ + Helper function to create the list of cartilage meshes from the list of cartilage + labels. + + Parameters + ---------- + image_smooth_var_cart : float + Variance to smooth cartilage segmentations before finding surface using continuous + marching cubes. + marching_cubes_threshold : float + Threshold value to create cartilage surface at from segmentation images. + + Notes + ----- + ?? Should this function just be everything inside the for loop and then that + function gets called somewhere else? + """ + + self._list_cartilage_meshes = [] + for cart_label_idx in self._list_cartilage_labels: + seg_array_view = sitk.GetArrayViewFromImage(self._seg_image) + n_pixels_with_cart = np.sum(seg_array_view == cart_label_idx) + if n_pixels_with_cart == 0: + warnings.warn( + f"Not analyzing cartilage for label {cart_label_idx} because it doesnt have any pixels!", + UserWarning + ) + else: + cart_mesh = CartilageMesh(seg_image=self._seg_image, + label_idx=cart_label_idx) + cart_mesh.create_mesh(smooth_image_var=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + self._list_cartilage_meshes.append(cart_mesh) + + + def calc_cartilage_thickness(self, + list_cartilage_labels=None, + list_cartilage_meshes=None, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25 + ): + """ + Using bone mesh (`_mesh`) and the list of cartilage meshes (`list_cartilage_meshes`) + calcualte the cartilage thickness for each node on the bone surface. + + Parameters + ---------- + list_cartilage_labels : list, optional + Cartilag labels to be used to create cartilage meshes (if they dont + exist), by default None + list_cartilage_meshes : list, optional + Cartilage meshes to be used for calculating cart thickness, by default None + image_smooth_var_cart : float, optional + Variance of gaussian filter to be applied to binary cartilage masks, + by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold to create bone surface at, by default 0.5 + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + + Raises + ------ + Exception + No cartilage available (either `list_cartilage_meshes` or `list_cartilage_labels`) + """ + # If new cartilage infor/labels are provided, then replace existing with these ones. + if list_cartilage_meshes is not None: self._list_cartilage_meshes = list_cartilage_meshes + if list_cartilage_labels is not None: self._list_cartilage_labels = list_cartilage_labels + + # If no cartilage stuff provided, then cant do this function - raise exception. + if (self._list_cartilage_meshes is None) & (self._list_cartilage_labels is None): + raise Exception('No cartilage meshes or list of cartilage labels are provided! - These can be provided either to the class function `calc_cartilage_thickness` directly, or can be specified at the time of instantiating the `BoneMesh` class.') + + # if cartilage meshes don't exist yet, then make them. + if self._list_cartilage_meshes is None: + self.create_cartilage_meshes(image_smooth_var_cart=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + + # pre-allocate empty thicknesses so that as labels are iterated over, they can all be appended to the same bone. + thicknesses = np.zeros(self._mesh.GetNumberOfPoints()) + + # iterate over meshes and add their thicknesses to the thicknesses list. + for cart_mesh in self._list_cartilage_meshes: + node_data = get_cartilage_properties_at_points(self._mesh, + cart_mesh._mesh, + t2_vtk_image=None, + # seg_vtk_image=vtk_seg if assign_seg_label_to_bone is True else None, + seg_vtk_image=None, + ray_cast_length=ray_cast_length, + percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + ) + thicknesses += node_data + + # Assign the thickness scalars to the bone mesh surface. + thickness_scalars = numpy_to_vtk(thicknesses) + thickness_scalars.SetName('thickness (mm)') + self._mesh.GetPointData().SetScalars(thickness_scalars) + + def assign_cartilage_regions(self, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25): + """ + Assign cartilage regions to the bone surface (e.g. medial/lateral tibial cartilage) + - Can also be used for femur sub-regions (anterior, medial weight-bearing, etc.) + + Parameters + ---------- + image_smooth_var_cart : float, optional + Variance of gaussian filter to be applied to binary cartilage masks, + by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold to create bone surface at, by default 0.5 + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + """ + tmp_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + '.nrrd' + path_save_tmp_file = os.path.join('/tmp', tmp_filename) + # if self._bone == 'femur': + # new_seg_image = qc.get_knee_segmentation_with_femur_subregions(seg_image, + # fem_cart_label_idx=1) + # sitk.WriteImage(new_seg_image, path_save_tmp_file) + # else: + sitk.WriteImage(self._seg_image, path_save_tmp_file) + vtk_seg_reader = read_nrrd(path_save_tmp_file, + set_origin_zero=True + ) + vtk_seg = vtk_seg_reader.GetOutput() + + seg_transformer = SitkVtkTransformer(self._seg_image) + + # Delete tmp files + safely_delete_tmp_file('/tmp', + tmp_filename) + + self.apply_transform_to_mesh(transform=seg_transformer.get_inverse_transform()) + labels = np.zeros(self._mesh.GetNumberOfPoints(), dtype=np.int) + + # if cartilage meshes don't exist yet, then make them. + if self._list_cartilage_meshes is None: + self.create_cartilage_meshes(image_smooth_var_cart=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + + # iterate over meshes and add their label (region) + for cart_mesh in self._list_cartilage_meshes: + cart_mesh.apply_transform_to_mesh(transform=seg_transformer.get_inverse_transform()) + node_data = get_cartilage_properties_at_points(self._mesh, + cart_mesh._mesh, + t2_vtk_image=None, + seg_vtk_image=vtk_seg, + ray_cast_length=ray_cast_length, + percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + ) + labels += node_data[1] + cart_mesh.reverse_all_transforms() + + # Assign the label (region) scalars to the bone mesh surface. + label_scalars = numpy_to_vtk(labels) + label_scalars.SetName('labels') + self._mesh.GetPointData().AddArray(label_scalars) + + self.reverse_all_transforms() + + def calc_cartilage_t2(self, + path_t2_nrrd, + path_seg_to_t2_transform=None, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25): + """ + Apply cartilage T2 values to bone surface. + + Parameters + ---------- + path_t2_nrrd : str + Path to nrrd image of T2 map to load / use. + path_seg_to_t2_transform : str, optional + Path to a transform file to be used for aligning T2 map with segmentations, + by default None + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + """ + print('Not yet implemented') + # if self._list_cartilage_meshes is None: + # raise('Should calculate cartialge thickness before getting T2') + # # ALTERNATIVELY - COULD ALLOW PASSING OF CARTILAGE REGIONS IN HERE + # # THOUGH, DOES THAT JUST COMPLICATE THINGS? + + # if path_seg_transform is not None: + # # this is in case there is a transformation needed to align the segmentation with the + # # underlying T2 image + # seg_transform = sitk.ReadTransform(path_seg_transform) + # seg_image = apply_transform_retain_array(self._seg_image, + # seg_transform, + # interpolator=sitk.sitkNearestNeighbor) + + + # versor = get_versor_from_transform(seg_transform) + # center_transform, rotate_transform, translate_transform = break_versor_into_center_rotate_translate_transforms(versor) + # # first apply negative of center of rotation to mesh + # self._mesh.apply_transform_to_mesh(transform=center_transform.GetInverse()) + # # now apply the transform (rotation then translation) + # self._mesh.apply_transform_to_mesh(transform=rotate_transform.GetInverse()) + # self._mesh.apply_transform_to_mesh(transform=translate_transform.GetInverse()) + # #then undo the center of rotation + # self._mesh.apply_transform_to_mesh(transform=center_transform) + + # # Read t2 map (vtk format) + # vtk_t2map_reader = read_nrrd(path_t2_nrrd, + # set_origin_zero=True) + # vtk_t2map = vtk_t2map_reader.GetOutput() + # sitk_t2map = sitk.ReadImage(path_t2_nrrd) + # t2_transformer = SitkVtkTransformer(sitk_t2map) + + # self._mesh.apply_transform_to_mesh(transform=t2_transformer.get_inverse_transform()) + + # t2 = np.zeros(self._mesh.GetNumberOfPoints()) + # # iterate over meshes and add their t2 to the t2 list. + # for cart_mesh in self._list_cartilage_meshes: + # if path_seg_to_t2_transform is not None: + # # first apply negative of center of rotation to mesh + # cart_mesh.apply_transform_to_mesh(transform=center_transform.GetInverse()) + # # now apply the transform (rotation then translation) + # cart_mesh.apply_transform_to_mesh(transform=rotate_transform.GetInverse()) + # cart_mesh.apply_transform_to_mesh(transform=translate_transform.GetInverse()) + # #then undo the center of rotation + # cart_mesh.apply_transform_to_mesh(transform=center_transform) + + # cart_mesh.apply_transform_to_mesh(transform=t2_transformer.get_inverse_transform()) + # _, t2_data = get_cartilage_properties_at_points(self._mesh, + # cart_mesh._mesh, + # t2_vtk_image=vtk_t2map, + # ray_cast_length=ray_cast_length, + # percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + # ) + # t2 += t2_data + # cart_mesh.reverse_all_transforms() + print('NOT DONE!!!') + + + + def smooth_surface_scalars(self, + smooth_only_cartilage=True, + scalar_sigma=1.6986436005760381, # This is a FWHM = 4 + scalar_array_name='thickness (mm)', + scalar_array_idx=None, + ): + + """ + Function to smooth the scalars with name `scalar_array_name` on the bone surface. + + Parameters + ---------- + smooth_only_cartilage : bool, optional + Should we only smooth where there is cartialge & ignore everywhere else, by default True + scalar_sigma : float, optional + Smoothing sigma (standard deviation or sqrt(variance)) for gaussian filter, by default 1.6986436005760381 + default is based on a Full Width Half Maximum (FWHM) of 4mm. + scalar_array_name : str + Name of scalar array to smooth, default 'thickness (mm)'. + scalar_array_idx : int, optional + Index of the scalar array to smooth (alternative to using `scalar_array_name`) , by default None + """ + if smooth_only_cartilage is True: + loc_cartilage = np.where(vtk_to_numpy(self._mesh.GetPointData().GetArray('thickness (mm)')) > 0.01)[0] + else: + loc_cartilage = None + self._mesh = gaussian_smooth_surface_scalars(self._mesh, + sigma=scalar_sigma, + idx_coords_to_smooth=loc_cartilage, + array_name=scalar_array_name, + array_idx=scalar_array_idx) + + @property + def list_cartilage_meshes(self): + """ + Convenience function to get the list of cartilage meshes + + Returns + ------- + list + A list of `CartilageMesh` objects associated with this bone + """ + return self._list_cartilage_meshes + + @list_cartilage_meshes.setter + def list_cartilage_meshes(self, new_list_cartilage_meshes): + """ + Convenience function to set the list of cartilage meshes + + Parameters + ---------- + new_list_cartilage_meshes : list + A list of `CartilageMesh` objects associated with this bone + """ + if type(new_list_cartilage_meshes) is list: + for mesh in new_list_cartilage_meshes: + if type(mesh) != pymskt.mesh.meshes.CartilageMesh: + raise TypeError('Item in `list_cartilage_meshes` is not a `CartilageMesh`') + elif type(new_list_cartilage_meshes) is pymskt.mesh.meshes.CartilageMesh: + new_list_cartilage_meshes = [new_list_cartilage_meshes,] + self._list_cartilage_meshes = new_list_cartilage_meshes + + @property + def list_cartilage_labels(self): + """ + Convenience function to get the list of labels for cartilage tissues associated + with this bone. + + Returns + ------- + list + list of `int`s for the cartilage tissues associated with this bone. + """ + return self._list_cartilage_labels + + @list_cartilage_labels.setter + def list_cartilage_labels(self, new_list_cartilage_labels): + """ + Convenience function to set the list of labels for cartilage tissues associated + with this bone + + Parameters + ---------- + new_list_cartilage_labels : list + list of `int`s for the cartilage tissues associated with this bone. + """ + if type(new_list_cartilage_labels) == list: + for label in new_list_cartilage_labels: + if type(label) != int: + raise TypeError(f'Item in `list_cartilage_labels` is not a `int` - got {type(label)}') + elif type(new_list_cartilage_labels) == int: + new_list_cartilage_labels = [new_list_cartilage_labels,] + self._list_cartilage_labels = new_list_cartilage_labels + + @property + def crop_percent(self): + """ + Convenience function to get the value that `crop_percent` is set to. + + Returns + ------- + float + Floating point > 0.0 indicating how much of the length of the bone should be included + when cropping - expressed as a proportion of the width. + """ + return self._crop_percent + + @crop_percent.setter + def crop_percent(self, new_crop_percent): + """ + Convenience function to set the value that `crop_percent` is set to. + + Parameters + ---------- + new_crop_percent : float + Floating point > 0.0 indicating how much of the length of the bone should be included + when cropping - expressed as a proportion of the width. + """ + if type(new_crop_percent) != float: + raise TypeError(f'New `crop_percent` provided is type {type(new_crop_percent)} - expected `float`') + self._crop_percent = new_crop_percent + + @property + def bone(self): + """ + Convenience function to get the name of the bone in this object. + + Returns + ------- + str + Name of the bone in this object - used to help identify how to crop the bone. + """ + return self._bone + + @bone.setter + def bone(self, new_bone): + """ + Convenience function to set the name of the bone in this object. + + Parameters + ---------- + new_bone : str + Name of the bone in this object - used to help identify how to crop the bone. + """ + if type(new_bone) != str: + raise TypeError(f'New bone provided is type {type(new_bone)} - expected `str`') + self._bone = new_bone + + + + </code></pre> +</details> +</section> +<section> +</section> +<section> +</section> +<section> +</section> +<section> +<h2 class="section-title" id="header-classes">Classes</h2> +<dl> +<dt id="pymskt.mesh.meshes.BoneMesh"><code class="flex name class"> +<span>class <span class="ident">BoneMesh</span></span> +<span>(</span><span>mesh=None, seg_image=None, path_seg_image=None, label_idx=None, min_n_pixels=5000, list_cartilage_meshes=None, list_cartilage_labels=None, crop_percent=None, bone='femur')</span> +</code></dt> +<dd> +<div class="desc"><p>Class to create, store, and process bone meshes</p> +<p>Intention is that this class includes functions to process other data & assign it to the bone surface. +It might be possible that instead this class & a cartilage class or, this class and image data etc. are +provided to another function or class that does those analyses.</p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>mesh</code></strong> : <code>vtk.vtkPolyData</code>, optional</dt> +<dd>vtkPolyData object that is basis of surface mesh, by default None</dd> +<dt><strong><code>seg_image</code></strong> : <code>SimpleITK.Image</code>, optional</dt> +<dd>Segmentation image that can be used to create surface mesh - used +instead of mesh, by default None</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a medical image (.nrrd) to load and create mesh from, +by default None</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label of anatomy of interest, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>All islands smaller than this size are dropped, by default 5000</dd> +<dt><strong><code>list_cartilage_meshes</code></strong> : <code>list</code>, optional</dt> +<dd>List object which contains 1+ <code><a title="pymskt.mesh.meshes.CartilageMesh" href="#pymskt.mesh.meshes.CartilageMesh">CartilageMesh</a></code> objects that wrap +a vtk.vtkPolyData surface mesh of cartilage, by default None</dd> +<dt><strong><code>list_cartilage_labels</code></strong> : <code>list</code>, optional</dt> +<dd>List of <code>int</code> values that represent the different cartilage +regions of interest appropriate for a single bone, by default None</dd> +<dt><strong><code>crop_percent</code></strong> : <code>float</code>, optional</dt> +<dd>Proportion value to crop long-axis of bone so it is proportional +to the width of the bone for standardization purposes, by default 1.0</dd> +<dt><strong><code>bone</code></strong> : <code>str</code>, optional</dt> +<dd>String indicating what bone is being analyzed so that cropping +can be applied appropriatey. {'femur', 'tibia'}, by default 'femur'. +Patella is not an option because we do not need to crop for the patella.</dd> +</dl> +<h2 id="attributes">Attributes</h2> +<dl> +<dt><strong><code>_mesh</code></strong> : <code>vtk.vtkPolyData</code></dt> +<dd>Item passed from <strong>init</strong>, or created during life of class. +This is the main surface mesh of this class.</dd> +<dt><strong><code>_seg_image</code></strong> : <code>SimpleITK.Image</code></dt> +<dd>Segmentation image that can be used to create mesh. This is optional.</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code></dt> +<dd>Path to medical image (.nrrd) that can be loaded to create <code>_seg_image</code> +and then creat surface mesh <code>_mesh</code></dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code></dt> +<dd>Integer of anatomy to create surface mesh from <code>_seg_image</code></dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code></dt> +<dd>Minimum number of pixels for an isolated island of a segmentation to be +retained</dd> +<dt><strong><code>list_applied_transforms</code></strong> : <code>list</code></dt> +<dd>A list of transformations applied to a surface mesh. +This list allows for undoing of most recent transform, or undoing +all of them by iterating over the list in reverse.</dd> +<dt><strong><code>crop_percent</code></strong> : <code>float</code></dt> +<dd>Percent of width to crop along long-axis of bone</dd> +<dt><strong><code>bone</code></strong> : <code>str</code></dt> +<dd>A string indicating what bone is being represented by this class.</dd> +<dt><strong><code>list_cartilage_meshes</code></strong> : <code>list</code></dt> +<dd>List of cartialge meshes assigned to this bone.</dd> +<dt><strong><code>list_cartilage_labels</code></strong> : <code>list</code></dt> +<dd>List of cartilage labels for the <code>_seg_image</code> that are associated +with this bone.</dd> +</dl> +<h2 id="methods">Methods</h2> +<p>Class initialization</p> +<h2 id="parameters_1">Parameters</h2> +<dl> +<dt><strong><code>mesh</code></strong> : <code>vtk.vtkPolyData</code>, optional</dt> +<dd>vtkPolyData object that is basis of surface mesh, by default None</dd> +<dt><strong><code>seg_image</code></strong> : <code>SimpleITK.Image</code>, optional</dt> +<dd>Segmentation image that can be used to create surface mesh - used +instead of mesh, by default None</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a medical image (.nrrd) to load and create mesh from, +by default None</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label of anatomy of interest, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>All islands smaller than this size are dropped, by default 5000</dd> +<dt><strong><code>list_cartilage_meshes</code></strong> : <code>list</code>, optional</dt> +<dd>List object which contains 1+ <code><a title="pymskt.mesh.meshes.CartilageMesh" href="#pymskt.mesh.meshes.CartilageMesh">CartilageMesh</a></code> objects that wrap +a vtk.vtkPolyData surface mesh of cartilage, by default None</dd> +<dt><strong><code>list_cartilage_labels</code></strong> : <code>list</code>, optional</dt> +<dd>List of <code>int</code> values that represent the different cartilage +regions of interest appropriate for a single bone, by default None</dd> +<dt><strong><code>crop_percent</code></strong> : <code>float</code>, optional</dt> +<dd>Proportion value to crop long-axis of bone so it is proportional +to the width of the bone for standardization purposes, by default 1.0</dd> +<dt><strong><code>bone</code></strong> : <code>str</code>, optional</dt> +<dd>String indicating what bone is being analyzed so that cropping +can be applied appropriatey. {'femur', 'tibia'}, by default 'femur'. +Patella is not an option because we do not need to crop for the patella.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">class BoneMesh(Mesh): + """ + Class to create, store, and process bone meshes + + Intention is that this class includes functions to process other data & assign it to the bone surface. + It might be possible that instead this class & a cartilage class or, this class and image data etc. are + provided to another function or class that does those analyses. + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + list_cartilage_meshes : list, optional + List object which contains 1+ `CartilageMesh` objects that wrap + a vtk.vtkPolyData surface mesh of cartilage, by default None + list_cartilage_labels : list, optional + List of `int` values that represent the different cartilage + regions of interest appropriate for a single bone, by default None + crop_percent : float, optional + Proportion value to crop long-axis of bone so it is proportional + to the width of the bone for standardization purposes, by default 1.0 + bone : str, optional + String indicating what bone is being analyzed so that cropping + can be applied appropriatey. {'femur', 'tibia'}, by default 'femur'. + Patella is not an option because we do not need to crop for the patella. + + + Attributes + ---------- + _mesh : vtk.vtkPolyData + Item passed from __init__, or created during life of class. + This is the main surface mesh of this class. + _seg_image : SimpleITK.Image + Segmentation image that can be used to create mesh. This is optional. + path_seg_image : str + Path to medical image (.nrrd) that can be loaded to create `_seg_image` + and then creat surface mesh `_mesh` + label_idx : int + Integer of anatomy to create surface mesh from `_seg_image` + min_n_pixels : int + Minimum number of pixels for an isolated island of a segmentation to be + retained + list_applied_transforms : list + A list of transformations applied to a surface mesh. + This list allows for undoing of most recent transform, or undoing + all of them by iterating over the list in reverse. + crop_percent : float + Percent of width to crop along long-axis of bone + bone : str + A string indicating what bone is being represented by this class. + list_cartilage_meshes : list + List of cartialge meshes assigned to this bone. + list_cartilage_labels : list + List of cartilage labels for the `_seg_image` that are associated + with this bone. + + Methods + ---------- + + """ + + def __init__(self, + mesh=None, + seg_image=None, + path_seg_image=None, + label_idx=None, + min_n_pixels=5000, + list_cartilage_meshes=None, + list_cartilage_labels=None, + crop_percent=None, + bone='femur', + ): + """ + Class initialization + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + list_cartilage_meshes : list, optional + List object which contains 1+ `CartilageMesh` objects that wrap + a vtk.vtkPolyData surface mesh of cartilage, by default None + list_cartilage_labels : list, optional + List of `int` values that represent the different cartilage + regions of interest appropriate for a single bone, by default None + crop_percent : float, optional + Proportion value to crop long-axis of bone so it is proportional + to the width of the bone for standardization purposes, by default 1.0 + bone : str, optional + String indicating what bone is being analyzed so that cropping + can be applied appropriatey. {'femur', 'tibia'}, by default 'femur'. + Patella is not an option because we do not need to crop for the patella. + """ + self._crop_percent = crop_percent + self._bone = bone + self._list_cartilage_meshes = list_cartilage_meshes + self._list_cartilage_labels = list_cartilage_labels + + super().__init__(mesh=mesh, + seg_image=seg_image, + path_seg_image=path_seg_image, + label_idx=label_idx, + min_n_pixels=min_n_pixels) + + + def create_mesh(self, + smooth_image=True, + smooth_image_var=0.3125 / 2, + marching_cubes_threshold=0.5, + label_idx=None, + min_n_pixels=None, + crop_percent=None + ): + """ + This is an extension of `Mesh.create_mesh` that enables cropping of bones. + Bones might need to be cropped (this isnt necessary for cartilage) + So, adding this functionality to the processing steps before the bone mesh is created. + + All functionality, except for that relevant to `crop_percent` is the same as: + `Mesh.create_mesh`. + + Create a surface mesh from the classes `_seg_image`. If `_seg_image` + does not exist, then read it in using `read_seg_image`. + + Parameters + ---------- + smooth_image : bool, optional + Should the `_seg_image` be gaussian filtered, by default True + smooth_image_var : float, optional + Variance of gaussian filter to apply to `_seg_image`, by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold contour level to create surface mesh on, by default 0.5 + label_idx : int, optional + Label value / index to create mesh from, by default None + min_n_pixels : int, optional + Minimum number of continuous pixels to include segmentation island + in the surface mesh creation, by default None + crop_percent : [type], optional + [description], by default None + + Raises + ------ + Exception + If cropping & bone is not femur or tibia, then raise an error. + Exception + If the total number of pixels segmentated (`n_pixels_labelled`) is + < `min_n_pixels` then there is no object in the image. + Exception + If no `_seg_image` and no `label_idx` then we don't know what tissue to create the + surface mesh from. + Exception + If no `_seg_image` or `path_seg_image` then we have no image to create mesh from. + """ + + if self._seg_image is None: + self.read_seg_image() + + # Bones might need to be cropped (this isnt necessary for cartilage) + # So, adding this functionality to the processing steps before the bone mesh is created + if crop_percent is not None: + self._crop_percent = crop_percent + if (self._crop_percent is not None) and (('femur' in self._bone) or ('tibia' in self._bone)): + if 'femur' in self._bone: + bone_crop_distal = True + elif 'tibia' in self._bone: + bone_crop_distal = False + else: + raise Exception('var bone should be "femur" or "tiba" got: {} instead'.format(self._bone)) + + self._seg_image = crop_bone_based_on_width(self._seg_image, + self._label_idx, + percent_width_to_crop_height=self._crop_percent, + bone_crop_distal=bone_crop_distal) + elif self._crop_percent is not None: + warnings.warn(f'Trying to crop bone, but {self._bone} specified and only bones `femur`', + 'or `tibia` currently supported for cropping. If using another bone, consider', + 'making a pull request. If cropping not desired, set `crop_percent=None`.' + ) + super().create_mesh(smooth_image=smooth_image, smooth_image_var=smooth_image_var, marching_cubes_threshold=marching_cubes_threshold, label_idx=label_idx, min_n_pixels=min_n_pixels) + + def create_cartilage_meshes(self, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5): + """ + Helper function to create the list of cartilage meshes from the list of cartilage + labels. + + Parameters + ---------- + image_smooth_var_cart : float + Variance to smooth cartilage segmentations before finding surface using continuous + marching cubes. + marching_cubes_threshold : float + Threshold value to create cartilage surface at from segmentation images. + + Notes + ----- + ?? Should this function just be everything inside the for loop and then that + function gets called somewhere else? + """ + + self._list_cartilage_meshes = [] + for cart_label_idx in self._list_cartilage_labels: + seg_array_view = sitk.GetArrayViewFromImage(self._seg_image) + n_pixels_with_cart = np.sum(seg_array_view == cart_label_idx) + if n_pixels_with_cart == 0: + warnings.warn( + f"Not analyzing cartilage for label {cart_label_idx} because it doesnt have any pixels!", + UserWarning + ) + else: + cart_mesh = CartilageMesh(seg_image=self._seg_image, + label_idx=cart_label_idx) + cart_mesh.create_mesh(smooth_image_var=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + self._list_cartilage_meshes.append(cart_mesh) + + + def calc_cartilage_thickness(self, + list_cartilage_labels=None, + list_cartilage_meshes=None, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25 + ): + """ + Using bone mesh (`_mesh`) and the list of cartilage meshes (`list_cartilage_meshes`) + calcualte the cartilage thickness for each node on the bone surface. + + Parameters + ---------- + list_cartilage_labels : list, optional + Cartilag labels to be used to create cartilage meshes (if they dont + exist), by default None + list_cartilage_meshes : list, optional + Cartilage meshes to be used for calculating cart thickness, by default None + image_smooth_var_cart : float, optional + Variance of gaussian filter to be applied to binary cartilage masks, + by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold to create bone surface at, by default 0.5 + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + + Raises + ------ + Exception + No cartilage available (either `list_cartilage_meshes` or `list_cartilage_labels`) + """ + # If new cartilage infor/labels are provided, then replace existing with these ones. + if list_cartilage_meshes is not None: self._list_cartilage_meshes = list_cartilage_meshes + if list_cartilage_labels is not None: self._list_cartilage_labels = list_cartilage_labels + + # If no cartilage stuff provided, then cant do this function - raise exception. + if (self._list_cartilage_meshes is None) & (self._list_cartilage_labels is None): + raise Exception('No cartilage meshes or list of cartilage labels are provided! - These can be provided either to the class function `calc_cartilage_thickness` directly, or can be specified at the time of instantiating the `BoneMesh` class.') + + # if cartilage meshes don't exist yet, then make them. + if self._list_cartilage_meshes is None: + self.create_cartilage_meshes(image_smooth_var_cart=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + + # pre-allocate empty thicknesses so that as labels are iterated over, they can all be appended to the same bone. + thicknesses = np.zeros(self._mesh.GetNumberOfPoints()) + + # iterate over meshes and add their thicknesses to the thicknesses list. + for cart_mesh in self._list_cartilage_meshes: + node_data = get_cartilage_properties_at_points(self._mesh, + cart_mesh._mesh, + t2_vtk_image=None, + # seg_vtk_image=vtk_seg if assign_seg_label_to_bone is True else None, + seg_vtk_image=None, + ray_cast_length=ray_cast_length, + percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + ) + thicknesses += node_data + + # Assign the thickness scalars to the bone mesh surface. + thickness_scalars = numpy_to_vtk(thicknesses) + thickness_scalars.SetName('thickness (mm)') + self._mesh.GetPointData().SetScalars(thickness_scalars) + + def assign_cartilage_regions(self, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25): + """ + Assign cartilage regions to the bone surface (e.g. medial/lateral tibial cartilage) + - Can also be used for femur sub-regions (anterior, medial weight-bearing, etc.) + + Parameters + ---------- + image_smooth_var_cart : float, optional + Variance of gaussian filter to be applied to binary cartilage masks, + by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold to create bone surface at, by default 0.5 + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + """ + tmp_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + '.nrrd' + path_save_tmp_file = os.path.join('/tmp', tmp_filename) + # if self._bone == 'femur': + # new_seg_image = qc.get_knee_segmentation_with_femur_subregions(seg_image, + # fem_cart_label_idx=1) + # sitk.WriteImage(new_seg_image, path_save_tmp_file) + # else: + sitk.WriteImage(self._seg_image, path_save_tmp_file) + vtk_seg_reader = read_nrrd(path_save_tmp_file, + set_origin_zero=True + ) + vtk_seg = vtk_seg_reader.GetOutput() + + seg_transformer = SitkVtkTransformer(self._seg_image) + + # Delete tmp files + safely_delete_tmp_file('/tmp', + tmp_filename) + + self.apply_transform_to_mesh(transform=seg_transformer.get_inverse_transform()) + labels = np.zeros(self._mesh.GetNumberOfPoints(), dtype=np.int) + + # if cartilage meshes don't exist yet, then make them. + if self._list_cartilage_meshes is None: + self.create_cartilage_meshes(image_smooth_var_cart=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + + # iterate over meshes and add their label (region) + for cart_mesh in self._list_cartilage_meshes: + cart_mesh.apply_transform_to_mesh(transform=seg_transformer.get_inverse_transform()) + node_data = get_cartilage_properties_at_points(self._mesh, + cart_mesh._mesh, + t2_vtk_image=None, + seg_vtk_image=vtk_seg, + ray_cast_length=ray_cast_length, + percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + ) + labels += node_data[1] + cart_mesh.reverse_all_transforms() + + # Assign the label (region) scalars to the bone mesh surface. + label_scalars = numpy_to_vtk(labels) + label_scalars.SetName('labels') + self._mesh.GetPointData().AddArray(label_scalars) + + self.reverse_all_transforms() + + def calc_cartilage_t2(self, + path_t2_nrrd, + path_seg_to_t2_transform=None, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25): + """ + Apply cartilage T2 values to bone surface. + + Parameters + ---------- + path_t2_nrrd : str + Path to nrrd image of T2 map to load / use. + path_seg_to_t2_transform : str, optional + Path to a transform file to be used for aligning T2 map with segmentations, + by default None + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + """ + print('Not yet implemented') + # if self._list_cartilage_meshes is None: + # raise('Should calculate cartialge thickness before getting T2') + # # ALTERNATIVELY - COULD ALLOW PASSING OF CARTILAGE REGIONS IN HERE + # # THOUGH, DOES THAT JUST COMPLICATE THINGS? + + # if path_seg_transform is not None: + # # this is in case there is a transformation needed to align the segmentation with the + # # underlying T2 image + # seg_transform = sitk.ReadTransform(path_seg_transform) + # seg_image = apply_transform_retain_array(self._seg_image, + # seg_transform, + # interpolator=sitk.sitkNearestNeighbor) + + + # versor = get_versor_from_transform(seg_transform) + # center_transform, rotate_transform, translate_transform = break_versor_into_center_rotate_translate_transforms(versor) + # # first apply negative of center of rotation to mesh + # self._mesh.apply_transform_to_mesh(transform=center_transform.GetInverse()) + # # now apply the transform (rotation then translation) + # self._mesh.apply_transform_to_mesh(transform=rotate_transform.GetInverse()) + # self._mesh.apply_transform_to_mesh(transform=translate_transform.GetInverse()) + # #then undo the center of rotation + # self._mesh.apply_transform_to_mesh(transform=center_transform) + + # # Read t2 map (vtk format) + # vtk_t2map_reader = read_nrrd(path_t2_nrrd, + # set_origin_zero=True) + # vtk_t2map = vtk_t2map_reader.GetOutput() + # sitk_t2map = sitk.ReadImage(path_t2_nrrd) + # t2_transformer = SitkVtkTransformer(sitk_t2map) + + # self._mesh.apply_transform_to_mesh(transform=t2_transformer.get_inverse_transform()) + + # t2 = np.zeros(self._mesh.GetNumberOfPoints()) + # # iterate over meshes and add their t2 to the t2 list. + # for cart_mesh in self._list_cartilage_meshes: + # if path_seg_to_t2_transform is not None: + # # first apply negative of center of rotation to mesh + # cart_mesh.apply_transform_to_mesh(transform=center_transform.GetInverse()) + # # now apply the transform (rotation then translation) + # cart_mesh.apply_transform_to_mesh(transform=rotate_transform.GetInverse()) + # cart_mesh.apply_transform_to_mesh(transform=translate_transform.GetInverse()) + # #then undo the center of rotation + # cart_mesh.apply_transform_to_mesh(transform=center_transform) + + # cart_mesh.apply_transform_to_mesh(transform=t2_transformer.get_inverse_transform()) + # _, t2_data = get_cartilage_properties_at_points(self._mesh, + # cart_mesh._mesh, + # t2_vtk_image=vtk_t2map, + # ray_cast_length=ray_cast_length, + # percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + # ) + # t2 += t2_data + # cart_mesh.reverse_all_transforms() + print('NOT DONE!!!') + + + + def smooth_surface_scalars(self, + smooth_only_cartilage=True, + scalar_sigma=1.6986436005760381, # This is a FWHM = 4 + scalar_array_name='thickness (mm)', + scalar_array_idx=None, + ): + + """ + Function to smooth the scalars with name `scalar_array_name` on the bone surface. + + Parameters + ---------- + smooth_only_cartilage : bool, optional + Should we only smooth where there is cartialge & ignore everywhere else, by default True + scalar_sigma : float, optional + Smoothing sigma (standard deviation or sqrt(variance)) for gaussian filter, by default 1.6986436005760381 + default is based on a Full Width Half Maximum (FWHM) of 4mm. + scalar_array_name : str + Name of scalar array to smooth, default 'thickness (mm)'. + scalar_array_idx : int, optional + Index of the scalar array to smooth (alternative to using `scalar_array_name`) , by default None + """ + if smooth_only_cartilage is True: + loc_cartilage = np.where(vtk_to_numpy(self._mesh.GetPointData().GetArray('thickness (mm)')) > 0.01)[0] + else: + loc_cartilage = None + self._mesh = gaussian_smooth_surface_scalars(self._mesh, + sigma=scalar_sigma, + idx_coords_to_smooth=loc_cartilage, + array_name=scalar_array_name, + array_idx=scalar_array_idx) + + @property + def list_cartilage_meshes(self): + """ + Convenience function to get the list of cartilage meshes + + Returns + ------- + list + A list of `CartilageMesh` objects associated with this bone + """ + return self._list_cartilage_meshes + + @list_cartilage_meshes.setter + def list_cartilage_meshes(self, new_list_cartilage_meshes): + """ + Convenience function to set the list of cartilage meshes + + Parameters + ---------- + new_list_cartilage_meshes : list + A list of `CartilageMesh` objects associated with this bone + """ + if type(new_list_cartilage_meshes) is list: + for mesh in new_list_cartilage_meshes: + if type(mesh) != pymskt.mesh.meshes.CartilageMesh: + raise TypeError('Item in `list_cartilage_meshes` is not a `CartilageMesh`') + elif type(new_list_cartilage_meshes) is pymskt.mesh.meshes.CartilageMesh: + new_list_cartilage_meshes = [new_list_cartilage_meshes,] + self._list_cartilage_meshes = new_list_cartilage_meshes + + @property + def list_cartilage_labels(self): + """ + Convenience function to get the list of labels for cartilage tissues associated + with this bone. + + Returns + ------- + list + list of `int`s for the cartilage tissues associated with this bone. + """ + return self._list_cartilage_labels + + @list_cartilage_labels.setter + def list_cartilage_labels(self, new_list_cartilage_labels): + """ + Convenience function to set the list of labels for cartilage tissues associated + with this bone + + Parameters + ---------- + new_list_cartilage_labels : list + list of `int`s for the cartilage tissues associated with this bone. + """ + if type(new_list_cartilage_labels) == list: + for label in new_list_cartilage_labels: + if type(label) != int: + raise TypeError(f'Item in `list_cartilage_labels` is not a `int` - got {type(label)}') + elif type(new_list_cartilage_labels) == int: + new_list_cartilage_labels = [new_list_cartilage_labels,] + self._list_cartilage_labels = new_list_cartilage_labels + + @property + def crop_percent(self): + """ + Convenience function to get the value that `crop_percent` is set to. + + Returns + ------- + float + Floating point > 0.0 indicating how much of the length of the bone should be included + when cropping - expressed as a proportion of the width. + """ + return self._crop_percent + + @crop_percent.setter + def crop_percent(self, new_crop_percent): + """ + Convenience function to set the value that `crop_percent` is set to. + + Parameters + ---------- + new_crop_percent : float + Floating point > 0.0 indicating how much of the length of the bone should be included + when cropping - expressed as a proportion of the width. + """ + if type(new_crop_percent) != float: + raise TypeError(f'New `crop_percent` provided is type {type(new_crop_percent)} - expected `float`') + self._crop_percent = new_crop_percent + + @property + def bone(self): + """ + Convenience function to get the name of the bone in this object. + + Returns + ------- + str + Name of the bone in this object - used to help identify how to crop the bone. + """ + return self._bone + + @bone.setter + def bone(self, new_bone): + """ + Convenience function to set the name of the bone in this object. + + Parameters + ---------- + new_bone : str + Name of the bone in this object - used to help identify how to crop the bone. + """ + if type(new_bone) != str: + raise TypeError(f'New bone provided is type {type(new_bone)} - expected `str`') + self._bone = new_bone </code></pre> +</details> +<h3>Ancestors</h3> +<ul class="hlist"> +<li><a title="pymskt.mesh.meshes.Mesh" href="#pymskt.mesh.meshes.Mesh">Mesh</a></li> +</ul> +<h3>Instance variables</h3> +<dl> +<dt id="pymskt.mesh.meshes.BoneMesh.bone"><code class="name">var <span class="ident">bone</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the name of the bone in this object. </p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>str</code></dt> +<dd>Name of the bone in this object - used to help identify how to crop the bone.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def bone(self): + """ + Convenience function to get the name of the bone in this object. + + Returns + ------- + str + Name of the bone in this object - used to help identify how to crop the bone. + """ + return self._bone</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.crop_percent"><code class="name">var <span class="ident">crop_percent</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the value that <code>crop_percent</code> is set to. </p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>float</code></dt> +<dd>Floating point > 0.0 indicating how much of the length of the bone should be included +when cropping - expressed as a proportion of the width.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def crop_percent(self): + """ + Convenience function to get the value that `crop_percent` is set to. + + Returns + ------- + float + Floating point > 0.0 indicating how much of the length of the bone should be included + when cropping - expressed as a proportion of the width. + """ + return self._crop_percent</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.list_cartilage_labels"><code class="name">var <span class="ident">list_cartilage_labels</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the list of labels for cartilage tissues associated +with this bone. </p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>list</code></dt> +<dd>list of <code>int</code>s for the cartilage tissues associated with this bone.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def list_cartilage_labels(self): + """ + Convenience function to get the list of labels for cartilage tissues associated + with this bone. + + Returns + ------- + list + list of `int`s for the cartilage tissues associated with this bone. + """ + return self._list_cartilage_labels</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.list_cartilage_meshes"><code class="name">var <span class="ident">list_cartilage_meshes</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the list of cartilage meshes</p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>list</code></dt> +<dd>A list of <code><a title="pymskt.mesh.meshes.CartilageMesh" href="#pymskt.mesh.meshes.CartilageMesh">CartilageMesh</a></code> objects associated with this bone</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def list_cartilage_meshes(self): + """ + Convenience function to get the list of cartilage meshes + + Returns + ------- + list + A list of `CartilageMesh` objects associated with this bone + """ + return self._list_cartilage_meshes</code></pre> +</details> +</dd> +</dl> +<h3>Methods</h3> +<dl> +<dt id="pymskt.mesh.meshes.BoneMesh.assign_cartilage_regions"><code class="name flex"> +<span>def <span class="ident">assign_cartilage_regions</span></span>(<span>self, image_smooth_var_cart=0.15625, marching_cubes_threshold=0.5, ray_cast_length=10.0, percent_ray_length_opposite_direction=0.25)</span> +</code></dt> +<dd> +<div class="desc"><p>Assign cartilage regions to the bone surface (e.g. medial/lateral tibial cartilage) +- Can also be used for femur sub-regions (anterior, medial weight-bearing, etc.)</p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>image_smooth_var_cart</code></strong> : <code>float</code>, optional</dt> +<dd>Variance of gaussian filter to be applied to binary cartilage masks, +by default 0.3125/2</dd> +<dt><strong><code>marching_cubes_threshold</code></strong> : <code>float</code>, optional</dt> +<dd>Threshold to create bone surface at, by default 0.5</dd> +<dt><strong><code>ray_cast_length</code></strong> : <code>float</code>, optional</dt> +<dd>Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & +outter shell), by default 10.0</dd> +<dt><strong><code>percent_ray_length_opposite_direction</code></strong> : <code>float</code>, optional</dt> +<dd>How far to project ray inside of the bone. This is done just in case the cartilage +surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def assign_cartilage_regions(self, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25): + """ + Assign cartilage regions to the bone surface (e.g. medial/lateral tibial cartilage) + - Can also be used for femur sub-regions (anterior, medial weight-bearing, etc.) + + Parameters + ---------- + image_smooth_var_cart : float, optional + Variance of gaussian filter to be applied to binary cartilage masks, + by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold to create bone surface at, by default 0.5 + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + """ + tmp_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + '.nrrd' + path_save_tmp_file = os.path.join('/tmp', tmp_filename) + # if self._bone == 'femur': + # new_seg_image = qc.get_knee_segmentation_with_femur_subregions(seg_image, + # fem_cart_label_idx=1) + # sitk.WriteImage(new_seg_image, path_save_tmp_file) + # else: + sitk.WriteImage(self._seg_image, path_save_tmp_file) + vtk_seg_reader = read_nrrd(path_save_tmp_file, + set_origin_zero=True + ) + vtk_seg = vtk_seg_reader.GetOutput() + + seg_transformer = SitkVtkTransformer(self._seg_image) + + # Delete tmp files + safely_delete_tmp_file('/tmp', + tmp_filename) + + self.apply_transform_to_mesh(transform=seg_transformer.get_inverse_transform()) + labels = np.zeros(self._mesh.GetNumberOfPoints(), dtype=np.int) + + # if cartilage meshes don't exist yet, then make them. + if self._list_cartilage_meshes is None: + self.create_cartilage_meshes(image_smooth_var_cart=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + + # iterate over meshes and add their label (region) + for cart_mesh in self._list_cartilage_meshes: + cart_mesh.apply_transform_to_mesh(transform=seg_transformer.get_inverse_transform()) + node_data = get_cartilage_properties_at_points(self._mesh, + cart_mesh._mesh, + t2_vtk_image=None, + seg_vtk_image=vtk_seg, + ray_cast_length=ray_cast_length, + percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + ) + labels += node_data[1] + cart_mesh.reverse_all_transforms() + + # Assign the label (region) scalars to the bone mesh surface. + label_scalars = numpy_to_vtk(labels) + label_scalars.SetName('labels') + self._mesh.GetPointData().AddArray(label_scalars) + + self.reverse_all_transforms()</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.calc_cartilage_t2"><code class="name flex"> +<span>def <span class="ident">calc_cartilage_t2</span></span>(<span>self, path_t2_nrrd, path_seg_to_t2_transform=None, ray_cast_length=10.0, percent_ray_length_opposite_direction=0.25)</span> +</code></dt> +<dd> +<div class="desc"><p>Apply cartilage T2 values to bone surface. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>path_t2_nrrd</code></strong> : <code>str</code></dt> +<dd>Path to nrrd image of T2 map to load / use.</dd> +<dt><strong><code>path_seg_to_t2_transform</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a transform file to be used for aligning T2 map with segmentations, +by default None</dd> +<dt><strong><code>ray_cast_length</code></strong> : <code>float</code>, optional</dt> +<dd>Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & +outter shell), by default 10.0</dd> +<dt><strong><code>percent_ray_length_opposite_direction</code></strong> : <code>float</code>, optional</dt> +<dd>How far to project ray inside of the bone. This is done just in case the cartilage +surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def calc_cartilage_t2(self, + path_t2_nrrd, + path_seg_to_t2_transform=None, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25): + """ + Apply cartilage T2 values to bone surface. + + Parameters + ---------- + path_t2_nrrd : str + Path to nrrd image of T2 map to load / use. + path_seg_to_t2_transform : str, optional + Path to a transform file to be used for aligning T2 map with segmentations, + by default None + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + """ + print('Not yet implemented') + # if self._list_cartilage_meshes is None: + # raise('Should calculate cartialge thickness before getting T2') + # # ALTERNATIVELY - COULD ALLOW PASSING OF CARTILAGE REGIONS IN HERE + # # THOUGH, DOES THAT JUST COMPLICATE THINGS? + + # if path_seg_transform is not None: + # # this is in case there is a transformation needed to align the segmentation with the + # # underlying T2 image + # seg_transform = sitk.ReadTransform(path_seg_transform) + # seg_image = apply_transform_retain_array(self._seg_image, + # seg_transform, + # interpolator=sitk.sitkNearestNeighbor) + + + # versor = get_versor_from_transform(seg_transform) + # center_transform, rotate_transform, translate_transform = break_versor_into_center_rotate_translate_transforms(versor) + # # first apply negative of center of rotation to mesh + # self._mesh.apply_transform_to_mesh(transform=center_transform.GetInverse()) + # # now apply the transform (rotation then translation) + # self._mesh.apply_transform_to_mesh(transform=rotate_transform.GetInverse()) + # self._mesh.apply_transform_to_mesh(transform=translate_transform.GetInverse()) + # #then undo the center of rotation + # self._mesh.apply_transform_to_mesh(transform=center_transform) + + # # Read t2 map (vtk format) + # vtk_t2map_reader = read_nrrd(path_t2_nrrd, + # set_origin_zero=True) + # vtk_t2map = vtk_t2map_reader.GetOutput() + # sitk_t2map = sitk.ReadImage(path_t2_nrrd) + # t2_transformer = SitkVtkTransformer(sitk_t2map) + + # self._mesh.apply_transform_to_mesh(transform=t2_transformer.get_inverse_transform()) + + # t2 = np.zeros(self._mesh.GetNumberOfPoints()) + # # iterate over meshes and add their t2 to the t2 list. + # for cart_mesh in self._list_cartilage_meshes: + # if path_seg_to_t2_transform is not None: + # # first apply negative of center of rotation to mesh + # cart_mesh.apply_transform_to_mesh(transform=center_transform.GetInverse()) + # # now apply the transform (rotation then translation) + # cart_mesh.apply_transform_to_mesh(transform=rotate_transform.GetInverse()) + # cart_mesh.apply_transform_to_mesh(transform=translate_transform.GetInverse()) + # #then undo the center of rotation + # cart_mesh.apply_transform_to_mesh(transform=center_transform) + + # cart_mesh.apply_transform_to_mesh(transform=t2_transformer.get_inverse_transform()) + # _, t2_data = get_cartilage_properties_at_points(self._mesh, + # cart_mesh._mesh, + # t2_vtk_image=vtk_t2map, + # ray_cast_length=ray_cast_length, + # percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + # ) + # t2 += t2_data + # cart_mesh.reverse_all_transforms() + print('NOT DONE!!!')</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.calc_cartilage_thickness"><code class="name flex"> +<span>def <span class="ident">calc_cartilage_thickness</span></span>(<span>self, list_cartilage_labels=None, list_cartilage_meshes=None, image_smooth_var_cart=0.15625, marching_cubes_threshold=0.5, ray_cast_length=10.0, percent_ray_length_opposite_direction=0.25)</span> +</code></dt> +<dd> +<div class="desc"><p>Using bone mesh (<code>_mesh</code>) and the list of cartilage meshes (<code>list_cartilage_meshes</code>) +calcualte the cartilage thickness for each node on the bone surface. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>list_cartilage_labels</code></strong> : <code>list</code>, optional</dt> +<dd>Cartilag labels to be used to create cartilage meshes (if they dont +exist), by default None</dd> +<dt><strong><code>list_cartilage_meshes</code></strong> : <code>list</code>, optional</dt> +<dd>Cartilage meshes to be used for calculating cart thickness, by default None</dd> +<dt><strong><code>image_smooth_var_cart</code></strong> : <code>float</code>, optional</dt> +<dd>Variance of gaussian filter to be applied to binary cartilage masks, +by default 0.3125/2</dd> +<dt><strong><code>marching_cubes_threshold</code></strong> : <code>float</code>, optional</dt> +<dd>Threshold to create bone surface at, by default 0.5</dd> +<dt><strong><code>ray_cast_length</code></strong> : <code>float</code>, optional</dt> +<dd>Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & +outter shell), by default 10.0</dd> +<dt><strong><code>percent_ray_length_opposite_direction</code></strong> : <code>float</code>, optional</dt> +<dd>How far to project ray inside of the bone. This is done just in case the cartilage +surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25</dd> +</dl> +<h2 id="raises">Raises</h2> +<dl> +<dt><code>Exception</code></dt> +<dd>No cartilage available (either <code>list_cartilage_meshes</code> or <code>list_cartilage_labels</code>)</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def calc_cartilage_thickness(self, + list_cartilage_labels=None, + list_cartilage_meshes=None, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5, + ray_cast_length=10.0, + percent_ray_length_opposite_direction=0.25 + ): + """ + Using bone mesh (`_mesh`) and the list of cartilage meshes (`list_cartilage_meshes`) + calcualte the cartilage thickness for each node on the bone surface. + + Parameters + ---------- + list_cartilage_labels : list, optional + Cartilag labels to be used to create cartilage meshes (if they dont + exist), by default None + list_cartilage_meshes : list, optional + Cartilage meshes to be used for calculating cart thickness, by default None + image_smooth_var_cart : float, optional + Variance of gaussian filter to be applied to binary cartilage masks, + by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold to create bone surface at, by default 0.5 + ray_cast_length : float, optional + Length (mm) of ray to cast from bone surface when trying to find cartilage (inner & + outter shell), by default 10.0 + percent_ray_length_opposite_direction : float, optional + How far to project ray inside of the bone. This is done just in case the cartilage + surface ends up slightly inside of (or coincident with) the bone surface, by default 0.25 + + Raises + ------ + Exception + No cartilage available (either `list_cartilage_meshes` or `list_cartilage_labels`) + """ + # If new cartilage infor/labels are provided, then replace existing with these ones. + if list_cartilage_meshes is not None: self._list_cartilage_meshes = list_cartilage_meshes + if list_cartilage_labels is not None: self._list_cartilage_labels = list_cartilage_labels + + # If no cartilage stuff provided, then cant do this function - raise exception. + if (self._list_cartilage_meshes is None) & (self._list_cartilage_labels is None): + raise Exception('No cartilage meshes or list of cartilage labels are provided! - These can be provided either to the class function `calc_cartilage_thickness` directly, or can be specified at the time of instantiating the `BoneMesh` class.') + + # if cartilage meshes don't exist yet, then make them. + if self._list_cartilage_meshes is None: + self.create_cartilage_meshes(image_smooth_var_cart=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + + # pre-allocate empty thicknesses so that as labels are iterated over, they can all be appended to the same bone. + thicknesses = np.zeros(self._mesh.GetNumberOfPoints()) + + # iterate over meshes and add their thicknesses to the thicknesses list. + for cart_mesh in self._list_cartilage_meshes: + node_data = get_cartilage_properties_at_points(self._mesh, + cart_mesh._mesh, + t2_vtk_image=None, + # seg_vtk_image=vtk_seg if assign_seg_label_to_bone is True else None, + seg_vtk_image=None, + ray_cast_length=ray_cast_length, + percent_ray_length_opposite_direction=percent_ray_length_opposite_direction + ) + thicknesses += node_data + + # Assign the thickness scalars to the bone mesh surface. + thickness_scalars = numpy_to_vtk(thicknesses) + thickness_scalars.SetName('thickness (mm)') + self._mesh.GetPointData().SetScalars(thickness_scalars)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.create_cartilage_meshes"><code class="name flex"> +<span>def <span class="ident">create_cartilage_meshes</span></span>(<span>self, image_smooth_var_cart=0.15625, marching_cubes_threshold=0.5)</span> +</code></dt> +<dd> +<div class="desc"><p>Helper function to create the list of cartilage meshes from the list of cartilage +labels. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>image_smooth_var_cart</code></strong> : <code>float</code></dt> +<dd>Variance to smooth cartilage segmentations before finding surface using continuous +marching cubes.</dd> +<dt><strong><code>marching_cubes_threshold</code></strong> : <code>float</code></dt> +<dd>Threshold value to create cartilage surface at from segmentation images.</dd> +</dl> +<h2 id="notes">Notes</h2> +<p>?? Should this function just be everything inside the for loop and then that +function gets called somewhere else?</p></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def create_cartilage_meshes(self, + image_smooth_var_cart=0.3125 / 2, + marching_cubes_threshold=0.5): + """ + Helper function to create the list of cartilage meshes from the list of cartilage + labels. + + Parameters + ---------- + image_smooth_var_cart : float + Variance to smooth cartilage segmentations before finding surface using continuous + marching cubes. + marching_cubes_threshold : float + Threshold value to create cartilage surface at from segmentation images. + + Notes + ----- + ?? Should this function just be everything inside the for loop and then that + function gets called somewhere else? + """ + + self._list_cartilage_meshes = [] + for cart_label_idx in self._list_cartilage_labels: + seg_array_view = sitk.GetArrayViewFromImage(self._seg_image) + n_pixels_with_cart = np.sum(seg_array_view == cart_label_idx) + if n_pixels_with_cart == 0: + warnings.warn( + f"Not analyzing cartilage for label {cart_label_idx} because it doesnt have any pixels!", + UserWarning + ) + else: + cart_mesh = CartilageMesh(seg_image=self._seg_image, + label_idx=cart_label_idx) + cart_mesh.create_mesh(smooth_image_var=image_smooth_var_cart, + marching_cubes_threshold=marching_cubes_threshold) + self._list_cartilage_meshes.append(cart_mesh)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.create_mesh"><code class="name flex"> +<span>def <span class="ident">create_mesh</span></span>(<span>self, smooth_image=True, smooth_image_var=0.15625, marching_cubes_threshold=0.5, label_idx=None, min_n_pixels=None, crop_percent=None)</span> +</code></dt> +<dd> +<div class="desc"><p>This is an extension of <code><a title="pymskt.mesh.meshes.Mesh.create_mesh" href="#pymskt.mesh.meshes.Mesh.create_mesh">Mesh.create_mesh()</a></code> that enables cropping of bones. +Bones might need to be cropped (this isnt necessary for cartilage) +So, adding this functionality to the processing steps before the bone mesh is created.</p> +<p>All functionality, except for that relevant to <code>crop_percent</code> is the same as: +<code><a title="pymskt.mesh.meshes.Mesh.create_mesh" href="#pymskt.mesh.meshes.Mesh.create_mesh">Mesh.create_mesh()</a></code>. </p> +<p>Create a surface mesh from the classes <code>_seg_image</code>. If <code>_seg_image</code> +does not exist, then read it in using <code>read_seg_image</code>. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>smooth_image</code></strong> : <code>bool</code>, optional</dt> +<dd>Should the <code>_seg_image</code> be gaussian filtered, by default True</dd> +<dt><strong><code>smooth_image_var</code></strong> : <code>float</code>, optional</dt> +<dd>Variance of gaussian filter to apply to <code>_seg_image</code>, by default 0.3125/2</dd> +<dt><strong><code>marching_cubes_threshold</code></strong> : <code>float</code>, optional</dt> +<dd>Threshold contour level to create surface mesh on, by default 0.5</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label value / index to create mesh from, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>Minimum number of continuous pixels to include segmentation island +in the surface mesh creation, by default None</dd> +<dt><strong><code>crop_percent</code></strong> : <code>[type]</code>, optional</dt> +<dd>[description], by default None</dd> +</dl> +<h2 id="raises">Raises</h2> +<dl> +<dt><code>Exception</code></dt> +<dd>If cropping & bone is not femur or tibia, then raise an error.</dd> +<dt><code>Exception</code></dt> +<dd>If the total number of pixels segmentated (<code>n_pixels_labelled</code>) is +< <code>min_n_pixels</code> then there is no object in the image.</dd> +<dt><code>Exception</code></dt> +<dd>If no <code>_seg_image</code> and no <code>label_idx</code> then we don't know what tissue to create the +surface mesh from.</dd> +<dt><code>Exception</code></dt> +<dd>If no <code>_seg_image</code> or <code>path_seg_image</code> then we have no image to create mesh from.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def create_mesh(self, + smooth_image=True, + smooth_image_var=0.3125 / 2, + marching_cubes_threshold=0.5, + label_idx=None, + min_n_pixels=None, + crop_percent=None + ): + """ + This is an extension of `Mesh.create_mesh` that enables cropping of bones. + Bones might need to be cropped (this isnt necessary for cartilage) + So, adding this functionality to the processing steps before the bone mesh is created. + + All functionality, except for that relevant to `crop_percent` is the same as: + `Mesh.create_mesh`. + + Create a surface mesh from the classes `_seg_image`. If `_seg_image` + does not exist, then read it in using `read_seg_image`. + + Parameters + ---------- + smooth_image : bool, optional + Should the `_seg_image` be gaussian filtered, by default True + smooth_image_var : float, optional + Variance of gaussian filter to apply to `_seg_image`, by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold contour level to create surface mesh on, by default 0.5 + label_idx : int, optional + Label value / index to create mesh from, by default None + min_n_pixels : int, optional + Minimum number of continuous pixels to include segmentation island + in the surface mesh creation, by default None + crop_percent : [type], optional + [description], by default None + + Raises + ------ + Exception + If cropping & bone is not femur or tibia, then raise an error. + Exception + If the total number of pixels segmentated (`n_pixels_labelled`) is + < `min_n_pixels` then there is no object in the image. + Exception + If no `_seg_image` and no `label_idx` then we don't know what tissue to create the + surface mesh from. + Exception + If no `_seg_image` or `path_seg_image` then we have no image to create mesh from. + """ + + if self._seg_image is None: + self.read_seg_image() + + # Bones might need to be cropped (this isnt necessary for cartilage) + # So, adding this functionality to the processing steps before the bone mesh is created + if crop_percent is not None: + self._crop_percent = crop_percent + if (self._crop_percent is not None) and (('femur' in self._bone) or ('tibia' in self._bone)): + if 'femur' in self._bone: + bone_crop_distal = True + elif 'tibia' in self._bone: + bone_crop_distal = False + else: + raise Exception('var bone should be "femur" or "tiba" got: {} instead'.format(self._bone)) + + self._seg_image = crop_bone_based_on_width(self._seg_image, + self._label_idx, + percent_width_to_crop_height=self._crop_percent, + bone_crop_distal=bone_crop_distal) + elif self._crop_percent is not None: + warnings.warn(f'Trying to crop bone, but {self._bone} specified and only bones `femur`', + 'or `tibia` currently supported for cropping. If using another bone, consider', + 'making a pull request. If cropping not desired, set `crop_percent=None`.' + ) + super().create_mesh(smooth_image=smooth_image, smooth_image_var=smooth_image_var, marching_cubes_threshold=marching_cubes_threshold, label_idx=label_idx, min_n_pixels=min_n_pixels)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.BoneMesh.smooth_surface_scalars"><code class="name flex"> +<span>def <span class="ident">smooth_surface_scalars</span></span>(<span>self, smooth_only_cartilage=True, scalar_sigma=1.6986436005760381, scalar_array_name='thickness (mm)', scalar_array_idx=None)</span> +</code></dt> +<dd> +<div class="desc"><p>Function to smooth the scalars with name <code>scalar_array_name</code> on the bone surface. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>smooth_only_cartilage</code></strong> : <code>bool</code>, optional</dt> +<dd>Should we only smooth where there is cartialge & ignore everywhere else, by default True</dd> +<dt><strong><code>scalar_sigma</code></strong> : <code>float</code>, optional</dt> +<dd>Smoothing sigma (standard deviation or sqrt(variance)) for gaussian filter, by default 1.6986436005760381 +default is based on a Full Width Half Maximum (FWHM) of 4mm.</dd> +<dt><strong><code>scalar_array_name</code></strong> : <code>str</code></dt> +<dd>Name of scalar array to smooth, default 'thickness (mm)'.</dd> +<dt><strong><code>scalar_array_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Index of the scalar array to smooth (alternative to using <code>scalar_array_name</code>) , by default None</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def smooth_surface_scalars(self, + smooth_only_cartilage=True, + scalar_sigma=1.6986436005760381, # This is a FWHM = 4 + scalar_array_name='thickness (mm)', + scalar_array_idx=None, + ): + + """ + Function to smooth the scalars with name `scalar_array_name` on the bone surface. + + Parameters + ---------- + smooth_only_cartilage : bool, optional + Should we only smooth where there is cartialge & ignore everywhere else, by default True + scalar_sigma : float, optional + Smoothing sigma (standard deviation or sqrt(variance)) for gaussian filter, by default 1.6986436005760381 + default is based on a Full Width Half Maximum (FWHM) of 4mm. + scalar_array_name : str + Name of scalar array to smooth, default 'thickness (mm)'. + scalar_array_idx : int, optional + Index of the scalar array to smooth (alternative to using `scalar_array_name`) , by default None + """ + if smooth_only_cartilage is True: + loc_cartilage = np.where(vtk_to_numpy(self._mesh.GetPointData().GetArray('thickness (mm)')) > 0.01)[0] + else: + loc_cartilage = None + self._mesh = gaussian_smooth_surface_scalars(self._mesh, + sigma=scalar_sigma, + idx_coords_to_smooth=loc_cartilage, + array_name=scalar_array_name, + array_idx=scalar_array_idx)</code></pre> +</details> +</dd> +</dl> +<h3>Inherited members</h3> +<ul class="hlist"> +<li><code><b><a title="pymskt.mesh.meshes.Mesh" href="#pymskt.mesh.meshes.Mesh">Mesh</a></b></code>: +<ul class="hlist"> +<li><code><a title="pymskt.mesh.meshes.Mesh.apply_transform_to_mesh" href="#pymskt.mesh.meshes.Mesh.apply_transform_to_mesh">apply_transform_to_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect" href="#pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect">copy_scalars_from_other_mesh_to_currect</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.label_idx" href="#pymskt.mesh.meshes.Mesh.label_idx">label_idx</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.list_applied_transforms" href="#pymskt.mesh.meshes.Mesh.list_applied_transforms">list_applied_transforms</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.mesh" href="#pymskt.mesh.meshes.Mesh.mesh">mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.min_n_pixels" href="#pymskt.mesh.meshes.Mesh.min_n_pixels">min_n_pixels</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.non_rigidly_register" href="#pymskt.mesh.meshes.Mesh.non_rigidly_register">non_rigidly_register</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.path_seg_image" href="#pymskt.mesh.meshes.Mesh.path_seg_image">path_seg_image</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.point_coords" href="#pymskt.mesh.meshes.Mesh.point_coords">point_coords</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.read_seg_image" href="#pymskt.mesh.meshes.Mesh.read_seg_image">read_seg_image</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.resample_surface" href="#pymskt.mesh.meshes.Mesh.resample_surface">resample_surface</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.reverse_all_transforms" href="#pymskt.mesh.meshes.Mesh.reverse_all_transforms">reverse_all_transforms</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.reverse_most_recent_transform" href="#pymskt.mesh.meshes.Mesh.reverse_most_recent_transform">reverse_most_recent_transform</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.rigidly_register" href="#pymskt.mesh.meshes.Mesh.rigidly_register">rigidly_register</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.save_mesh" href="#pymskt.mesh.meshes.Mesh.save_mesh">save_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.seg_image" href="#pymskt.mesh.meshes.Mesh.seg_image">seg_image</a></code></li> +</ul> +</li> +</ul> +</dd> +<dt id="pymskt.mesh.meshes.CartilageMesh"><code class="flex name class"> +<span>class <span class="ident">CartilageMesh</span></span> +<span>(</span><span>mesh=None, seg_image=None, path_seg_image=None, label_idx=None, min_n_pixels=1000)</span> +</code></dt> +<dd> +<div class="desc"><p>Class to create, store, and process cartilage meshes</p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>mesh</code></strong> : <code>vtk.vtkPolyData</code>, optional</dt> +<dd>vtkPolyData object that is basis of surface mesh, by default None</dd> +<dt><strong><code>seg_image</code></strong> : <code>SimpleITK.Image</code>, optional</dt> +<dd>Segmentation image that can be used to create surface mesh - used +instead of mesh, by default None</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a medical image (.nrrd) to load and create mesh from, +by default None</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label of anatomy of interest, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>All islands smaller than this size are dropped, by default 5000</dd> +</dl> +<h2 id="attributes">Attributes</h2> +<dl> +<dt><strong><code>_mesh</code></strong> : <code>vtk.vtkPolyData</code></dt> +<dd>Item passed from <strong>init</strong>, or created during life of class. +This is the main surface mesh of this class.</dd> +<dt><strong><code>_seg_image</code></strong> : <code>SimpleITK.Image</code></dt> +<dd>Segmentation image that can be used to create mesh. This is optional.</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code></dt> +<dd>Path to medical image (.nrrd) that can be loaded to create <code>_seg_image</code> +and then creat surface mesh <code>_mesh</code></dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code></dt> +<dd>Integer of anatomy to create surface mesh from <code>_seg_image</code></dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code></dt> +<dd>Minimum number of pixels for an isolated island of a segmentation to be +retained</dd> +<dt><strong><code>list_applied_transforms</code></strong> : <code>list</code></dt> +<dd>A list of transformations applied to a surface mesh. +This list allows for undoing of most recent transform, or undoing +all of them by iterating over the list in reverse.</dd> +</dl> +<h2 id="methods">Methods</h2> +<p>Initialize Mesh class</p> +<h2 id="parameters_1">Parameters</h2> +<dl> +<dt><strong><code>mesh</code></strong> : <code>vtk.vtkPolyData</code>, optional</dt> +<dd>vtkPolyData object that is basis of surface mesh, by default None</dd> +<dt><strong><code>seg_image</code></strong> : <code>SimpleITK.Image</code>, optional</dt> +<dd>Segmentation image that can be used to create surface mesh - used +instead of mesh, by default None</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a medical image (.nrrd) to load and create mesh from, +by default None</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label of anatomy of interest, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>All islands smaller than this size are dropped, by default 5000</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">class CartilageMesh(Mesh): + """ + Class to create, store, and process cartilage meshes + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + + + Attributes + ---------- + _mesh : vtk.vtkPolyData + Item passed from __init__, or created during life of class. + This is the main surface mesh of this class. + _seg_image : SimpleITK.Image + Segmentation image that can be used to create mesh. This is optional. + path_seg_image : str + Path to medical image (.nrrd) that can be loaded to create `_seg_image` + and then creat surface mesh `_mesh` + label_idx : int + Integer of anatomy to create surface mesh from `_seg_image` + min_n_pixels : int + Minimum number of pixels for an isolated island of a segmentation to be + retained + list_applied_transforms : list + A list of transformations applied to a surface mesh. + This list allows for undoing of most recent transform, or undoing + all of them by iterating over the list in reverse. + + Methods + ---------- + + """ + + def __init__(self, + mesh=None, + seg_image=None, + path_seg_image=None, + label_idx=None, + min_n_pixels=1000 + ): + super().__init__(mesh=mesh, + seg_image=seg_image, + path_seg_image=path_seg_image, + label_idx=label_idx, + min_n_pixels=min_n_pixels)</code></pre> +</details> +<h3>Ancestors</h3> +<ul class="hlist"> +<li><a title="pymskt.mesh.meshes.Mesh" href="#pymskt.mesh.meshes.Mesh">Mesh</a></li> +</ul> +<h3>Inherited members</h3> +<ul class="hlist"> +<li><code><b><a title="pymskt.mesh.meshes.Mesh" href="#pymskt.mesh.meshes.Mesh">Mesh</a></b></code>: +<ul class="hlist"> +<li><code><a title="pymskt.mesh.meshes.Mesh.apply_transform_to_mesh" href="#pymskt.mesh.meshes.Mesh.apply_transform_to_mesh">apply_transform_to_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect" href="#pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect">copy_scalars_from_other_mesh_to_currect</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.create_mesh" href="#pymskt.mesh.meshes.Mesh.create_mesh">create_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.label_idx" href="#pymskt.mesh.meshes.Mesh.label_idx">label_idx</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.list_applied_transforms" href="#pymskt.mesh.meshes.Mesh.list_applied_transforms">list_applied_transforms</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.mesh" href="#pymskt.mesh.meshes.Mesh.mesh">mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.min_n_pixels" href="#pymskt.mesh.meshes.Mesh.min_n_pixels">min_n_pixels</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.non_rigidly_register" href="#pymskt.mesh.meshes.Mesh.non_rigidly_register">non_rigidly_register</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.path_seg_image" href="#pymskt.mesh.meshes.Mesh.path_seg_image">path_seg_image</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.point_coords" href="#pymskt.mesh.meshes.Mesh.point_coords">point_coords</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.read_seg_image" href="#pymskt.mesh.meshes.Mesh.read_seg_image">read_seg_image</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.resample_surface" href="#pymskt.mesh.meshes.Mesh.resample_surface">resample_surface</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.reverse_all_transforms" href="#pymskt.mesh.meshes.Mesh.reverse_all_transforms">reverse_all_transforms</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.reverse_most_recent_transform" href="#pymskt.mesh.meshes.Mesh.reverse_most_recent_transform">reverse_most_recent_transform</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.rigidly_register" href="#pymskt.mesh.meshes.Mesh.rigidly_register">rigidly_register</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.save_mesh" href="#pymskt.mesh.meshes.Mesh.save_mesh">save_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.seg_image" href="#pymskt.mesh.meshes.Mesh.seg_image">seg_image</a></code></li> +</ul> +</li> +</ul> +</dd> +<dt id="pymskt.mesh.meshes.Mesh"><code class="flex name class"> +<span>class <span class="ident">Mesh</span></span> +<span>(</span><span>mesh=None, seg_image=None, path_seg_image=None, label_idx=None, min_n_pixels=5000)</span> +</code></dt> +<dd> +<div class="desc"><p>An object to contain surface meshes for musculoskeletal anatomy. Includes helper +functions to build surface meshes, to process them, and to save them. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>mesh</code></strong> : <code>vtk.vtkPolyData</code>, optional</dt> +<dd>vtkPolyData object that is basis of surface mesh, by default None</dd> +<dt><strong><code>seg_image</code></strong> : <code>SimpleITK.Image</code>, optional</dt> +<dd>Segmentation image that can be used to create surface mesh - used +instead of mesh, by default None</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a medical image (.nrrd) to load and create mesh from, +by default None</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label of anatomy of interest, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>All islands smaller than this size are dropped, by default 5000</dd> +</dl> +<h2 id="attributes">Attributes</h2> +<dl> +<dt><strong><code>_mesh</code></strong> : <code>vtk.vtkPolyData</code></dt> +<dd>Item passed from <strong>init</strong>, or created during life of class. +This is the main surface mesh of this class.</dd> +<dt><strong><code>_seg_image</code></strong> : <code>SimpleITK.Image</code></dt> +<dd>Segmentation image that can be used to create mesh. This is optional.</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code></dt> +<dd>Path to medical image (.nrrd) that can be loaded to create <code>_seg_image</code> +and then creat surface mesh <code>_mesh</code></dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code></dt> +<dd>Integer of anatomy to create surface mesh from <code>_seg_image</code></dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code></dt> +<dd>Minimum number of pixels for an isolated island of a segmentation to be +retained</dd> +<dt><strong><code>list_applied_transforms</code></strong> : <code>list</code></dt> +<dd>A list of transformations applied to a surface mesh. +This list allows for undoing of most recent transform, or undoing +all of them by iterating over the list in reverse.</dd> +</dl> +<h2 id="methods">Methods</h2> +<p>Initialize Mesh class</p> +<h2 id="parameters_1">Parameters</h2> +<dl> +<dt><strong><code>mesh</code></strong> : <code>vtk.vtkPolyData</code>, optional</dt> +<dd>vtkPolyData object that is basis of surface mesh, by default None</dd> +<dt><strong><code>seg_image</code></strong> : <code>SimpleITK.Image</code>, optional</dt> +<dd>Segmentation image that can be used to create surface mesh - used +instead of mesh, by default None</dd> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to a medical image (.nrrd) to load and create mesh from, +by default None</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label of anatomy of interest, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>All islands smaller than this size are dropped, by default 5000</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">class Mesh: + """ + An object to contain surface meshes for musculoskeletal anatomy. Includes helper + functions to build surface meshes, to process them, and to save them. + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + + + Attributes + ---------- + _mesh : vtk.vtkPolyData + Item passed from __init__, or created during life of class. + This is the main surface mesh of this class. + _seg_image : SimpleITK.Image + Segmentation image that can be used to create mesh. This is optional. + path_seg_image : str + Path to medical image (.nrrd) that can be loaded to create `_seg_image` + and then creat surface mesh `_mesh` + label_idx : int + Integer of anatomy to create surface mesh from `_seg_image` + min_n_pixels : int + Minimum number of pixels for an isolated island of a segmentation to be + retained + list_applied_transforms : list + A list of transformations applied to a surface mesh. + This list allows for undoing of most recent transform, or undoing + all of them by iterating over the list in reverse. + + Methods + ---------- + + """ + def __init__(self, + mesh=None, + seg_image=None, + path_seg_image=None, + label_idx=None, + min_n_pixels=5000 + ): + """ + Initialize Mesh class + + Parameters + ---------- + mesh : vtk.vtkPolyData, optional + vtkPolyData object that is basis of surface mesh, by default None + seg_image : SimpleITK.Image, optional + Segmentation image that can be used to create surface mesh - used + instead of mesh, by default None + path_seg_image : str, optional + Path to a medical image (.nrrd) to load and create mesh from, + by default None + label_idx : int, optional + Label of anatomy of interest, by default None + min_n_pixels : int, optional + All islands smaller than this size are dropped, by default 5000 + """ + if type(mesh) in (str,): #accept path like objects? + print('mesh string passed, loading mesh from disk') + self._mesh = io.read_vtk(mesh) + else: + self._mesh = mesh + self._seg_image = seg_image + self._path_seg_image = path_seg_image + self._label_idx = label_idx + self._min_n_pixels = min_n_pixels + + self._list_applied_transforms = [] + + def read_seg_image(self, + path_seg_image=None): + """ + Read segmentation image from disk. Must be a single file (e.g., nrrd, 3D dicom) + + Parameters + ---------- + path_seg_image : str, optional + Path to the medical image file to be loaded in, by default None + + Raises + ------ + Exception + If path_seg_image does not exist, exception is raised. + """ + # If passing new location/seg image name, then update variables. + if path_seg_image is not None: + self._path_seg_image = path_seg_image + + # If seg image location / name exist, then load image else raise exception + if (self._path_seg_image is not None): + self._seg_image = sitk.ReadImage(self._path_seg_image) + else: + raise Exception('No file path (self._path_seg_image) provided.') + + def create_mesh(self, + smooth_image=True, + smooth_image_var=0.3125 / 2, + marching_cubes_threshold=0.5, + label_idx=None, + min_n_pixels=None): + """ + Create a surface mesh from the classes `_seg_image`. If `_seg_image` + does not exist, then read it in using `read_seg_image`. + + Parameters + ---------- + smooth_image : bool, optional + Should the `_seg_image` be gaussian filtered, by default True + smooth_image_var : float, optional + Variance of gaussian filter to apply to `_seg_image`, by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold contour level to create surface mesh on, by default 0.5 + label_idx : int, optional + Label value / index to create mesh from, by default None + min_n_pixels : int, optional + Minimum number of continuous pixels to include segmentation island + in the surface mesh creation, by default None + + Raises + ------ + Exception + If the total number of pixels segmentated (`n_pixels_labelled`) is + < `min_n_pixels` then there is no object in the image. + Exception + If no `_seg_image` and no `label_idx` then we don't know what tissue to create the + surface mesh from. + Exception + If no `_seg_image` or `path_seg_image` then we have no image to create mesh from. + """ + # allow assigning label idx during mesh creation step. + if label_idx is not None: + self._label_idx = label_idx + + if self._seg_image is None: + self.read_seg_image() + + # Ensure the image has a certain number of pixels with the label of interest, otherwise there might be an issue. + if min_n_pixels is None: min_n_pixels = self._min_n_pixels + seg_view = sitk.GetArrayViewFromImage(self._seg_image) + n_pixels_labelled = sum(seg_view[seg_view == self._label_idx]) + + if n_pixels_labelled < min_n_pixels: + raise Exception('The mesh does not exist in this segmentation!, only {} pixels detected, threshold # is {}'.format(n_pixels_labelled, + marching_cubes_threshold)) + tmp_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + '.nrrd' + self._mesh = create_surface_mesh(self._seg_image, + self._label_idx, + smooth_image_var, + loc_tmp_save='/tmp', + tmp_filename=tmp_filename, + mc_threshold=marching_cubes_threshold, + filter_binary_image=smooth_image + ) + safely_delete_tmp_file('/tmp', + tmp_filename) + + def save_mesh(self, + filepath): + """ + Save the surface mesh from this class to disk. + + Parameters + ---------- + filepath : str + Location & filename to save the surface mesh (vtk.vtkPolyData) to. + """ + io.write_vtk(self._mesh, filepath) + + def resample_surface(self, + subdivisions=2, + clusters=10000 + ): + """ + Resample a surface mesh using the ACVD algorithm: + Version used: + - https://github.com/pyvista/pyacvd + Original version w/ more references: + - https://github.com/valette/ACVD + + Parameters + ---------- + subdivisions : int, optional + Subdivide the mesh to have more points before clustering, by default 2 + Probably not necessary for very dense meshes. + clusters : int, optional + The number of clusters (points/vertices) to create during resampling + surafce, by default 10000 + - This is not exact, might have slight differences. + """ + self._mesh = resample_surface(self._mesh, subdivisions=subdivisions, clusters=clusters) + + def apply_transform_to_mesh(self, + transform=None, + transformer=None, + save_transform=True): + """ + Apply a transformation to the surface mesh. + + Parameters + ---------- + transform : vtk.vtkTransform, optional + Transformation to apply to mesh, by default None + transformer : vtk.vtkTransformFilter, optional + Can supply transformFilter directly, by default None + save_transform : bool, optional + Should transform be saved to list of applied transforms, by default True + + Raises + ------ + Exception + No `transform` or `transformer` supplied - have not transformation + to apply. + """ + if (transform is not None) & (transformer is None): + transformer = vtk.vtkTransformPolyDataFilter() + transformer.SetTransform(transform) + + elif (transform is None) & (transformer is not None): + transform = transformer.GetTransform() + + if transformer is not None: + transformer.SetInputData(self._mesh) + transformer.Update() + self._mesh = vtk_deep_copy(transformer.GetOutput()) + + if save_transform is True: + self._list_applied_transforms.append(transform) + + else: + raise Exception('No transform or transformer provided') + + def reverse_most_recent_transform(self): + """ + Function to undo the most recent transformation stored in self._list_applied_transforms + """ + transform = self._list_applied_transforms.pop() + transform.Inverse() + self.apply_transform_to_mesh(transform=transform, save_transform=False) + + def reverse_all_transforms(self): + """ + Function to iterate over all of the self._list_applied_transforms (in reverse order) and undo them. + """ + while len(self._list_applied_transforms) > 0: + self.reverse_most_recent_transform() + + def non_rigidly_register( + self, + other_mesh, + as_source=True, + apply_transform_to_mesh=True, + return_transformed_mesh=False, + **kwargs + ): + """ + Function to perform non rigid registration between this mesh and another mesh. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Other mesh to use in registration process + as_source : bool, optional + Should the current mesh (in this object) be the source or the target, by default True + apply_transform_to_mesh : bool, optional + If as_source is True should we apply transformation to internal mesh, by default True + return_transformed_mesh : bool, optional + Should we return the registered mesh, by default False + + Returns + ------- + _type_ + _description_ + """ + # Setup the source & target meshes based on `as_source`` + if as_source is True: + source = self._mesh + target = other_mesh + elif as_source is False: + source = other_mesh + target = self._mesh + + # Get registered mesh (source to target) + source_transformed_to_target = non_rigidly_register( + target_mesh=target, + source_mesh=source, + **kwargs + ) + + # If current mesh is source & apply_transform_to_mesh is true then replace current mesh. + if (as_source is True) & (apply_transform_to_mesh is True): + self._mesh = source_transformed_to_target + + # curent mesh is target, or is source & want to return mesh, then return it. + if (as_source is False) or ((as_source is True) & (return_transformed_mesh is True)): + return source_transformed_to_target + + def rigidly_register( + self, + other_mesh, + as_source=True, + apply_transform_to_mesh=True, + return_transformed_mesh=False, + return_transform=False, + max_n_iter=100, + n_landmarks=1000, + reg_mode='similarity' + + ): + """ + Function to perform rigid registration between this mesh and another mesh. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Other mesh to use in registration process + as_source : bool, optional + Should the current mesh (in this object) be the source or the target, by default True + apply_transform_to_mesh : bool, optional + If as_source is True should we apply transformation to internal mesh, by default True + return_transformed_mesh : bool, optional + Should we return the registered mesh, by default False + max_n_iter : int, optional + Maximum number of iterations to perform, by default 100 + n_landmarks : int, optional + Number of landmarks to use in registration, by default 1000 + reg_mode : str, optional + Mode of registration to use, by default 'similarity' (similarity, rigid, or affine) + + Returns + ------- + _type_ + _description_ + """ + + if (return_transform is True) & (return_transformed_mesh is True): + raise Exception('Cannot return both transformed mesh and transform') + + if type(other_mesh) in (pymskt.mesh.meshes.BoneMesh, pymskt.mesh.meshes.Mesh): + other_mesh = other_mesh.mesh + + # Setup the source & target meshes based on `as_source`` + if as_source is True: + source = self._mesh + target = other_mesh + elif as_source is False: + source = other_mesh + target = self._mesh + + icp_transform = get_icp_transform( + source=source, + target=target, + max_n_iter=max_n_iter, + n_landmarks=n_landmarks, + reg_mode=reg_mode + ) + + # If current mesh is source & apply_transform_to_mesh is true then replace current mesh. + if (as_source is True) & (apply_transform_to_mesh is True): + self.apply_transform_to_mesh(transform=icp_transform) + + if return_transformed_mesh is True: + return self._mesh + + elif return_transform is True: + return icp_transform + + # curent mesh is target, or is source & want to return mesh, then return it. + elif (as_source is False) & (return_transformed_mesh is True): + return apply_transform(source=source, transform=icp_transform) + + else: + raise Exception('Nothing to return from rigid registration.') + + def copy_scalars_from_other_mesh_to_currect( + self, + other_mesh, + new_scalars_name='scalars_from_other_mesh', + weighted_avg=True, # Use weighted average, otherwise guassian smooth transfer + n_closest=3, + sigma=1., + idx_coords_to_smooth_base=None, + idx_coords_to_smooth_other=None, + set_non_smoothed_scalars_to_zero=True, + ): + """ + Convenience function to enable easy transfer scalars from another mesh to the current. + Can use either a gaussian smoothing function, or transfer using nearest neighbours. + + ** This function requires that the `other_mesh` is non-rigidly registered to the surface + of the mesh inside of this class. Or rigidly registered but using the same anatomy that + VERY closely matches. Otherwise, the transfered scalars will be bad. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Mesh we want to copy + new_scalars_name : str, optional + What to name the scalars being transfered to this current mesh, by default 'scalars_from_other_mesh' + weighted_avg : bool, optional + Should we use `weighted average` or `gaussian smooth` methods for transfer, by default True + n_closest : int, optional + If `weighted_avg` True, the number of nearest neighbours to use, by default 3 + sigma : float, optional + If `weighted_avg` False, the standard deviation of gaussian kernel, by default 1. + idx_coords_to_smooth_base : list, optional + If `weighted_avg` False, list of indices from current mesh to use in transfer, by default None + idx_coords_to_smooth_other : list, optional + If `weighted_avg` False, list of indices from `other_mesh` to use in transfer, by default None + set_non_smoothed_scalars_to_zero : bool, optional + Should all other indices (not included in idx_coords_to_smooth_other) be set to 0, by default True + """ + if type(other_mesh) is Mesh: + other_mesh = other_mesh.mesh + elif type(other_mesh) is vtk.vtkPolyData: + pass + else: + raise TypeError(f'other_mesh must be type `pymskt.mesh.Mesh` or `vtk.vtkPolyData` and received: {type(other_mesh)}') + + if weighted_avg is True: + transferred_scalars = transfer_mesh_scalars_get_weighted_average_n_closest( + self._mesh, + other_mesh, + n=n_closest + ) + else: + transferred_scalars = smooth_scalars_from_second_mesh_onto_base( + self._mesh, + other_mesh, + sigma=sigma, + idx_coords_to_smooth_base=idx_coords_to_smooth_base, + idx_coords_to_smooth_second=idx_coords_to_smooth_other, + set_non_smoothed_scalars_to_zero=set_non_smoothed_scalars_to_zero + ) + if (new_scalars_name is None) & (weighted_avg is True): + if transferred_scalars.shape[1] > 1: + n_arrays = other_mesh.GetPointData().GetNumberOfArrays() + array_names = [other_mesh.GetPointData().GetArray(array_idx).GetName() for array_idx in range(n_arrays)] + for idx, array_name in enumerate(array_names): + vtk_transferred_scalars = numpy_to_vtk(transferred_scalars[:,idx]) + vtk_transferred_scalars.SetName(array_name) + self._mesh.GetPointData().AddArray(vtk_transferred_scalars) + return + + vtk_transferred_scalars = numpy_to_vtk(transferred_scalars) + vtk_transferred_scalars.SetName(new_scalars_name) + self._mesh.GetPointData().AddArray(vtk_transferred_scalars) + + @property + def seg_image(self): + """ + Return the `_seg_image` object + + Returns + ------- + SimpleITK.Image + Segmentation image used to build the surface mesh + """ + return self._seg_image + + @seg_image.setter + def seg_image(self, new_seg_image): + """ + Set the `_seg_image` of the class to be the inputted `new_seg_image`. + + Parameters + ---------- + new_seg_image : SimpleITK.Image + New image to use for creating surface mesh. This can be used to provide image to + class if it was not provided during `__init__` + """ + self._seg_image = new_seg_image + + @property + def mesh(self): + """ + Return the `_mesh` object + + Returns + ------- + vtk.vtkPolyData + The main mesh of this class. + """ + return self._mesh + + @mesh.setter + def mesh(self, new_mesh): + """ + Set the `_mesh` of the class to be the inputted `new_mesh` + + Parameters + ---------- + new_mesh : vtk.vtkPolyData + New mesh for this class - or a method to provide a mesh to the class + after `__init__` has already been run. + """ + self._mesh = new_mesh + + @property + def point_coords(self): + """ + Convenience function to return the vertices (point coordinates) for the surface mesh. + + Returns + ------- + numpy.ndarray + Mx3 numpy array containing the x/y/z position of each vertex of the mesh. + """ + return get_mesh_physical_point_coords(self._mesh) + + @point_coords.setter + def point_coords(self, new_point_coords): + """ + Convenience function to change/update the vertices/points locations + + Parameters + ---------- + new_point_coords : numpy.ndarray + n_points X 3 numpy array to replace exisiting point coordinate locations + This can be used to easily/quickly update the x/y/z position of a set of points on a surface mesh. + The `new_point_coords` must include the same number of points as the mesh contains. + """ + orig_point_coords = get_mesh_physical_point_coords(self._mesh) + if new_point_coords.shape == orig_point_coords.shape: + self._mesh.GetPoints().SetData(numpy_to_vtk(new_point_coords)) + + + @property + def path_seg_image(self): + """ + Convenience function to get the `path_seg_image` + + Returns + ------- + str + Path to the segmentation image + """ + return self._path_seg_image + + @path_seg_image.setter + def path_seg_image(self, new_path_seg_image): + """ + Convenience function to set the `path_seg_image` + + Parameters + ---------- + new_path_seg_image : str + String to where segmentation image that should be loaded is. + """ + self._path_seg_image = new_path_seg_image + + @property + def label_idx(self): + """ + Convenience function to get `label_idx` + + Returns + ------- + int + Integer indeicating the index/value of the tissues in `seg_image` associated with this mesh. + """ + return self._label_idx + + @label_idx.setter + def label_idx(self, new_label_idx): + """ + Convenience function to set `label_idx` + + Parameters + ---------- + new_label_idx : int + Integer indeicating the index/value of the tissues in `seg_image` associated with this mesh. + """ + self._label_idx = new_label_idx + + @property + def min_n_pixels(self): + """ + Convenience function to get the minimum number of pixels for a segmentation region to be created as a mesh. + + Returns + ------- + int + Minimum number of pixels needed to create a mesh. Less than this and it will be skipped / error raised. + """ + return self._min_n_pixels + + @min_n_pixels.setter + def min_n_pixels(self, new_min_n_pixels): + """ + Convenience function to set the minimum number of pixels for a segmentation region to be created as a mesh. + + Parameters + ---------- + new_min_n_pixels : int + Minimum number of pixels needed to create a mesh. Less than this and it will be skipped / error raised. + """ + self._min_n_pixels = new_min_n_pixels + + @property + def list_applied_transforms(self): + """ + Convenience function to get the list of transformations that have been applied to this mesh. + + Returns + ------- + list + List of vtk.vtkTransform objects that have been applied to the current mesh. + """ + return self._list_applied_transforms</code></pre> +</details> +<h3>Subclasses</h3> +<ul class="hlist"> +<li><a title="pymskt.mesh.meshes.BoneMesh" href="#pymskt.mesh.meshes.BoneMesh">BoneMesh</a></li> +<li><a title="pymskt.mesh.meshes.CartilageMesh" href="#pymskt.mesh.meshes.CartilageMesh">CartilageMesh</a></li> +</ul> +<h3>Instance variables</h3> +<dl> +<dt id="pymskt.mesh.meshes.Mesh.label_idx"><code class="name">var <span class="ident">label_idx</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get <code>label_idx</code></p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>int</code></dt> +<dd>Integer indeicating the index/value of the tissues in <code>seg_image</code> associated with this mesh.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def label_idx(self): + """ + Convenience function to get `label_idx` + + Returns + ------- + int + Integer indeicating the index/value of the tissues in `seg_image` associated with this mesh. + """ + return self._label_idx</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.list_applied_transforms"><code class="name">var <span class="ident">list_applied_transforms</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the list of transformations that have been applied to this mesh. </p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>list</code></dt> +<dd>List of vtk.vtkTransform objects that have been applied to the current mesh.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def list_applied_transforms(self): + """ + Convenience function to get the list of transformations that have been applied to this mesh. + + Returns + ------- + list + List of vtk.vtkTransform objects that have been applied to the current mesh. + """ + return self._list_applied_transforms</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.mesh"><code class="name">var <span class="ident">mesh</span></code></dt> +<dd> +<div class="desc"><p>Return the <code>_mesh</code> object</p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>vtk.vtkPolyData</code></dt> +<dd>The main mesh of this class.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def mesh(self): + """ + Return the `_mesh` object + + Returns + ------- + vtk.vtkPolyData + The main mesh of this class. + """ + return self._mesh</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.min_n_pixels"><code class="name">var <span class="ident">min_n_pixels</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the minimum number of pixels for a segmentation region to be created as a mesh. </p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>int</code></dt> +<dd>Minimum number of pixels needed to create a mesh. Less than this and it will be skipped / error raised.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def min_n_pixels(self): + """ + Convenience function to get the minimum number of pixels for a segmentation region to be created as a mesh. + + Returns + ------- + int + Minimum number of pixels needed to create a mesh. Less than this and it will be skipped / error raised. + """ + return self._min_n_pixels</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.path_seg_image"><code class="name">var <span class="ident">path_seg_image</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to get the <code>path_seg_image</code></p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>str</code></dt> +<dd>Path to the segmentation image</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def path_seg_image(self): + """ + Convenience function to get the `path_seg_image` + + Returns + ------- + str + Path to the segmentation image + """ + return self._path_seg_image</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.point_coords"><code class="name">var <span class="ident">point_coords</span></code></dt> +<dd> +<div class="desc"><p>Convenience function to return the vertices (point coordinates) for the surface mesh. </p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>numpy.ndarray</code></dt> +<dd>Mx3 numpy array containing the x/y/z position of each vertex of the mesh.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def point_coords(self): + """ + Convenience function to return the vertices (point coordinates) for the surface mesh. + + Returns + ------- + numpy.ndarray + Mx3 numpy array containing the x/y/z position of each vertex of the mesh. + """ + return get_mesh_physical_point_coords(self._mesh)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.seg_image"><code class="name">var <span class="ident">seg_image</span></code></dt> +<dd> +<div class="desc"><p>Return the <code>_seg_image</code> object</p> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>SimpleITK.Image</code></dt> +<dd>Segmentation image used to build the surface mesh</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">@property +def seg_image(self): + """ + Return the `_seg_image` object + + Returns + ------- + SimpleITK.Image + Segmentation image used to build the surface mesh + """ + return self._seg_image</code></pre> +</details> +</dd> +</dl> +<h3>Methods</h3> +<dl> +<dt id="pymskt.mesh.meshes.Mesh.apply_transform_to_mesh"><code class="name flex"> +<span>def <span class="ident">apply_transform_to_mesh</span></span>(<span>self, transform=None, transformer=None, save_transform=True)</span> +</code></dt> +<dd> +<div class="desc"><p>Apply a transformation to the surface mesh. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>transform</code></strong> : <code>vtk.vtkTransform</code>, optional</dt> +<dd>Transformation to apply to mesh, by default None</dd> +<dt><strong><code>transformer</code></strong> : <code>vtk.vtkTransformFilter</code>, optional</dt> +<dd>Can supply transformFilter directly, by default None</dd> +<dt><strong><code>save_transform</code></strong> : <code>bool</code>, optional</dt> +<dd>Should transform be saved to list of applied transforms, by default True</dd> +</dl> +<h2 id="raises">Raises</h2> +<dl> +<dt><code>Exception</code></dt> +<dd>No <code>transform</code> or <code>transformer</code> supplied - have not transformation +to apply.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def apply_transform_to_mesh(self, + transform=None, + transformer=None, + save_transform=True): + """ + Apply a transformation to the surface mesh. + + Parameters + ---------- + transform : vtk.vtkTransform, optional + Transformation to apply to mesh, by default None + transformer : vtk.vtkTransformFilter, optional + Can supply transformFilter directly, by default None + save_transform : bool, optional + Should transform be saved to list of applied transforms, by default True + + Raises + ------ + Exception + No `transform` or `transformer` supplied - have not transformation + to apply. + """ + if (transform is not None) & (transformer is None): + transformer = vtk.vtkTransformPolyDataFilter() + transformer.SetTransform(transform) + + elif (transform is None) & (transformer is not None): + transform = transformer.GetTransform() + + if transformer is not None: + transformer.SetInputData(self._mesh) + transformer.Update() + self._mesh = vtk_deep_copy(transformer.GetOutput()) + + if save_transform is True: + self._list_applied_transforms.append(transform) + + else: + raise Exception('No transform or transformer provided')</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect"><code class="name flex"> +<span>def <span class="ident">copy_scalars_from_other_mesh_to_currect</span></span>(<span>self, other_mesh, new_scalars_name='scalars_from_other_mesh', weighted_avg=True, n_closest=3, sigma=1.0, idx_coords_to_smooth_base=None, idx_coords_to_smooth_other=None, set_non_smoothed_scalars_to_zero=True)</span> +</code></dt> +<dd> +<div class="desc"><p>Convenience function to enable easy transfer scalars from another mesh to the current. +Can use either a gaussian smoothing function, or transfer using nearest neighbours. </p> +<p>** This function requires that the <code>other_mesh</code> is non-rigidly registered to the surface +of the mesh inside of this class. Or rigidly registered but using the same anatomy that +VERY closely matches. Otherwise, the transfered scalars will be bad. +</p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>other_mesh</code></strong> : <code>pymskt.mesh.Mesh</code> or <code>vtk.vtkPolyData</code></dt> +<dd>Mesh we want to copy</dd> +<dt><strong><code>new_scalars_name</code></strong> : <code>str</code>, optional</dt> +<dd> </dd> +<dt>What to name the scalars being transfered to this current mesh, by default 'scalars_from_other_mesh'</dt> +<dt><strong><code>weighted_avg</code></strong> : <code>bool</code>, optional</dt> +<dd>Should we use <code>weighted average</code> or <code>gaussian smooth</code> methods for transfer, by default True</dd> +<dt><strong><code>n_closest</code></strong> : <code>int</code>, optional</dt> +<dd>If <code>weighted_avg</code> True, the number of nearest neighbours to use, by default 3</dd> +<dt><strong><code>sigma</code></strong> : <code>float</code>, optional</dt> +<dd>If <code>weighted_avg</code> False, the standard deviation of gaussian kernel, by default 1.</dd> +<dt><strong><code>idx_coords_to_smooth_base</code></strong> : <code>list</code>, optional</dt> +<dd>If <code>weighted_avg</code> False, list of indices from current mesh to use in transfer, by default None</dd> +<dt><strong><code>idx_coords_to_smooth_other</code></strong> : <code>list</code>, optional</dt> +<dd>If <code>weighted_avg</code> False, list of indices from <code>other_mesh</code> to use in transfer, by default None</dd> +<dt><strong><code>set_non_smoothed_scalars_to_zero</code></strong> : <code>bool</code>, optional</dt> +<dd>Should all other indices (not included in idx_coords_to_smooth_other) be set to 0, by default True</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def copy_scalars_from_other_mesh_to_currect( + self, + other_mesh, + new_scalars_name='scalars_from_other_mesh', + weighted_avg=True, # Use weighted average, otherwise guassian smooth transfer + n_closest=3, + sigma=1., + idx_coords_to_smooth_base=None, + idx_coords_to_smooth_other=None, + set_non_smoothed_scalars_to_zero=True, +): + """ + Convenience function to enable easy transfer scalars from another mesh to the current. + Can use either a gaussian smoothing function, or transfer using nearest neighbours. + + ** This function requires that the `other_mesh` is non-rigidly registered to the surface + of the mesh inside of this class. Or rigidly registered but using the same anatomy that + VERY closely matches. Otherwise, the transfered scalars will be bad. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Mesh we want to copy + new_scalars_name : str, optional + What to name the scalars being transfered to this current mesh, by default 'scalars_from_other_mesh' + weighted_avg : bool, optional + Should we use `weighted average` or `gaussian smooth` methods for transfer, by default True + n_closest : int, optional + If `weighted_avg` True, the number of nearest neighbours to use, by default 3 + sigma : float, optional + If `weighted_avg` False, the standard deviation of gaussian kernel, by default 1. + idx_coords_to_smooth_base : list, optional + If `weighted_avg` False, list of indices from current mesh to use in transfer, by default None + idx_coords_to_smooth_other : list, optional + If `weighted_avg` False, list of indices from `other_mesh` to use in transfer, by default None + set_non_smoothed_scalars_to_zero : bool, optional + Should all other indices (not included in idx_coords_to_smooth_other) be set to 0, by default True + """ + if type(other_mesh) is Mesh: + other_mesh = other_mesh.mesh + elif type(other_mesh) is vtk.vtkPolyData: + pass + else: + raise TypeError(f'other_mesh must be type `pymskt.mesh.Mesh` or `vtk.vtkPolyData` and received: {type(other_mesh)}') + + if weighted_avg is True: + transferred_scalars = transfer_mesh_scalars_get_weighted_average_n_closest( + self._mesh, + other_mesh, + n=n_closest + ) + else: + transferred_scalars = smooth_scalars_from_second_mesh_onto_base( + self._mesh, + other_mesh, + sigma=sigma, + idx_coords_to_smooth_base=idx_coords_to_smooth_base, + idx_coords_to_smooth_second=idx_coords_to_smooth_other, + set_non_smoothed_scalars_to_zero=set_non_smoothed_scalars_to_zero + ) + if (new_scalars_name is None) & (weighted_avg is True): + if transferred_scalars.shape[1] > 1: + n_arrays = other_mesh.GetPointData().GetNumberOfArrays() + array_names = [other_mesh.GetPointData().GetArray(array_idx).GetName() for array_idx in range(n_arrays)] + for idx, array_name in enumerate(array_names): + vtk_transferred_scalars = numpy_to_vtk(transferred_scalars[:,idx]) + vtk_transferred_scalars.SetName(array_name) + self._mesh.GetPointData().AddArray(vtk_transferred_scalars) + return + + vtk_transferred_scalars = numpy_to_vtk(transferred_scalars) + vtk_transferred_scalars.SetName(new_scalars_name) + self._mesh.GetPointData().AddArray(vtk_transferred_scalars)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.create_mesh"><code class="name flex"> +<span>def <span class="ident">create_mesh</span></span>(<span>self, smooth_image=True, smooth_image_var=0.15625, marching_cubes_threshold=0.5, label_idx=None, min_n_pixels=None)</span> +</code></dt> +<dd> +<div class="desc"><p>Create a surface mesh from the classes <code>_seg_image</code>. If <code>_seg_image</code> +does not exist, then read it in using <code>read_seg_image</code>. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>smooth_image</code></strong> : <code>bool</code>, optional</dt> +<dd>Should the <code>_seg_image</code> be gaussian filtered, by default True</dd> +<dt><strong><code>smooth_image_var</code></strong> : <code>float</code>, optional</dt> +<dd>Variance of gaussian filter to apply to <code>_seg_image</code>, by default 0.3125/2</dd> +<dt><strong><code>marching_cubes_threshold</code></strong> : <code>float</code>, optional</dt> +<dd>Threshold contour level to create surface mesh on, by default 0.5</dd> +<dt><strong><code>label_idx</code></strong> : <code>int</code>, optional</dt> +<dd>Label value / index to create mesh from, by default None</dd> +<dt><strong><code>min_n_pixels</code></strong> : <code>int</code>, optional</dt> +<dd>Minimum number of continuous pixels to include segmentation island +in the surface mesh creation, by default None</dd> +</dl> +<h2 id="raises">Raises</h2> +<dl> +<dt><code>Exception</code></dt> +<dd>If the total number of pixels segmentated (<code>n_pixels_labelled</code>) is +< <code>min_n_pixels</code> then there is no object in the image.</dd> +<dt><code>Exception</code></dt> +<dd>If no <code>_seg_image</code> and no <code>label_idx</code> then we don't know what tissue to create the +surface mesh from.</dd> +<dt><code>Exception</code></dt> +<dd>If no <code>_seg_image</code> or <code>path_seg_image</code> then we have no image to create mesh from.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def create_mesh(self, + smooth_image=True, + smooth_image_var=0.3125 / 2, + marching_cubes_threshold=0.5, + label_idx=None, + min_n_pixels=None): + """ + Create a surface mesh from the classes `_seg_image`. If `_seg_image` + does not exist, then read it in using `read_seg_image`. + + Parameters + ---------- + smooth_image : bool, optional + Should the `_seg_image` be gaussian filtered, by default True + smooth_image_var : float, optional + Variance of gaussian filter to apply to `_seg_image`, by default 0.3125/2 + marching_cubes_threshold : float, optional + Threshold contour level to create surface mesh on, by default 0.5 + label_idx : int, optional + Label value / index to create mesh from, by default None + min_n_pixels : int, optional + Minimum number of continuous pixels to include segmentation island + in the surface mesh creation, by default None + + Raises + ------ + Exception + If the total number of pixels segmentated (`n_pixels_labelled`) is + < `min_n_pixels` then there is no object in the image. + Exception + If no `_seg_image` and no `label_idx` then we don't know what tissue to create the + surface mesh from. + Exception + If no `_seg_image` or `path_seg_image` then we have no image to create mesh from. + """ + # allow assigning label idx during mesh creation step. + if label_idx is not None: + self._label_idx = label_idx + + if self._seg_image is None: + self.read_seg_image() + + # Ensure the image has a certain number of pixels with the label of interest, otherwise there might be an issue. + if min_n_pixels is None: min_n_pixels = self._min_n_pixels + seg_view = sitk.GetArrayViewFromImage(self._seg_image) + n_pixels_labelled = sum(seg_view[seg_view == self._label_idx]) + + if n_pixels_labelled < min_n_pixels: + raise Exception('The mesh does not exist in this segmentation!, only {} pixels detected, threshold # is {}'.format(n_pixels_labelled, + marching_cubes_threshold)) + tmp_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(10)) + '.nrrd' + self._mesh = create_surface_mesh(self._seg_image, + self._label_idx, + smooth_image_var, + loc_tmp_save='/tmp', + tmp_filename=tmp_filename, + mc_threshold=marching_cubes_threshold, + filter_binary_image=smooth_image + ) + safely_delete_tmp_file('/tmp', + tmp_filename)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.non_rigidly_register"><code class="name flex"> +<span>def <span class="ident">non_rigidly_register</span></span>(<span>self, other_mesh, as_source=True, apply_transform_to_mesh=True, return_transformed_mesh=False, **kwargs)</span> +</code></dt> +<dd> +<div class="desc"><p>Function to perform non rigid registration between this mesh and another mesh. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>other_mesh</code></strong> : <code>pymskt.mesh.Mesh</code> or <code>vtk.vtkPolyData</code></dt> +<dd>Other mesh to use in registration process</dd> +<dt><strong><code>as_source</code></strong> : <code>bool</code>, optional</dt> +<dd>Should the current mesh (in this object) be the source or the target, by default True</dd> +<dt><strong><code>apply_transform_to_mesh</code></strong> : <code>bool</code>, optional</dt> +<dd>If as_source is True should we apply transformation to internal mesh, by default True</dd> +<dt><strong><code>return_transformed_mesh</code></strong> : <code>bool</code>, optional</dt> +<dd>Should we return the registered mesh, by default False</dd> +</dl> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>_type_</code></dt> +<dd><em>description</em></dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def non_rigidly_register( + self, + other_mesh, + as_source=True, + apply_transform_to_mesh=True, + return_transformed_mesh=False, + **kwargs +): + """ + Function to perform non rigid registration between this mesh and another mesh. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Other mesh to use in registration process + as_source : bool, optional + Should the current mesh (in this object) be the source or the target, by default True + apply_transform_to_mesh : bool, optional + If as_source is True should we apply transformation to internal mesh, by default True + return_transformed_mesh : bool, optional + Should we return the registered mesh, by default False + + Returns + ------- + _type_ + _description_ + """ + # Setup the source & target meshes based on `as_source`` + if as_source is True: + source = self._mesh + target = other_mesh + elif as_source is False: + source = other_mesh + target = self._mesh + + # Get registered mesh (source to target) + source_transformed_to_target = non_rigidly_register( + target_mesh=target, + source_mesh=source, + **kwargs + ) + + # If current mesh is source & apply_transform_to_mesh is true then replace current mesh. + if (as_source is True) & (apply_transform_to_mesh is True): + self._mesh = source_transformed_to_target + + # curent mesh is target, or is source & want to return mesh, then return it. + if (as_source is False) or ((as_source is True) & (return_transformed_mesh is True)): + return source_transformed_to_target</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.read_seg_image"><code class="name flex"> +<span>def <span class="ident">read_seg_image</span></span>(<span>self, path_seg_image=None)</span> +</code></dt> +<dd> +<div class="desc"><p>Read segmentation image from disk. Must be a single file (e.g., nrrd, 3D dicom)</p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>path_seg_image</code></strong> : <code>str</code>, optional</dt> +<dd>Path to the medical image file to be loaded in, by default None</dd> +</dl> +<h2 id="raises">Raises</h2> +<dl> +<dt><code>Exception</code></dt> +<dd>If path_seg_image does not exist, exception is raised.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def read_seg_image(self, + path_seg_image=None): + """ + Read segmentation image from disk. Must be a single file (e.g., nrrd, 3D dicom) + + Parameters + ---------- + path_seg_image : str, optional + Path to the medical image file to be loaded in, by default None + + Raises + ------ + Exception + If path_seg_image does not exist, exception is raised. + """ + # If passing new location/seg image name, then update variables. + if path_seg_image is not None: + self._path_seg_image = path_seg_image + + # If seg image location / name exist, then load image else raise exception + if (self._path_seg_image is not None): + self._seg_image = sitk.ReadImage(self._path_seg_image) + else: + raise Exception('No file path (self._path_seg_image) provided.')</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.resample_surface"><code class="name flex"> +<span>def <span class="ident">resample_surface</span></span>(<span>self, subdivisions=2, clusters=10000)</span> +</code></dt> +<dd> +<div class="desc"><p>Resample a surface mesh using the ACVD algorithm: +Version used: +- <a href="https://github.com/pyvista/pyacvd">https://github.com/pyvista/pyacvd</a> +Original version w/ more references: +- <a href="https://github.com/valette/ACVD">https://github.com/valette/ACVD</a></p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>subdivisions</code></strong> : <code>int</code>, optional</dt> +<dd>Subdivide the mesh to have more points before clustering, by default 2 +Probably not necessary for very dense meshes.</dd> +<dt><strong><code>clusters</code></strong> : <code>int</code>, optional</dt> +<dd>The number of clusters (points/vertices) to create during resampling +surafce, by default 10000 +- This is not exact, might have slight differences.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def resample_surface(self, + subdivisions=2, + clusters=10000 + ): + """ + Resample a surface mesh using the ACVD algorithm: + Version used: + - https://github.com/pyvista/pyacvd + Original version w/ more references: + - https://github.com/valette/ACVD + + Parameters + ---------- + subdivisions : int, optional + Subdivide the mesh to have more points before clustering, by default 2 + Probably not necessary for very dense meshes. + clusters : int, optional + The number of clusters (points/vertices) to create during resampling + surafce, by default 10000 + - This is not exact, might have slight differences. + """ + self._mesh = resample_surface(self._mesh, subdivisions=subdivisions, clusters=clusters)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.reverse_all_transforms"><code class="name flex"> +<span>def <span class="ident">reverse_all_transforms</span></span>(<span>self)</span> +</code></dt> +<dd> +<div class="desc"><p>Function to iterate over all of the self._list_applied_transforms (in reverse order) and undo them.</p></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def reverse_all_transforms(self): + """ + Function to iterate over all of the self._list_applied_transforms (in reverse order) and undo them. + """ + while len(self._list_applied_transforms) > 0: + self.reverse_most_recent_transform()</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.reverse_most_recent_transform"><code class="name flex"> +<span>def <span class="ident">reverse_most_recent_transform</span></span>(<span>self)</span> +</code></dt> +<dd> +<div class="desc"><p>Function to undo the most recent transformation stored in self._list_applied_transforms</p></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def reverse_most_recent_transform(self): + """ + Function to undo the most recent transformation stored in self._list_applied_transforms + """ + transform = self._list_applied_transforms.pop() + transform.Inverse() + self.apply_transform_to_mesh(transform=transform, save_transform=False)</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.rigidly_register"><code class="name flex"> +<span>def <span class="ident">rigidly_register</span></span>(<span>self, other_mesh, as_source=True, apply_transform_to_mesh=True, return_transformed_mesh=False, return_transform=False, max_n_iter=100, n_landmarks=1000, reg_mode='similarity')</span> +</code></dt> +<dd> +<div class="desc"><p>Function to perform rigid registration between this mesh and another mesh. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>other_mesh</code></strong> : <code>pymskt.mesh.Mesh</code> or <code>vtk.vtkPolyData</code></dt> +<dd>Other mesh to use in registration process</dd> +<dt><strong><code>as_source</code></strong> : <code>bool</code>, optional</dt> +<dd>Should the current mesh (in this object) be the source or the target, by default True</dd> +<dt><strong><code>apply_transform_to_mesh</code></strong> : <code>bool</code>, optional</dt> +<dd>If as_source is True should we apply transformation to internal mesh, by default True</dd> +<dt><strong><code>return_transformed_mesh</code></strong> : <code>bool</code>, optional</dt> +<dd>Should we return the registered mesh, by default False</dd> +<dt><strong><code>max_n_iter</code></strong> : <code>int</code>, optional</dt> +<dd>Maximum number of iterations to perform, by default 100</dd> +<dt><strong><code>n_landmarks</code></strong> : <code>int</code>, optional</dt> +<dd>Number of landmarks to use in registration, by default 1000</dd> +<dt><strong><code>reg_mode</code></strong> : <code>str</code>, optional</dt> +<dd>Mode of registration to use, by default 'similarity' (similarity, rigid, or affine)</dd> +</dl> +<h2 id="returns">Returns</h2> +<dl> +<dt><code>_type_</code></dt> +<dd><em>description</em></dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def rigidly_register( + self, + other_mesh, + as_source=True, + apply_transform_to_mesh=True, + return_transformed_mesh=False, + return_transform=False, + max_n_iter=100, + n_landmarks=1000, + reg_mode='similarity' + +): + """ + Function to perform rigid registration between this mesh and another mesh. + + Parameters + ---------- + other_mesh : pymskt.mesh.Mesh or vtk.vtkPolyData + Other mesh to use in registration process + as_source : bool, optional + Should the current mesh (in this object) be the source or the target, by default True + apply_transform_to_mesh : bool, optional + If as_source is True should we apply transformation to internal mesh, by default True + return_transformed_mesh : bool, optional + Should we return the registered mesh, by default False + max_n_iter : int, optional + Maximum number of iterations to perform, by default 100 + n_landmarks : int, optional + Number of landmarks to use in registration, by default 1000 + reg_mode : str, optional + Mode of registration to use, by default 'similarity' (similarity, rigid, or affine) + + Returns + ------- + _type_ + _description_ + """ + + if (return_transform is True) & (return_transformed_mesh is True): + raise Exception('Cannot return both transformed mesh and transform') + + if type(other_mesh) in (pymskt.mesh.meshes.BoneMesh, pymskt.mesh.meshes.Mesh): + other_mesh = other_mesh.mesh + + # Setup the source & target meshes based on `as_source`` + if as_source is True: + source = self._mesh + target = other_mesh + elif as_source is False: + source = other_mesh + target = self._mesh + + icp_transform = get_icp_transform( + source=source, + target=target, + max_n_iter=max_n_iter, + n_landmarks=n_landmarks, + reg_mode=reg_mode + ) + + # If current mesh is source & apply_transform_to_mesh is true then replace current mesh. + if (as_source is True) & (apply_transform_to_mesh is True): + self.apply_transform_to_mesh(transform=icp_transform) + + if return_transformed_mesh is True: + return self._mesh + + elif return_transform is True: + return icp_transform + + # curent mesh is target, or is source & want to return mesh, then return it. + elif (as_source is False) & (return_transformed_mesh is True): + return apply_transform(source=source, transform=icp_transform) + + else: + raise Exception('Nothing to return from rigid registration.')</code></pre> +</details> +</dd> +<dt id="pymskt.mesh.meshes.Mesh.save_mesh"><code class="name flex"> +<span>def <span class="ident">save_mesh</span></span>(<span>self, filepath)</span> +</code></dt> +<dd> +<div class="desc"><p>Save the surface mesh from this class to disk. </p> +<h2 id="parameters">Parameters</h2> +<dl> +<dt><strong><code>filepath</code></strong> : <code>str</code></dt> +<dd>Location & filename to save the surface mesh (vtk.vtkPolyData) to.</dd> +</dl></div> +<details class="source"> +<summary> +<span>Expand source code</span> +</summary> +<pre><code class="python">def save_mesh(self, + filepath): + """ + Save the surface mesh from this class to disk. + + Parameters + ---------- + filepath : str + Location & filename to save the surface mesh (vtk.vtkPolyData) to. + """ + io.write_vtk(self._mesh, filepath)</code></pre> +</details> +</dd> +</dl> +</dd> +</dl> +</section> +</article> +<nav id="sidebar"> +<h1>Index</h1> +<div class="toc"> +<ul></ul> +</div> +<ul id="index"> +<li><h3>Super-module</h3> +<ul> +<li><code><a title="pymskt.mesh" href="index.html">pymskt.mesh</a></code></li> +</ul> +</li> +<li><h3><a href="#header-classes">Classes</a></h3> +<ul> +<li> +<h4><code><a title="pymskt.mesh.meshes.BoneMesh" href="#pymskt.mesh.meshes.BoneMesh">BoneMesh</a></code></h4> +<ul class=""> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.assign_cartilage_regions" href="#pymskt.mesh.meshes.BoneMesh.assign_cartilage_regions">assign_cartilage_regions</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.bone" href="#pymskt.mesh.meshes.BoneMesh.bone">bone</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.calc_cartilage_t2" href="#pymskt.mesh.meshes.BoneMesh.calc_cartilage_t2">calc_cartilage_t2</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.calc_cartilage_thickness" href="#pymskt.mesh.meshes.BoneMesh.calc_cartilage_thickness">calc_cartilage_thickness</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.create_cartilage_meshes" href="#pymskt.mesh.meshes.BoneMesh.create_cartilage_meshes">create_cartilage_meshes</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.create_mesh" href="#pymskt.mesh.meshes.BoneMesh.create_mesh">create_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.crop_percent" href="#pymskt.mesh.meshes.BoneMesh.crop_percent">crop_percent</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.list_cartilage_labels" href="#pymskt.mesh.meshes.BoneMesh.list_cartilage_labels">list_cartilage_labels</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.list_cartilage_meshes" href="#pymskt.mesh.meshes.BoneMesh.list_cartilage_meshes">list_cartilage_meshes</a></code></li> +<li><code><a title="pymskt.mesh.meshes.BoneMesh.smooth_surface_scalars" href="#pymskt.mesh.meshes.BoneMesh.smooth_surface_scalars">smooth_surface_scalars</a></code></li> +</ul> +</li> +<li> +<h4><code><a title="pymskt.mesh.meshes.CartilageMesh" href="#pymskt.mesh.meshes.CartilageMesh">CartilageMesh</a></code></h4> +</li> +<li> +<h4><code><a title="pymskt.mesh.meshes.Mesh" href="#pymskt.mesh.meshes.Mesh">Mesh</a></code></h4> +<ul class=""> +<li><code><a title="pymskt.mesh.meshes.Mesh.apply_transform_to_mesh" href="#pymskt.mesh.meshes.Mesh.apply_transform_to_mesh">apply_transform_to_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect" href="#pymskt.mesh.meshes.Mesh.copy_scalars_from_other_mesh_to_currect">copy_scalars_from_other_mesh_to_currect</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.create_mesh" href="#pymskt.mesh.meshes.Mesh.create_mesh">create_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.label_idx" href="#pymskt.mesh.meshes.Mesh.label_idx">label_idx</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.list_applied_transforms" href="#pymskt.mesh.meshes.Mesh.list_applied_transforms">list_applied_transforms</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.mesh" href="#pymskt.mesh.meshes.Mesh.mesh">mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.min_n_pixels" href="#pymskt.mesh.meshes.Mesh.min_n_pixels">min_n_pixels</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.non_rigidly_register" href="#pymskt.mesh.meshes.Mesh.non_rigidly_register">non_rigidly_register</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.path_seg_image" href="#pymskt.mesh.meshes.Mesh.path_seg_image">path_seg_image</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.point_coords" href="#pymskt.mesh.meshes.Mesh.point_coords">point_coords</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.read_seg_image" href="#pymskt.mesh.meshes.Mesh.read_seg_image">read_seg_image</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.resample_surface" href="#pymskt.mesh.meshes.Mesh.resample_surface">resample_surface</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.reverse_all_transforms" href="#pymskt.mesh.meshes.Mesh.reverse_all_transforms">reverse_all_transforms</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.reverse_most_recent_transform" href="#pymskt.mesh.meshes.Mesh.reverse_most_recent_transform">reverse_most_recent_transform</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.rigidly_register" href="#pymskt.mesh.meshes.Mesh.rigidly_register">rigidly_register</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.save_mesh" href="#pymskt.mesh.meshes.Mesh.save_mesh">save_mesh</a></code></li> +<li><code><a title="pymskt.mesh.meshes.Mesh.seg_image" href="#pymskt.mesh.meshes.Mesh.seg_image">seg_image</a></code></li> +</ul> +</li> +</ul> +</li> +</ul> +</nav> +</main> +<footer id="footer"> +<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.10.0</a>.</p> +</footer> +</body> +</html> \ No newline at end of file