Skip to content

Commit 0ce1ec5

Browse files
committed
BF+TEST - I was using the intercept when scale was 0, because I was fixing scale=0 to scale=1
1 parent ac879aa commit 0ce1ec5

File tree

8 files changed

+218
-107
lines changed

8 files changed

+218
-107
lines changed

nibabel/analyze.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,11 @@ def raw_data_from_fileobj(self, fileobj):
550550
def data_from_fileobj(self, fileobj):
551551
''' Read scaled data array from `fileobj`
552552
553+
Use this routine to get the scaled image data from an image file
554+
`fileobj`, given a header `self`. "Scaled" means, with any header
555+
scaling factors applied to the raw data in the file. Use
556+
`raw_data_from_fileobj` to get the raw data.
557+
553558
Parameters
554559
----------
555560
fileobj : file-like
@@ -559,12 +564,19 @@ def data_from_fileobj(self, fileobj):
559564
-------
560565
arr : ndarray
561566
scaled data array
567+
568+
Notes
569+
-----
570+
We use the header to get any scale or intercept values to apply to the
571+
data. Raw Analyze files don't have scale factors or intercepts, but
572+
this routine also works with formats based on Analyze, that do have
573+
scaling, such as SPM analyze formats and NIfTI.
562574
'''
563575
# read unscaled data
564576
data = self.raw_data_from_fileobj(fileobj)
565-
# get scalings from header
577+
# get scalings from header. Value of None means not present in header
566578
slope, inter = self.get_slope_inter()
567-
if slope is None or (slope==1.0 and not inter):
579+
if slope is None or (slope==1.0 and (inter is None or inter == 0)):
568580
return data
569581
# in-place multiplication and addition on integer types leads to
570582
# integer output types, and disastrous integer rounding.
@@ -1086,33 +1098,30 @@ def get_slope_inter(self):
10861098
10871099
These are not implemented for basic Analyze
10881100
'''
1089-
return 1.0, 0.0
1101+
return None, None
10901102

1091-
def set_slope_inter(self, slope, inter=0.0):
1103+
def set_slope_inter(self, slope, inter=None):
10921104
''' Set slope and / or intercept into header
10931105
10941106
Set slope and intercept for image data, such that, if the image
10951107
data is ``arr``, then the scaled image data will be ``(arr *
10961108
slope) + inter``
10971109
1098-
Note that trying to set not-default values raises error for
1099-
Analyze header - which cannot contain slope or intercept terms.
1110+
In this case, for Analyze images, we can't store the slope or the
1111+
intercept, so this method only checks that `slope` is None or 1.0, and
1112+
that `inter` is None or 0.
11001113
11011114
Parameters
11021115
----------
11031116
slope : None or float
1104-
If None, implies `slope` of 1.0, `inter` of 0.0 (i.e. no
1105-
scaling of the image data). If `slope` is None, we ignore
1106-
the passed value of `inter`
1107-
inter : float, optional
1108-
intercept
1117+
If float, value must be 1.0 or we raise a ``HeaderTypeError``
1118+
inter : None or float, optional
1119+
If float, value must be 0.0 or we raise a ``HeaderTypeError``
11091120
'''
1110-
if slope is None:
1111-
slope = 1.0
1112-
inter = 0.0
1113-
if slope != 1.0 or inter:
1114-
raise HeaderTypeError('Cannot set slope or intercept '
1115-
'for Analyze headers')
1121+
if (slope is None or slope == 1.0) and (inter is None or inter == 0):
1122+
return
1123+
raise HeaderTypeError('Cannot set slope != 1 or intercept != 0 '
1124+
'for Analyze headers')
11161125

11171126
def scaling_from_data(self, data):
11181127
''' Calculate slope, intercept, min, max from data given header

nibabel/nifti1.py

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from .quaternions import fillpositive, quat2mat, mat2quat
1818
from . import analyze # module import
1919
from .spm99analyze import SpmAnalyzeHeader
20-
from .fileholders import copy_file_map
2120

