diff --git a/.mailmap b/.mailmap index ae1aa78f87..79ce4939e6 100644 --- a/.mailmap +++ b/.mailmap @@ -40,3 +40,4 @@ Satrajit Ghosh Satrajit Ghosh Jasper J.F. van den Bosch Jasper Gregory R. Lee Gregory R. Lee Demian Wassermann Demian Wassermann +Paul McCarthy Paul McCarthy diff --git a/Changelog b/Changelog index 02fd993e0c..4c65cf0bfe 100644 --- a/Changelog +++ b/Changelog @@ -24,6 +24,25 @@ Gerhard (SG) and Eric Larson (EL). References like "pr/298" refer to github pull request numbers. +2.2.1 (Wednesday 22 November 2017) +================================== + +Bug fixes +--------- + +* Set L/R labels in orthoview correctly (pr/564) (CM) +* Defer use of ufunc / memmap test - allows "freezing" (pr/572) (MB, reviewed + by Satra Ghosh) +* Fix doctest failures with pre-release numpy (pr/582) (MB, reviewed by CM) + +Maintenance +----------- + +* Update documentation around NIfTI qform/sform codes (pr/576) (Paul McCarthy, + reviewed by MB, CM) + (pr/580) (Bennet Fauber, reviewed by Paul McCarthy) +* Skip precision test on macOS, newer numpy (pr/583) (MB, reviewed by CM) +* Simplify AppVeyor script, removing conda (pr/584) (MB, reviewed by CM) + 2.2 (Friday 13 October 2017) ============================ diff --git a/appveyor.yml b/appveyor.yml index ae711f8e97..e41aee90c8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,60 +1,34 @@ # vim ft=yaml # CI on Windows via appveyor -# This file was based on Olivier Grisel's python-appveyor-demo environment: matrix: - - PYTHON: "C:\\Python27-conda32" - PYTHON_VERSION: "2.7" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34-conda32" - PYTHON_VERSION: "3.4" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34-conda64" - PYTHON_VERSION: "3.4" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python35-conda64" - PYTHON_VERSION: "3.5" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python35-conda32" - PYTHON_VERSION: "3.5" - PYTHON_ARCH: "32" + - PYTHON: C:\Python27 + - PYTHON: C:\Python27-x64 + - PYTHON: C:\Python34 + - PYTHON: C:\Python34-x64 + - PYTHON: C:\Python35 + - PYTHON: C:\Python35-x64 + - PYTHON: C:\Python36 + - PYTHON: C:\Python36-x64 install: - # Install miniconda Python - - "powershell ./tools/install_python.ps1" - # Prepend newly installed Python to the PATH of this build (this cannot be # done from inside the powershell script as it would require to restart # the parent CMD process). - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - # Set up a conda environment: - - conda config --set always_yes yes - - conda update -q conda - - conda info -a - - conda create -q -n test-environment python=%PYTHON_VERSION% - - activate test-environment - - # Check that we have the expected version and architecture for Python - - "python --version" - - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + - SET PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% # Install the dependencies of the project. - - "conda install --yes --quiet numpy scipy matplotlib nose h5py mock" - - "pip install pydicom" - - "python setup.py install" - - "SET NIBABEL_DATA_DIR=%CD%\\nibabel-data" + - pip install numpy scipy matplotlib nose h5py mock + - pip install pydicom + - pip install . + - SET NIBABEL_DATA_DIR=%CD%\nibabel-data build: false # Not a C# project, build stuff at the test step instead. test_script: # Change into an innocuous directory and find tests from installation - - "mkdir for_testing" - - "cd for_testing" - - "nosetests --with-doctest nibabel" + - mkdir for_testing + - cd for_testing + - nosetests --with-doctest nibabel diff --git a/doc/source/index.rst b/doc/source/index.rst index 85c847fb9b..f623c931d2 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -79,6 +79,7 @@ contributed code and discussion (in rough order of appearance): * Venky Reddy * Mark Hymers * Jasper J.F. van den Bosch +* Bennet Fauber License reprise =============== diff --git a/doc/source/nifti_images.rst b/doc/source/nifti_images.rst index 257465f3a9..5bf423571c 100644 --- a/doc/source/nifti_images.rst +++ b/doc/source/nifti_images.rst @@ -239,7 +239,7 @@ You can get the affine and the code using the ``coded=True`` argument to [ 0. , 0.32, 2.17, -7.25], [ 0. , 0. , 0. , 1. ]]), array(1, dtype=int16)) -You can set the sform with with the ``get_sform()`` method of the header and +You can set the sform with the ``set_sform()`` method of the header and the image. >>> n1_header.set_sform(np.diag([2, 3, 4, 1])) @@ -314,6 +314,63 @@ The algorithm is defined in the ``get_best_affine()`` method. It is: #. If ``qform_code`` != 0 ('unknown') use the qform affine; else #. Use the fall-back affine. +.. _default-sform-qform-codes: + +Default sform and qform codes +============================= + +If you create a new image, e.g.: + +>>> data = np.random.random((20, 20, 20)) +>>> xform = np.eye(4) * 2 +>>> img = nib.nifti1.Nifti1Image(data, xform) + +The sform and qform codes will be initialised to 2 (aligned) and 0 (unknown) +respectively: + +>>> img.get_sform(coded=True) # doctest: +NORMALIZE_WHITESPACE +(array([[ 2., 0., 0., 0.], + [ 0., 2., 0., 0.], + [ 0., 0., 2., 0.], + [ 0., 0., 0., 1.]]), array(2, dtype=int16)) +>>> img.get_qform(coded=True) +(None, 0) + +This is based on the assumption that the affine you specify for a newly +created image will align the image to some known coordinate system. According +to the `NIfTI specification `_, the qform is intended to encode a +transformation into scanner coordinates - for a programmatically created +image, we have no way of knowing what the scanner coordinate system is; +furthermore, the qform cannot be used to store an arbitrary affine transform, +as it is unable to encode shears. So the provided affine will be stored in the +sform, and the qform will be left uninitialised. + +If you create a new image and specify an existing header, e.g.: + +>>> example_ni1 = os.path.join(data_path, 'example4d.nii.gz') +>>> n1_img = nib.load(example_ni1) +>>> new_header = header=n1_img.header.copy() +>>> new_data = np.random.random(n1_img.shape[:3]) +>>> new_img = nib.nifti1.Nifti1Image(data, None, header=new_header) + +then the newly created image will inherit the same sform and qform codes that +are in the provided header. However, if you create a new image with both an +affine and a header specified, e.g.: + +>>> xform = np.eye(4) +>>> new_img = nib.nifti1.Nifti1Image(data, xform, header=new_header) + +then the sform and qform codes will *only* be preserved if the provided affine +is the same as the affine in the provided header. If the affines do not match, +the sform and qform codes will be set to their default values of 2 and 0 +respectively. This is done on the basis that, if you are changing the affine, +you are likely to be changing the space to which the affine is pointing. So +the original sform and qform codes can no longer be assumed to be valid. + +If you wish to set the sform and qform affines and/or codes to some other +value, you can always set them after creation using the ``set_sform`` and +``set_qform`` methods, as described above. + ************ Data scaling ************ diff --git a/nibabel/benchmarks/bench_arrayproxy_slicing.py b/nibabel/benchmarks/bench_arrayproxy_slicing.py index a824822d3c..321a0779d5 100644 --- a/nibabel/benchmarks/bench_arrayproxy_slicing.py +++ b/nibabel/benchmarks/bench_arrayproxy_slicing.py @@ -191,7 +191,7 @@ def testfunc(): data[:, 1] = [r[4] for r in results] try: data[:, 2] = [r[3] / r[4] for r in results] - except: + except ZeroDivisionError: data[:, 2] = np.nan data[:, 3] = [r[5] - r[6] for r in results] diff --git a/nibabel/ecat.py b/nibabel/ecat.py index 6b193acdcc..3c0957e11d 100644 --- a/nibabel/ecat.py +++ b/nibabel/ecat.py @@ -468,13 +468,13 @@ def get_series_framenumbers(mlist): mlist_nframes = len(frames_order) trueframenumbers = np.arange(nframes - mlist_nframes, nframes) frame_dict = {} - try: - for frame_stored, (true_order, _) in frames_order.items(): - # frame as stored in file -> true number in series + for frame_stored, (true_order, _) in frames_order.items(): + # frame as stored in file -> true number in series + try: frame_dict[frame_stored] = trueframenumbers[true_order] + 1 - return frame_dict - except: - raise IOError('Error in header or mlist order unknown') + except IndexError: + raise IOError('Error in header or mlist order unknown') + return frame_dict def read_subheaders(fileobj, mlist, endianness): diff --git a/nibabel/info.py b/nibabel/info.py index 142a4ebedf..602a77c1c9 100644 --- a/nibabel/info.py +++ b/nibabel/info.py @@ -19,8 +19,8 @@ _version_major = 2 _version_minor = 2 _version_micro = 1 -_version_extra = 'dev' -# _version_extra = '' +# _version_extra = 'dev' +_version_extra = '' # Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z" __version__ = "%s.%s.%s%s" % (_version_major, diff --git a/nibabel/nicom/dwiparams.py b/nibabel/nicom/dwiparams.py index 19ffbafa0f..10fdce0d5b 100644 --- a/nibabel/nicom/dwiparams.py +++ b/nibabel/nicom/dwiparams.py @@ -21,7 +21,7 @@ ''' import numpy as np import numpy.linalg as npl -from ..testing import setup_test # flake8: noqa F401 +from ..testing import setup_test as setup_module # flake8: noqa F401 def B2q(B, tol=None): diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 4d766a76f5..ff3ee32118 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1764,7 +1764,20 @@ def __init__(self, dataobj, affine, header=None, if header is None and affine is not None: self._affine2header() # Copy docstring - __init__.doc = analyze.AnalyzeImage.__init__.__doc__ + __init__.__doc__ = analyze.AnalyzeImage.__init__.__doc__ + ''' + Notes + ----- + + If both a `header` and an `affine` are specified, and the `affine` does + not match the affine that is in the `header`, the `affine` will be used, + but the ``sform_code`` and ``qform_code`` fields in the header will be + re-initialised to their default values. This is performed on the basis + that, if you are changing the affine, you are likely to be changing the + space to which the affine is pointing. The :meth:`set_sform` and + :meth:`set_qform` methods can be used to update the codes after an image + has been created - see those methods, and the :ref:`manual + ` for more details. ''' + def update_header(self): ''' Harmonize header with image data and affine diff --git a/nibabel/testing/__init__.py b/nibabel/testing/__init__.py index 2fa7f809f6..60ecd2ceea 100644 --- a/nibabel/testing/__init__.py +++ b/nibabel/testing/__init__.py @@ -32,7 +32,7 @@ data_path = abspath(pjoin(dirname(__file__), '..', 'tests', 'data')) -from .np_features import VIRAL_MEMMAP +from .np_features import memmap_after_ufunc def assert_dt_equal(a, b): """ Assert two numpy dtype specifiers are equal @@ -218,4 +218,4 @@ def setup_test(): """ from distutils.version import LooseVersion if LooseVersion(np.__version__) >= LooseVersion('1.14'): - np.set_printoptions(sign='legacy') + np.set_printoptions(legacy="1.13") diff --git a/nibabel/testing/np_features.py b/nibabel/testing/np_features.py index 1473cb0c72..8919542d1c 100644 --- a/nibabel/testing/np_features.py +++ b/nibabel/testing/np_features.py @@ -4,16 +4,20 @@ import numpy as np -def _memmap_after_ufunc(): +def memmap_after_ufunc(): """ Return True if ufuncs on memmap arrays always return memmap arrays This should be True for numpy < 1.12, False otherwise. + + Memoize after first call. We do this to avoid having to call this when + importing nibabel.testing, because we cannot depend on the source file + being present - see gh-571. """ + if memmap_after_ufunc.result is not None: + return memmap_after_ufunc.result with open(__file__, 'rb') as fobj: mm_arr = np.memmap(fobj, mode='r', shape=(10,), dtype=np.uint8) - mm_preserved = isinstance(mm_arr + 1, np.memmap) - return mm_preserved - + memmap_after_ufunc.result = isinstance(mm_arr + 1, np.memmap) + return memmap_after_ufunc.result -# True if ufunc on memmap always returns a memmap -VIRAL_MEMMAP = _memmap_after_ufunc() +memmap_after_ufunc.result = None diff --git a/nibabel/tests/test_arrayproxy.py b/nibabel/tests/test_arrayproxy.py index 70265a5860..9e9e54d0bd 100644 --- a/nibabel/tests/test_arrayproxy.py +++ b/nibabel/tests/test_arrayproxy.py @@ -30,7 +30,7 @@ from numpy.testing import assert_array_equal, assert_array_almost_equal from nose.tools import (assert_true, assert_false, assert_equal, assert_not_equal, assert_raises) -from nibabel.testing import VIRAL_MEMMAP +from nibabel.testing import memmap_after_ufunc from .test_fileslice import slicer_samples from .test_openers import patch_indexed_gzip @@ -298,6 +298,8 @@ def check_mmap(hdr, offset, proxy_class, # Whether scaled array memory backed by memory map (regardless of what # numpy says). scaled_really_mmap = unscaled_really_mmap and not has_scaling + # Whether ufunc on memmap return memmap + viral_memmap = memmap_after_ufunc() with InTemporaryDirectory(): with open(fname, 'wb') as fobj: fobj.write(b' ' * offset) @@ -324,9 +326,9 @@ def check_mmap(hdr, offset, proxy_class, assert_false(back_is_mmap) else: assert_equal(unscaled_is_mmap, - VIRAL_MEMMAP or unscaled_really_mmap) + viral_memmap or unscaled_really_mmap) assert_equal(back_is_mmap, - VIRAL_MEMMAP or scaled_really_mmap) + viral_memmap or scaled_really_mmap) if scaled_really_mmap: assert_equal(back_data.mode, expected_mode) del prox, back_data diff --git a/nibabel/tests/test_floating.py b/nibabel/tests/test_floating.py index 8f6d998a0c..3022265df4 100644 --- a/nibabel/tests/test_floating.py +++ b/nibabel/tests/test_floating.py @@ -4,6 +4,8 @@ PY2 = sys.version_info[0] < 3 +from distutils.version import LooseVersion + import numpy as np from ..casting import (floor_exact, ceil_exact, as_int, FloatingError, @@ -103,7 +105,12 @@ def test_check_nmant_nexp(): ti = type_info(t) if ti['nmant'] != 106: # This check does not work for PPC double pair assert_true(_check_nmant(t, ti['nmant'])) - assert_true(_check_maxexp(t, ti['maxexp'])) + # Test fails for longdouble after blacklisting of OSX powl as of numpy + # 1.12 - see https://github.com/numpy/numpy/issues/8307 + if (t != np.longdouble or + sys.platform != 'darwin' or + LooseVersion(np.__version__) < LooseVersion('1.12')): + assert_true(_check_maxexp(t, ti['maxexp'])) def test_as_int(): diff --git a/nibabel/tests/test_image_api.py b/nibabel/tests/test_image_api.py index d71fbcdb30..be270795e9 100644 --- a/nibabel/tests/test_image_api.py +++ b/nibabel/tests/test_image_api.py @@ -377,9 +377,9 @@ def validate_shape(self, imaker, params): def validate_shape_deprecated(self, imaker, params): # Check deprecated get_shape API + img = imaker() with clear_and_catch_warnings() as w: warnings.simplefilter('always', DeprecationWarning) - img = imaker() assert_equal(img.get_shape(), params['shape']) assert_equal(len(w), 1) diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index 1bcdbd19f7..9cb4759f50 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -25,7 +25,7 @@ from .test_helpers import bytesio_round_trip from ..testing import (clear_and_catch_warnings, suppress_warnings, - VIRAL_MEMMAP) + memmap_after_ufunc) from ..tmpdirs import InTemporaryDirectory from .. import load as top_load @@ -464,6 +464,7 @@ def get_disk_image(self): def test_load_mmap(self): # Test memory mapping when loading images img_klass = self.image_class + viral_memmap = memmap_after_ufunc() with InTemporaryDirectory(): img, fname, has_scaling = self.get_disk_image() file_map = img.file_map.copy() @@ -485,7 +486,7 @@ def test_load_mmap(self): # numpies returned a memmap object, even though the array # has no mmap memory backing. See: # https://github.com/numpy/numpy/pull/7406 - if has_scaling and not VIRAL_MEMMAP: + if has_scaling and not viral_memmap: expected_mode = None kwargs = {} if mmap is not None: diff --git a/nibabel/tripwire.py b/nibabel/tripwire.py index 3850281587..e31cfe7258 100644 --- a/nibabel/tripwire.py +++ b/nibabel/tripwire.py @@ -24,7 +24,7 @@ def is_tripwire(obj): obj.any_attribute except TripWireError: return True - except: + except Exception: pass return False diff --git a/nibabel/viewers.py b/nibabel/viewers.py index b99c6328cf..f97f0c3bbb 100644 --- a/nibabel/viewers.py +++ b/nibabel/viewers.py @@ -135,7 +135,7 @@ def __init__(self, data, affine=None, axes=None, title=None): self._scalers[self._order[1]] / self._scalers[self._order[0]]] self._sizes = [self._data.shape[order] for order in self._order] for ii, xax, yax, ratio, label in zip([0, 1, 2], [1, 0, 0], [2, 2, 1], - r, ('SAIP', 'SLIR', 'ALPR')): + r, ('SAIP', 'SRIL', 'ARPL')): ax = self._axes[ii] d = np.zeros((self._sizes[yax], self._sizes[xax])) im = self._axes[ii].imshow(