Skip to content

Commit 67616a4

Browse files
committed
Merge branch 'extensions-in-hdr' into main-master
* extensions-in-hdr: RF - fixed remaining test for extensions; refactored extension io and single/pair nifti code RF - moved extensions into header; fixed __eq__ bug with tests
2 parents e3605bf + bf5d6aa commit 67616a4

File tree

3 files changed

+181
-109
lines changed

3 files changed

+181
-109
lines changed

nibabel/nifti1.py

Lines changed: 97 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ def __repr__(self):
366366
return s
367367

368368
def __eq__(self, other):
369+
if len(self) != len(other):
370+
return False
369371
for i, e in enumerate(self):
370372
if not e == other[i]:
371373
return False
@@ -387,13 +389,6 @@ def write_to(self, fileobj, byteswap):
387389
-------
388390
None
389391
'''
390-
# not extensions -> nothing to do
391-
if not len(self):
392-
return
393-
394-
# since we have extensions write the appropriate flag
395-
fileobj.write(np.array((1, 0, 0, 0), dtype=np.int8).tostring())
396-
# and now each extension
397392
for e in self:
398393
e.write_to(fileobj, byteswap)
399394

@@ -404,43 +399,24 @@ def from_fileobj(klass, fileobj, size, byteswap):
404399
Parameters
405400
----------
406401
fileobj : file-like object
407-
It is assumed to be positions right after the NIfTI magic field.
402+
We begin reading the extensions at the current file position
408403
size : int
409-
Number of bytes to read. If negative, fileobj will be read till its
410-
end.
404+
Number of bytes to read. If negative, fileobj will be read till its
405+
end.
411406
byteswap : boolean
412-
Flag if byteswapping the read data is required.
407+
Flag if byteswapping the read data is required.
413408
414409
Returns
415410
-------
416-
An extension list. This list might be empty in case not extensions
417-
were present in fileobj.
411+
An extension list. This list might be empty in case not extensions
412+
were present in fileobj.
418413
'''
419414
# make empty extension list
420415
extensions = klass()
421-
# assume the fileptr is just after header (magic field)
422-
# try reading the next 4 bytes after the initial header
423-
extension_status = fileobj.read(4)
424-
if not len(extension_status):
425-
# if there is nothing the NIfTI standard requires to assume zeros
426-
extension_status = np.zeros((4,), dtype=np.int8)
427-
else:
428-
extension_status = np.fromstring(extension_status, dtype=np.int8)
429-
if byteswap:
430-
extension_status = extension_status.byteswap()
431-
432-
# NIfTI1 says: if first element is non-zero there are extensions present
433-
# if not there is nothing left to do
434-
if not extension_status[0]:
435-
return extensions
436-
437-
# note that we read the extension flag
438-
if not size < 0:
439-
size = size - 4
416+
# assume the file pointer is at the beginning of any extensions.
440417
# read until the whole header is parsed (each extension is a multiple
441418
# of 16 bytes) or in case of a separate header file till the end
442419
# (break inside the body)
443-
# XXX not sure if the separate header behavior is sane
444420
while size >= 16 or size < 0:
445421
# the next 8 bytes should have esize and ecode
446422
ext_def = fileobj.read(8)
@@ -481,7 +457,18 @@ def from_fileobj(klass, fileobj, size, byteswap):
481457

482458

483459
class Nifti1Header(SpmAnalyzeHeader):
484-
''' Class for NIFTI1 header '''
460+
''' Class for NIFTI1 header
461+
462+
The NIFTI1 header has many more coded fields than the simpler Analyze
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.
471+
'''
485472
# Copies of module level definitions
486473
_dtype = header_dtype
487474
_data_type_codes = data_type_codes
@@ -497,6 +484,68 @@ class Nifti1Header(SpmAnalyzeHeader):
497484
has_data_slope = True
498485
has_data_intercept = True
499486

