From ccf2ae4db49e8cdba4566fb50393049c083c3f89 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 May 2016 23:30:52 -0700 Subject: [PATCH] Fix PolyLineROI.getArrayRegion and a few other bugs --- pyqtgraph/graphicsItems/ROI.py | 105 ++++++++++++--------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 33 +++---- pyqtgraph/graphicsItems/tests/test_ROI.py | 6 +- 3 files changed, 79 insertions(+), 65 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 8a12ff3b..ac2c6a9d 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -991,8 +991,9 @@ class ROI(GraphicsObject): # p.restore() 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. - Also returns the transform which maps the ROI into data coordinates. + """Return a tuple of slice objects that can be used to slice the region + 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 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 `. """ + # 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: return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) else: @@ -1087,7 +1090,7 @@ class ROI(GraphicsObject): mapped = fn.transformCoordinates(img.transform(), coords) 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 ` (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(): 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)) ## 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) lvy = np.sqrt(vy.x()**2 + vy.y()**2) 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 ? sx = pxLen / lvx sy = pxLen / lvy 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)] - origin = (origin.x(), origin.y()) 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): """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())) return pos - def getArrayRegion(self, arr, img=None, axes=(0,1)): + def getArrayRegion(self, arr, img=None, axes=(0,1), **kwds): rgns = [] for l in self.lines: - rgn = l.getArrayRegion(arr, img, axes=axes) + rgn = l.getArrayRegion(arr, img, axes=axes, **kwds) if rgn is None: continue #return None @@ -1652,6 +1679,7 @@ class MultiLineROI(MultiRectROI): def __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)") + class EllipseROI(ROI): """ @@ -1682,19 +1710,27 @@ class EllipseROI(ROI): 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 of the ROI. Regions outside the ellipse are set to 0. """ - arr = ROI.getArrayRegion(self, arr, img) - if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: - return None - w = arr.shape[0] - h = arr.shape[1] + # Note: we could use the same method as used by PolyLineROI, but this + # implementation produces a nicer mask. + arr = ROI.getArrayRegion(self, arr, img, axes, **kwds) + if arr is None or arr.shape[axes[0]] == 0 or arr.shape[axes[1]] == 0: + return arr + w = arr.shape[axes[0]] + h = arr.shape[axes[1]] ## 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)) - + + # 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 def shape(self): @@ -1775,6 +1811,7 @@ class PolygonROI(ROI): #sc['handles'] = self.handles return sc + class PolyLineROI(ROI): """ Container class for multiple connected LineSegmentROIs. @@ -1923,20 +1960,10 @@ class PolyLineROI(ROI): return len(self.handles) > 2 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 def boundingRect(self): 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): p = QtGui.QPainterPath() @@ -1948,30 +1975,18 @@ class PolyLineROI(ROI): p.lineTo(self.handles[0]['item'].pos()) 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 ROI. Values outside the ROI shape are set to 0. """ - sl = self.getArraySlice(data, img, axes=(0,1)) - if sl is None: - 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. + sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True) + mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]]) shape = [1] * data.ndim shape[axes[0]] = sliced.shape[axes[0]] shape[axes[1]] = sliced.shape[axes[1]] - return sliced * mask.reshape(shape) + mask = mask.reshape(shape) + return sliced * mask def setPen(self, *args, **kwds): ROI.setPen(self, *args, **kwds) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 768bbdcf..4cab8662 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1042,7 +1042,6 @@ class ViewBox(GraphicsWidget): finally: view.blockLink(False) - def screenGeometry(self): """return the screen geometry of the viewbox""" v = self.getViewWidget() @@ -1053,8 +1052,6 @@ class ViewBox(GraphicsWidget): pos = v.mapToGlobal(v.pos()) wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) return wr - - def itemsChanged(self): ## called when items are added/removed from self.childGroup @@ -1067,18 +1064,23 @@ class ViewBox(GraphicsWidget): self.update() #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): """ By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. """ - if self.state['yInverted'] == 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])) + self._invertAxis(1, b) def yInverted(self): 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. """ - if self.state['xInverted'] == 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])) + self._invertAxis(0, b) def xInverted(self): return self.state['xInverted'] diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 7eeb99cc..ff1d20da 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -12,6 +12,7 @@ def test_getArrayRegion(): (pg.ROI([1, 1], [27, 28], pen='y'), 'baseroi'), (pg.RectROI([1, 1], [27, 28], pen='y'), 'rectroi'), (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: check_getArrayRegion(roi, name) @@ -45,7 +46,7 @@ def check_getArrayRegion(roi, name): vb1.addItem(roi) 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]) vb2.setAspectLocked() vb2.enableAutoRange(True, True) @@ -111,6 +112,9 @@ def check_getArrayRegion(roi, name): # restore state # getarrayregion # getarrayslice + # returnMappedCoords + # getAffineSliceParams + # getGlobalTransform # # test conditions: # y inverted