Meshes and mesh operations
Remeshing
Sometimes it is useful to remesh a triangular surface as a tetrahedral mesh and
vice-versa - say, if one wants to derive 3D surfaces within subcortex. This routine
is implemented within the eigenstrapping.geometry.remesh() function, which
automatically recognizes the surface type (triangles or tetrahedra) and maps the
surface to the other type.
For example, you have a hippocampal volume in MNI152 space (we can load this
from the eigenstrapping.datasets.fetch_data() function), you can remesh
it from a volume to a triangular mesh. By default these will be generated in the
same folder that the original volume/mesh is in.
>>> from eigenstrapping.datasets import fetch_data
>>> hipp_lh = fetch_data(
name='brainmaps',
hemi='lh',
format='hippocampus'
)
>>> hipp_lh
'/mnt/eigenstrapping_data/brainmaps/space-MNI152_res-2mm_hemi-lh_hippocampus.nii.gz'
>>> from eigenstrapping.geometry import remesh
>>> hipp_tria_lh = remesh(hipp_lh)
>>> hipp_tria_lh
'/mnt/eigenstrapping_data/brainmaps/space-MNI152_res-2mm_hemi-lh_hippocampus.tria.vtk'
You can also remesh from a triangular mesh to a tetrahedral one:
>>> hipp_tetra_lh = remesh(hipp_tria_lh)
>>> hipp_tetra_lh
'/mnt/eigenstrapping_data/brainmaps/space-MNI152_res-2mm_hemi-lh_hippocampus.tria.tetra.vtk'
Deriving eigenmodes from a mesh
Deriving LBO vector solutions to the Helmholtz equation, or eigenmodes as we refer to them, is a fairly trivial process once a mesh has been defined. As graph representations of meshes are sparse by their very nature (on a triangular mesh, a single vertex only ever has three edges at the most; four in a tetrahedral mesh), we can use sparse methods of deriving these modes.
We use the finite element method as implemented in ShapeDNA, the details
of which can be found in the requisite papers in References.
The form matrices, A and B, are derived either through Cholesky decomposition
using the scikit-sparse libraries, or LU decomposition in scipy.sparse.splu
if the former is not installed. We recommend, if possible, to install the scikit-sparse
libraries as Cholesky decomposition is much faster than LU.
Let’s take our remeshed tetrahedral hippocampus and use eigenstrapping.geometry.calc_eig():
>>> from eigenstrapping import geometry
>>> tetra = geometry.load_mesh(hipp_tetra_lh)
>>> # in this case, we'll use eigenstrapping.geometry.calc_eig without sksparse
>>> emodes, evals = geometry.calc_eig(tetra, num_modes=20)
TetMesh with regular Laplace
Solver: spsolve (LU decomposition) ...
>>> # by default, this function will remove the first (constant) mode
>>> # this behavior can be changed by setting `return_zero=True` in
>>> # the calc_eig function
>>> emodes
array([[ 0.01210057, 0.01249517, 0.00465954, ..., -0.00146934,
-0.00544099, 0.0271714 ],
[ 0.01209385, 0.01253431, 0.00548657, ..., -0.00434336,
-0.00504929, 0.0242422 ],
[ 0.01198019, 0.01202892, 0.00418164, ..., -0.00095343,
-0.0038022 , 0.02745249],
...,
[-0.00412068, -0.01866016, 0.00250916, ..., 0.00476944,
-0.00805506, -0.00263126],
[ 0.00052041, -0.01061745, -0.00131715, ..., 0.00542975,
-0.00041589, 0.01133534],
[-0.01403812, -0.0024371 , 0.00991763, ..., 0.02579329,
-0.02810065, 0.00869789]], dtype=float32)
>>> evals
array([0.00365112, 0.01341067, 0.02674828, 0.03218102, 0.04489195,
0.05093223, 0.06653866, 0.07089102, 0.09374754, 0.09695608,
0.10020526, 0.11701339, 0.12567881, 0.13450024, 0.14623912,
0.15450524, 0.15965985, 0.16787478, 0.17781733, 0.18272817],
dtype=float32)
>>> # as you can see, the first column of `emodes` is not constant.
Now, let’s plot the first non-constant eigenmode on the surface of the mesh.
>>> from eigenstrapping import plotting
>>> plotting.meshplot(hipp_tetra_lh, emodes[:, 0], vrange=0.01, colorbar=True)
Mesh distance calculations
We also provide geodesic (surface) and Euclidean (volumetric) distance calculations for meshes. Geodesic distance calculation is performed using a heat kernel on each vertex, and takes about 2 hours for a 32k standard hemisphere (fsLR). See the paper in References for specific details on the implementation.
In order to calculate the geodesic distance matrix, we use the eigenstrapping.geometry.geodesic_distmat()
function, which takes an input mesh:
>>> from eigenstrapping import datasets
>>> surf_lh, *_ = datasets.load_surface_examples(with_surface=True)
>>> surf_lh
'/mnt/eigenstrapping-data/surfaces/space-fsaverage_den-10k_hemi-lh_pial.surf.gii'
>>> # for our purposes, we won't use sksparse libraries, but to do so, we
>>> # pass `use_cholmod=True` to the function
>>> distmat_lh = geometry.geodesic_distmat(surf_lh, use_cholmod=False)
# eventually ...
>>> distmat_lh.shape
(10242, 10242)
>>> distmat_lh
array([[ 0. , 91.41330719, 73.28165436, ..., 184.62254333,
186.47727966, 187.92669678],
[ 91.41330719, 0. , 81.62067413, ..., 127.93047333,
129.78521729, 131.2346344 ],
[ 73.28165436, 81.62067413, 0. , ..., 114.22114563,
116.07588959, 117.52529907],
...,
[184.62254333, 127.93047333, 114.22114563, ..., 0. ,
2.80057883, 4.78903866],
[186.47727966, 129.78521729, 116.07588959, ..., 2.80057883,
0. , 1.98845983],
[187.92669678, 131.2346344 , 117.52529907, ..., 4.78903866,
1.98845983, 0. ]])
You can speed things up in the eigenstrapping.geometry.geodesic_distmat()
function by passing n_jobs=<number of workers>. For instance, to use eight threads,
you would pass n_jobs=8. To use every available thread, pass n_jobs=-1.
Euclidean distance calculation is performed in a similar way:
>>> distmat_lh = geometry.euclidean_distmat(mesh) # much quicker than geodesic
>>> distmat_lh.shape
(10242, 10242)
>>> distmat_lh
array([[ 0. , 54.72087326, 37.01728554, ..., 94.95060009,
93.16259385, 92.16275463],
[54.72087326, 0. , 61.99785175, ..., 98.85110323,
98.2701815 , 98.01616578],
[37.01728554, 61.99785175, 0. , ..., 78.64858645,
77.51088511, 77.12512891],
...,
[94.95060009, 98.85110323, 78.64858645, ..., 0. ,
2.80057878, 4.7547238 ],
[93.16259385, 98.2701815 , 77.51088511, ..., 2.80057878,
0. , 1.98845989],
[92.16275463, 98.01616578, 77.12512891, ..., 4.7547238 ,
1.98845989, 0. ]])
Notice the pronounced difference between the two methods. This is because the cortex is constructed as a 2D sheet (basically the surface of a sphere), so it is not advisable to use Euclidean distance calculations. For the subcortical volumes, you would use the Euclidean distance.