487+
# Extension class; should implement __call__ for contruction, and
488+
# ``from_fileobj`` for reading from file
489+
exts_klass = Nifti1Extensions
490+
491+
# Signal whether this is single (header + data) file
492+
is_single = True
493+
494+
def __init__(self,
495+
binaryblock=None,
496+
endianness=None,
497+
check=True,
498+
extensions=()):
499+
''' Initialize header from binary data block and extensions
500+
'''
501+
super(Nifti1Header, self).__init__(binaryblock,
502+
endianness,
503+
check)
504+
self.extensions = self.exts_klass(extensions)
505+
506+
def copy(self):
507+
''' Return copy of header
508+
509+
Take reference to extensions as well as copy of header contents
510+
'''
511+
return self.__class__(
512+
self.binaryblock,
513+
self.endianness,
514+
False,
515+
self.extensions)
516+
517+
@classmethod
518+
def from_fileobj(klass, fileobj, endianness=None, check=True):
519+
raw_str = fileobj.read(klass._dtype.itemsize)
520+
hdr = klass(raw_str, endianness, check)
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:
529+
extsize = -1
530+
else: # otherwise read until the beginning of the data
531+
extsize = hdr._header_data['vox_offset'] - fileobj.tell()
532+
byteswap = endian_codes['native'] != hdr.endianness
533+
hdr.extensions = klass.exts_klass.from_fileobj(fileobj, extsize, byteswap)
534+
return hdr
535+
536+
def write_to(self, fileobj):
537+
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')
546+
byteswap = endian_codes['native'] != self.endianness
547+
self.extensions.write_to(fileobj, byteswap)
548+
500549
def get_best_affine(self):
501550
''' Select best of available transforms '''
502551
hdr = self._header_data
@@ -510,8 +559,12 @@ def _empty_headerdata(self, endianness=None):
510559
''' Create empty header binary block with given endianness '''
511560
hdr_data = analyze.AnalyzeHeader._empty_headerdata(self, endianness)
512561
hdr_data['scl_slope'] = 1
513-
hdr_data['magic'] = 'n+1'
514-
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
515568
return hdr_data
516569

517570
def get_qform_quaternion(self):
@@ -1149,9 +1202,12 @@ def set_xyzt_units(self, xyz=None, t=None):
11491202

11501203
def _set_format_specifics(self):
11511204
''' Utility routine to set format specific header stuff '''
1152-
self._header_data['magic'] = 'n+1'
1153-
if self._header_data['vox_offset'] < 352:
1154-
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'
11551211

11561212
''' Checks only below here '''
11571213

@@ -1266,17 +1322,8 @@ def _chk_xform_code(klass, code_type, hdr, fix):
12661322

12671323
class Nifti1PairHeader(Nifti1Header):
12681324
''' Class for nifti1 pair header '''
1269-
def _empty_headerdata(self, endianness=None):
1270-
''' Create empty header binary block with given endianness '''
1271-
hdr_data = analyze.AnalyzeHeader._empty_headerdata(self, endianness)
1272-
hdr_data['scl_slope'] = 1
1273-
hdr_data['magic'] = 'ni1'
1274-
hdr_data['vox_offset'] = 0
1275-
return hdr_data
1276-
1277-
def _set_format_specifics(self):
1278-
''' Utility routine to set format specific header stuff '''
1279-
self._header_data['magic'] = 'ni1'
1325+
# Signal whether this is single (header + data) file
1326+
is_single = False
12801327

12811328

12821329
class Nifti1Pair(analyze.AnalyzeImage):
@@ -1287,20 +1334,6 @@ def from_file_map(klass, file_map):
12871334
hdrf, imgf = klass._get_open_files(file_map, 'rb')
12881335
header = klass.header_class.from_fileobj(hdrf)
12891336
extra = None
1290-
# handle extensions
1291-
# assume the fileptr is just after header (magic field)
1292-
# determine how much to read when parsing the extensions
1293-
if header['vox_offset'] == 0:
1294-
# read till the end of the header
1295-
extsize = -1
1296-
else:
1297-
extsize = header['vox_offset'] - hdrf.tell()
1298-
extensions = Nifti1Extensions.from_fileobj(
1299-
hdrf, extsize,
1300-
endian_codes['native'] != header.endianness)
1301-
# XXX maybe always do that?
1302-
if len(extensions):
1303-
extra = {'extensions': extensions}
13041337
affine = header.get_best_affine()
13051338
hdr_copy = header.copy()
13061339
data = klass.ImageArrayProxy(imgf, hdr_copy)
@@ -1315,13 +1348,6 @@ def _write_header(self, header_file, header, slope, inter):
13151348
header,
13161349
slope,
13171350
inter)
1318-
if not self.extra.has_key('extensions'):
1319-
# no extensions: be nice and write appropriate flag
1320-
header_file.write(np.array((0, 0, 0, 0), dtype=np.int8).tostring())
1321-
else:
1322-
self.extra['extensions'].write_to(
1323-
header_file,
1324-
endian_codes['native'] != header.endianness)
13251351

13261352
def update_header(self):
13271353
''' Harmonize header with image data and affine

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()

0 commit comments

Comments
 (0)