Skip to content

Commit 5a42cb3

Browse files
committed
Merge pull request #404 from Eric89GXL/pauls-viewer
MRG: Slice viewer Orthogonal slice viewer for 3D and 4D images. Based on Paul Ivanov's prototype. Viewers can be yoked so that crosshairs correspond to matching voxel coordinates according to the image affines.
2 parents a7c6523 + 2a8a73c commit 5a42cb3

File tree

5 files changed

+621
-0
lines changed

5 files changed

+621
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# munges each line before executing it to print out the exit status. It's okay
44
# for it to be on multiple physical lines, so long as you remember: - There
55
# can't be any leading "-"s - All newlines will be removed, so use ";"s
6+
67
language: python
78

89
# Run jobs on container-based infrastructure, can be overridden per job

nibabel/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from .imageclasses import class_map, ext_map, all_image_classes
6565
from . import trackvis
6666
from . import mriutils
67+
from . import viewers
6768

6869
# be friendly on systems with ancient numpy -- no tests, but at least
6970
# importable

nibabel/spatialimages.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139

140140
from .filebasedimages import FileBasedHeader, FileBasedImage
141141
from .filebasedimages import ImageFileError # flake8: noqa; for back-compat
142+
from .viewers import OrthoSlicer3D
142143
from .volumeutils import shape_zoom_affine
143144

144145

@@ -661,3 +662,20 @@ def __getitem__(self):
661662
raise TypeError("Cannot slice image objects; consider slicing image "
662663
"array data with `img.dataobj[slice]` or "
663664
"`img.get_data()[slice]`")
665+
666+
def orthoview(self):
667+
"""Plot the image using OrthoSlicer3D
668+
669+
Returns
670+
-------
671+
viewer : instance of OrthoSlicer3D
672+
The viewer.
673+
674+
Notes
675+
-----
676+
This requires matplotlib. If a non-interactive backend is used,
677+
consider using viewer.show() (equivalently plt.show()) to show
678+
the figure.
679+
"""
680+
return OrthoSlicer3D(self.dataobj, self.affine,
681+
title=self.get_filename())

nibabel/tests/test_viewers.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4+
#
5+
# See COPYING file distributed along with the NiBabel package for the
6+
# copyright and license terms.
7+
#
8+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9+
10+
import numpy as np
11+
from collections import namedtuple as nt
12+
13+
14+
from ..optpkg import optional_package
15+
from ..viewers import OrthoSlicer3D
16+
17+
from numpy.testing.decorators import skipif
18+
from numpy.testing import assert_array_equal, assert_equal
19+
20+
from nose.tools import assert_raises, assert_true
21+
22+
matplotlib, has_mpl = optional_package('matplotlib')[:2]
23+
needs_mpl = skipif(not has_mpl, 'These tests need matplotlib')
24+
if has_mpl:
25+
matplotlib.use('Agg')
26+
27+
28+
@needs_mpl
29+
def test_viewer():
30+
# Test viewer
31+
plt = optional_package('matplotlib.pyplot')[0]
32+
a = np.sin(np.linspace(0, np.pi, 20))
33+
b = np.sin(np.linspace(0, np.pi*5, 30))
34+
data = (np.outer(a, b)[..., np.newaxis] * a)[:, :, :, np.newaxis]
35+
data = data * np.array([1., 2.]) # give it a # of volumes > 1
36+
v = OrthoSlicer3D(data)
37+
assert_array_equal(v.position, (0, 0, 0))
38+
assert_true('OrthoSlicer3D' in repr(v))
39+
40+
# fake some events, inside and outside axes
41+
v._on_scroll(nt('event', 'button inaxes key')('up', None, None))
42+
for ax in (v._axes[0], v._axes[3]):
43+
v._on_scroll(nt('event', 'button inaxes key')('up', ax, None))
44+
v._on_scroll(nt('event', 'button inaxes key')('up', ax, 'shift'))
45+
# "click" outside axes, then once in each axis, then move without click
46+
v._on_mouse(nt('event', 'xdata ydata inaxes button')(0.5, 0.5, None, 1))
47+
for ax in v._axes:
48+
v._on_mouse(nt('event', 'xdata ydata inaxes button')(0.5, 0.5, ax, 1))
49+
v._on_mouse(nt('event', 'xdata ydata inaxes button')(0.5, 0.5, None, None))
50+
v.set_volume_idx(1)
51+
v.cmap = 'hot'
52+
v.clim = (0, 3)
53+
assert_raises(ValueError, OrthoSlicer3D.clim.fset, v, (0.,)) # bad limits
54+
assert_raises(ValueError, OrthoSlicer3D.cmap.fset, v, 'foo') # wrong cmap
55+
56+
# decrement/increment volume numbers via keypress
57+
v.set_volume_idx(1) # should just pass
58+
v._on_keypress(nt('event', 'key')('-')) # decrement
59+
assert_equal(v._data_idx[3], 0)
60+
v._on_keypress(nt('event', 'key')('+')) # increment
61+
assert_equal(v._data_idx[3], 1)
62+
v._on_keypress(nt('event', 'key')('-'))
63+
v._on_keypress(nt('event', 'key')('=')) # alternative increment key
64+
assert_equal(v._data_idx[3], 1)
65+
66+
v.close()
67+
v._draw() # should be safe
68+
69+
# non-multi-volume
70+
v = OrthoSlicer3D(data[:, :, :, 0])
71+
v._on_scroll(nt('event', 'button inaxes key')('up', v._axes[0], 'shift'))
72+
v._on_keypress(nt('event', 'key')('escape'))
73+
v.close()
74+
75+
# complex input should raise a TypeError prior to figure creation
76+
assert_raises(TypeError, OrthoSlicer3D,
77+
data[:, :, :, 0].astype(np.complex64))
78+
79+
# other cases
80+
fig, axes = plt.subplots(1, 4)
81+
plt.close(fig)
82+
v1 = OrthoSlicer3D(data, axes=axes)
83+
aff = np.array([[0, 1, 0, 3], [-1, 0, 0, 2], [0, 0, 2, 1], [0, 0, 0, 1]],
84+
float)
85+
v2 = OrthoSlicer3D(data, affine=aff, axes=axes[:3])
86+
# bad data (not 3+ dim)
87+
assert_raises(ValueError, OrthoSlicer3D, data[:, :, 0, 0])
88+
# bad affine (not 4x4)
89+
assert_raises(ValueError, OrthoSlicer3D, data, affine=np.eye(3))
90+
assert_raises(TypeError, v2.link_to, 1)
91+
v2.link_to(v1)
92+
v2.link_to(v1) # shouldn't do anything
93+
v1.close()
94+
v2.close()

0 commit comments

Comments
 (0)