2221
# nifti1 flat header definition for Analyze-like first 348 bytes
2322
# first number in comments indicates offset in file header in bytes
@@ -752,14 +751,13 @@ def get_slope_inter(self):
752751
Returns
753752
-------
754753
slope : None or float
755-
scaling (slope). None if there is no valid scaling from
756-
these fields
754+
scaling (slope). None if there is no valid scaling from these fields
757755
inter : None or float
758-
offset (intercept). Also None if there is no valid scaling, offset
756+
offset (intercept). None if there is no valid scaling or if offset is
757+
not finite.
759758
760759
Examples
761760
--------
762-
>>> fields = {'scl_slope':1,'scl_inter':0}
763761
>>> hdr = Nifti1Header()
764762
>>> hdr.get_slope_inter()
765763
(1.0, 0.0)
@@ -775,14 +773,14 @@ def get_slope_inter(self):
775773
(1.0, 1.0)
776774
>>> hdr['scl_inter'] = np.inf
777775
>>> hdr.get_slope_inter()
778-
(1.0, 0.0)
776+
(1.0, None)
779777
'''
780778
scale = float(self['scl_slope'])
781779
dc_offset = float(self['scl_inter'])
782-
if not scale or not np.isfinite(scale):
780+
if scale == 0 or not np.isfinite(scale):
783781
return None, None
784782
if not np.isfinite(dc_offset):
785-
dc_offset = 0.0
783+
dc_offset = None
786784
return scale, dc_offset
787785

788786
def set_slope_inter(self, slope, inter=0.0):
@@ -795,14 +793,15 @@ def set_slope_inter(self, slope, inter=0.0):
795793
Parameters
796794
----------
797795
slope : None or float
798-
If None, implies `slope` of 1.0, `inter` of 0.0 (i.e. no
799-
scaling of the image data). If `slope` is not, we ignore the
800-
passed value of `inter`
801-
inter : float, optional
802-
intercept
796+
If None, implies `slope` of 0. When the slope is set to 0 or a
797+
not-finite value, ``get_slope_inter`` returns (None, None), i.e.
798+
`inter` is ignored unless there is a valid value for `slope`.
799+
inter : None or float, optional
800+
intercept. None implies inter value of 0.
803801
'''
804802
if slope is None:
805-
slope = 1.0
803+
slope = 0.0
804+
if inter is None:
806805
inter = 0.0
807806
self._header_data['scl_slope'] = slope
808807
self._header_data['scl_inter'] = inter
@@ -1224,37 +1223,54 @@ def _get_checks(klass):
12241223
klass._chk_datatype,
12251224
klass._chk_bitpix,
12261225
klass._chk_pixdims,
1227-
klass._chk_scale_slope,
12281226
klass._chk_scale_inter,
12291227
klass._chk_qfac,
12301228
klass._chk_magic_offset,
12311229
klass._chk_qform_code,
12321230
klass._chk_sform_code)
12331231

