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