Merge pull request #415 from campagnola/fix-getarrayregion

Fix getarrayregion
This commit is contained in:
Luke Campagnola 2017-01-14 11:08:38 -08:00 committed by GitHub
commit 7b20b33a06
7 changed files with 175 additions and 129 deletions

View File

@ -51,7 +51,7 @@ install:
- conda update conda --yes
- conda create -n test_env python=${PYTHON} --yes
- source activate test_env
- conda install numpy pyopengl pytest flake8 six coverage --yes
- conda install numpy scipy pyopengl pytest flake8 six coverage --yes
- echo ${QT}
- echo ${TEST}
- echo ${PYTHON}

View File

@ -409,12 +409,45 @@ def eq(a, b):
else:
raise Exception("== operator returned type %s" % str(type(e)))
def affineSliceCoords(shape, origin, vectors, axes):
"""Return the array of coordinates used to sample data arrays in affineSlice().
"""
# sanity check
if len(shape) != len(vectors):
raise Exception("shape and vectors must have same length.")
if len(origin) != len(axes):
raise Exception("origin and axes must have same length.")
for v in vectors:
if len(v) != len(axes):
raise Exception("each vector must be same length as axes.")
shape = list(map(np.ceil, shape))
## make sure vectors are arrays
if not isinstance(vectors, np.ndarray):
vectors = np.array(vectors)
if not isinstance(origin, np.ndarray):
origin = np.array(origin)
origin.shape = (len(axes),) + (1,)*len(shape)
## Build array of sample locations.
grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes
x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic
x += origin
return x
def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs):
"""
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays
such as MRI images for viewing as 1D or 2D data.
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used.
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is
possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger
datasets. The original data is interpolated onto a new array of coordinates using either interpolateArray if order<2
or scipy.ndimage.map_coordinates otherwise.
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
@ -453,47 +486,24 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
"""
try:
import scipy.ndimage
have_scipy = True
except ImportError:
have_scipy = False
have_scipy = False
# sanity check
if len(shape) != len(vectors):
raise Exception("shape and vectors must have same length.")
if len(origin) != len(axes):
raise Exception("origin and axes must have same length.")
for v in vectors:
if len(v) != len(axes):
raise Exception("each vector must be same length as axes.")
shape = list(map(np.ceil, shape))
x = affineSliceCoords(shape, origin, vectors, axes)
## transpose data so slice axes come first
trAx = list(range(data.ndim))
for x in axes:
trAx.remove(x)
for ax in axes:
trAx.remove(ax)
tr1 = tuple(axes) + tuple(trAx)
data = data.transpose(tr1)
#print "tr1:", tr1
## dims are now [(slice axes), (other axes)]
## make sure vectors are arrays
if not isinstance(vectors, np.ndarray):
vectors = np.array(vectors)
if not isinstance(origin, np.ndarray):
origin = np.array(origin)
origin.shape = (len(axes),) + (1,)*len(shape)
## Build array of sample locations.
grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes
x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic
x += origin
## iterate manually over unused axes since map_coordinates won't do it for us
if have_scipy:
if order > 1:
try:
import scipy.ndimage
except ImportError:
raise ImportError("Interpolating with order > 1 requires the scipy.ndimage module, but it could not be imported.")
# iterate manually over unused axes since map_coordinates won't do it for us
extraShape = data.shape[len(axes):]
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
for inds in np.ndindex(*extraShape):
@ -502,8 +512,8 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
else:
# map_coordinates expects the indexes as the first axis, whereas
# interpolateArray expects indexes at the last axis.
tr = tuple(range(1,x.ndim)) + (0,)
output = interpolateArray(data, x.transpose(tr))
tr = tuple(range(1, x.ndim)) + (0,)
output = interpolateArray(data, x.transpose(tr), order=order)
tr = list(range(output.ndim))
trb = []
@ -520,16 +530,24 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
else:
return output
def interpolateArray(data, x, default=0.0):
def interpolateArray(data, x, default=0.0, order=1):
"""
N-dimensional interpolation similar to scipy.ndimage.map_coordinates.
This function returns linearly-interpolated values sampled from a regular
grid of data.
grid of data. It differs from `ndimage.map_coordinates` by allowing broadcasting
within the input array.
*data* is an array of any shape containing the values to be interpolated.
*x* is an array with (shape[-1] <= data.ndim) containing the locations
within *data* to interpolate.
============== ===========================================================================================
**Arguments:**
*data* Array of any shape containing the values to be interpolated.
*x* Array with (shape[-1] <= data.ndim) containing the locations within *data* to interpolate.
(note: the axes for this argument are transposed relative to the same argument for
`ndimage.map_coordinates`).
*default* Value to return for locations in *x* that are outside the bounds of *data*.
*order* Order of interpolation: 0=nearest, 1=linear.
============== ===========================================================================================
Returns array of shape (x.shape[:-1] + data.shape[x.shape[-1]:])
@ -574,53 +592,66 @@ def interpolateArray(data, x, default=0.0):
This is useful for interpolating from arrays of colors, vertexes, etc.
"""
if order not in (0, 1):
raise ValueError("interpolateArray requires order=0 or 1 (got %s)" % order)
prof = debug.Profiler()
nd = data.ndim
md = x.shape[-1]
if md > nd:
raise TypeError("x.shape[-1] must be less than or equal to data.ndim")
# First we generate arrays of indexes that are needed to
# extract the data surrounding each point
fields = np.mgrid[(slice(0,2),) * md]
xmin = np.floor(x).astype(int)
xmax = xmin + 1
indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]])
fieldInds = []
totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes
for ax in range(md):
mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1)
# keep track of points that need to be set to default
totalMask &= mask
if order == 0:
xinds = np.round(x).astype(int) # NOTE: for 0.5 this rounds to the nearest *even* number
for ax in range(md):
mask = (xinds[...,ax] >= 0) & (xinds[...,ax] <= data.shape[ax]-1)
xinds[...,ax][~mask] = 0
# keep track of points that need to be set to default
totalMask &= mask
result = data[tuple([xinds[...,i] for i in range(xinds.shape[-1])])]
# ..and keep track of indexes that are out of bounds
# (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out
# of bounds, but the interpolation will work anyway)
mask &= (xmax[...,ax] < data.shape[ax])
axisIndex = indexes[...,ax][fields[ax]]
axisIndex[axisIndex < 0] = 0
axisIndex[axisIndex >= data.shape[ax]] = 0
fieldInds.append(axisIndex)
prof()
elif order == 1:
# First we generate arrays of indexes that are needed to
# extract the data surrounding each point
fields = np.mgrid[(slice(0,order+1),) * md]
xmin = np.floor(x).astype(int)
xmax = xmin + 1
indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]])
fieldInds = []
for ax in range(md):
mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1)
# keep track of points that need to be set to default
totalMask &= mask
# ..and keep track of indexes that are out of bounds
# (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out
# of bounds, but the interpolation will work anyway)
mask &= (xmax[...,ax] < data.shape[ax])
axisIndex = indexes[...,ax][fields[ax]]
axisIndex[axisIndex < 0] = 0
axisIndex[axisIndex >= data.shape[ax]] = 0
fieldInds.append(axisIndex)
prof()
# Get data values surrounding each requested point
fieldData = data[tuple(fieldInds)]
prof()
# Get data values surrounding each requested point
fieldData = data[tuple(fieldInds)]
prof()
## Interpolate
s = np.empty((md,) + fieldData.shape, dtype=float)
dx = x - xmin
# reshape fields for arithmetic against dx
for ax in range(md):
f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1))
sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax])
sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim))
s[ax] = sax
s = np.product(s, axis=0)
result = fieldData * s
for i in range(md):
result = result.sum(axis=0)
## Interpolate
s = np.empty((md,) + fieldData.shape, dtype=float)
dx = x - xmin
# reshape fields for arithmetic against dx
for ax in range(md):
f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1))
sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax])
sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim))
s[ax] = sax
s = np.product(s, axis=0)
result = fieldData * s
for i in range(md):
result = result.sum(axis=0)
prof()

