Skip to content

Commit bb5d1da

Browse files
committed
NF - add keyword arguments to sform/qform get/set
Add 'coded' argument to getters; returns both affine (or None if code is 0) and code. Add ability to pass None as affine (defaulting to code being 0) Add `strip_shears` keyword to qform to point out shears are being stripped.
1 parent 6ff3604 commit bb5d1da

File tree

2 files changed

+183
-30
lines changed

2 files changed

+183
-30
lines changed

nibabel/nifti1.py

Lines changed: 107 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, :]

nibabel/tests/test_nifti1.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ..casting import type_info
1818
from ..tmpdirs import InTemporaryDirectory
1919
from ..spatialimages import HeaderDataError
20+
from ..affines import from_matvec
2021
from .. import nifti1 as nifti1
2122
from ..nifti1 import (load, Nifti1Header, Nifti1PairHeader, Nifti1Image,
2223
Nifti1Pair, Nifti1Extension, Nifti1Extensions,
@@ -213,6 +214,81 @@ def test_freesurfer_hack(self):
213214
assert_equal(hdr.get_data_shape(), shape)
214215

215216

217+
def test_qform_sform(self):
218+
HC = self.header_class
219+
hdr = HC()
220+
assert_array_equal(hdr.get_qform(), np.eye(4))
221+
empty_sform = np.zeros((4,4))
222+
empty_sform[-1,-1] = 1
223+
assert_array_equal(hdr.get_sform(), empty_sform)
224+
assert_equal(hdr.get_qform(coded=True), (None, 0))
225+
assert_equal(hdr.get_sform(coded=True), (None, 0))
226+
# Affine with no shears
227+
nice_aff = np.diag([2, 3, 4, 1])
228+
# Affine with shears
229+
nasty_aff = from_matvec(np.arange(9).reshape((3,3)), [9, 10, 11])
230+
fixed_aff = unshear_44(nasty_aff)
231+
for in_meth, out_meth in ((hdr.set_qform, hdr.get_qform),
232+
(hdr.set_sform, hdr.get_sform)):
233+
in_meth(nice_aff, 2)
234+
aff, code = out_meth(coded=True)
235+
assert_array_equal(aff, nice_aff)
236+
assert_equal(code, 2)
237+
assert_array_equal(out_meth(), nice_aff) # non coded
238+
# Affine can also be passed if code == 0, affine will be suitably set
239+
in_meth(nice_aff, 0)
240+
assert_equal(out_meth(coded=True), (None, 0))
241+
assert_array_almost_equal(out_meth(), nice_aff)
242+
# Default qform code when previous == 0 is 2
243+
in_meth(nice_aff)
244+
aff, code = out_meth(coded=True)
245+
assert_equal(code, 2)
246+
# Unless code was non-zero before
247+
in_meth(nice_aff, 1)
248+
in_meth(nice_aff)
249+
aff, code = out_meth(coded=True)
250+
assert_equal(code, 1)
251+
# Can set code without modifying affine, by passing affine=None
252+
assert_array_equal(aff, nice_aff) # affine same as before
253+
in_meth(None, 3)
254+
aff, code = out_meth(coded=True)
255+
assert_array_equal(aff, nice_aff) # affine same as before
256+
assert_equal(code, 3)
257+
# affine is None on its own, or with code==0, resets code to 0
258+
in_meth(None, 0)
259+
assert_equal(out_meth(coded=True), (None, 0))
260+
in_meth(None)
261+
assert_equal(out_meth(coded=True), (None, 0))
262+
# List works as input
263+
in_meth(nice_aff.tolist())
264+
assert_array_equal(out_meth(), nice_aff)
265+
# Qform specifics
266+
# inexact set (with shears) is OK
267+
hdr.set_qform(nasty_aff, 1)
268+
assert_array_almost_equal(hdr.get_qform(), fixed_aff)
269+
# Unless allow_shears is False
270+
assert_raises(HeaderDataError, hdr.set_qform, nasty_aff, 1, False)
271+
# Reset sform, give qform a code, to test sform
272+
hdr.set_sform(None)
273+
hdr.set_qform(nice_aff, 1)
274+
# Check sform unchanged by setting qform
275+
assert_equal(hdr.get_sform(coded=True), (None, 0))
276+
# Setting does change the sform ouput
277+
hdr.set_sform(nasty_aff, 1)
278+
aff, code = hdr.get_sform(coded=True)
279+
assert_array_equal(aff, nasty_aff)
280+
assert_equal(code, 1)
281+
282+
283+
def unshear_44(affine):
284+
RZS = affine[:3, :3]
285+
zooms = np.sqrt(np.sum(RZS * RZS, axis=0))
286+
R = RZS / zooms
287+
P, S, Qs = np.linalg.svd(R)
288+
PR = np.dot(P, Qs)
289+
return from_matvec(PR * zooms, affine[:3,3])
290+
291+
216292
class TestNifti1SingleHeader(TestNifti1PairHeader):
217293

218294
header_class = Nifti1Header

0 commit comments

Comments
 (0)