8000 ENH,WIP: New transforms module by oesteban · Pull Request #656 · nipy/nibabel · GitHub
[go: up one dir, main page]

Skip to content

ENH,WIP: New transforms module #656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3cfff0d
ENH: Add an utility to calculate obliquity of affines
oesteban Sep 20, 2019
c92d560
enh(tests): add not-oblique test, move tests to ``test_affines.py``
oesteban Sep 20, 2019
da8da56
enh: return radians unless degrees=True
oesteban Sep 22, 2019
253a256
enh: address @matthew-brett's comments
oesteban Sep 23, 2019
04584b6
doc: add link to AFNI's documentation about *obliquity* [skip ci]
oesteban Sep 24, 2019
70d9620
Initial commit
oesteban Aug 1, 2018
36a1764
add TransformBase
oesteban Aug 1, 2018
2a700c9
add new module
oesteban Aug 1, 2018
fda2bfe
add documentation, core functionality
oesteban Aug 1, 2018
3ef5adc
add a deformation field transform
oesteban Aug 1, 2018
189a662
optimize deformation field
oesteban Aug 2, 2018
2619ae4
pre-cache transformed indexes
oesteban Aug 2, 2018
4e7c19b
used cached field
oesteban Aug 2, 2018
23d7002
add caching of deltas in voxel coordinates
oesteban Aug 3, 2018
2acd9f3
add bspline cython extension
oesteban Aug 3, 2018
385ceff
a smarter ImageSpace
oesteban Aug 4, 2018
fa030a4
add comment
oesteban Aug 10, 2018
fd418fe
remove cython module
oesteban Aug 10, 2018
13d722f
cleanup
oesteban Aug 11, 2018
253aed5
starting with bspline transform
oesteban Aug 11, 2018
47c2a57
finishing b-spline interpolation
oesteban Aug 13, 2018
4478c06
Add base implementation of transforms and change base implementation …
oesteban Mar 12, 2019
99fa15d
export to hdf5
oesteban Mar 13, 2019
5eed01d
corrections to adhere the current x5 format draft
oesteban Mar 14, 2019
632e068
fix coordinates translation in affines, import itk affines
oesteban Mar 15, 2019
f2c062e
wip: reads & writes ITK's MatrixOffsetTransformBase transforms, with …
oesteban Mar 16, 2019
8eb670d
fix: print warnings to stderr, sty: minimal fixes
oesteban Mar 16, 2019
799fb6e
enh(affines): write out AFNI 12-parameters files
oesteban Mar 19, 2019
ce36f06
enh(transforms): finish up a x5-to-fsl writer of affines
oesteban Mar 21, 2019
8e1bf44
enh(transforms): improve generation of x5 structures of reference spaces
oesteban Mar 21, 2019
e2df7cc
fix(transforms): string formating forgotten when writing ITK transforms
oesteban Mar 22, 2019
c5d10ed
wip(transforms): writing up some linear transform readers
oesteban Mar 22, 2019
a18ce1d
enh(transform): HDF5 - set values as attributes; generalize affines t…
oesteban Mar 22, 2019
9902f50
enh: apply some early comments from @effigies.
oesteban Sep 19, 2019
fe74efb
fix: add a first battery of tests
oesteban Sep 26, 2019
596994c
enh: add tests for affines stored in AFNI format (non-oblique images)
oesteban Sep 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
enh(transform): HDF5 - set values as attributes; generalize affines t…
…o N transforms (through time axis)
  • Loading branch information
oesteban committed Sep 27, 2019
commit a18ce1d735b336dba909ba209949e46b94b5d220
8 changes: 4 additions & 4 deletions nibabel/transform/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ def map_voxels(self, coordinates):
coordinates, axes=1)[:3, ...]

def _to_hdf5(self, group):
group['Type'] = 'image'
group.attrs['Type'] = 'image'
group.attrs['ndim'] = self.ndim
group.create_dataset('affine', data=self.affine)
group.create_dataset('shape', data=self.shape)
group.create_dataset('ndim', data=self.ndim)

