1
1
.. _biap9 :
2
2
3
- #############################
4
- BIAP9 - The Surface Image API
5
- #############################
3
+ ################################
4
+ BIAP9 - The Coordinate Image API
5
+ ################################
6
6
7
7
:Author: Chris Markiewicz
8
8
:Status: Draft
@@ -18,7 +18,7 @@ Surface data is generally kept separate from geometric metadata
18
18
19
19
In contrast to volumetric data, whose geometry can be fully encoded in the
20
20
shape of a data array and a 4x4 affine matrix, data sampled to a surface
21
- requires the location of each sample to be explicitly represented by a
21
+ require the location of each sample to be explicitly represented by a
22
22
coordinate. In practice, the most common approach is to have a geometry file
23
23
and a data file.
24
24
@@ -42,10 +42,12 @@ For the purposes of this BIAP, the following terms are used:
42
42
* Topology - vertex adjacency data, independent of vertex coordinates,
43
43
typically in the form of a list of triangles
44
44
* Geometry - topology + a specific set of coordinates for a surface
45
- * Patch - a connected subset of vertices (can be the full topology)
45
+ * Parcel - a subset of vertices; can be the full topology. Special cases include:
46
+ * Patch - a connected subspace
47
+ * Decimated mesh - a subspace that has a desired density of vertices
48
+ * Subspace sequence - an ordered set of subspaces
46
49
* Data array - an n-dimensional array with one axis corresponding to the
47
- vertices (typical) OR faces (more rare) in a patch
48
-
50
+ vertices (typical) OR faces (more rare) in a patch sequence
49
51
50
52
Currently supported surface formats
51
53
===================================
@@ -65,14 +67,29 @@ Currently supported surface formats
65
67
coordinates, topology, or data (further subdivided by type and intent)
66
68
* CIFTI-2: :py:class: `~nibabel.cifti2.cifti2.Cifti2Image `
67
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
68
74
* Geometry referred to by an associated ``wb.spec `` file
69
75
(no current implementation in NiBabel)
70
76
* Possible to have one with no geometric information, e.g., parcels x time
71
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
72
89
73
- *********************************
74
- Desiderata for a SurfaceImage API
75
- *********************************
90
+ *****************************************
91
+ Desiderata for an API supporting surfaces
92
+ *****************************************
76
93
77
94
The following are provisional guiding principles
78
95
@@ -116,7 +133,55 @@ make sense for NiBabel to provide methods.
116
133
Proposal
117
134
********
118
135
119
- The basic API is as follows:
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:
120
185
121
186
.. code-block :: python
122
187
@@ -150,54 +215,40 @@ The basic API is as follows:
150
215
preserves the geometry of the original """
151
216
# To be overridden when a format provides optimization opportunities
152
217
153
- def load_vertex_data (self , filename ):
154
- """ Return a SurfaceImage with data corresponding to each vertex """
155
-
156
- def load_face_data (self , filename ):
157
- """ Return a SurfaceImage with data corresponding to each face """
158
-
159
-
160
- class SurfaceHeader (FileBasedHeader ):
161
- def get_geometry (self ):
162
- """ Construct a geometry from the header if possible, or return None """
163
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 """
164
228
165
- class SurfaceImage (FileBasedImage ):
166
- @ property
167
- def header (self ):
168
- """ A SurfaceHeader """
169
-
170
- @ property
171
- def geometry (self ):
172
- """ A Pointset or None """
173
-
174
- @ property
175
- def dataobj (self ):
176
- """ An ndarray or ArrayProxy with one of the following properties:
177
-
178
- 1) self.dataobj.shape[0] == self.header.ncoords
179
- 2) self.dataobj.shape[0] == self.header.nfaces
180
- """
181
-
182
- def load_geometry (self , pathlike ):
183
- """ Specify a geometry for a data-only image """
184
229
230
+ The ``NdGrid `` class allows raveled volumetric data to be treated the same as
231
+ triangular mesh or other coordinate data.
185
232
186
- To enable a similar interface to raveled voxel data:
233
+ Finally, a structure for containing a collection of related geometric files is
234
+ defined:
187
235
188
236
.. code-block :: python
189
237
190
- class VolumeGeometry ( Pointset ) :
191
- _affine # Or _affines, if we want multiple, e.g. qform, sform
192
- _shape
193
- _ijk_coords
194
-
195
- def n_coords ( self ):
196
- """ Number of coordinates """
238
+ class GeometryCollection :
239
+ """
240
+ Attributes
241
+ ----------
242
+ structures : dict
243
+ Mapping from structure names to ``Pointset``
244
+ """
197
245
198
- def get_coords (self ):
199
- """ Nx3 array of coordinates in RAS+ space """
246
+ @ classmethod
247
+ def from_spec (klass , pathlike ):
248
+ """ Load a collection of geometries from a specification. """
200
249
250
+ The canonical example of a geometry collection is a left hemisphere mesh,
251
+ right hemisphere mesh.
201
252
202
253
Here we present common use cases:
203
254
@@ -209,30 +260,35 @@ Modeling
209
260
210
261
from nilearn.glm.first_level import make_first_level_design_matrix, run_glm
211
262
212
- bold = SurfaceImage .from_filename(" /data/func/hemi-L_bold.func.gii" )
263
+ bold = CoordinateImage .from_filename(" /data/func/hemi-L_bold.func.gii" )
213
264
dm = make_first_level_design_matrix(... )
214
- labels, results = run_glm(bold.get_data (), dm)
215
- betas = bold. __class__ (results[" betas" ], bold.header)
265
+ labels, results = run_glm(bold.get_fdata (), dm)
266
+ betas = CoordinateImage (results[" betas" ], bold.coordaxis , bold.header)
216
267
betas.to_filename(" /data/stats/hemi-L_betas.mgz" )
217
268
218
- Data images have their own metadata apart from geometry that needs handling in a header:
219
-
220
- * Axis labels (time series, labels, tensors)
221
- * Data dtype
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.
222
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.
223
277
224
278
Smoothing
225
279
=========
226
280
227
281
.. code-block :: python
228
282
229
- bold = SurfaceImage.from_filename(" /data/func/hemi-L_bold.func.gii" )
230
- bold.load_geometry(" /data/anat/hemi-L_midthickness.surf.gii" )
231
- # Not implementing networkx weighted graph here, so assume we have a method
232
- graph = bold.geometry.get_graph()
233
- distances = distance_matrix(graph) # n_coords x n_coords matrix
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
234
289
weights = normalize(gaussian(distances, sigma))
235
- smoothed = bold.__class__ (bold.get_fdata() @ weights, bold.header)
290
+ # Wildly inefficient smoothing algorithm
291
+ smoothed = CoordinateImage(weights @ bold.get_fdata(), bold.coordaxis, bold.header)
236
292
smoothed.to_filename(f " /data/func/hemi-L_smooth- { sigma} _bold.func.gii " )
237
293
238
294
@@ -247,64 +303,33 @@ With the proposed API, we could interface as follows:
247
303
248
304
def plot_surf_img (img , surface = " inflated" ):
249
305
from nilearn.plotting import plot_surf
250
- coords, triangles = img.geometry .get_mesh(name = surface)
306
+ coords, triangles = img.coordaxis.parcels[ 0 ] .get_mesh(name = surface)
251
307
252
- data = tstats.get_data ()
308
+ data = img.get_fdata ()
253
309
254
310
return plot_surf((triangles, coords), data)
255
311
256
- tstats = SurfaceImage.from_filename(" /data/stats/hemi-L_contrast-taskVsBase_tstat.mgz" )
257
- tstats.load_geometry(" /data/subjects/fsaverage5" )
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" ))
258
316
plot_surf_img(tstats)
259
317
260
- This assumes that ``load_geometry() `` will be able to identify a FreeSurfer subject directory.
261
-
262
-
263
- **************
264
- Open questions
265
- **************
266
-
267
- 1) Can/should image and geometry objects be promiscuous? If I load a GIFTI geometry,
268
- should ``geometry.load_vertex_data() `` accept a FreeSurfer curvature file, or
269
- should there be a bit more ecosystem-local logic?
270
-
271
-
272
- ********************************
273
- Concatenable structures proposal
274
- ********************************
275
-
276
- A brain is typically described with multiple structures,
277
- such as left and right hemispheres and a volumetric subcortical region.
278
- We will consider two reference cases:
279
-
280
- 1) FreeSurfer, in which a subject directory can be used to retrieve
281
- multiple surfaces or volumetric regions. Data arrays are specific to a
282
- single structure.
283
- 2) CIFTI-2, which permits an axis to have type ``CIFTI_INDEX_TYPE_BRAIN_MODELS ``,
284
- indicating that each index along an axis corresponds to a vertex or voxel in
285
- an associated surface or volume.
286
- The data array thus may span multiple structures.
287
-
288
- In the former case, the structures may be considered an unordered collection;
289
- in the latter, the sequence of structures directly corresponds to segments of
290
- the data array.
318
+ Subsampling CIFTI-2
319
+ ===================
291
320
292
321
.. code-block :: python
293
322
294
- class GeometryCollection :
295
- def get_geometry (self , name ):
296
- """ Return a geometry by name """
297
-
298
- @ property
299
- def names (self ):
300
- """ A set of structure names """
301
-
302
- @ classmethod
303
- def from_spec (klass , pathlike ):
304
- """ Load a collection of geometries from a specification, broadly construed. """
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)
305
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)
306
331
307
- class PointsetSequence ( GeometryCollection , Pointset ):
308
- # Inherit and override get_coords, n_coords from Pointset
309
- def get_indices ( self , name ):
310
- """ Return indices for data array corresponding to a named geometry """
332
+ # Now load geometry so we can plot
333
+ wbspec = CaretSpec( " fsLR.wb.spec " )
334
+ dlpfc_img.coordaxis.load_structures(wbspec)
335
+ ...
0 commit comments