12341232
@staticmethod
1235-
def _chk_scale_slope(hdr, fix=False):
1233+
def _chk_scale_inter(hdr, fix=False):
12361234
rep = Report(HeaderDataError)
12371235
scale = hdr['scl_slope']
1238-
if scale and np.isfinite(scale):
1236+
offset = hdr['scl_inter']
1237+
usable_scale = np.isfinite(scale) and scale !=0
1238+
# Nonzero finite scale, and valid offset
1239+
if usable_scale and np.isfinite(offset) or (offset, scale) == (0, 0):
12391240
return hdr, rep
1240-
rep.problem_level = 30
1241-
rep.problem_msg = '"scl_slope" is %s; should !=0 and be finite' % scale
1242-
if fix:
1243-
hdr['scl_slope'] = 1
1244-
rep.fix_msg = 'setting "scl_slope" to 1'
1245-
return hdr, rep
1246-
1247-
@staticmethod
1248-
def _chk_scale_inter(hdr, fix=False):
1249-
rep = Report(HeaderDataError)
1250-
scale = hdr['scl_inter']
1251-
if np.isfinite(scale):
1241+
# If scale is usable but the intercept is not finite, that's a serious
1242+
# problem
1243+
if usable_scale and not np.isfinite(offset):
1244+
rep.problem_level = 40
1245+
rep.problem_msg = ('"scl_slope" is %s; but "scl_inter" is %s; '
1246+
'"scl_inter" should be finite'
1247+
% (scale, offset))
1248+
if fix:
1249+
hdr['scl_inter'] = 0
1250+
rep.fix_msg = 'setting "scl_inter" to 0'
12521251
return hdr, rep
1253-
rep.problem_level = 30
1254-
rep.problem_msg = '"scl_inter" is %s; should be finite' % scale
1255-
if fix:
1256-
hdr['scl_inter'] = 0
1257-
rep.fix_msg = 'setting "scl_inter" to 0'
1252+
level = 0
1253+
msgs = []
1254+
fix_msgs = []
1255+
# Non-finite scale is obviously an error. We still need to check the
1256+
# intercept though
1257+
if not np.isfinite(scale):
1258+
level = 30
1259+
msgs.append('"scl_slope" is %s; should be finite' % scale)
1260+
if fix:
1261+
hdr['scl_slope'] = 0
1262+
fix_msgs.append('setting "scl_slope" to 0 (no scaling)')
1263+
# We've established scale is not usable, so inter will be ignored. That
1264+
# means we can go a bit easy on bad intercepts
1265+
if offset != 0:
1266+
if level == 0: level = 20
1267+
msgs.append('Unused "scl_inter" is %s; should be 0' % offset)
1268+
if fix:
1269+
hdr['scl_inter'] = 0
1270+
fix_msgs.append('setting "scl_inter" to 0')
1271+
rep.problem_level = level
1272+
rep.problem_msg = '; '.join(msgs)
1273+
rep.fix_msg = '; '.join(fix_msgs)
12581274
return hdr, rep
12591275

12601276
@staticmethod

