Skip to content

Commit 6e8de04

Browse files
committed
Merge pull request #74 from matthew-brett/nifti-codes-cycle-fix
Nifti codes cycle fix mrbago and I spent some time rolling the options around and this seemed the best stop-gap solution at least.
2 parents 3c4d7b2 + c8e9c34 commit 6e8de04

File tree

7 files changed

+114
-16
lines changed

7 files changed

+114
-16
lines changed

nibabel/analyze.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,12 +1053,20 @@ def update_header(self):
10531053
(1.0, 2.0, 3.0)
10541054
'''
10551055
hdr = self._header
1056+
# We need to update the header if the data shape has changed. It's a
1057+
# bit difficult to change the data shape using the standard API, but
1058+
# maybe it happened
10561059
if not self._data is None:
10571060
hdr.set_data_shape(self._data.shape)
1058-
if not self._affine is None:
1059-
RZS = self._affine[:3, :3]
1060-
vox = np.sqrt(np.sum(RZS * RZS, axis=0))
1061-
hdr['pixdim'][1:4] = vox
1061+
# If the affine is not None, and it is different from the main affine in
1062+
# the header, update the heaader
1063+
if self._affine is None:
1064+
return
1065+
if np.all(self._affine == hdr.get_best_affine()):
1066+
return
1067+
RZS = self._affine[:3, :3]
1068+
vox = np.sqrt(np.sum(RZS * RZS, axis=0))
1069+
hdr['pixdim'][1:4] = vox
10621070

10631071

10641072
load = AnalyzeImage.load

nibabel/nifti1.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,14 +1428,16 @@ def update_header(self):
14281428
super(Nifti1Pair, self).update_header()
14291429
hdr = self._header
14301430
hdr['magic'] = 'ni1'
1431-
if not self._affine is None:
1432-
# Set affine into sform
1433-
hdr.set_sform(self._affine, code='aligned')
1434-
# Make qform 'unknown', set voxel sizes from affine
1435-
hdr['qform_code'] = 0
1436-
RZS = self._affine[:3, :3]
1437-
zooms = np.sqrt(np.sum(RZS * RZS, axis=0))
1438-
hdr['pixdim'][1:4] = zooms
1431+
# If the affine is not None, and it is different from the main affine in
1432+
# the header, update the heaader
1433+
if self._affine is None:
1434+
return
1435+
if np.all(self._affine == hdr.get_best_affine()):
1436+
return
1437+
# Set affine into sform with default code
1438+
hdr.set_sform(self._affine, code='aligned')
1439+
# Make qform 'unknown'
1440+
hdr.set_qform(self._affine, code='unknown')
14391441

14401442

14411443
class Nifti1Image(Nifti1Pair):

nibabel/spatialimages.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,8 @@ 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-
affine = np.asarray(affine)
304+
# Copy affine to isolate from environment
305+
affine = np.array(affine, copy=True)
305306
if not affine.shape == (4,4):
306307
raise ValueError('Affine should be shape 4,4')
307308
self._affine = affine

nibabel/tests/test_analyze.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,37 @@ def test_affine_44(self):
532532
# Not OK - affine wrong shape
533533
assert_raises(ValueError, IC, data, np.diag([2, 3, 4]))
534534

535+
def test_header_updating(self):
536+
# Only update on changes
537+
img_klass = self.image_class
538+
# With a None affine - don't overwrite zooms
539+
img = img_klass(np.zeros((2,3,4)), None)
540+
hdr = img.get_header()
541+
hdr.set_zooms((4,5,6))
542+
# Save / reload using bytes IO objects
543+
for key, value in img.file_map.items():
544+
value.fileobj = BytesIO()
545+
img.to_file_map()
546+
hdr_back = img.from_file_map(img.file_map).get_header()
547+
assert_array_equal(hdr_back.get_zooms(), (4,5,6))
548+
# With a real affine, update zooms
549+
img = img_klass(np.zeros((2,3,4)), np.diag([2,3,4,1]), hdr)
550+
hdr = img.get_header()
551+
assert_array_equal(hdr.get_zooms(), (2, 3, 4))
552+
# Modify affine in-place? Update on save.
553+
img.get_affine()[0,0] = 9
554+
for key, value in img.file_map.items():
555+
value.fileobj = BytesIO()
556+
img.to_file_map()
557+
hdr_back = img.from_file_map(img.file_map).get_header()
558+
assert_array_equal(hdr.get_zooms(), (9, 3, 4))
559+
# Modify data in-place? Update on save
560+
data = img.get_data()
561+
data.shape = (3, 2, 4)
562+
img.to_file_map()
563+
img_back = img.from_file_map(img.file_map)
564+
assert_array_equal(img_back.shape, (3, 2, 4))
565+
535566

536567
def test_unsupported():
537568
# analyze does not support uint32

nibabel/tests/test_nifti1.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,46 @@ def test_binblock_is_file(self):
174174

175175

176176
class TestNifti1Image(tana.TestAnalyzeImage):
177-
# class for testing images
177+
# Run analyze-flavor spatialimage tests
178178
image_class = Nifti1Image
179179

180-
181-
class TestNifti1Pair(tana.TestAnalyzeImage):
180+
def _qform_rt(self, img):
181+
# Round trip image after setting qform, sform codes
182+
hdr = img.get_header()
183+
hdr['qform_code'] = 3
184+
hdr['sform_code'] = 4
185+
# Save / reload using bytes IO objects
186+
for key, value in img.file_map.items():
187+
value.fileobj = BytesIO()
188+
img.to_file_map()
189+
return img.from_file_map(img.file_map)
190+
191+
def test_qform_cycle(self):
192+
# Qform load save cycle
193+
img_klass = self.image_class
194+
# None affine
195+
img = img_klass(np.zeros((2,3,4)), None)
196+
hdr_back = self._qform_rt(img).get_header()
197+
assert_equal(hdr_back['qform_code'], 3)
198+
assert_equal(hdr_back['sform_code'], 4)
199+
# Try non-None affine
200+
img = img_klass(np.zeros((2,3,4)), np.eye(4))
201+
hdr_back = self._qform_rt(img).get_header()
202+
assert_equal(hdr_back['qform_code'], 3)
203+
assert_equal(hdr_back['sform_code'], 4)
204+
# Modify affine in-place - does it hold?
205+
img.get_affine()[0,0] = 9
206+
img.to_file_map()
207+
img_back = img.from_file_map(img.file_map)
208+
exp_aff = np.diag([9,1,1,1])
209+
assert_array_equal(img_back.get_affine(), exp_aff)
210+
hdr_back = img.get_header()
211+
assert_array_equal(hdr_back.get_sform(), exp_aff)
212+
assert_array_equal(hdr_back.get_qform(), exp_aff)
213+
214+
215+
class TestNifti1Pair(TestNifti1Image):
216+
# Run analyze-flavor spatialimage tests
182217
image_class = Nifti1Pair
183218

184219

nibabel/tests/test_spatialimages.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,23 @@ class TestSpatialImage(TestCase):
177177
# class for testing images
178178
image_class = SpatialImage
179179

180+
def test_isolation(self):
181+
# Test image isolated from external changes to header and affine
182+
img_klass = self.image_class
183+
arr = np.arange(3, dtype=np.int16)
184+
aff = np.eye(4)
185+
img = img_klass(arr, aff)
186+
assert_array_equal(img.get_affine(), aff)
187+
aff[0,0] = 99
188+
assert_false(np.all(img.get_affine() == aff))
189+
# header, created by image creation
190+
ihdr = img.get_header()
191+
# Pass it back in
192+
img = img_klass(arr, aff, ihdr)
193+
# Check modifying header outside does not modify image
194+
ihdr.set_zooms((4,))
195+
assert_not_equal(img.get_header(), ihdr)
196+
180197
def test_images(self):
181198
# Assumes all possible images support int16
182199
# See https://github.com/nipy/nibabel/issues/58

nibabel/tests/test_spm99analyze.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ class TestSpm99AnalyzeImage(test_analyze.TestAnalyzeImage):
105105
test_analyze.TestAnalyzeImage.test_data_hdr_cache
106106
))
107107

108+
test_header_updating = (scipy_skip(
109+
test_analyze.TestAnalyzeImage.test_header_updating
110+
))
111+
108112
@scipy_skip
109113
def test_mat_read(self):
110114
# Test mat file reading and writing for the SPM analyze types

0 commit comments

Comments
 (0)