Fix PolyLineROI.getArrayRegion and a few other bugs

This commit is contained in:
Luke Campagnola 2016-05-13 23:30:52 -07:00
parent d4cc2e8b5d
commit ccf2ae4db4
3 changed files with 79 additions and 65 deletions

View File

@ -991,8 +991,9 @@ class ROI(GraphicsObject):
# p.restore() # p.restore()
def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): def getArraySlice(self, data, img, axes=(0,1), returnSlice=True):
"""Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. """Return a tuple of slice objects that can be used to slice the region
Also returns the transform which maps the ROI into data coordinates. from *data* that is covered by the bounding rectangle of this ROI.
Also returns the transform that maps the ROI into data coordinates.
If returnSlice is set to False, the function returns a pair of tuples with the values that would have If returnSlice is set to False, the function returns a pair of tuples with the values that would have
been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))
@ -1075,8 +1076,10 @@ class ROI(GraphicsObject):
All extra keyword arguments are passed to :func:`affineSlice <pyqtgraph.affineSlice>`. All extra keyword arguments are passed to :func:`affineSlice <pyqtgraph.affineSlice>`.
""" """
# this is a hidden argument for internal use
fromBR = kwds.pop('fromBoundingRect', False)
shape, vectors, origin = self.getAffineSliceParams(data, img, axes) shape, vectors, origin = self.getAffineSliceParams(data, img, axes, fromBoundingRect=fromBR)
if not returnMappedCoords: if not returnMappedCoords:
return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds)
else: else:
@ -1087,7 +1090,7 @@ class ROI(GraphicsObject):
mapped = fn.transformCoordinates(img.transform(), coords) mapped = fn.transformCoordinates(img.transform(), coords)
return result, mapped return result, mapped
def getAffineSliceParams(self, data, img, axes=(0,1)): def getAffineSliceParams(self, data, img, axes=(0,1), fromBoundingRect=False):
""" """
Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>` Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>`
(shape, vectors, origin) to extract a subset of *data* using this ROI (shape, vectors, origin) to extract a subset of *data* using this ROI
@ -1098,8 +1101,6 @@ class ROI(GraphicsObject):
if self.scene() is not img.scene(): if self.scene() is not img.scene():
raise Exception("ROI and target item must be members of the same scene.") raise Exception("ROI and target item must be members of the same scene.")
shape = self.state['size']
origin = self.mapToItem(img, QtCore.QPointF(0, 0)) origin = self.mapToItem(img, QtCore.QPointF(0, 0))
## vx and vy point in the directions of the slice axes, but must be scaled properly ## vx and vy point in the directions of the slice axes, but must be scaled properly
@ -1109,17 +1110,43 @@ class ROI(GraphicsObject):
lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2)
pxLen = img.width() / float(data.shape[axes[0]]) pxLen = img.width() / float(data.shape[axes[0]])
#img.width is number of pixels or width of item? #img.width is number of pixels, not width of item.
#need pxWidth and pxHeight instead of pxLen ? #need pxWidth and pxHeight instead of pxLen ?
sx = pxLen / lvx sx = pxLen / lvx
sy = pxLen / lvy sy = pxLen / lvy
vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy))
shape = self.state['size'] if fromBoundingRect is True:
shape = self.boundingRect().width(), self.boundingRect().height()
origin = self.mapToItem(img, self.boundingRect().topLeft())
origin = (origin.x(), origin.y())
else:
shape = self.state['size']
origin = (origin.x(), origin.y())
shape = [abs(shape[0]/sx), abs(shape[1]/sy)] shape = [abs(shape[0]/sx), abs(shape[1]/sy)]
origin = (origin.x(), origin.y())
return shape, vectors, origin return shape, vectors, origin
def renderShapeMask(self, width, height):
"""Return an array of 0.0-1.0 into which the shape of the item has been drawn.
This can be used to mask array selections.
"""
# QImage(width, height, format)
im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
im.fill(0x0)
p = QtGui.QPainter(im)
p.setPen(fn.mkPen(None))
p.setBrush(fn.mkBrush('w'))
shape = self.shape()
bounds = shape.boundingRect()
p.scale(im.width() / bounds.width(), im.height() / bounds.height())
p.translate(-bounds.topLeft())
p.drawPath(shape)
p.end()
mask = fn.imageToArray(im)[:,:,0].astype(float) / 255.
return mask
def getGlobalTransform(self, relativeTo=None): def getGlobalTransform(self, relativeTo=None):
"""Return global transformation (rotation angle+translation) required to move """Return global transformation (rotation angle+translation) required to move
@ -1579,10 +1606,10 @@ class MultiRectROI(QtGui.QGraphicsObject):
pos.append(self.mapFromScene(l.getHandles()[1].scenePos())) pos.append(self.mapFromScene(l.getHandles()[1].scenePos()))
return pos return pos
def getArrayRegion(self, arr, img=None, axes=(0,1)): def getArrayRegion(self, arr, img=None, axes=(0,1), **kwds):
rgns = [] rgns = []
for l in self.lines: for l in self.lines:
rgn = l.getArrayRegion(arr, img, axes=axes) rgn = l.getArrayRegion(arr, img, axes=axes, **kwds)
if rgn is None: if rgn is None:
continue continue
#return None #return None
@ -1652,6 +1679,7 @@ class MultiLineROI(MultiRectROI):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
MultiRectROI.__init__(self, *args, **kwds) MultiRectROI.__init__(self, *args, **kwds)
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
class EllipseROI(ROI): class EllipseROI(ROI):
""" """
@ -1682,19 +1710,27 @@ class EllipseROI(ROI):
p.drawEllipse(r) p.drawEllipse(r)
def getArrayRegion(self, arr, img=None): def getArrayRegion(self, arr, img=None, axes=(0, 1), **kwds):
""" """
Return the result of ROI.getArrayRegion() masked by the elliptical shape Return the result of ROI.getArrayRegion() masked by the elliptical shape
of the ROI. Regions outside the ellipse are set to 0. of the ROI. Regions outside the ellipse are set to 0.
""" """
arr = ROI.getArrayRegion(self, arr, img) # Note: we could use the same method as used by PolyLineROI, but this
if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: # implementation produces a nicer mask.
return None arr = ROI.getArrayRegion(self, arr, img, axes, **kwds)
w = arr.shape[0] if arr is None or arr.shape[axes[0]] == 0 or arr.shape[axes[1]] == 0:
h = arr.shape[1] return arr
w = arr.shape[axes[0]]
h = arr.shape[axes[1]]
## generate an ellipsoidal mask ## generate an ellipsoidal mask
mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h))
# reshape to match array axes
if axes[0] > axes[1]:
mask = mask.T
shape = [(n if i in axes else 1) for i,n in enumerate(arr.shape)]
mask = mask.reshape(shape)
return arr * mask return arr * mask
def shape(self): def shape(self):
@ -1775,6 +1811,7 @@ class PolygonROI(ROI):
#sc['handles'] = self.handles #sc['handles'] = self.handles
return sc return sc
class PolyLineROI(ROI): class PolyLineROI(ROI):
""" """
Container class for multiple connected LineSegmentROIs. Container class for multiple connected LineSegmentROIs.
@ -1923,20 +1960,10 @@ class PolyLineROI(ROI):
return len(self.handles) > 2 return len(self.handles) > 2
def paint(self, p, *args): def paint(self, p, *args):
#for s in self.segments:
#s.update()
#p.setPen(self.currentPen)
#p.setPen(fn.mkPen('w'))
#p.drawRect(self.boundingRect())
#p.drawPath(self.shape())
pass pass
def boundingRect(self): def boundingRect(self):
return self.shape().boundingRect() return self.shape().boundingRect()
#r = QtCore.QRectF()
#for h in self.handles:
#r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs
#return r
def shape(self): def shape(self):
p = QtGui.QPainterPath() p = QtGui.QPainterPath()
@ -1948,30 +1975,18 @@ class PolyLineROI(ROI):
p.lineTo(self.handles[0]['item'].pos()) p.lineTo(self.handles[0]['item'].pos())
return p return p
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): def getArrayRegion(self, data, img, axes=(0,1)):
""" """
Return the result of ROI.getArrayRegion(), masked by the shape of the Return the result of ROI.getArrayRegion(), masked by the shape of the
ROI. Values outside the ROI shape are set to 0. ROI. Values outside the ROI shape are set to 0.
""" """
sl = self.getArraySlice(data, img, axes=(0,1)) sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True)
if sl is None: mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]])
return None
sliced = data[sl[0]]
im = QtGui.QImage(sliced.shape[axes[0]], sliced.shape[axes[1]], QtGui.QImage.Format_ARGB32)
im.fill(0x0)
p = QtGui.QPainter(im)
p.setPen(fn.mkPen(None))
p.setBrush(fn.mkBrush('w'))
p.setTransform(self.itemTransform(img)[0])
bounds = self.mapRectToItem(img, self.boundingRect())
p.translate(-bounds.left(), -bounds.top())
p.drawPath(self.shape())
p.end()
mask = fn.imageToArray(im)[:,:,0].astype(float) / 255.
shape = [1] * data.ndim shape = [1] * data.ndim
shape[axes[0]] = sliced.shape[axes[0]] shape[axes[0]] = sliced.shape[axes[0]]
shape[axes[1]] = sliced.shape[axes[1]] shape[axes[1]] = sliced.shape[axes[1]]
return sliced * mask.reshape(shape) mask = mask.reshape(shape)
return sliced * mask
def setPen(self, *args, **kwds): def setPen(self, *args, **kwds):
ROI.setPen(self, *args, **kwds) ROI.setPen(self, *args, **kwds)

