Skip to content

Commit f34affc

Browse files
committed
enh(transform): HDF5 - set values as attributes; generalize affines to N transforms (through time axis)
1 parent 1663a32 commit f34affc

File tree

2 files changed

+102
-49
lines changed

2 files changed

+102
-49
lines changed

nibabel/transform/base.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ def map_voxels(self, coordinates):
7878
coordinates, axes=1)[:3, ...]
7979

8080
def _to_hdf5(self, group):
81-
group['Type'] = 'image'
81+
group.attrs['Type'] = 'image'
82+
group.attrs['ndim'] = self.ndim
8283
group.create_dataset('affine', data=self.affine)
8384
group.create_dataset('shape', data=self.shape)
84-
group.create_dataset('ndim', data=self.ndim)
8585

8686
def __eq__(self, other):
8787
try:
@@ -188,8 +188,8 @@ def to_filename(self, filename, fmt='X5'):
188188
'''Store the transform in BIDS-Transforms HDF5 file format (.x5).
189189
'''
190190
with h5py.File(filename, 'w') as out_file:
191-
out_file['Format'] = 'X5'
192-
out_file['Version'] = np.uint16(1)
191+
out_file.attrs['Format'] = 'X5'
192+
out_file.attrs['Version'] = np.uint16(1)
193193
root = out_file.create_group('/0')
194194
self._to_hdf5(root)
195195

nibabel/transform/linear.py

