@@ -182,3 +182,159 @@ def test_to_mask(self):
182182 ],
183183 )
184184 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