nibabel/spm2analyze.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ def get_slope_inter(self):
115115
def _chk_scale(klass, hdr, fix=True):
116116
rep = Report(HeaderDataError)
117117
scale, offset = hdr.get_slope_inter()
118-
if not scale is None:
118+
# scl_slope of 0 is valid and implies no scaling OR intercept
119+
if not scale is None or hdr['scl_slope'] == 0:
119120
return hdr, rep
120121
rep.problem_level = 30
121122
rep.problem_msg = ('no valid scaling in scalefactor (=%s) '

nibabel/spm99analyze.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,17 @@ def _empty_headerdata(self, endianness=None):
5050
return hdr_data
5151

5252
def get_slope_inter(self):
53-
''' Get scalefactor and intercept '''
53+
''' Get scalefactor and intercept
54+
55+
If scalefactor is 0.0 return None to indicate no scalefactor. Intercept
56+
is always None because SPM99 analyze cannot store intercepts.
57+
'''
5458
slope = self._header_data['scl_slope']
55-
inter = 0.0
56-
return slope, inter
59+
if slope == 0.0:
60+
return None, None
61+
return slope, None
5762

58-
def set_slope_inter(self, slope, inter=0.0):
63+
def set_slope_inter(self, slope, inter=None):
5964
''' Set slope and / or intercept into header
6065
6166
Set slope and intercept for image data, such that, if the image
@@ -71,16 +76,17 @@ def set_slope_inter(self, slope, inter=0.0):
7176
If None, implies `slope` of 1.0, `inter` of 0.0 (i.e. no
7277
scaling of the image data). If `slope` is not, we ignore the
7378
passed value of `inter`
74-
inter : float, optional
75-
intercept
79+
inter : None or float, optional
80+
intercept (dc offset). If float, must be 0, because SPM99 cannot
81+
store intercepts.
7682
'''
7783
if slope is None:
78-
slope = 1.0
79-
inter = 0.0
84+
slope = 0.0
8085
self._header_data['scl_slope'] = slope
81-
if inter:
82-
raise HeaderTypeError('Cannot set non-zero intercept '
83-
'for SPM headers')
86+
if inter is None or inter == 0:
87+
return
88+
raise HeaderTypeError('Cannot set non-zero intercept '
89+
'for SPM headers')
8490

8591
@classmethod
8692
def _get_checks(klass):
@@ -91,10 +97,10 @@ def _get_checks(klass):
9197
def _chk_scale(hdr, fix=False):
9298
rep = Report(HeaderDataError)
9399
scale = hdr['scl_slope']
94-
if scale and np.isfinite(scale):
100+
if np.isfinite(scale):
95101
return hdr, rep
96102
rep.problem_level = 30
97-
rep.problem_msg = ('scale slope is %s; should !=0 and be finite'
103+
rep.problem_msg = ('scale slope is %s; should be finite'
98104
% scale)
99105
if fix:
100106
hdr['scl_slope'] = 1

nibabel/tests/test_analyze.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,31 @@
6767

6868
def _log_chk(hdr, level):
6969
# utility function to check header checking / logging
70+
# If level == 0, this header should always be OK
7071
str_io = StringIO()
7172
logger = logging.getLogger('test.logger')
7273
handler = logging.StreamHandler(str_io)
7374
logger.addHandler(handler)
7475
str_io.truncate(0)
76+
hdrc = hdr.copy()
77+
if level == 0: # Should never log or raise error
78+
logger.setLevel(0)
79+
hdrc.check_fix(logger=logger, error_level=0)
80+
assert_true(str_io.getvalue() == '')
81+
logger.removeHandler(handler)
82+
return hdrc, '', ()
83+
# Non zero level, test above and below threshold
84+
# Logging level above threshold, no log
7585
logger.setLevel(level+1)
7686
e_lev = level+1
77-
hdrc = hdr.copy()
7887
hdrc.check_fix(logger=logger, error_level=e_lev)
79-
assert(str_io.getvalue() == '')
88+
assert_true(str_io.getvalue() == '')
89+
# Logging level below threshold, log appears
90+
logger.setLevel(level+1)
8091
logger.setLevel(level-1)
8192
hdrc = hdr.copy()
8293
hdrc.check_fix(logger=logger, error_level=e_lev)
83-
assert(str_io.getvalue() != '')
94+
assert_true(str_io.getvalue() != '')
8495
message = str_io.getvalue().strip()
8596
logger.removeHandler(handler)
8697
hdrc2 = hdr.copy()
@@ -91,7 +102,6 @@ def _log_chk(hdr, level):
91102
return hdrc, message, raiser
92103

93104

94-
95105
class TestAnalyzeHeader(tb._TestBinaryHeader):
96106
header_class = AnalyzeHeader
97107
example_file = header_file
@@ -338,15 +348,19 @@ def test_scaling():
338348
yield assert_false(np.allclose(data_p5, rdata))
339349

340350

341-
@parametric
342351
def test_slope_inter():
343352
hdr = AnalyzeHeader()
344-
yield assert_equal(hdr.get_slope_inter(), (1.0, 0.0))
345-
hdr.set_slope_inter(None)
346-
yield assert_equal(hdr.get_slope_inter(), (1.0, 0.0))
347-
hdr.set_slope_inter(1.0)
348-
yield assert_equal(hdr.get_slope_inter(), (1.0, 0.0))
349-
yield assert_raises(HeaderTypeError, hdr.set_slope_inter, 1.1)
353+
assert_equal(hdr.get_slope_inter(), (None, None))
354+
for slinter in ((None,),
355+
(None, None),
356+
(1.0,),
357+
(1.0, None),
358+
(None, 0),
359+
(1.0, 0)):
360+
hdr.set_slope_inter(*slinter)
361+
assert_equal(hdr.get_slope_inter(), (None, None))
362+
assert_raises(HeaderTypeError, hdr.set_slope_inter, 1.1)
363+
assert_raises(HeaderTypeError, hdr.set_slope_inter, 1.0, 0.1)
350364

351365

352366
class TestAnalyzeImage(tsi.TestSpatialImage):

0 commit comments

Comments
 (0)