Skip to content

Commit ca72cfa

Browse files
committed
Merge branch 'sform-qform-getset' into main-master
* sform-qform-getset: ENH + TEST: enforce float image affines TEST - refactor Bago's tests to match new API TEST - added test for set_qform and set_sform NF - add get/set for sform/qform on image NF - add keyword arguments to sform/qform get/set
2 parents 6ff3604 + da7cd5b commit ca72cfa

File tree

4 files changed

+460
-32
lines changed

4 files changed

+460
-32
lines changed

nibabel/nifti1.py

Lines changed: 271 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -701,9 +701,29 @@ def get_qform_quaternion(self):
701701
# Adjust threshold to fact that source data was float32
702702
return fillpositive(bcd, FLOAT32_EPS_3)
703703

704-
def get_qform(self):
705-
''' Return 4x4 affine matrix from qform parameters in header '''
704+
def get_qform(self, coded=False):
705+
""" Return 4x4 affine matrix from qform parameters in header
706+
707+
Parameters
708+
----------
709+
coded : bool, optional
710+
If True, return {affine or None}, and qform code. If False, just
711+
return affine. {affine or None} means, return None if qform code ==
712+
0, and affine otherwise.
713+
714+
Returns
715+
-------
716+
affine : None or (4,4) ndarray
717+
If `coded` is False, always return affine reconstructed from qform
718+
quaternion. If `coded` is True, return None if qform code is 0,
719+
else return the affine.
720+
code : int
721+
Qform code. Only returned if `coded` is True.
722+
"""
706723
hdr = self._structarr
724+
code = hdr['qform_code']
725+
if code == 0 and coded:
726+
return None, 0
707727
quat = self.get_qform_quaternion()
708728
R = quat2mat(quat)
709729
vox = hdr['pixdim'][1:4].copy()
@@ -718,30 +738,39 @@ def get_qform(self):
718738
out = np.eye(4)
719739
out[0:3, 0:3] = M
720740
out[0:3, 3] = [hdr['qoffset_x'], hdr['qoffset_y'], hdr['qoffset_z']]
741+
if coded:
742+
return out, code
721743
return out
722744

723-
def set_qform(self, affine, code=None):
745+
def set_qform(self, affine, code=None, strip_shears=True):
724746
''' Set qform header values from 4x4 affine
725747
726748
Parameters
727749
----------
728750
hdr : nifti1 header
729-
affine : 4x4 array
730-
affine transform to write into qform
751+
affine : None or 4x4 array
752+
affine transform to write into sform. If None, only set code.
731753
code : None, string or integer
732754
String or integer giving meaning of transform in *affine*.
733-
The default is None. If code is None, then {if current
734-
qform code is not 0, leave code as it is in the header; else
735-
set to 2 ('aligned')}.
755+
The default is None. If code is None, then:
756+
* If affine is None, `code`-> 0
757+
* If affine not None and sform code in header == 0, `code`-> 2
758+
(aligned)
759+
* If affine not None and sform code in header != 0, `code`-> sform
760+
code in header
761+
strip_shears : bool, optional
762+
Whether to strip shears in `affine`. If True, shears will be
763+
silently stripped. If False, the presence of shears will raise a
764+
``HeaderDataError``
736765
737766
Notes
738767
-----
739768
The qform transform only encodes translations, rotations and
740-
zooms. If there are shear components to the `affine` transform,
741-
the written qform gives the closest approximation where the
742-
rotation matrix is orthogonal. This is to allow quaternion
743-
representation. The orthogonal representation enforces
744-
orthogonal axes.
769+
zooms. If there are shear components to the `affine` transform, and
770+
`strip_shears` is True (the default), the written qform gives the
771+
closest approximation where the rotation matrix is orthogonal. This is
772+
to allow quaternion representation. The orthogonal representation
773+
enforces orthogonal axes.
745774
746775
Examples
747776
--------
@@ -765,15 +794,25 @@ def set_qform(self, affine, code=None):
765794
>>> hdr.set_qform(affine, code='scanner')
766795
>>> int(hdr['qform_code'])
767796
1
797+
>>> hdr.set_qform(None)
798+
>>> int(hdr['qform_code'])
799+
0
768800
'''
769801
hdr = self._structarr
802+
old_code = hdr['qform_code']
770803
if code is None:
771-
code = hdr['qform_code']
772-
if code == 0: # default is 'aligned'
773-
hdr['qform_code'] = 2
774-
else:
804+
if affine is None:
805+
code = 0
806+
elif old_code == 0:
807+
code = 2 # aligned
808+
else:
809+
code = old_code
810+
else: # code set
775811
code = self._field_recoders['qform_code'][code]
776-
hdr['qform_code'] = code
812+
hdr['qform_code'] = code
813+
if affine is None:
814+
return
815+
affine = np.asarray(affine)
777816
if not affine.shape == (4, 4):
778817
raise TypeError('Need 4x4 affine as input')
779818
trans = affine[:3, 3]
@@ -793,6 +832,9 @@ def set_qform(self, affine, code=None):
793832
# orthogonal matrix PR, to input R
794833
P, S, Qs = npl.svd(R)
795834
PR = np.dot(P, Qs)
835+
if not strip_shears and not np.allclose(PR, R):
836+
raise HeaderDataError("Shears in affine and `strip_shears` is "
837+
"False")
796838
# Convert to quaternion
797839
quat = mat2quat(PR)
798840
# Set into header
@@ -801,13 +843,35 @@ def set_qform(self, affine, code=None):
801843
hdr['pixdim'][1:4] = zooms
802844
hdr['quatern_b'], hdr['quatern_c'], hdr['quatern_d'] = quat[1:]
803845