View File

@ -1042,7 +1042,6 @@ class ViewBox(GraphicsWidget):
finally: finally:
view.blockLink(False) view.blockLink(False)
def screenGeometry(self): def screenGeometry(self):
"""return the screen geometry of the viewbox""" """return the screen geometry of the viewbox"""
v = self.getViewWidget() v = self.getViewWidget()
@ -1053,8 +1052,6 @@ class ViewBox(GraphicsWidget):
pos = v.mapToGlobal(v.pos()) pos = v.mapToGlobal(v.pos())
wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
return wr return wr
def itemsChanged(self): def itemsChanged(self):
## called when items are added/removed from self.childGroup ## called when items are added/removed from self.childGroup
@ -1067,18 +1064,23 @@ class ViewBox(GraphicsWidget):
self.update() self.update()
#self.updateAutoRange() #self.updateAutoRange()
def _invertAxis(self, ax, inv):
key = 'xy'[ax] + 'Inverted'
if self.state[key] == inv:
return
self.state[key] = inv
self._matrixNeedsUpdate = True # updateViewRange won't detect this for us
self.updateViewRange()
self.update()
self.sigStateChanged.emit(self)
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax]))
def invertY(self, b=True): def invertY(self, b=True):
""" """
By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
""" """
if self.state['yInverted'] == b: self._invertAxis(1, b)
return
self.state['yInverted'] = b
self._matrixNeedsUpdate = True # updateViewRange won't detect this for us
self.updateViewRange()
self.sigStateChanged.emit(self)
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
def yInverted(self): def yInverted(self):
return self.state['yInverted'] return self.state['yInverted']
@ -1087,14 +1089,7 @@ class ViewBox(GraphicsWidget):
""" """
By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis. By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis.
""" """
if self.state['xInverted'] == b: self._invertAxis(0, b)
return
self.state['xInverted'] = b
#self.updateMatrix(changed=(False, True))
self.updateViewRange()
self.sigStateChanged.emit(self)
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
def xInverted(self): def xInverted(self):
return self.state['xInverted'] return self.state['xInverted']