+98-45
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,19 @@ def __init__(self, matrix=None, reference=None):
4848
4949
'''
5050
if matrix is None:
51-
matrix = np.eye(4)
51+
matrix = [np.eye(4)]
52+
53+
if np.ndim(matrix) == 2:
54+
matrix = [matrix]
5255

5356
self._matrix = np.array(matrix)
54-
assert self._matrix.ndim in (2, 3), 'affine matrix should be 2D or 3D'
55-
assert self._matrix.shape[0] == self._matrix.shape[1], 'affine matrix is not square'
57+
assert self._matrix.ndim == 3, 'affine matrix should be 3D'
58+
assert self._matrix.shape[-2] == self._matrix.shape[-1], 'affine matrix is not square'
5659
super(Affine, self).__init__()
5760

5861
if reference:
62+
if isinstance(reference, str):
63+
reference = loadimg(reference)
5964
self.reference = reference
6065

6166
@property
@@ -116,28 +121,55 @@ def resample(self, moving, order=3, mode='constant', cval=0.0, prefilter=True,
116121
file=sys.stderr)
117122
reference = moving
118123

124+
nvols = 1
125+
if len(moving.shape) > 3:
126+
nvols = moving.shape[3]
127+
128+
movaff = moving.affine
129+
movingdata = moving.get_data()
130+
if nvols == 1:
131+
movingdata = movingdata[..., np.newaxis]
132+
133+
nmats = self._matrix.shape[0]
134+
if nvols != nmats and nmats > 1:
135+
raise ValueError("""\
136+
The moving image contains {0} volumes, while the transform is defined for \
137+
{1} volumes""".format(nvols, nmats))
138+
139+
singlemat = None
140+
if nmats == 1:
141+
singlemat = np.linalg.inv(movaff).dot(self._matrix[0].dot(reference.affine))
142+
143+
if singlemat is not None and nvols > nmats:
144+
print('Warning: resampling a 4D volume with a single affine matrix',
145+
file=sys.stderr)
146+
119147
# Compose an index to index affine matrix
120-
matrix = np.linalg.inv(moving.affine).dot(self._matrix.dot(reference.affine))
121-
mdata = moving.get_data()
122-
moved = ndi.affine_transform(
123-
mdata, matrix=matrix[:mdata.ndim, :mdata.ndim],
124-
offset=matrix[:mdata.ndim, mdata.ndim],
125-
output_shape=reference.shape, order=order, mode=mode,
126-
cval=cval, prefilter=prefilter)
127-
128-
moved_image = moving.__class__(moved, reference.affine, moving.header)
148+
moved = []
149+
for i in range(nvols):
150+
i2imat = singlemat if singlemat is not None else np.linalg.inv(
151+
movaff).dot(self._matrix[i].dot(reference.affine))
152+
mdata = movingdata[..., i]
153+
moved += [ndi.affine_transform(
154+
mdata, matrix=i2imat[:-1, :-1],
155+
offset=i2imat[:-1, -1],
156+
output_shape=reference.shape, order=order, mode=mode,
157+
cval=cval, prefilter=prefilter)]
158+
159+
moved_image = moving.__class__(
160+
np.squeeze(np.stack(moved, -1)), reference.affine, moving.header)
129161
moved_image.header.set_data_dtype(output_dtype)
130162

131163
return moved_image
132164

133-
def map_point(self, coords, forward=True):
165+
def map_point(self, coords, index=0, forward=True):
134166
coords = np.array(coords)
135-
if coords.shape[0] == self._matrix.shape[0] - 1:
167+
if coords.shape[0] == self._matrix[index].shape[0] - 1:
136168
coords = np.append(coords, [1])
137-
affine = self._matrix if forward else np.linalg.inv(self._matrix)
169+
affine = self._matrix[index] if forward else np.linalg.inv(self._matrix[index])
138170
return affine.dot(coords)[:-1]
139171

140-
def map_voxel(self, index, moving=None):
172+
def map_voxel(self, index, nindex=0, moving=None):
141173
try:
142174
reference = self.reference
143175
except ValueError:
@@ -152,16 +184,16 @@ def map_voxel(self, index, moving=None):
152184
raise ValueError('Reference and moving spaces are both undefined')
153185

154186
index = np.array(index)
155-
if index.shape[0] == self._matrix.shape[0] - 1:
187+
if index.shape[0] == self._matrix[nindex].shape[0] - 1:
156188
index = np.append(index, [1])
157189

158-
matrix = reference.affine.dot(self._matrix.dot(np.linalg.inv(moving.affine)))
190+
matrix = reference.affine.dot(self._matrix[nindex].dot(np.linalg.inv(moving.affine)))
159191
return tuple(matrix.dot(index)[:-1])
160192

161193
def _to_hdf5(self, x5_root):
162-
x5_root.create_dataset('Transform', data=self._matrix[:3, :])
163-
x5_root.create_dataset('Inverse', data=np.linalg.inv(self._matrix)[:3, :])
164-
x5_root['Type'] = 'affine'
194+
xform = x5_root.create_dataset('Transform', data=self._matrix)
195+
xform.attrs['Type'] = 'affine'
196+
x5_root.create_dataset('Inverse', data=np.linalg.inv(self._matrix))
165197

166198
if self._reference:
167199
self.reference._to_hdf5(x5_root.create_group('Reference'))
@@ -171,24 +203,26 @@ def to_filename(self, filename, fmt='X5', moving=None):
171203
'''
172204