View File

@ -2070,9 +2070,9 @@ class LineSegmentROI(ROI):
if len(positions) > 2:
raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.")
self.endpoints = []
for i, p in enumerate(positions):
self.addFreeHandle(p, item=handles[i])
self.endpoints.append(self.addFreeHandle(p, item=handles[i]))
def listPoints(self):
return [p['item'].pos() for p in self.handles]
@ -2080,8 +2080,8 @@ class LineSegmentROI(ROI):
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen)
h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos()
h1 = self.endpoints[0].pos()
h2 = self.endpoints[1].pos()
p.drawLine(h1, h2)
def boundingRect(self):
@ -2090,8 +2090,8 @@ class LineSegmentROI(ROI):
def shape(self):
p = QtGui.QPainterPath()
h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos()
h1 = self.endpoints[0].pos()
h2 = self.endpoints[1].pos()
dh = h2-h1
if dh.length() == 0:
return p
@ -2109,7 +2109,7 @@ class LineSegmentROI(ROI):
return p
def getArrayRegion(self, data, img, axes=(0,1), order=1, **kwds):
def getArrayRegion(self, data, img, axes=(0,1), order=1, returnMappedCoords=False, **kwds):
"""
Use the position of this ROI relative to an imageItem to pull a slice
from an array.
@ -2120,15 +2120,15 @@ class LineSegmentROI(ROI):
See ROI.getArrayRegion() for a description of the arguments.
"""
imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles]
imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints]
rgns = []
for i in range(len(imgPts)-1):
d = Point(imgPts[i+1] - imgPts[i])
o = Point(imgPts[i])
r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=order, **kwds)
rgns.append(r)
return np.concatenate(rgns, axis=axes[0])
coords = []
d = Point(imgPts[1] - imgPts[0])
o = Point(imgPts[0])
rgn = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=order, returnCoords=returnMappedCoords, **kwds)
return rgn
class _PolyLineSegment(LineSegmentROI):