View File

@ -12,6 +12,7 @@ def test_getArrayRegion():
(pg.ROI([1, 1], [27, 28], pen='y'), 'baseroi'), (pg.ROI([1, 1], [27, 28], pen='y'), 'baseroi'),
(pg.RectROI([1, 1], [27, 28], pen='y'), 'rectroi'), (pg.RectROI([1, 1], [27, 28], pen='y'), 'rectroi'),
(pg.EllipseROI([1, 1], [27, 28], pen='y'), 'ellipseroi'), (pg.EllipseROI([1, 1], [27, 28], pen='y'), 'ellipseroi'),
(pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True), 'polylineroi'),
] ]
for roi, name in rois: for roi, name in rois:
check_getArrayRegion(roi, name) check_getArrayRegion(roi, name)
@ -45,7 +46,7 @@ def check_getArrayRegion(roi, name):
vb1.addItem(roi) vb1.addItem(roi)
rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) rgn = roi.getArrayRegion(data, img1, axes=(1, 2))
assert np.all(rgn == data[:, 1:-2, 1:-2, :]) #assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0))
img2.setImage(rgn[0, ..., 0]) img2.setImage(rgn[0, ..., 0])
vb2.setAspectLocked() vb2.setAspectLocked()
vb2.enableAutoRange(True, True) vb2.enableAutoRange(True, True)
@ -111,6 +112,9 @@ def check_getArrayRegion(roi, name):
# restore state # restore state
# getarrayregion # getarrayregion
# getarrayslice # getarrayslice
# returnMappedCoords
# getAffineSliceParams
# getGlobalTransform
# #
# test conditions: # test conditions:
# y inverted # y inverted