Skip to content

Commit bf5d6aa

Browse files
committed
RF - fixed remaining test for extensions; refactored extension io and single/pair nifti code
1 parent 6321479 commit bf5d6aa

File tree

3 files changed

+91
-77
lines changed

3 files changed

+91
-77
lines changed

nibabel/nifti1.py

Lines changed: 51 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -389,14 +389,6 @@ def write_to(self, fileobj, byteswap):
389389
-------
390390
None
391391
'''
392-
# not extensions -> nothing to do
393-
if not len(self):
394-
# no extensions: be nice and write appropriate flag
395-
fileobj.write(np.array((0, 0, 0, 0), dtype=np.int8).tostring())
396-
return
397-
# since we have extensions write the appropriate flag
398-
fileobj.write(np.array((1, 0, 0, 0), dtype=np.int8).tostring())
399-
# and now each extension
400392
for e in self:
401393
e.write_to(fileobj, byteswap)
402394

@@ -407,41 +399,24 @@ def from_fileobj(klass, fileobj, size, byteswap):
407399
Parameters
408400
----------
409401
fileobj : file-like object
410-
It is assumed to be positions right after the NIfTI magic field.
402+
We begin reading the extensions at the current file position
411403
size : int
412-
Number of bytes to read. If negative, fileobj will be read till its
413-
end.
404+
Number of bytes to read. If negative, fileobj will be read till its
405+
end.
414406
byteswap : boolean
415-
Flag if byteswapping the read data is required.
407+
Flag if byteswapping the read data is required.
416408
417409
Returns
418410
-------
419-
An extension list. This list might be empty in case not extensions
420-
were present in fileobj.
411+
An extension list. This list might be empty in case not extensions
412+
were present in fileobj.
421413
'''
422414
# make empty extension list
423415
extensions = klass()
424-
# assume the fileptr is just after header (magic field)
425-
# try reading the next 4 bytes after the initial header
426-
extension_status = fileobj.read(4)
427-
if not len(extension_status):
428-
# if there is nothing the NIfTI standard requires to assume zeros
429-
extension_status = np.zeros((4,), dtype=np.int8)
430-
else:
431-
extension_status = np.fromstring(extension_status, dtype=np.int8)
432-
if byteswap:
433-
extension_status = extension_status.byteswap()
434-
# NIfTI1 says: if first element is non-zero there are extensions present
435-
# if not there is nothing left to do
436-
if not extension_status[0]:
437-
return extensions
438-
# note that we read the extension flag
439-
if not size < 0:
440-
size = size - 4
416+
# assume the file pointer is at the beginning of any extensions.
441417
# read until the whole header is parsed (each extension is a multiple
442418
# of 16 bytes) or in case of a separate header file till the end
443419
# (break inside the body)
444-
# XXX not sure if the separate header behavior is sane
445420
while size >= 16 or size < 0:
446421
# the next 8 bytes should have esize and ecode
447422
ext_def = fileobj.read(8)
@@ -485,7 +460,14 @@ class Nifti1Header(SpmAnalyzeHeader):
485460
''' Class for NIFTI1 header
486461
487462
The NIFTI1 header has many more coded fields than the simpler Analyze
488-
variants. Analyze headers also have extensions
463+
variants. Nifti1 headers also have extensions.
464+
465+
Nifti allows the header to be a separate file, as part of a nifti image /
466+
header pair, or to precede the data in a single file. The object needs to
467+
know which type it is, in order to manage the voxel offset pointing to the
468+
data, extension reading, and writing the correct magic string.
469+
470+
This class handles the header-preceding-data case.
489471
'''
490472
# Copies of module level definitions
491473
_dtype = header_dtype
@@ -506,6 +488,9 @@ class Nifti1Header(SpmAnalyzeHeader):
506488
# ``from_fileobj`` for reading from file
507489
exts_klass = Nifti1Extensions
508490

