Skip to content

Commit e14461c

Browse files
committed
BIAP: CoordinateImage API
This branch culminates ~6 months of intermittent tinkering and revision of the concepts needed to support data sampled to surfaces. The particular use cases of CIFTI-2 and MNE's STC formats have driven a CoordinateAxis model that describes an axis that describes a sequence of sub-sampled surfaces and (raveled) volumes. Test implementations exist in another branch, but the target of this branch is simply to merge the BIAP to render in the docs. For posterity, here are the squashed commit messages: DOC: First pass at SurfaceImage BIAP DOC: Address suggestions DOC: Clarify use cases are motivating, not necessarily implementation target DOC: Small updates DOC: Update BIAP with a couple examples leading to further questions DOC: Separate Geometry and Header objects DOC: Smoothing example, typo ENH: First pass at surfaceimage template classes TEST: Build HDF5/Numpy-based surface classes DOC: Add VolumeGeometry stub DOC: Fix header formatting TEST: Example FreeSurfer subject (not fitting into class hierarchy) DOC: Add concatenable structure proposal ENH: Add structure collection API TEST: Rewrite FreeSurferSubject as GeometryCollection ENH: Possible VolumeGeometry STY: Geometry -> Pointset Rename SurfaceGeometry to TriangularMesh FIX: FreeSurfer example implementation ENH: Flesh out VolumeGeometry BIAP: Add SurfaceHeader.get_geometry() method Rename Geometry -> Pointset DOC: Commit current thinking on BIAP0009 RF: Remove test implementations for this branch
1 parent b8445fc commit e14461c

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed

