Skip to content

Commit d14f287

Browse files
committed
Merge branch 'refactor-analyze-map' into main-master
* refactor-analyze-map: RF+DOC: refactor Analyze-type header conversion
2 parents 5216a25 + eff6b31 commit d14f287

File tree

4 files changed

+131
-33
lines changed

4 files changed

+131
-33
lines changed

nibabel/analyze.py

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -373,26 +373,23 @@ def from_header(klass, header=None, check=True):
373373
obj = klass(check=check)
374374
if header is None:
375375
return obj
376-
try: # check if there is a specific conversion routine
376+
if hasattr(header, 'as_analyze_map'):
377+
# header is convertible from a field mapping
377378
mapping = header.as_analyze_map()
378-
except AttributeError:
379-
# most basic conversion
380-
obj.set_data_dtype(header.get_data_dtype())
381-
obj.set_data_shape(header.get_data_shape())
382-
obj.set_zooms(header.get_zooms())
383-
return obj
384-
# header is convertible from a field mapping
385-
for key, value in mapping.items():
386-
try:
387-
obj[key] = value
388-
except (ValueError, KeyError):
389-
# the presence of the mapping certifies the fields as
390-
# being of the same meaning as for Analyze types
391-
pass
392-
# set any fields etc that are specific to this format (overriden by
393-
# sub-classes)
394-
obj._set_format_specifics()
395-
# Check for unsupported datatypes
379+
for key in mapping:
380+
try:
381+
obj[key] = mapping[key]
382+
except (ValueError, KeyError):
383+
# the presence of the mapping certifies the fields as being
384+
# of the same meaning as for Analyze types, so we can
385+
# safely discard fields with names not known to this header
386+
# type on the basis they are from the wrong Analyze dialect
387+
pass
388+
# set any fields etc that are specific to this format (overriden by
389+
# sub-classes)
390+
obj._clean_after_mapping()
391+
# Fallback basic conversion always done.
392+
# More specific warning for unsupported datatypes
396393
orig_code = header.get_data_dtype()
397394
try:
398395
obj.set_data_dtype(orig_code)
@@ -402,13 +399,32 @@ def from_header(klass, header=None, check=True):
402399
% (header.__class__,
403400
header.get_value_label('datatype'),
404401
klass))
402+
obj.set_data_dtype(header.get_data_dtype())
403+
obj.set_data_shape(header.get_data_shape())
404+
obj.set_zooms(header.get_zooms())
405405
if check:
406406
obj.check_fix()
407407
return obj
408408

