@@ -182,3 +182,159 @@ def test_to_mask(self):
182
182
],
183
183
)
184
184
assert np .array_equal (mask_img .affine , np .eye (4 ))
185
+
186
+
187
+ class TestTriangularMeshes (TestPointsets ):
188
+ ...
189
+
190
+
191
+ class H5ArrayProxy :
192
+ def __init__ (self , file_like , dataset_name ):
193
+ self .file_like = file_like
194
+ self .dataset_name = dataset_name
195
+ with h5 .File (file_like , 'r' ) as h5f :
196
+ arr = h5f [dataset_name ]
197
+ self ._shape = arr .shape
198
+ self ._dtype = arr .dtype
199
+
200
+ @property
201
+ def is_proxy (self ):
202
+ return True
203
+
204
+ @property
205
+ def shape (self ):
206
+ return self ._shape
207
+
208
+ @property
209
+ def ndim (self ):
210
+ return len (self .shape )
211
+
212
+ @property
213
+ def dtype (self ):
214
+ return self ._dtype
215
+
216
+ def __array__ (self , dtype = None ):
217
+ with h5 .File (self .file_like , 'r' ) as h5f :
218
+ return np .asanyarray (h5f [self .dataset_name ], dtype )
219
+
220
+ def __getitem__ (self , slicer ):
221
+ with h5 .File (self .file_like , 'r' ) as h5f :
222
+ return h5f [self .dataset_name ][slicer ]
223
+
224
+
225
+ class H5Geometry (ps .TriMeshFamily ):
226
+ """Simple Geometry file structure that combines a single topology
227
+ with one or more coordinate sets
228
+ """
229
+
230
+ @classmethod
231
+ def from_filename (klass , pathlike ):
232
+ meshes = {}
233
+ with h5 .File (pathlike , 'r' ) as h5f :
234
+ triangles = H5ArrayProxy (pathlike , '/topology' )
235
+ for name in h5f ['coordinates' ]:
236
+ meshes [name ] = (H5ArrayProxy (pathlike , f'/coordinates/{ name } ' ), triangles )
237
+ return klass (meshes )
238
+
239
+ def to_filename (self , pathlike ):
240
+ with h5 .File (pathlike , 'w' ) as h5f :
241
+ h5f .create_dataset ('/topology' , data = self .get_triangles ())
242
+ for name , coord in self ._coords .items ():
243
+ h5f .create_dataset (f'/coordinates/{ name } ' , data = coord )
244
+
245
+
246
+ class FSGeometryProxy :
247
+ def __init__ (self , pathlike ):
248
+ self ._file_like = str (Path (pathlike ))
249
+ self ._offset = None
250
+ self ._vnum = None
251
+ self ._fnum = None
252
+
253
+ def _peek (self ):
254
+ from nibabel .freesurfer .io import _fread3
255
+
256
+ with open (self ._file_like , 'rb' ) as fobj :
257
+ magic = _fread3 (fobj )
258
+ if magic != 16777214 :
259
+ raise NotImplementedError ('Triangle files only!' )
260
+ fobj .readline ()
261
+ fobj .readline ()
262
+ self ._vnum = np .fromfile (fobj , '>i4' , 1 )[0 ]
263
+ self ._fnum = np .fromfile (fobj , '>i4' , 1 )[0 ]
264
+ self ._offset = fobj .tell ()
265
+
266
+ @property
267
+ def vnum (self ):
268
+ if self ._vnum is None :
269
+ self ._peek ()
270
+ return self ._vnum
271
+
272
+ @property
273
+ def fnum (self ):
274
+ if self ._fnum is None :
275
+ self ._peek ()
276
+ return self ._fnum
277
+
278
+ @property
279
+ def offset (self ):
280
+ if self ._offset is None :
281
+ self ._peek ()
282
+ return self ._offset
283
+
284
+ @auto_attr
285
+ def coords (self ):
286
+ ap = ArrayProxy (self ._file_like , ((self .vnum , 3 ), '>f4' , self .offset ))
287
+ ap .order = 'C'
288
+ return ap
289
+
290
+ @auto_attr
291
+ def triangles (self ):
292
+ offset = self .offset + 12 * self .vnum
293
+ ap = ArrayProxy (self ._file_like , ((self .fnum , 3 ), '>i4' , offset ))
294
+ ap .order = 'C'
295
+ return ap
296
+
297
+
298
+ class FreeSurferHemisphere (ps .TriMeshFamily ):
299
+ @classmethod
300
+ def from_filename (klass , pathlike ):
301
+ path = Path (pathlike )
302
+ hemi , default = path .name .split ('.' )
303
+ mesh_names = (
304
+ 'orig' ,
305
+ 'white' ,
306
+ 'smoothwm' ,
307
+ 'pial' ,
308
+ 'inflated' ,
309
+ 'sphere' ,
310
+ 'midthickness' ,
311
+ 'graymid' ,
312
+ ) # Often created
313
+ if default not in mesh_names :
314
+ mesh_names .append (default )
315
+ meshes = {}
316
+ for mesh in mesh_names :
317
+ fpath = path .parent / f'{ hemi } .{ mesh } '
318
+ if fpath .exists ():
319
+ meshes [mesh ] = FSGeometryProxy (fpath )
320
+ hemi = klass (meshes )
321
+ hemi ._default = default
322
+ return hemi
323
+
324
+
325
+ def test_FreeSurferHemisphere ():
326
+ lh = FreeSurferHemisphere .from_filename (FS_DATA / 'fsaverage/surf/lh.white' )
327
+ assert lh .n_coords == 163842
328
+ assert lh .n_triangles == 327680
329
+
330
+
331
+ @skipUnless (has_h5py , reason = 'Test requires h5py' )
332
+ def test_make_H5Geometry (tmp_path ):
333
+ lh = FreeSurferHemisphere .from_filename (FS_DATA / 'fsaverage/surf/lh.white' )
334
+ h5geo = H5Geometry ({name : lh .get_mesh (name ) for name in ('white' , 'pial' )})
335
+ h5geo .to_filename (tmp_path / 'geometry.h5' )
336
+
337
+ rt_h5geo = H5Geometry .from_filename (tmp_path / 'geometry.h5' )
338
+ assert set (h5geo ._coords ) == set (rt_h5geo ._coords )
339
+ assert np .array_equal (lh .get_coords ('white' ), rt_h5geo .get_coords ('white' ))
340
+ assert np .array_equal (lh .get_triangles (), rt_h5geo .get_triangles ())
0 commit comments