173205
if fmt.lower() in ['itk', 'ants', 'elastix', 'nifty']:
174-
parameters = LPS.dot(self.matrix.dot(LPS))
175-
parameters = parameters[:3, :3].reshape(-1).tolist() + parameters[:3, 3].tolist()
176-
itkfmt = """\
177-
#Insight Transform File V1.0
178-
#Transform 0
206+
with open(filename, 'w') as f:
207+
f.write('#Insight Transform File V1.0\n')
208+
209+
for i in range(self.matrix.shape[0]):
210+
parameters = LPS.dot(self.matrix[i].dot(LPS))
211+
parameters = parameters[:3, :3].reshape(-1).tolist() + \
212+
parameters[:3, 3].tolist()
213+
itkfmt = """\
214+
#Transform {0}
179215
Transform: MatrixOffsetTransformBase_double_3_3
180-
Parameters: {}
216+
Parameters: {1}
181217
FixedParameters: 0 0 0\n""".format
182-
with open(filename, 'w') as f:
183-
f.write(itkfmt(' '.join(['%g' % p for p in parameters])))
218+
f.write(itkfmt(i, ' '.join(['%g' % p for p in parameters])))
184219
return filename
185220

186221
if fmt.lower() == 'afni':
187-
parameters = LPS.dot(self.matrix.dot(LPS))
188-
parameters = parameters[:3, :].reshape(-1).tolist()
189-
np.savetxt(filename, np.atleast_2d(parameters),
190-
delimiter='\t', header="""\
191-
3dvolreg matrices (DICOM-to-DICOM, row-by-row):""")
222+
parameters = np.swapaxes(LPS.dot(self.matrix.dot(LPS)), 0, 1)
223+
parameters = parameters[:, :3, :].reshape((self.matrix.shape[0], -1))
224+
np.savetxt(filename, parameters, delimiter='\t', header="""\
225+
3dvolreg matrices (DICOM-to-DICOM, row-by-row):""", fmt='%g')
192226
return filename
193227

194228
if fmt.lower() == 'fsl':
@@ -209,8 +243,15 @@ def to_filename(self, filename, fmt='X5', moving=None):
209243
moving.affine)))
210244

211245
# Compose FSL transform
212-
mat = np.linalg.inv(post.dot(self.matrix.dot(pre)))
213-
np.savetxt(filename, mat, delimiter=' ', fmt='%g')
246+
mat = np.linalg.inv(
247+
np.swapaxes(post.dot(self.matrix.dot(pre)), 0, 1))
248+
249+
if self.matrix.shape[0] > 1:
250+
for i in range(self.matrix.shape[0]):
251+
np.savetxt('%s.%03d' % (filename, i), mat[i],
252+
delimiter=' ', fmt='%g')
253+
else:
254+
np.savetxt(filename, mat[0], delimiter=' ', fmt='%g')
214255
return filename
215256
return super(Affine, self).to_filename(filename, fmt=fmt)
216257

@@ -222,17 +263,29 @@ def load(filename, fmt='X5', reference=None):
222263
with open(filename) as itkfile:
223264
itkxfm = itkfile.read().splitlines()
224265

225-
parameters = np.fromstring(itkxfm[3].split(':')[-1].strip(), dtype=float, sep=' ')
226-
offset = np.fromstring(itkxfm[4].split(':')[-1].strip(), dtype=float, sep=' ')
227-
if len(parameters) == 12:
228-
matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:])
229-
c_neg = from_matvec(np.eye(3), offset * -1.0)
230-
c_pos = from_matvec(np.eye(3), offset)
231-
matrix = LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS))))
232-
233-
# if fmt.lower() == 'afni':
266+
if 'Insight Transform File' not in itkxfm[0]:
267+
raise ValueError(
268+
"File %s does not look like an ITK transform text file." % filename)
269+
270+
matlist = []
271+
for nxfm in range(len(itkxfm[1:]) // 4):
272+
parameters = np.fromstring(itkxfm[nxfm * 4 + 3].split(':')[-1].strip(),
273+
dtype=float, sep=' ')
274+
offset = np.fromstring(itkxfm[nxfm * 4 + 4].split(':')[-1].strip(),
275+
dtype=float, sep=' ')
276+
if len(parameters) == 12:
277+
matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:])
278+
c_neg = from_matvec(np.eye(3), offset * -1.0)
279+
c_pos = from_matvec(np.eye(3), offset)
280+
matlist.append(LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS)))))
281+
matrix = np.stack(matlist)
282+
# elif fmt.lower() == 'afni':
234283
# parameters = LPS.dot(self.matrix.dot(LPS))
235284
# parameters = parameters[:3, :].reshape(-1).tolist()
285+
elif fmt.lower() in ('x5', 'bids'):
286+
raise NotImplementedError
287+
else:
288+
raise NotImplementedError
236289

237290
if reference and isinstance(reference, str):
238291
reference = loadimg(reference)

0 commit comments

Comments
 (0)