doc/source/devel/biaps/biap_0009.rst

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
.. _biap9:
2+
3+
################################
4+
BIAP9 - The Coordinate Image API
5+
################################
6+
7+
:Author: Chris Markiewicz
8+
:Status: Draft
9+
:Type: Standards
10+
:Created: 2021-09-16
11+
12+
**********
13+
Background
14+
**********
15+
16+
Surface data is generally kept separate from geometric metadata
17+
===============================================================
18+
19+
In contrast to volumetric data, whose geometry can be fully encoded in the
20+
shape of a data array and a 4x4 affine matrix, data sampled to a surface
21+
require the location of each sample to be explicitly represented by a
22+
coordinate. In practice, the most common approach is to have a geometry file
23+
and a data file.
24+
25+
A geometry file consists of a vertex coordinate array and a triangle array
26+
describing the adjacency of vertices, while a data file is an n-dimensional
27+
array with one axis corresponding to vertex.
28+
29+
Keeping these files separate is a pragmatic optimization to avoid costly
30+
reproductions of geometric data, but presents an administrative burden to
31+
direct consumers of the data.
32+
33+
Terminology
34+
===========
35+
36+
For the purposes of this BIAP, the following terms are used:
37+
38+
* Coordinate - a triplet of floating point values in RAS+ space
39+
* Vertex - an index into a table of coordinates
40+
* Triangle (or face) - a triplet of adjacent vertices (A-B-C);
41+
the normal vector for the face is ($\overline{AB}\times\overline{AC}$)
42+
* Topology - vertex adjacency data, independent of vertex coordinates,
43+
typically in the form of a list of triangles
44+
* Geometry - topology + a specific set of coordinates for a surface
45+
* Parcel - a subset of vertices; can be the full topology. Special cases include:
46+
* Patch - a connected parcel
47+
* Decimated mesh - a parcel that has a desired density of vertices
48+
* Parcel sequence - an ordered set of parcels
49+
* Data array - an n-dimensional array with one axis corresponding to the
50+
vertices (typical) OR faces (more rare) in a patch sequence
51+
52+
Currently supported surface formats
53+
===================================
54+
55+
* FreeSurfer
56+
* Geometry (e.g. ``lh.pial``):
57+
:py:func:`~nibabel.freesurfer.io.read_geometry` /
58+
:py:func:`~nibabel.freesurfer.io.write_geometry`
59+
* Data
60+
* Morphometry:
61+
:py:func:`~nibabel.freesurfer.io.read_morph_data` /
62+
:py:func:`~nibabel.freesurfer.io.write_morph_data`
63+
* Labels: :py:func:`~nibabel.freesurfer.io.read_label`
64+
* MGH: :py:class:`~nibabel.freesurfer.mghformat.MGHImage`
65+
* GIFTI: :py:class:`~nibabel.gifti.gifti.GiftiImage`
66+
* Every image contains a collection of data arrays, which may be
67+
coordinates, topology, or data (further subdivided by type and intent)
68+
* CIFTI-2: :py:class:`~nibabel.cifti2.cifti2.Cifti2Image`
69+
* Pure data array, with image header containing flexible axes
70+
* The ``BrainModelAxis`` is a subspace sequence including patches for
71+
each hemisphere (cortex without the medial wall) and subcortical
72+
structures defined by indices into three-dimensional array and an
73+
affine matrix
74+
* Geometry referred to by an associated ``wb.spec`` file
75+
(no current implementation in NiBabel)
76+
* Possible to have one with no geometric information, e.g., parcels x time
77+
78+
Other relevant formats
79+
======================
80+
81+
* MNE's STC (source time course) format. Contains:
82+
* Subject name (resolvable with a FreeSurfer ``SUBJECTS_DIR``)
83+
* Index arrays into left and right hemisphere surfaces (subspace sequence)
84+
* Data, one of:
85+
* ndarray of shape ``(n_verts, n_times)``
86+
* tuple of ndarrays of shapes ``(n_verts, n_sensors)`` and ``(n_sensors, n_times)``
87+
* Time start
88+
* Time step
89+
90+
*****************************************
91+
Desiderata for an API supporting surfaces
92+
*****************************************
93+
94+
The following are provisional guiding principles:
95+
96+
1. A surface image (data array) should carry a reference to geometric metadata
97+
that is easily transferred to a new image.
98+
2. Partial images (data only or geometry only) should be possible. Absence of
99+
components should have a well-defined signature, such as a property that is
100+
``None`` or a specific ``Exception`` is raised.
101+
3. All arrays (coordinates, triangles, data arrays) should be proxied to
102+
avoid excess memory consumption
103+
4. Selecting among coordinates (e.g., gray/white boundary, inflated surface)
104+
for a single topology should be possible.
105+
5. Combining multiple brain structures (canonically, left and right hemispheres)
106+
in memory should be easy; serializing to file may be format-specific.
107+
6. Splitting a data array into independent patches that can be separately
108+
operated on and serialized should be possible.
109+
110+
111+
Prominent use cases
112+
===================
113+
114+
We consider the following use cases for working with surface data.
115+
A good API will make retrieving the components needed for each use case
116+
straightforward, as well as storing the results in new images.
117+
118+
* Arithmetic/modeling - per-vertex mathematical operations
119+
* Smoothing - topology/geometry-respecting smoothing
120+
* Plotting - paint the data array as a texture on a surface
121+
* Decimation - subsampling a topology (possibly a subset, possibly with
122+
interpolated vertex locations)
123+
* Resampling to a geometrically-aligned surface
124+
* Downsampling by decimating, smoothing, resampling
125+
* Inter-subject resampling by using ``?h.sphere.reg``
126+
* Interpolation of per-vertex and per-face data arrays
127+
128+
When possible, we prefer to expose NumPy ``ndarray``\s and
129+
allow use of numpy, scipy, scikit-learn. In some cases, it may
130+
make sense for NiBabel to provide methods.
131+
132+
********
133+
Proposal
134+
********
135+
136+
A ``CoordinateImage`` is an N-dimensional array, where one axis corresponds
137+
to a sequence of points in one or more parcels.
138+
139+
.. code-block:: python
140+
141+
class CoordinateImage:
142+
"""
143+
Attributes
144+
----------
145+
header : a file-specific header
146+
coordaxis : ``CoordinateAxis``
147+
dataobj : array-like
148+
"""
149+
150+
class CoordinateAxis:
151+
"""
152+
Attributes
153+
----------
154+
parcels : list of ``Parcel`` objects
155+
"""
156+
157+
def load_structures(self, mapping):
158+
"""
159+
Associate parcels to ``Pointset`` structures
160+
"""
161+
162+
def __getitem__(self, slicer):
163+
"""
164+
Return a sub-sampled CoordinateAxis containing structures
165+
matching the indices provided.
166+
"""
167+
168+
def get_indices(self, parcel, indices=None):
169+
"""
170+
Return the indices in the full axis that correspond to the
171+
requested parcel. If indices are provided, further subsample
172+
the requested parcel.
173+
"""
174+
175+
class Parcel:
176+
"""
177+
Attributes
178+
----------
179+
name : str
180+
structure : ``Pointset``
181+
indices : object that selects a subset of coordinates in structure
182+
"""
183+
184+
To describe coordinate geometry, the following structures are proposed:
185+
186+
.. code-block:: python
187+
188+
class Pointset:
189+
@property
190+
def n_coords(self):
191+
""" Number of coordinates """
192+
193+
def get_coords(self, name=None):
194+
""" Nx3 array of coordinates in RAS+ space """
195+
196+
197+
class TriangularMesh(Pointset):
198+
@property
199+
def n_triangles(self):
200+
""" Number of faces """
201+
202+
def get_triangles(self, name=None):
203+
""" Mx3 array of indices into coordinate table """
204+
205+
def get_mesh(self, name=None):
206+
return self.get_coords(name=name), self.get_triangles(name=name)
207+
208+
def get_names(self):
209+
""" List of surface names that can be passed to
210+
``get_{coords,triangles,mesh}``
211+
"""
212+
213+
def decimate(self, *, n_coords=None, ratio=None):
214+
""" Return a TriangularMesh with a smaller number of vertices that
215+
preserves the geometry of the original """
216+
# To be overridden when a format provides optimization opportunities
217+
218+
219+
class NdGrid(Pointset):
220+
"""
221+
Attributes
222+
----------
223+
shape : 3-tuple
224+
number of coordinates in each dimension of grid
225+
"""
226+
def get_affine(self, name=None):
227+
""" 4x4 array """
228+
229+
230+
The ``NdGrid`` class allows raveled volumetric data to be treated the same as
231+
triangular mesh or other coordinate data.
232+
233+
Finally, a structure for containing a collection of related geometric files is
234+
defined:
235+
236+
.. code-block:: python
237+
238+
class GeometryCollection:
239+
"""
240+
Attributes
241+
----------
242+
structures : dict
243+
Mapping from structure names to ``Pointset``
244+
"""
245+
246+
@classmethod
247+
def from_spec(klass, pathlike):
248+
""" Load a collection of geometries from a specification. """
249+
250+
The canonical example of a geometry collection is a left hemisphere mesh,
251+
right hemisphere mesh.
252+
253+
Here we present common use cases:
254+
255+
256+
Modeling
257+
========
258+
259+
.. code-block:: python
260+
261+
from nilearn.glm.first_level import make_first_level_design_matrix, run_glm
262+
263+
bold = CoordinateImage.from_filename("/data/func/hemi-L_bold.func.gii")
264+
dm = make_first_level_design_matrix(...)
265+
labels, results = run_glm(bold.get_fdata(), dm)
266+
betas = CoordinateImage(results["betas"], bold.coordaxis, bold.header)
267+
betas.to_filename("/data/stats/hemi-L_betas.mgz")
268+
269+
In this case, no reference to the surface structure is needed, as the operations
270+
occur on a per-vertex basis.
271+
The coordinate axis and header are preserved to ensure that any metadata is
272+
not lost.
273+
274+
Here we assume that ``CoordinateImage`` is able to make the appropriate
275+
translations between formats (GIFTI, MGH). This is not guaranteed in the final
276+
API.
277+
278+
Smoothing
279+
=========
280+
281+
.. code-block:: python
282+
283+
bold = CoordinateImage.from_filename("/data/func/hemi-L_bold.func.gii")
284+
bold.coordaxis.load_structures({"lh": "/data/anat/hemi-L_midthickness.surf.gii"})
285+
# Not implementing networkx weighted graph here, so assume we have a function
286+
# that retrieves a graph for each structure
287+
graphs = get_graphs(bold.coordaxis)
288+
distances = distance_matrix(graphs['lh']) # n_coords x n_coords matrix
289+
weights = normalize(gaussian(distances, sigma))
290+
# Wildly inefficient smoothing algorithm
291+
smoothed = CoordinateImage(weights @ bold.get_fdata(), bold.coordaxis, bold.header)
292+
smoothed.to_filename(f"/data/func/hemi-L_smooth-{sigma}_bold.func.gii")
293+
294+
295+
Plotting
296+
========
297+
298+
Nilearn currently provides a
299+
`plot_surf <https://nilearn.github.io/modules/generated/nilearn.plotting.plot_surf.html>`_ function.
300+
With the proposed API, we could interface as follows:
301+
302+
.. code-block:: python
303+
304+
def plot_surf_img(img, surface="inflated"):
305+
from nilearn.plotting import plot_surf
306+
coords, triangles = img.coordaxis.parcels[0].get_mesh(name=surface)
307+
308+
data = img.get_fdata()
309+
310+
return plot_surf((triangles, coords), data)
311+
312+
tstats = CoordinateImage.from_filename("/data/stats/hemi-L_contrast-taskVsBase_tstat.mgz")
313+
# Assume a GeometryCollection that reads a FreeSurfer subject directory
314+
fs_subject = FreeSurferSubject.from_spec("/data/subjects/fsaverage5")
315+
tstats.coordaxis.load_structures(fs_subject.get_structure("lh"))
316+
plot_surf_img(tstats)
317+
318+
Subsampling CIFTI-2
319+
===================
320+
321+
.. code-block:: python
322+
323+
img = nb.load("sub-01_task-rest_bold.dtseries.nii") # Assume CIFTI CoordinateImage
324+
parcel = nb.load("sub-fsLR_hemi-L_label-DLPFC_mask.label.gii") # GiftiImage
325+
structure = parcel.meta.metadata['AnatomicalStructurePrimary'] # "CortexLeft"
326+
vtx_idcs = np.where(parcel.agg_data())[0]
327+
dlpfc_idcs = img.coordaxis.get_indices(parcel=structure, indices=vtx_idcs)
328+
329+
# Subsampled coordinate axes will override any duplicate information from header
330+
dlpfc_img = CoordinateImage(img.dataobj[dlpfc_idcs], img.coordaxis[dlpfc_idcs], img.header)
331+
332+
# Now load geometry so we can plot
333+
wbspec = CaretSpec("fsLR.wb.spec")
334+
dlpfc_img.coordaxis.load_structures(wbspec)
335+
...

doc/source/devel/biaps/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ proposals.
1919
biap_0006
2020
biap_0007
2121
biap_0008
22+
biap_0009
2223

2324
.. toctree::
2425
:hidden:

0 commit comments

Comments
 (0)