409-
def _set_format_specifics(self):
410-
''' Utility routine to set format specific header stuff
409+
def _clean_after_mapping(self):
410+
''' Set format-specific stuff after converting header from mapping
411+
412+
This routine cleans up Analyze-type headers that have had their fields
413+
set from an Analyze map returned by the ``as_analyze_map`` method.
414+
Nifti 1 / 2, SPM Analyze, Analyze are all Analyze-type headers.
415+
Because this map can set fields that are illegal for particular
416+
subtypes of the Analyze header, this routine cleans these up before the
417+
resulting header is checked and returned.
418+
419+
For example, a Nifti1 single (``.nii``) header has magic "n+1".
420+
Passing the nifti single header for conversion to a Nifti1Pair header
421+
using the ``as_analyze_map`` method will by default set the header
422+
magic to "n+1", when it should be "ni1" for the pair header. This
423+
method is for that kind of case - so the specific header can set fields
424+
like magic correctly, even though the mapping has given a wrong value.
411425
'''
426+
# All current Nifti etc fields that are present in the Analyze header
427+
# have the same meaning as they do for Analyze.
412428
pass
413429

414430
def raw_data_from_fileobj(self, fileobj):
@@ -688,6 +704,42 @@ def set_zooms(self, zooms):
688704
pixdims[1:ndim+1] = zooms[:]
689705

690706
def as_analyze_map(self):
707+
""" Return header as mapping for conversion to Analyze types
708+
709+
Collect data from custom header type to fill in fields for Analyze and
710+
derived header types (such as Nifti1 and Nifti2).
711+
712+
When Analyze types convert another header type to their own type, they
713+
call this this method to check if there are other Analyze / Nifti
714+
fields that the source header would like to set.
715+
716+
Returns
717+
-------
718+
analyze_map : mapping
719+
Object that can be used as a mapping thus::
720+
721+
for key in analyze_map:
722+
value = analyze_map[key]
723+
724+
where ``key`` is the name of a field that can be set in an Analyze
725+
header type, such as Nifti1, and ``value`` is a value for the
726+
field. For example, `analyze_map` might be a something like
727+
``dict(regular='y', slice_duration=0.3)`` where ``regular`` is a
728+
field present in both Analyze and Nifti1, and ``slice_duration`` is
729+
a field restricted to Nifti1 and Nifti2. If a particular Analyze
730+
header type does not recognize the field name, it will throw away
731+
the value without error. See :meth:`Analyze.from_header`.
732+
733+
Notes
734+
-----
735+
You can also return a Nifti header with the relevant fields set.
736+
737+
Your header still needs methods ``get_data_dtype``, ``get_data_shape``
738+
and ``get_zooms``, for the conversion, and these get called *after*
739+
using the analyze map, so the methods will override values set in the
740+
map.
741+
"""
742+
# In the case of Analyze types, the header is already such a mapping
691743
return self
692744

693745
def set_data_offset(self, offset):

nibabel/freesurfer/mghformat.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,10 +402,6 @@ def _empty_headerdata(self):
402402
hdr_data['mrparms'] = np.array([0, 0, 0, 0])
403403
return hdr_data
404404

405-
def _set_format_specifics(self):
406-
''' Set MGH specific header stuff'''
407-
self._header_data['version'] = 1
408-
409405
def _set_affine_default(self, hdr):
410406
''' If goodRASFlag is 0, return the default delta, Mdc and Pxyz_c
411407
'''

nibabel/nifti1.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,14 +1466,17 @@ def set_xyzt_units(self, xyz=None, t=None):
14661466
t_code = unit_codes[t]
14671467
self.structarr['xyzt_units'] = xyz_code + t_code
14681468

1469-
def _set_format_specifics(self):
1470-
''' Utility routine to set format specific header stuff '''
1471-
if self.is_single:
1472-
self._structarr['magic'] = self.single_magic
1473-
if self._structarr['vox_offset'] < self.single_vox_offset:
1474-
self._structarr['vox_offset'] = self.single_vox_offset
1475-
else:
1476-
self._structarr['magic'] = self.pair_magic
1469+
def _clean_after_mapping(self):
1470+
''' Set format-specific stuff after converting header from mapping
1471+
1472+
Clean up header after it has been initialized from an
1473+
``as_analyze_map`` method of another header type
1474+
1475+
See :meth:`nibabel.analyze.AnalyzeHeader._clean_after_mapping` for a
1476+
more detailed description.
1477+
'''
1478+
self._structarr['magic'] = (self.single_magic if self.is_single
1479+
else self.pair_magic)
14771480

14781481
''' Checks only below here '''
14791482

nibabel/tests/test_analyze.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,53 @@ def test_slope_inter(self):
560560
assert_raises(HeaderTypeError, hdr.set_slope_inter, 1.1)
561561
assert_raises(HeaderTypeError, hdr.set_slope_inter, 1.0, 0.1)
562562

563+
def test_from_analyze_map(self):
564+
# Test that any header can pass values from a mapping
565+
klass = self.header_class
566+
# Header needs to implement data_dtype, data_shape, zooms
567+
class H1(object): pass
568+
assert_raises(AttributeError, klass.from_header, H1())
569+
class H2(object):
570+
def get_data_dtype(self):
571+
return np.dtype('u1')
572+
assert_raises(AttributeError, klass.from_header, H2())
573+
class H3(H2):
574+
def get_data_shape(self):
575+
return (2, 3, 4)
576+
assert_raises(AttributeError, klass.from_header, H3())
577+
class H4(H3):
578+
def get_zooms(self):
579+
return 4., 5., 6.
580+
exp_hdr = klass()
581+
exp_hdr.set_data_dtype(np.dtype('u1'))
582+
exp_hdr.set_data_shape((2, 3, 4))
583+
exp_hdr.set_zooms((4, 5, 6))
584+
assert_equal(klass.from_header(H4()), exp_hdr)
585+
# cal_max, cal_min get properly set from ``as_analyze_map``
586+
class H5(H4):
587+
def as_analyze_map(self):
588+
return dict(cal_min=-100, cal_max=100)
589+
exp_hdr['cal_min'] = -100
590+
exp_hdr['cal_max'] = 100
591+
assert_equal(klass.from_header(H5()), exp_hdr)
592+
# set_* methods override fields fron header
593+
class H6(H5):
594+
def as_analyze_map(self):
595+
return dict(datatype=4, bitpix=32,
596+
cal_min=-100, cal_max=100)
597+
assert_equal(klass.from_header(H6()), exp_hdr)
598+
# Any mapping will do, including a Nifti header
599+
class H7(H5):
600+
def as_analyze_map(self):
601+
n_hdr = Nifti1Header()
602+
n_hdr.set_data_dtype(np.dtype('i2'))
603+
n_hdr['cal_min'] = -100
604+
n_hdr['cal_max'] = 100
605+
return n_hdr
606+
# Values from methods still override values from header (shape, dtype,
607+
# zooms still at defaults from n_hdr header fields above)
608+
assert_equal(klass.from_header(H7()), exp_hdr)
609+
563610

564611
def test_best_affine():
565612
hdr = AnalyzeHeader()

0 commit comments

Comments
 (0)