View File

@ -2,7 +2,7 @@ import numpy as np
import pytest
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtTest
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow
app = pg.mkQApp()
@ -43,8 +43,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
#win = pg.GraphicsLayoutWidget()
win = pg.GraphicsView()
win.show()
win.resize(200, 400)
resizeWindow(win, 200, 400)
# Don't use Qt's layouts for testing--these generate unpredictable results.
#vb1 = win.addViewBox()
#win.nextRow()
@ -97,7 +96,6 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False):
vb2.enableAutoRange(True, True)
app.processEvents()
assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.')
with pytest.raises(TypeError):
@ -159,7 +157,7 @@ def test_PolyLineROI():
#plt = pg.plot()
plt = pg.GraphicsView()
plt.show()
plt.resize(200, 200)
resizeWindow(plt, 200, 200)
vb = pg.ViewBox()
plt.scene().addItem(vb)
vb.resize(200, 200)

View File

@ -1,2 +1,2 @@
from .image_testing import assertImageApproved, TransposedImageItem
from .ui_testing import mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick
from .ui_testing import resizeWindow, mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick

View File

@ -22,9 +22,17 @@ def testSolve3D():
assert_array_almost_equal(tr[:3], tr2[:3])
def test_interpolateArray():
def test_interpolateArray_order0():
check_interpolateArray(order=0)
def test_interpolateArray_order1():
check_interpolateArray(order=1)
def check_interpolateArray(order):
def interpolateArray(data, x):
result = pg.interpolateArray(data, x)
result = pg.interpolateArray(data, x, order=order)
assert result.shape == x.shape[:-1] + data.shape[x.shape[-1]:]
return result
@ -48,17 +56,17 @@ def test_interpolateArray():
with pytest.raises(TypeError):
interpolateArray(data, np.ones((5, 5, 3,)))
x = np.array([[ 0.3, 0.6],
[ 1. , 1. ],
[ 0.5, 1. ],
[ 0.5, 2.5],
[ 0.501, 1. ], # NOTE: testing at exactly 0.5 can yield different results from map_coordinates
[ 0.501, 2.501], # due to differences in rounding
[ 10. , 10. ]])
result = interpolateArray(data, x)
#import scipy.ndimage
#spresult = scipy.ndimage.map_coordinates(data, x.T, order=1)
spresult = np.array([ 5.92, 20. , 11. , 0. , 0. ]) # generated with the above line
# make sure results match ndimage.map_coordinates
import scipy.ndimage
spresult = scipy.ndimage.map_coordinates(data, x.T, order=order)
#spresult = np.array([ 5.92, 20. , 11. , 0. , 0. ]) # generated with the above line
assert_array_almost_equal(result, spresult)
@ -74,28 +82,17 @@ def test_interpolateArray():
# test mapping 2D array of locations
x = np.array([[[0.5, 0.5], [0.5, 1.0], [0.5, 1.5]],
[[1.5, 0.5], [1.5, 1.0], [1.5, 1.5]]])
x = np.array([[[0.501, 0.501], [0.501, 1.0], [0.501, 1.501]],
[[1.501, 0.501], [1.501, 1.0], [1.501, 1.501]]])
r1 = interpolateArray(data, x)
#r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=1)
r2 = np.array([[ 8.25, 11. , 16.5 ], # generated with the above line
[ 82.5 , 110. , 165. ]])
r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=order)
#r2 = np.array([[ 8.25, 11. , 16.5 ], # generated with the above line
#[ 82.5 , 110. , 165. ]])
assert_array_almost_equal(r1, r2)
# test interpolate where data.ndim > x.shape[1]
data = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]) # 2x2x3
x = np.array([[1, 1], [0, 0.5], [5, 5]])
r1 = interpolateArray(data, x)
assert np.all(r1[0] == data[1, 1])
assert np.all(r1[1] == 0.5 * (data[0, 0] + data[0, 1]))
assert np.all(r1[2] == 0)
def test_subArray():
a = np.array([0, 0, 111, 112, 113, 0, 121, 122, 123, 0, 0, 0, 211, 212, 213, 0, 221, 222, 223, 0, 0, 0, 0])
b = pg.subArray(a, offset=2, shape=(2,2,3), stride=(10,4,1))