def __eq__(self, other):
try:
Expand Down Expand Up @@ -188,8 +188,8 @@ def to_filename(self, filename, fmt='X5'):
'''Store the transform in BIDS-Transforms HDF5 file format (.x5).
'''
with h5py.File(filename, 'w') as out_file:
out_file['Format'] = 'X5'
out_file['Version'] = np.uint16(1)
out_file.attrs['Format'] = 'X5'
out_file.attrs['Version'] = np.uint16(1)
root = out_file.create_group('/0')
self._to_hdf5(root)

Expand Down
143 changes: 98 additions & 45 deletions nibabel/transform/linear.py
9E88
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,19 @@ def __init__(self, matrix=None, reference=None):

'''
if matrix is None:
matrix = np.eye(4)
matrix = [np.eye(4)]

if np.ndim(matrix) == 2:
matrix = [matrix]

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

if reference:
if isinstance(reference, str):
reference = loadimg(reference)
self.reference = reference

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

nvols = 1
if len(moving.shape) > 3:
nvols = moving.shape[3]

movaff = moving.affine
movingdata = moving.get_data()
if nvols == 1:
movingdata = movingdata[..., np.newaxis]

nmats = self._matrix.shape[0]
if nvols != nmats and nmats > 1:
raise ValueError("""\
The moving image contains {0} volumes, while the transform is defined for \
{1} volumes""".format(nvols, nmats))

singlemat = None
if nmats == 1:
singlemat = np.linalg.inv(movaff).dot(self._matrix[0].dot(reference.affine))

if singlemat is not None and nvols > nmats:
print('Warning: resampling a 4D volume with a single affine matrix',
file=sys.stderr)

# Compose an index to index affine matrix
matrix = np.linalg.inv(moving.affine).dot(self._matrix.dot(reference.affine))
mdata = moving.get_data()
moved = ndi.affine_transform(
mdata, matrix=matrix[:mdata.ndim, :mdata.ndim],
offset=matrix[:mdata.ndim, mdata.ndim],
output_shape=reference.shape, order=order, mode=mode,
cval=cval, prefilter=prefilter)

moved_image = moving.__class__(moved, reference.affine, moving.header)
moved = []
for i in range(nvols):
i2imat = singlemat if singlemat is not None else np.linalg.inv(
movaff).dot(self._matrix[i].dot(reference.affine))
mdata = movingdata[..., i]
moved += [ndi.affine_transform(
mdata, matrix=i2imat[:-1, :-1],
offset=i2imat[:-1, -1],
output_shape=reference.shape, order=order, mode=mode,
cval=cval, prefilter=prefilter)]

moved_image = moving.__class__(
np.squeeze(np.stack(moved, -1)), reference.affine, moving.header)
moved_image.header.set_data_dtype(output_dtype)

return moved_image

def map_point(self, coords, forward=True):
def map_point(self, coords, index=0, forward=True):
coords = np.array(coords)
if coords.shape[0] == self._matrix.shape[0] - 1:
if coords.shape[0] == self._matrix[index].shape[0] - 1:
coords = np.append(coords, [1])
affine = self._matrix if forward else np.linalg.inv(self._matrix)
affine = self._matrix[index] if forward else np.linalg.inv(self._matrix[index])
return affine.dot(coords)[:-1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this [:-1] be conditional on coords being appended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pushing this to the backlog checks that need be done before merge. For now I'm just working on a very much needed rebase and implementing all the functionalities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I've just realized why this [:-1] is here - to drop the last element of the homogeneous coordinate, which is always a one.


def map_voxel(self, index, moving=None):
def map_voxel(self, index, nindex=0, moving=None):
try:
reference = self.reference
except ValueError:
Expand All @@ -152,16 +184,16 @@ def map_voxel(self, index, moving=None):
raise ValueError('Reference and moving spaces are both undefined')

index = np.array(index)
if index.shape[0] == self._matrix.shape[0] - 1:
if index.shape[0] == self._matrix[nindex].shape[0] - 1:
index = np.append(index, [1])

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

def _to_hdf5(self, x5_root):
x5_root.create_dataset('Transform', data=self._matrix[:3, :])
x5_root.create_dataset('Inverse', data=np.linalg.inv(self._matrix)[:3, :])
x5_root['Type'] = 'affine'
xform = x5_root.create_dataset('Transform', data=self._matrix)
xform.attrs['Type'] = 'affine'
x5_root.create_dataset('Inverse', data=np.linalg.inv(self._matrix))

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

if fmt.lower() in ['itk', 'ants', 'elastix', 'nifty']:
parameters = LPS.dot(self.matrix.dot(LPS))
parameters = parameters[:3, :3].reshape(-1).tolist() + parameters[:3, 3].tolist()
itkfmt = """\
#Insight Transform File V1.0
#Transform 0
with open(filename, 'w') as f:
f.write('#Insight Transform File V1.0\n')

for i in range(self.matrix.shape[0]):
parameters = LPS.dot(self.matrix[i].dot(LPS))
parameters = parameters[:3, :3].reshape(-1).tolist() + \
parameters[:3, 3].tolist()
itkfmt = """\
#Transform {0}
Transform: MatrixOffsetTransformBase_double_3_3
Parameters: {}
Parameters: {1}
FixedParameters: 0 0 0\n""".format
with open(filename, 'w') as f:
f.write(itkfmt(' '.join(['%g' % p for p in parameters])))
f.write(itkfmt(i, ' '.join(['%g' % p for p in parameters])))
return filename

if fmt.lower() == 'afni':
parameters = LPS.dot(self.matrix.dot(LPS))
parameters = parameters[:3, :].reshape(-1).tolist()
np.savetxt(filename, np.atleast_2d(parameters),
delimiter='\t', header="""\
3dvolreg matrices (DICOM-to-DICOM, row-by-row):""")
parameters = np.swapaxes(LPS.dot(self.matrix.dot(LPS)), 0, 1)
parameters = parameters[:, :3, :].reshape((self.matrix.shape[0], -1))
np.savetxt(filename, parameters, delimiter='\t', header="""\
3dvolreg matrices (DICOM-to-DICOM, row-by-row):""", fmt='%g')
return filename

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

# Compose FSL transform
mat = np.linalg.inv(post.dot(self.matrix.dot(pre)))
np.savetxt(filename, mat, delimiter=' ', fmt='%g')
mat = np.linalg.inv(
np.swapaxes(post.dot(self.matrix.dot(pre)), 0, 1))

if self.matrix.shape[0] > 1:
for i in range(self.matrix.shape[0]):
np.savetxt('%s.%03d' % (filename, i), mat[i],
delimiter=' ', fmt='%g')
else:
np.savetxt(filename, mat[0], delimiter=' ', fmt='%g')
return filename
return super(Affine, self).to_filename(filename, fmt=fmt)

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

parameters = np.fromstring(itkxfm[3].split(':')[-1].strip(), dtype=float, sep=' ')
offset = np.fromstring(itkxfm[4].split(':')[-1].strip(), dtype=float, sep=' ')
if len(parameters) == 12:
matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:])
c_neg = from_matvec(np.eye(3), offset * -1.0)
c_pos = from_matvec(np.eye(3), offset)
matrix = LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS))))

# if fmt.lower() == 'afni':
if 'Insight Transform File' not in itkxfm[0]:
raise ValueError(
"File %s does not look like an ITK transform text file." % filename)

matlist = []
for nxfm in range(len(itkxfm[1:]) // 4):
parameters = np.fromstring(itkxfm[nxfm * 4 + 3].split(':')[-1].strip(),
dtype=float, sep=' ')
offset = np.fromstring(itkxfm[nxfm * 4 + 4].split(':')[-1].strip(),
dtype=float, sep=' ')
if len(parameters) == 12:
matrix = from_matvec(parameters[:9].reshape((3, 3)), parameters[9:])
c_neg = from_matvec(np.eye(3), offset * -1.0)
c_pos = from_matvec(np.eye(3), offset)
matlist.append(LPS.dot(c_pos.dot(matrix.dot(c_neg.dot(LPS)))))
matrix = np.stack(matlist)
# elif fmt.lower() == 'afni':
# parameters = LPS.dot(self.matrix.dot(LPS))
# parameters = parameters[:3, :].reshape(-1).tolist()
elif fmt.lower() in ('x5', 'bids'):
raise NotImplementedError
else:
raise NotImplementedError

if reference and isinstance(reference, str):
reference = loadimg(reference)
Expand Down
0