804-
def get_sform(self):
805-
''' Return sform 4x4 affine matrix from header '''
846+
def get_sform(self, coded=False):
847+
""" Return 4x4 affine matrix from sform parameters in header
848+
849+
Parameters
850+
----------
851+
coded : bool, optional
852+
If True, return {affine or None}, and sform code. If False, just
853+
return affine. {affine or None} means, return None if sform code ==
854+
0, and affine otherwise.
855+
856+
Returns
857+
-------
858+
affine : None or (4,4) ndarray
859+
If `coded` is False, always return affine from sform fields. If
860+
`coded` is True, return None if sform code is 0, else return the
861+
affine.
862+
code : int
863+
Sform code. Only returned if `coded` is True.
864+
"""
806865
hdr = self._structarr
866+
code = hdr['sform_code']
867+
if code == 0 and coded:
868+
return None, 0
807869
out = np.eye(4)
808870
out[0, :] = hdr['srow_x'][:]
809871
out[1, :] = hdr['srow_y'][:]
810872
out[2, :] = hdr['srow_z'][:]
873+
if coded:
874+
return out, code
811875
return out
812876

813877
def set_sform(self, affine, code=None):
@@ -816,13 +880,16 @@ def set_sform(self, affine, code=None):
816880
Parameters
817881
----------
818882
hdr : nifti1 header
819-
affine : 4x4 array
820-
affine transform to write into sform
883+
affine : None or 4x4 array
884+
affine transform to write into sform. If None, only set `code`
821885
code : None, string or integer
822886
String or integer giving meaning of transform in *affine*.
823-
The default is None. If code is None, then {if current
824-
sform code is not 0, leave code as it is in the header; else
825-
set to 2 ('aligned')}.
887+
The default is None. If code is None, then:
888+
* If affine is None, `code`-> 0
889+
* If affine not None and sform code in header == 0, `code`-> 2
890+
(aligned)
891+
* If affine not None and sform code in header != 0, `code`-> sform
892+
code in header
826893
827894
Examples
828895
--------
@@ -846,15 +913,25 @@ def set_sform(self, affine, code=None):
846913
>>> hdr.set_sform(affine, code='scanner')
847914
>>> int(hdr['sform_code'])
848915
1
916+
>>> hdr.set_sform(None)
917+
>>> int(hdr['sform_code'])
918+
0
849919
'''
850920
hdr = self._structarr
921+
old_code = hdr['sform_code']
851922
if code is None:
852-
code = hdr['sform_code']
853-
if code == 0:
854-
hdr['sform_code'] = 2 # aligned
855-
else:
923+
if affine is None:
924+
code = 0
925+
elif old_code == 0:
926+
code = 2 # aligned
927+
else:
928+
code = old_code
929+
else: # code set
856930
code = self._field_recoders['sform_code'][code]
857-
hdr['sform_code'] = code
931+
hdr['sform_code'] = code
932+
if affine is None:
933+
return
934+
affine = np.asarray(affine)
858935
hdr['srow_x'][:] = affine[0, :]
859936
hdr['srow_y'][:] = affine[1, :]
860937
hdr['srow_z'][:] = affine[2, :]
@@ -1512,6 +1589,170 @@ def update_header(self):
15121589
# Make qform 'unknown'
15131590
hdr.set_qform(self._affine, code='unknown')
15141591

1592+
def get_qform(self, coded=False):
1593+
""" Return 4x4 affine matrix from qform parameters in header
1594+
1595+
Parameters
1596+
----------
1597+
coded : bool, optional
1598+
If True, return {affine or None}, and qform code. If False, just
1599+
return affine. {affine or None} means, return None if qform code ==
1600+
0, and affine otherwise.
1601+
1602+
Returns
1603+
-------
1604+
affine : None or (4,4) ndarray
1605+
If `coded` is False, always return affine reconstructed from qform
1606+
quaternion. If `coded` is True, return None if qform code is 0,
1607+
else return the affine.
1608+
code : int
1609+
Qform code. Only returned if `coded` is True.
1610+
1611+
See also
1612+
--------
1613+
Nifti1Header.set_qform
1614+
"""
1615+
return self._header.get_qform(coded)
1616+
1617+
def set_qform(self, affine, code=None, strip_shears=True, **kwargs):
1618+
''' Set qform header values from 4x4 affine
1619+
1620+
Parameters
1621+
----------
1622+
hdr : nifti1 header
1623+
affine : None or 4x4 array
1624+
affine transform to write into sform. If None, only set code.
1625+
code : None, string or integer
1626+
String or integer giving meaning of transform in *affine*.
1627+
The default is None. If code is None, then:
1628+
* If affine is None, `code`-> 0
1629+
* If affine not None and sform code in header == 0, `code`-> 2
1630+
(aligned)
1631+
* If affine not None and sform code in header != 0, `code`-> sform
1632+
code in header
1633+
strip_shears : bool, optional
1634+
Whether to strip shears in `affine`. If True, shears will be
1635+
silently stripped. If False, the presence of shears will raise a
1636+
``HeaderDataError``
1637+
update_affine : bool, optional
1638+
Whether to update the image affine from the header best affine after
1639+
setting the qform. Must be keyword argumemt (because of different
1640+
position in `set_qform`). Default is True
1641+
1642+
See also
1643+
--------
1644+
Nifti1Header.set_qform
1645+
1646+
Examples
1647+
--------
1648+
>>> data = np.arange(24).reshape((2,3,4))
1649+
>>> aff = np.diag([2, 3, 4, 1])
1650+
>>> img = Nifti1Pair(data, aff)
1651+
>>> img.get_qform()
1652+
array([[ 2., 0., 0., 0.],
1653+
[ 0., 3., 0., 0.],
1654+
[ 0., 0., 4., 0.],
1655+
[ 0., 0., 0., 1.]])
1656+
>>> img.get_qform(coded=True)
1657+
(None, 0)
1658+
>>> aff2 = np.diag([3, 4, 5, 1])
1659+
>>> img.set_qform(aff2, 'talairach')
1660+
>>> qaff, code = img.get_qform(coded=True)
1661+
>>> np.all(qaff == aff2)
1662+
True
1663+
>>> int(code)
1664+
3
1665+
'''
1666+
update_affine = kwargs.pop('update_affine', True)
1667+
if kwargs:
1668+
raise TypeError('Unexpected keyword argument(s) %s' % kwargs)
1669+
self._header.set_qform(affine, code, strip_shears)
1670+
if update_affine:
1671+
self._affine[:] = self._header.get_best_affine()
1672+
1673+
def get_sform(self, coded=False):
1674+
""" Return 4x4 affine matrix from sform parameters in header
1675+
1676+
Parameters
1677+
----------
1678+
coded : bool, optional
1679+
If True, return {affine or None}, and sform code. If False, just
1680+
return affine. {affine or None} means, return None if sform code ==
1681+
0, and affine otherwise.
1682+
1683+
Returns
1684+
-------
1685+
affine : None or (4,4) ndarray
1686+
If `coded` is False, always return affine from sform fields. If
1687+
`coded` is True, return None if sform code is 0, else return the
1688+
affine.
1689+
code : int
1690+
Sform code. Only returned if `coded` is True.
1691+
1692+
See also
1693+
--------
1694+
Nifti1Header.get_sform
1695+
"""
1696+
return self._header.get_sform(coded)
1697+
1698+
def set_sform(self, affine, code=None, **kwargs):
1699+
''' Set sform transform from 4x4 affine
1700+
1701+
Parameters
1702+
----------
1703+
hdr : nifti1 header
1704+
affine : None or 4x4 array
1705+
affine transform to write into sform. If None, only set `code`
1706+
code : None, string or integer
1707+
String or integer giving meaning of transform in *affine*.
1708+
The default is None. If code is None, then:
1709+
* If affine is None, `code`-> 0
1710+
* If affine not None and sform code in header == 0, `code`-> 2
1711+
(aligned)
1712+
* If affine not None and sform code in header != 0, `code`-> sform
1713+
code in header
1714+
update_affine : bool, optional
1715+
Whether to update the image affine from the header best affine after
1716+
setting the qform. Must be keyword argumemt (because of different
1717+
position in `set_qform`). Default is True
1718+
1719+
See also
1720+
--------
1721+
Nifti1Header.set_sform
1722+
1723+
Examples
1724+
--------
1725+
>>> data = np.arange(24).reshape((2,3,4))
1726+
>>> aff = np.diag([2, 3, 4, 1])
1727+
>>> img = Nifti1Pair(data, aff)
1728+
>>> img.get_sform()
1729+
array([[ 2., 0., 0., 0.],
1730+
[ 0., 3., 0., 0.],
1731+
[ 0., 0., 4., 0.],
1732+
[ 0., 0., 0., 1.]])
1733+
>>> saff, code = img.get_sform(coded=True)
1734+
>>> saff
1735+
array([[ 2., 0., 0., 0.],
1736+
[ 0., 3., 0., 0.],
1737+
[ 0., 0., 4., 0.],
1738+
[ 0., 0., 0., 1.]])
1739+
>>> int(code)
1740+
2
1741+
>>> aff2 = np.diag([3, 4, 5, 1])
1742+
>>> img.set_sform(aff2, 'talairach')
1743+
>>> saff, code = img.get_sform(coded=True)
1744+
>>> np.all(saff == aff2)
1745+
True
1746+
>>> int(code)
1747+
3
1748+
'''
1749+
update_affine = kwargs.pop('update_affine', True)
1750+
if kwargs:
1751+
raise TypeError('Unexpected keyword argument(s) %s' % kwargs)
1752+
self._header.set_sform(affine, code)
1753+
if update_affine:
1754+
self._affine[:] = self._header.get_best_affine()
1755+
15151756

15161757
class Nifti1Image(Nifti1Pair):
15171758
header_class = Nifti1Header

nibabel/spatialimages.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,9 @@ def __init__(self, data, affine, header=None,
301301
# Check that affine is array-like 4,4. Maybe this is too strict at
302302
# this abstract level, but so far I think all image formats we know
303303
# do need 4,4.
304-
# Copy affine to isolate from environment
305-
affine = np.array(affine, copy=True)
304+
# Copy affine to isolate from environment. Specify float type to
305+
# avoid surprising integer rounding when setting values into affine
306+
affine = np.array(affine, dtype=np.float64, copy=True)
306307
if not affine.shape == (4,4):
307308
raise ValueError('Affine should be shape 4,4')
308309
self._affine = affine

0 commit comments

Comments
 (0)