diff --git a/nibabel/spatialimages.py b/nibabel/spatialimages.py index ede0820065..a52b7fbc09 100644 --- a/nibabel/spatialimages.py +++ b/nibabel/spatialimages.py @@ -588,9 +588,18 @@ def __getitem__(self, idx): "slicing image array data with `img.dataobj[slice]` or " "`img.get_data()[slice]`") - def orthoview(self): + def orthoview(self, axes=None, vlim=None): """Plot the image using OrthoSlicer3D + Parameters + ------------------ + axes : tuple of mpl.Axes or None, optional + 3 or 4 axes instances for the 3 slices plus volumes, + or None (default). + vlim : array-like or None, optional + Value limits to display image and time series. Can be None + (default) to derive limits from data. + Returns ------- viewer : instance of OrthoSlicer3D @@ -602,8 +611,8 @@ def orthoview(self): consider using viewer.show() (equivalently plt.show()) to show the figure. """ - return OrthoSlicer3D(self.dataobj, self.affine, - title=self.get_filename()) + return OrthoSlicer3D(self.dataobj, self.affine, axes=axes, + title=self.get_filename(), vlim=vlim) def as_reoriented(self, ornt): """Apply an orientation change and return a new image diff --git a/nibabel/tests/test_spatialimages.py b/nibabel/tests/test_spatialimages.py index b0f571023d..bdbcf9710a 100644 --- a/nibabel/tests/test_spatialimages.py +++ b/nibabel/tests/test_spatialimages.py @@ -25,6 +25,7 @@ from numpy.testing import assert_array_equal, assert_array_almost_equal from .test_helpers import bytesio_round_trip +from .test_viewers import needs_mpl from ..testing import (clear_and_catch_warnings, suppress_warnings, memmap_after_ufunc) from ..tmpdirs import InTemporaryDirectory @@ -539,6 +540,16 @@ def test_slicer(self): assert_array_equal(sliced_data, img.dataobj[sliceobj]) assert_array_equal(sliced_data, img.get_data()[sliceobj]) + @needs_mpl + def test_orthoview(self): + # Assumes all possible images support int16 + # See https://github.com/nipy/nibabel/issues/58 + arr = np.arange(24, dtype=np.int16).reshape((2, 3, 4)) + img = self.image_class(arr, None) + img.orthoview().close() + img.orthoview(vlim=(5, 10)).close() + img.orthoview(slicer=Ellipsis).close() + def test_api_deprecations(self): class FakeImage(self.image_class): diff --git a/nibabel/tests/test_viewers.py b/nibabel/tests/test_viewers.py index 68710b3126..5edb99a940 100644 --- a/nibabel/tests/test_viewers.py +++ b/nibabel/tests/test_viewers.py @@ -17,7 +17,7 @@ from ..testing import skipif from numpy.testing import assert_array_equal, assert_equal -from nose.tools import assert_raises, assert_true +from nose.tools import assert_raises, assert_true, assert_false # Need at least MPL 1.3 for viewer tests. matplotlib, has_mpl, _ = optional_package('matplotlib', min_version='1.3') @@ -68,6 +68,23 @@ def test_viewer(): v.close() v._draw() # should be safe + # Manually set value limits + vlim = np.array([-20, 20]) + v = OrthoSlicer3D(data, vlim=vlim) + assert_array_equal(v._clim, vlim) + for im in v._ims: + assert_array_equal(im.get_clim(), vlim) + assert_array_equal(v._axes[3].get_ylim(), vlim) + v.close() + v1 = OrthoSlicer3D(data) + v2 = OrthoSlicer3D(data, vlim=('1%', '99%')) + assert_array_equal(v1.clim, v2.clim) + v2.close() + v2 = OrthoSlicer3D(data, vlim=('2%', '98%')) + assert_false(np.array_equal(v1.clim, v2.clim)) + v2.close() + v1.close() + # non-multi-volume v = OrthoSlicer3D(data[:, :, :, 0]) v._on_scroll(nt('event', 'button inaxes key')('up', v._axes[0], 'shift')) diff --git a/nibabel/viewers.py b/nibabel/viewers.py index 7a0f4d93d7..0a29c8d17b 100644 --- a/nibabel/viewers.py +++ b/nibabel/viewers.py @@ -42,7 +42,7 @@ class OrthoSlicer3D(object): """ # Skip doctest above b/c not all systems have mpl installed - def __init__(self, data, affine=None, axes=None, title=None): + def __init__(self, data, affine=None, axes=None, title=None, vlim=None): """ Parameters ---------- @@ -61,6 +61,10 @@ def __init__(self, data, affine=None, axes=None, title=None): title : str or None, optional The title to display. Can be None (default) to display no title. + vlim : array-like or None, optional + Value limits to display image and time series. Can be None + (default) to derive limits from data. Bounds can be of the + form ``'x%'`` to use the ``x`` percentile of the data. """ # Use these late imports of matplotlib so that we have some hope that # the test functions are the first to set the matplotlib backend. The @@ -79,6 +83,13 @@ def __init__(self, data, affine=None, axes=None, title=None): affine = np.array(affine, float) if affine is not None else np.eye(4) if affine.shape != (4, 4): raise ValueError('affine must be a 4x4 matrix') + + if vlim is not None: + percentiles = all(isinstance(lim, str) and lim[-1] == '%' + for lim in vlim) + if percentiles: + vlim = np.percentile(data, [float(lim[:-1]) for lim in vlim]) + # determine our orientation self._affine = affine codes = axcodes2ornt(aff2axcodes(self._affine)) @@ -91,7 +102,7 @@ def __init__(self, data, affine=None, axes=None, title=None): self._volume_dims = data.shape[3:] self._current_vol_data = data[:, :, :, 0] if data.ndim > 3 else data self._data = data - self._clim = np.percentile(data, (1., 99.)) + self._clim = np.percentile(data, (1., 99.)) if vlim is None else vlim del data if axes is None: # make the axes @@ -184,8 +195,11 @@ def __init__(self, data, affine=None, axes=None, title=None): ax.set_xticks(np.unique(np.linspace(0, self.n_volumes - 1, 5).astype(int))) ax.set_xlim(x[0], x[-1]) - yl = [self._data.min(), self._data.max()] - yl = [l + s * np.diff(lims)[0] for l, s in zip(yl, [-1.01, 1.01])] + if vlim is None: + yl = [self._data.min(), self._data.max()] + yl = [l + s * np.diff(lims)[0] for l, s in zip(yl, [-1.01, 1.01])] + else: + yl = vlim patch = mpl_patch.Rectangle([-0.5, yl[0]], 1., np.diff(yl)[0], fill=True, facecolor=(0, 1, 0), edgecolor=(0, 1, 0), alpha=0.25)