View File

@ -1,11 +1,32 @@
import time
from ..Qt import QtCore, QtGui, QtTest, QT_LIB
def resizeWindow(win, w, h, timeout=2.0):
"""Resize a window and wait until it has the correct size.
This is required for unit testing on some platforms that do not guarantee
immediate response from the windowing system.
"""
QtGui.QApplication.processEvents()
# Sometimes the window size will switch multiple times before settling
# on its final size. Adding qWaitForWindowShown seems to help with this.
QtTest.QTest.qWaitForWindowShown(win)
win.resize(w, h)
start = time.time()
while True:
w1, h1 = win.width(), win.height()
if (w,h) == (w1,h1):
return
QtTest.QTest.qWait(10)
if time.time()-start > timeout:
raise TimeoutError("Window resize failed (requested %dx%d, got %dx%d)" % (w, h, w1, h1))
# Functions for generating user input events.
# We would like to use QTest for this purpose, but it seems to be broken.
# See: http://stackoverflow.com/questions/16299779/qt-qgraphicsview-unit-testing-how-to-keep-the-mouse-in-a-pressed-state
from ..Qt import QtCore, QtGui, QT_LIB
def mousePress(widget, pos, button, modifier=None):
if isinstance(widget, QtGui.QGraphicsView):
widget = widget.viewport()
@ -52,4 +73,3 @@ def mouseClick(widget, pos, button, modifier=None):
mouseMove(widget, pos)
mousePress(widget, pos, button, modifier)
mouseRelease(widget, pos, button, modifier)