Merge pull request #415 from campagnola/fix-getarrayregion
Fix getarrayregion
This commit is contained in:
commit
7b20b33a06
@ -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}
|
||||
|
@ -410,11 +410,44 @@ def eq(a, b):
|
||||
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)
|
||||
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.")
|
||||
|
||||
## 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:
|
||||
# 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):
|
||||
@ -503,7 +513,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
||||
# 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))
|
||||
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,6 +592,9 @@ 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
|
||||
@ -581,14 +602,24 @@ def interpolateArray(data, x, default=0.0):
|
||||
if md > nd:
|
||||
raise TypeError("x.shape[-1] must be less than or equal to data.ndim")
|
||||
|
||||
totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes
|
||||
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])])]
|
||||
|
||||
elif order == 1:
|
||||
# First we generate arrays of indexes that are needed to
|
||||
# extract the data surrounding each point
|
||||
fields = np.mgrid[(slice(0,2),) * md]
|
||||
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 = []
|
||||
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
|
||||
|
@ -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)
|
||||
coords = []
|
||||
|
||||
return np.concatenate(rgns, axis=axes[0])
|
||||
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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user