Skip to content

Commit d0cbc11

Browse files
committed
ENH: Add test implementations of pointsets in test_pointset.py
1 parent 78954bc commit d0cbc11

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

nibabel/tests/test_pointset.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from pathlib import Path
2+
3+
import numpy as np
4+
5+
from nibabel import pointset as ps
6+
from nibabel.arrayproxy import ArrayProxy
7+
from nibabel.optpkg import optional_package
8+
9+
h5, has_h5py, _ = optional_package('h5py')
10+
11+
12+
class H5ArrayProxy:
13+
def __init__(self, file_like, dataset_name):
14+
self.file_like = file_like
15+
self.dataset_name = dataset_name
16+
with h5.File(file_like, 'r') as h5f:
17+
arr = h5f[dataset_name]
18+
self._shape = arr.shape
19+
self._dtype = arr.dtype
20+
21+
@property
22+
def is_proxy(self):
23+
return True
24+
25+
@property
26+
def shape(self):
27+
return self._shape
28+
29+
@property
30+
def ndim(self):
31+
return len(self.shape)
32+
33+
@property
34+
def dtype(self):
35+
return self._dtype
36+
37+
def __array__(self, dtype=None):
38+
with h5.File(self.file_like, 'r') as h5f:
39+
return np.asanyarray(h5f[self.dataset_name], dtype)
40+
41+
def __slicer__(self, slicer):
42+
with h5.File(self.file_like, 'r') as h5f:
43+
return h5f[self.dataset_name][slicer]
44+
45+
46+
class H5Geometry(ps.TriangularMesh):
47+
"""Simple Geometry file structure that combines a single topology
48+
with one or more coordinate sets
49+
"""
50+
51+
@classmethod
52+
def from_filename(klass, pathlike):
53+
meshes = {}
54+
with h5.File(pathlike, 'r') as h5f:
55+
triangles = h5f['topology']
56+
for name, coords in h5f['coordinates'].items():
57+
meshes[name] = (coords, triangles)
58+
return klass(meshes)
59+
60+
def to_filename(self, pathlike):
61+
topology = None
62+
coordinates = {}
63+
for name, mesh in self.meshes.items():
64+
coords, faces = mesh
65+
if topology is None:
66+
topology = faces
67+
elif not np.array_equal(faces, topology):
68+
raise ValueError('Inconsistent topology')
69+
coordinates[name] = coords
70+
71+
with h5.File(pathlike, 'w') as h5f:
72+
h5f.create_dataset('/topology', topology)
73+
for name, coord in coordinates.items():
74+
h5f.create_dataset(f'/coordinates/{name}', coord)
75+
76+
def get_coords(self, name=None):
77+
if name is None:
78+
name = next(iter(self._meshes))
79+
coords, _ = self._meshes[name]
80+
return coords
81+
82+
def get_triangles(self, name=None):
83+
if name is None:
84+
name = next(iter(self._meshes))
85+
_, triangles = self._meshes[name]
86+
return triangles
87+
88+
89+
class FSGeometryProxy:
90+
def __init__(self, pathlike):
91+
self._file_like = str(Path(pathlike))
92+
self._offset = None
93+
self._vnum = None
94+
self._fnum = None
95+
96+
def _peek(self):
97+
from nibabel.freesurfer.io import _fread3
98+
99+
with open(self._file_like, 'rb') as fobj:
100+
magic = _fread3(fobj)
101+
if magic != 16777214:
102+
raise NotImplementedError('Triangle files only!')
103+
fobj.readline()
104+
fobj.readline()
105+
self._vnum = np.fromfile(fobj, '>i4', 1)[0]
106+
self._fnum = np.fromfile(fobj, '>i4', 1)[0]
107+
self._offset = fobj.tell()
108+
109+
@property
110+
def vnum(self):
111+
if self._vnum is None:
112+
self._peek()
113+
return self._vnum
114+
115+
@property
116+
def fnum(self):
117+
if self._fnum is None:
118+
self._peek()
119+
return self._fnum
120+
121+
@property
122+
def offset(self):
123+
if self._offset is None:
124+
self._peek()
125+
return self._offset
126+
127+
@property
128+
def coords(self):
129+
ap = ArrayProxy(self._file_like, ((self.vnum, 3), '>f4', self.offset))
130+
ap.order = 'C'
131+
return ap
132+
133+
@property
134+
def triangles(self):
135+
offset = self.offset + 12 * self.vnum
136+
ap = ArrayProxy(self._file_like, ((self.fnum, 3), '>i4', offset))
137+
ap.order = 'C'
138+
return ap
139+
140+
141+
class FreeSurferHemisphere(ps.TriangularMesh):
142+
@classmethod
143+
def from_filename(klass, pathlike):
144+
path = Path(pathlike)
145+
hemi, default = path.name.split('.')
146+
mesh_names = (
147+
'orig',
148+
'white',
149+
'smoothwm',
150+
'pial',
151+
'inflated',
152+
'sphere',
153+
'midthickness',
154+
'graymid',
155+
) # Often created
156+
if default not in mesh_names:
157+
mesh_names.append(default)
158+
meshes = {}
159+
for mesh in mesh_names:
160+
fpath = path.parent / f'{hemi}.{mesh}'
161+
if fpath.exists():
162+
meshes[mesh] = FSGeometryProxy(fpath)
163+
hemi = klass(meshes)
164+
hemi._default = default
165+
return hemi
166+
167+
def get_coords(self, name=None):
168+
if name is None:
169+
name = self._default
170+
return self._meshes[name].coords
171+
172+
def get_triangles(self, name=None):
173+
if name is None:
174+
name = self._default
175+
return self._meshes[name].triangles
176+
177+
@property
178+
def n_coords(self):
179+
return self.meshes[self._default].vnum
180+
181+
@property
182+
def n_triangles(self):
183+
return self.meshes[self._default].fnum

0 commit comments

Comments
 (0)