491+
# Signal whether this is single (header + data) file
492+
is_single = True
493+
509494
def __init__(self,
510495
binaryblock=None,
511496
endianness=None,
@@ -521,7 +506,7 @@ def __init__(self,
521506
def copy(self):
522507
''' Return copy of header
523508
524-
Take extensions as well as header
509+
Take reference to extensions as well as copy of header contents
525510
'''
526511
return self.__class__(
527512
self.binaryblock,
@@ -533,29 +518,34 @@ def copy(self):
533518
def from_fileobj(klass, fileobj, endianness=None, check=True):
534519
raw_str = fileobj.read(klass._dtype.itemsize)
535520
hdr = klass(raw_str, endianness, check)
536-
hdr_len = hdr._header_len()
537-
if hdr_len == -1:
521+
# Read next 4 bytes to see if we have extensions. The nifti standard
522+
# has this as a 4 byte string; if the first value is not zero, then we
523+
# have extensions.
524+
extension_status = fileobj.read(4)
525+
if len(extension_status) < 4 or extension_status[0] == '\x00':
526+
return hdr
527+
# If this is a detached header file read to end
528+
if not klass.is_single:
538529
extsize = -1
539-
else:
540-
extsize = hdr_len - fileobj.tell()
530+
else: # otherwise read until the beginning of the data
531+
extsize = hdr._header_data['vox_offset'] - fileobj.tell()
541532
byteswap = endian_codes['native'] != hdr.endianness
542533
hdr.extensions = klass.exts_klass.from_fileobj(fileobj, extsize, byteswap)
543534
return hdr
544535

545536
def write_to(self, fileobj):
546537
super(Nifti1Header, self).write_to(fileobj)
538+
n_exts = len(self.extensions)
539+
if n_exts == 0:
540+
# If single file, write required 0 stream to signal no extensions
541+
if self.is_single:
542+
fileobj.write('\x00' * 4)
543+
return
544+
# Signal there are extensions that follow
545+
fileobj.write('\x01\x00\x00\x00')
547546
byteswap = endian_codes['native'] != self.endianness
548547
self.extensions.write_to(fileobj, byteswap)
549548

550-
def _header_len(self):
551-
''' Return header length in bytes or -1 for unknown
552-
553-
This will be -1 for headers that are their own files, as in the .hdr
554-
file of a nifti pair, or the same as the start of the data (vox_offset)
555-
in single file niftis
556-
'''
557-
return self._header_data['vox_offset']
558-
559549
def get_best_affine(self):
560550
''' Select best of available transforms '''
561551
hdr = self._header_data
@@ -569,8 +559,12 @@ def _empty_headerdata(self, endianness=None):
569559
''' Create empty header binary block with given endianness '''
570560
hdr_data = analyze.AnalyzeHeader._empty_headerdata(self, endianness)
571561
hdr_data['scl_slope'] = 1
572-
hdr_data['magic'] = 'n+1'
573-
hdr_data['vox_offset'] = 352
562+
if self.is_single:
563+
hdr_data['magic'] = 'n+1'
564+
hdr_data['vox_offset'] = 352
565+
else:
566+
hdr_data['magic'] = 'ni1'
567+
hdr_data['vox_offset'] = 0
574568
return hdr_data
575569

576570
def get_qform_quaternion(self):
@@ -1208,9 +1202,12 @@ def set_xyzt_units(self, xyz=None, t=None):
12081202

12091203
def _set_format_specifics(self):
12101204
''' Utility routine to set format specific header stuff '''
1211-
self._header_data['magic'] = 'n+1'
1212-
if self._header_data['vox_offset'] < 352:
1213-
self._header_data['vox_offset'] = 352
1205+
if self.is_single:
1206+
self._header_data['magic'] = 'n+1'
1207+
if self._header_data['vox_offset'] < 352:
1208+
self._header_data['vox_offset'] = 352
1209+
else:
1210+
self._header_data['magic'] = 'ni1'
12141211

12151212
''' Checks only below here '''
12161213

@@ -1325,26 +1322,9 @@ def _chk_xform_code(klass, code_type, hdr, fix):
13251322

13261323
class Nifti1PairHeader(Nifti1Header):
13271324
''' Class for nifti1 pair header '''
1328-
def _empty_headerdata(self, endianness=None):
1329-
''' Create empty header binary block with given endianness '''
1330-
hdr_data = analyze.AnalyzeHeader._empty_headerdata(self, endianness)
1331-
hdr_data['scl_slope'] = 1
1332-
hdr_data['magic'] = 'ni1'
1333-
hdr_data['vox_offset'] = 0
1334-
return hdr_data
1335-
1336-
def _set_format_specifics(self):
1337-
''' Utility routine to set format specific header stuff '''
1338-
self._header_data['magic'] = 'ni1'
1339-
1340-
def _header_len(self):
1341-
''' Return header length in bytes or -1 for unknown
1325+
# Signal whether this is single (header + data) file
1326+
is_single = False
13421327

1343-
This will be -1 for headers that are their own files, as in the .hdr
1344-
file of a nifti pair, or the same as the start of the data (vox_offset)
1345-
in single file niftis
1346-
'''
1347-
return -1
13481328

13491329
class Nifti1Pair(analyze.AnalyzeImage):
13501330
header_class = Nifti1PairHeader

nibabel/tests/test_binary.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,24 @@ def test_from_to_fileobj(self):
127127
hdr = self.header_class()
128128
str_io = StringIO()
129129
hdr.write_to(str_io)
130-
yield assert_equal(str_io.getvalue(), hdr.binaryblock)
131130
str_io.seek(0)
132131
hdr2 = self.header_class.from_fileobj(str_io)
133132
yield assert_equal(hdr2.endianness, native_code)
134133
yield assert_equal(hdr2.binaryblock, hdr.binaryblock)
135134

135+
def test_binblock_is_file(self):
136+
# Checks that the binary string respresentation is the whole of the
137+
# header file. This is true for Analyze types, but not true Nifti
138+
# single file headers, for example, because they will have extension
139+
# strings following. More generally, there may be other perhaps
140+
# optional data after the binary block, in which case you will need to
141+
# override this test
142+
hdr = self.header_class()
143+
str_io = StringIO()
144+
hdr.write_to(str_io)
145+
assert_equal(str_io.getvalue(), hdr.binaryblock)
146+
147+
136148
def test_structarr(self):
137149
# structarr attribute also read only
138150
hdr = self.header_class()

nibabel/tests/test_nifti1.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from ..tmpdirs import InTemporaryDirectory
1818
from ..spatialimages import HeaderDataError
1919
from .. import nifti1 as nifti1
20-
from ..nifti1 import (load, Nifti1Header, Nifti1Image,
20+
from ..nifti1 import (load, Nifti1Header, Nifti1PairHeader, Nifti1Image,
2121
Nifti1Pair, Nifti1Extension, Nifti1Extensions,
2222
data_type_codes, extension_codes, slice_order_codes)
2323

@@ -44,17 +44,17 @@
4444
A[:3,3] = T
4545

4646

47-
class TestNiftiHeader(tana.TestAnalyzeHeader):
48-
header_class = Nifti1Header
47+
class TestNifti1PairHeader(tana.TestAnalyzeHeader):
48+
header_class = Nifti1PairHeader
4949
example_file = header_file
5050

5151
def test_empty(self):
5252
hdr = self.header_class()
5353
for tests in tana.TestAnalyzeHeader.test_empty(self):
5454
yield tests
55-
yield assert_equal(hdr['magic'], 'n+1')
55+
yield assert_equal(hdr['magic'], 'ni1')
5656
yield assert_equal(hdr['scl_slope'], 1)
57-
yield assert_equal(hdr['vox_offset'], 352)
57+
yield assert_equal(hdr['vox_offset'], 0)
5858

5959
def test_from_eg_file(self):
6060
hdr = Nifti1Header.from_fileobj(open(self.example_file))
@@ -117,6 +117,28 @@ def test_nifti_log_checks(self):
117117
'setting to 0')
118118

119119

120+
class TestNifti1SingleHeader(TestNifti1PairHeader):
121+
122+
header_class = Nifti1Header
123+
124+
def test_empty(self):
125+
hdr = self.header_class()
126+
for tests in tana.TestAnalyzeHeader.test_empty(self):
127+
yield tests
128+
yield assert_equal(hdr['magic'], 'n+1')
129+
yield assert_equal(hdr['scl_slope'], 1)
130+
yield assert_equal(hdr['vox_offset'], 352)
131+
132+
def test_binblock_is_file(self):
133+
# Override test that binary string is the same as the file on disk; in
134+
# the case of the single file version of the header, we need to append
135+
# the extension string (4 0s)
136+
hdr = self.header_class()
137+
str_io = StringIO()
138+
hdr.write_to(str_io)
139+
assert_equal(str_io.getvalue(), hdr.binaryblock + '\x00' * 4)
140+
141+
120142
class TestNifti1Image(tana.TestAnalyzeImage):
121143
# class for testing images
122144
image_class = Nifti1Image

0 commit comments

Comments
 (0)