From 2ab52808d39eba737a592b5385cd9dcb1871b407 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 May 2016 09:20:23 -0700 Subject: [PATCH 01/15] added simple roi tests (these do not check output) --- pyqtgraph/graphicsItems/tests/test_ROI.py | 54 +++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pyqtgraph/graphicsItems/tests/test_ROI.py diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py new file mode 100644 index 00000000..15901490 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -0,0 +1,54 @@ +import pyqtgraph as pg +pg.mkQApp() + +vb = pg.ViewBox() +data = pg.np.ones((7, 100, 110, 5)) +image_tx = pg.ImageItem(data[:, :, 0, 0]) +image_xy = pg.ImageItem(data[0, :, :, 0]) +image_yz = pg.ImageItem(data[0, 0, :, :]) +vb.addItem(image_tx) +vb.addItem(image_xy) +vb.addItem(image_yz) + +size = (10, 15) +pos = (0, 0) +rois = [ + pg.ROI(pos, size), + pg.RectROI(pos, size), + pg.EllipseROI(pos, size), + pg.CircleROI(pos, size), + pg.PolyLineROI([pos, size]), +] + +for roi in rois: + vb.addItem(roi) + + +def test_getArrayRegion(): + global vb, image, rois, data, size + + # Test we can call getArrayRegion without errors + # (not checking for data validity) + for roi in rois: + arr = roi.getArrayRegion(data, image_tx) + assert arr.shape == size + data.shape[2:] + + arr = roi.getArrayRegion(data, image_tx, axes=(0, 1)) + assert arr.shape == size + data.shape[2:] + + arr = roi.getArrayRegion(data.transpose(1, 0, 2, 3), image_tx, axes=(1, 0)) + assert arr.shape == size + data.shape[2:] + + arr = roi.getArrayRegion(data, image_xy, axes=(1, 2)) + assert arr.shape == data.shape[:1] + size + data.shape[3:] + + arr = roi.getArrayRegion(data.transpose(0, 2, 1, 3), image_xy, axes=(2, 1)) + assert arr.shape == data.shape[:1] + size + data.shape[3:] + + arr, coords = roi.getArrayRegion(data, image_xy, axes=(1, 2), returnMappedCoords=True) + assert arr.shape == data.shape[:1] + size + data.shape[3:] + assert coords.shape == (2,) + size + + + + \ No newline at end of file From b4e41012d815bc12d60cd689f38b244c311d173d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 9 May 2016 08:56:21 -0700 Subject: [PATCH 02/15] Correct color handling in test images --- pyqtgraph/tests/image_testing.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 5d05c2c3..18f06297 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -110,6 +110,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): painter = QtGui.QPainter(qimg) w.render(painter) painter.end() + + # transpose BGRA to RGBA + image = image[..., [2, 1, 0, 3]] if message is None: code = inspect.currentframe().f_back.f_code @@ -144,7 +147,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): " different than standard image shape %s." % (ims1, ims2)) sr = np.round(sr).astype(int) - image = downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) + image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) assertImageMatch(image, stdImage, **kwargs) except Exception: @@ -159,7 +162,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): print('Saving new standard image to "%s"' % stdFileName) if not os.path.isdir(stdPath): os.makedirs(stdPath) - img = fn.makeQImage(image, alpha=True, copy=False, transpose=False) + img = fn.makeQImage(image, alpha=True, transpose=False) img.save(stdFileName) else: if stdImage is None: From 5c58448658bb63673b52139bac20599f16fa2b93 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 9 May 2016 09:00:41 -0700 Subject: [PATCH 03/15] minor ROI corrections --- pyqtgraph/graphicsItems/ROI.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 3aa19daa..8a12ff3b 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -225,7 +225,9 @@ class ROI(GraphicsObject): multiple change functions to be called sequentially while minimizing processing overhead and repeated signals. Setting update=False also forces finish=False. """ - + # This avoids the temptation to do setPos(x, y) + if not isinstance(update, bool): + raise TypeError("update argument must be bool.") pos = Point(pos) self.state['pos'] = pos QtGui.QGraphicsItem.setPos(self, pos) @@ -944,6 +946,7 @@ class ROI(GraphicsObject): if finish: self.stateChangeFinished() + self.informViewBoundsChanged() def stateChangeFinished(self): self.sigRegionChangeFinished.emit(self) From d4cc2e8b5da52af8432df44e1644af6618ee4d52 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 9 May 2016 09:00:58 -0700 Subject: [PATCH 04/15] Add getArrayRegion tests for ROI, RectROI, and EllipseROI --- pyqtgraph/graphicsItems/tests/test_ROI.py | 163 ++++++++++++++++------ 1 file changed, 118 insertions(+), 45 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 15901490..7eeb99cc 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -1,54 +1,127 @@ +import numpy as np +import pytest import pyqtgraph as pg -pg.mkQApp() +from pyqtgraph.tests import assertImageApproved -vb = pg.ViewBox() -data = pg.np.ones((7, 100, 110, 5)) -image_tx = pg.ImageItem(data[:, :, 0, 0]) -image_xy = pg.ImageItem(data[0, :, :, 0]) -image_yz = pg.ImageItem(data[0, 0, :, :]) -vb.addItem(image_tx) -vb.addItem(image_xy) -vb.addItem(image_yz) -size = (10, 15) -pos = (0, 0) -rois = [ - pg.ROI(pos, size), - pg.RectROI(pos, size), - pg.EllipseROI(pos, size), - pg.CircleROI(pos, size), - pg.PolyLineROI([pos, size]), -] - -for roi in rois: - vb.addItem(roi) +app = pg.mkQApp() def test_getArrayRegion(): - global vb, image, rois, data, size + rois = [ + (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'), + ] + for roi, name in rois: + check_getArrayRegion(roi, name) - # Test we can call getArrayRegion without errors - # (not checking for data validity) - for roi in rois: - arr = roi.getArrayRegion(data, image_tx) - assert arr.shape == size + data.shape[2:] - - arr = roi.getArrayRegion(data, image_tx, axes=(0, 1)) - assert arr.shape == size + data.shape[2:] - - arr = roi.getArrayRegion(data.transpose(1, 0, 2, 3), image_tx, axes=(1, 0)) - assert arr.shape == size + data.shape[2:] - - arr = roi.getArrayRegion(data, image_xy, axes=(1, 2)) - assert arr.shape == data.shape[:1] + size + data.shape[3:] - - arr = roi.getArrayRegion(data.transpose(0, 2, 1, 3), image_xy, axes=(2, 1)) - assert arr.shape == data.shape[:1] + size + data.shape[3:] - - arr, coords = roi.getArrayRegion(data, image_xy, axes=(1, 2), returnMappedCoords=True) - assert arr.shape == data.shape[:1] + size + data.shape[3:] - assert coords.shape == (2,) + size - - + +def check_getArrayRegion(roi, name): + win = pg.GraphicsLayoutWidget() + win.show() + win.resize(200, 400) + + vb1 = win.addViewBox() + win.nextRow() + vb2 = win.addViewBox() + img1 = pg.ImageItem(border='w') + img2 = pg.ImageItem(border='w') + vb1.addItem(img1) + vb2.addItem(img2) + + np.random.seed(0) + data = np.random.normal(size=(7, 30, 31, 5)) + data[0, :, :, :] += 10 + data[:, 1, :, :] += 10 + data[:, :, 2, :] += 10 + data[:, :, :, 3] += 10 + + img1.setImage(data[0, ..., 0]) + vb1.setAspectLocked() + vb1.enableAutoRange(True, True) + + roi.setZValue(10) + vb1.addItem(roi) + + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + assert np.all(rgn == data[:, 1:-2, 1:-2, :]) + img2.setImage(rgn[0, ..., 0]) + vb2.setAspectLocked() + vb2.enableAutoRange(True, True) + + app.processEvents() + + assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.') + + with pytest.raises(TypeError): + roi.setPos(0, 0) + + roi.setPos([0.5, 1.5]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.') + + roi.setAngle(45) + roi.setPos([3, 0]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.') + + roi.setSize([60, 60]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.') + + img1.scale(1, -1) + img1.setPos(0, img1.height()) + img1.rotate(20) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.') + + vb1.invertY() + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.') + + roi.setAngle(0) + roi.setSize(30, 30) + roi.setPos([0, 0]) + img1.resetTransform() + img1.setPos(0, 0) + img1.scale(1, 0.5) + #img1.scale(0.5, 1) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.') + + # test features: + # pen / hoverpen + # handle pen / hoverpen + # handle types + mouse interaction + # getstate + # savestate + # restore state + # getarrayregion + # getarrayslice + # + # test conditions: + # y inverted + # extra array axes + # imageAxisOrder + # roi classes + # image transforms--rotation, scaling, flip + # view transforms--anisotropic scaling + # ROI transforms + # ROI parent transforms + + \ No newline at end of file From ccf2ae4db49e8cdba4566fb50393049c083c3f89 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 13 May 2016 23:30:52 -0700 Subject: [PATCH 05/15] 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 From bb507cf6d089a6aa7dcbbd8656a75969b754815f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 May 2016 18:04:52 -0700 Subject: [PATCH 06/15] ROI tests pass FIX: PolyLineROI.setPoints() did not clear points previously API: Allow ROI.setPos(x, y) in addition to setPos([x, y]) --- pyqtgraph/graphicsItems/ROI.py | 43 +++++++++++++++++------ pyqtgraph/graphicsItems/tests/test_ROI.py | 33 +++++++++-------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index ac2c6a9d..a9bcac06 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -213,7 +213,7 @@ class ROI(GraphicsObject): """Return the angle of the ROI in degrees.""" return self.getState()['angle'] - def setPos(self, pos, update=True, finish=True): + def setPos(self, pos, y=None, update=True, finish=True): """Set the position of the ROI (in the parent's coordinate system). By default, this will cause both sigRegionChanged and sigRegionChangeFinished to be emitted. @@ -225,10 +225,13 @@ class ROI(GraphicsObject): multiple change functions to be called sequentially while minimizing processing overhead and repeated signals. Setting update=False also forces finish=False. """ - # This avoids the temptation to do setPos(x, y) - if not isinstance(update, bool): - raise TypeError("update argument must be bool.") - pos = Point(pos) + if y is None: + pos = Point(pos) + else: + # avoid ambiguity where update is provided as a positional argument + if isinstance(y, bool): + raise TypeError("Positional arguments to setPos() must be numerical.") + pos = Point(pos, y) self.state['pos'] = pos QtGui.QGraphicsItem.setPos(self, pos) if update: @@ -921,8 +924,9 @@ class ROI(GraphicsObject): if self.lastState is None: changed = True else: - for k in list(self.state.keys()): - if self.state[k] != self.lastState[k]: + state = self.getState() + for k in list(state.keys()): + if state[k] != self.lastState[k]: changed = True self.prepareGeometryChange() @@ -942,7 +946,7 @@ class ROI(GraphicsObject): self.sigRegionChanged.emit(self) self.freeHandleMoved = False - self.lastState = self.stateCopy() + self.lastState = self.getState() if finish: self.stateChangeFinished() @@ -1133,6 +1137,9 @@ class ROI(GraphicsObject): This can be used to mask array selections. """ + if width == 0 or height == 0: + return np.empty((width, height), dtype=float) + # QImage(width, height, format) im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32) im.fill(0x0) @@ -1864,6 +1871,8 @@ class PolyLineROI(ROI): if closed is not None: self.closed = closed + self.clearPoints() + for p in points: self.addFreeHandle(p) @@ -1877,7 +1886,14 @@ class PolyLineROI(ROI): Remove all handles and segments. """ while len(self.handles) > 0: - self.removeHandle(self.handles[0]['item']) + update = len(self.handles) == 1 + self.removeHandle(self.handles[0]['item'], updateSegments=update) + + def getState(self): + state = ROI.getState(self) + state['closed'] = self.closed + state['points'] = [Point(h.pos()) for h in self.getHandles()] + return state def saveState(self): state = ROI.saveState(self) @@ -1887,7 +1903,6 @@ class PolyLineROI(ROI): def setState(self, state): ROI.setState(self, state) - self.clearPoints() self.setPoints(state['points'], closed=state['closed']) def addSegment(self, h1, h2, index=None): @@ -1912,6 +1927,7 @@ class PolyLineROI(ROI): def addHandle(self, info, index=None): h = ROI.addHandle(self, info, index=index) h.sigRemoveRequested.connect(self.removeHandle) + self.stateChanged(finish=True) return h def segmentClicked(self, segment, ev=None, pos=None): ## pos should be in this item's coordinate system @@ -1944,6 +1960,7 @@ class PolyLineROI(ROI): handles.remove(handle) segments[0].replaceHandle(handle, handles[0]) self.removeSegment(segments[1]) + self.stateChanged(finish=True) def removeSegment(self, seg): for handle in seg.handles[:]: @@ -1973,19 +1990,23 @@ class PolyLineROI(ROI): for i in range(len(self.handles)): p.lineTo(self.handles[i]['item'].pos()) p.lineTo(self.handles[0]['item'].pos()) - return p + return p 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. """ + br = self.boundingRect() + if br.width() > 1000: + raise Exception() 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]] mask = mask.reshape(shape) + return sliced * mask def setPen(self, *args, **kwds): diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index ff1d20da..6b589edc 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -8,17 +8,24 @@ app = pg.mkQApp() def test_getArrayRegion(): + pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) + pr.setPos(1, 1) rois = [ (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'), + (pr, 'polylineroi'), ] for roi, name in rois: - check_getArrayRegion(roi, name) + # For some ROIs, resize should not be used. + testResize = not isinstance(roi, pg.PolyLineROI) + + check_getArrayRegion(roi, 'roi/'+name, testResize) -def check_getArrayRegion(roi, name): +def check_getArrayRegion(roi, name, testResize=True): + initState = roi.getState() + win = pg.GraphicsLayoutWidget() win.show() win.resize(200, 400) @@ -46,7 +53,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, :]) | (rgn == 0)) + assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) img2.setImage(rgn[0, ..., 0]) vb2.setAspectLocked() vb2.enableAutoRange(True, True) @@ -56,7 +63,7 @@ def check_getArrayRegion(roi, name): assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.') with pytest.raises(TypeError): - roi.setPos(0, 0) + roi.setPos(0, False) roi.setPos([0.5, 1.5]) rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) @@ -71,11 +78,12 @@ def check_getArrayRegion(roi, name): app.processEvents() assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.') - roi.setSize([60, 60]) - rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) - img2.setImage(rgn[0, ..., 0]) - app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.') + if testResize: + roi.setSize([60, 60]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.') img1.scale(1, -1) img1.setPos(0, img1.height()) @@ -91,13 +99,10 @@ def check_getArrayRegion(roi, name): app.processEvents() assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.') - roi.setAngle(0) - roi.setSize(30, 30) - roi.setPos([0, 0]) + roi.setState(initState) img1.resetTransform() img1.setPos(0, 0) img1.scale(1, 0.5) - #img1.scale(0.5, 1) rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() From 8f7b55302fcba3f85f005e84a31df47d43c84e8c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 30 May 2016 18:00:19 -0700 Subject: [PATCH 07/15] Added PolyLineROI unit tests, fixed several bugs in mouse interaction with PolyLineROI. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 9 +-- pyqtgraph/GraphicsScene/mouseEvents.py | 2 - pyqtgraph/graphicsItems/ROI.py | 63 ++++++++++++----- pyqtgraph/graphicsItems/tests/test_ROI.py | 86 +++++++++++++++++------ pyqtgraph/tests/image_testing.py | 12 +++- 5 files changed, 121 insertions(+), 51 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index bab0f776..952a2415 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -135,7 +135,6 @@ class GraphicsScene(QtGui.QGraphicsScene): self._moveDistance = d def mousePressEvent(self, ev): - #print 'scenePress' QtGui.QGraphicsScene.mousePressEvent(self, ev) if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events if self.lastHoverEvent is not None: @@ -173,8 +172,8 @@ class GraphicsScene(QtGui.QGraphicsScene): continue if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0] - dist = Point(ev.screenPos() - cev.screenPos()) - if dist.length() < self._moveDistance and now - cev.time() < self.minDragTime: + dist = Point(ev.scenePos() - cev.scenePos()).length() + if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime): continue init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True self.dragButtons.append(int(btn)) @@ -231,8 +230,6 @@ class GraphicsScene(QtGui.QGraphicsScene): prevItems = list(self.hoverItems.keys()) - #print "hover prev items:", prevItems - #print "hover test items:", items for item in items: if hasattr(item, 'hoverEvent'): event.currentItem = item @@ -247,7 +244,7 @@ class GraphicsScene(QtGui.QGraphicsScene): item.hoverEvent(event) except: debug.printExc("Error sending hover event:") - + event.enter = False event.exit = True #print "hover exit items:", prevItems diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index 2e472e04..fb9d3683 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -276,8 +276,6 @@ class HoverEvent(object): self._modifiers = moveEvent.modifiers() else: self.exit = True - - def isEnter(self): """Returns True if the mouse has just entered the item's shape""" diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index a9bcac06..4cee274e 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -531,7 +531,7 @@ class ROI(GraphicsObject): if isinstance(handle, Handle): index = [i for i, info in enumerate(self.handles) if info['item'] is handle] if len(index) == 0: - raise Exception("Cannot remove handle; it is not attached to this ROI") + raise Exception("Cannot return handle index; not attached to this ROI") return index[0] else: return handle @@ -641,11 +641,20 @@ class ROI(GraphicsObject): if self.mouseHovering == hover: return self.mouseHovering = hover - if hover: - self.currentPen = fn.mkPen(255, 255, 0) + self._updateHoverColor() + + def _updateHoverColor(self): + pen = self._makePen() + if self.currentPen != pen: + self.currentPen = pen + self.update() + + def _makePen(self): + # Generate the pen color for this ROI based on its current state. + if self.mouseHovering: + return fn.mkPen(255, 255, 0) else: - self.currentPen = self.pen - self.update() + return self.pen def contextMenuEnabled(self): return self.removable @@ -1818,7 +1827,7 @@ class PolygonROI(ROI): #sc['handles'] = self.handles return sc - + class PolyLineROI(ROI): """ Container class for multiple connected LineSegmentROIs. @@ -1848,12 +1857,6 @@ class PolyLineROI(ROI): ROI.__init__(self, pos, size=[1,1], **args) self.setPoints(positions) - #for p in positions: - #self.addFreeHandle(p) - - #start = -1 if self.closed else 0 - #for i in range(start, len(self.handles)-1): - #self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) def setPoints(self, points, closed=None): """ @@ -1880,14 +1883,12 @@ class PolyLineROI(ROI): for i in range(start, len(self.handles)-1): self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) - def clearPoints(self): """ Remove all handles and segments. """ while len(self.handles) > 0: - update = len(self.handles) == 1 - self.removeHandle(self.handles[0]['item'], updateSegments=update) + self.removeHandle(self.handles[0]['item']) def getState(self): state = ROI.getState(self) @@ -1906,7 +1907,7 @@ class PolyLineROI(ROI): self.setPoints(state['points'], closed=state['closed']) def addSegment(self, h1, h2, index=None): - seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False) + seg = _PolyLineSegment(handles=(h1, h2), pen=self.pen, parent=self, movable=False) if index is None: self.segments.append(seg) else: @@ -1922,7 +1923,7 @@ class PolyLineROI(ROI): ## Inform all the ROI's segments that the mouse is(not) hovering over it ROI.setMouseHover(self, hover) for s in self.segments: - s.setMouseHover(hover) + s.setParentHover(hover) def addHandle(self, info, index=None): h = ROI.addHandle(self, info, index=index) @@ -1955,7 +1956,7 @@ class PolyLineROI(ROI): if len(segments) == 1: self.removeSegment(segments[0]) - else: + elif len(segments) > 1: handles = [h['item'] for h in segments[1].handles] handles.remove(handle) segments[0].replaceHandle(handle, handles[0]) @@ -2101,6 +2102,32 @@ class LineSegmentROI(ROI): return np.concatenate(rgns, axis=axes[0]) +class _PolyLineSegment(LineSegmentROI): + # Used internally by PolyLineROI + def __init__(self, *args, **kwds): + self._parentHovering = False + LineSegmentROI.__init__(self, *args, **kwds) + + def setParentHover(self, hover): + # set independently of own hover state + if self._parentHovering != hover: + self._parentHovering = hover + self._updateHoverColor() + + def _makePen(self): + if self.mouseHovering or self._parentHovering: + return fn.mkPen(255, 255, 0) + else: + return self.pen + + def hoverEvent(self, ev): + # accept drags even though we discard them to prevent competition with parent ROI + # (unless parent ROI is not movable) + if self.parentItem().translatable: + ev.acceptDrags(QtCore.Qt.LeftButton) + return LineSegmentROI.hoverEvent(self, ev) + + class SpiralROI(ROI): def __init__(self, pos=None, size=None, **args): if size == None: diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 6b589edc..a23cd86b 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -1,7 +1,8 @@ import numpy as np import pytest import pyqtgraph as pg -from pyqtgraph.tests import assertImageApproved +from pyqtgraph.Qt import QtCore, QtTest +from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick app = pg.mkQApp() @@ -108,29 +109,68 @@ def check_getArrayRegion(roi, name, testResize=True): app.processEvents() assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.') - # test features: - # pen / hoverpen - # handle pen / hoverpen - # handle types + mouse interaction - # getstate - # savestate - # restore state - # getarrayregion - # getarrayslice - # returnMappedCoords - # getAffineSliceParams - # getGlobalTransform - # - # test conditions: - # y inverted - # extra array axes - # imageAxisOrder - # roi classes - # image transforms--rotation, scaling, flip - # view transforms--anisotropic scaling - # ROI transforms - # ROI parent transforms +def test_PolyLineROI(): + rois = [ + (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), 'closed'), + (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=False, pen=0.3), 'open') + ] + plt = pg.plot() + plt.resize(200, 200) + plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. + + # seemingly arbitrary requirements; might need longer wait time for some platforms.. + QtTest.QTest.qWaitForWindowShown(plt) + QtTest.QTest.qWait(100) + for r, name in rois: + plt.clear() + plt.addItem(r) + plt.autoRange() + app.processEvents() + + assertImageApproved(plt, 'roi/polylineroi/'+name+'_init', 'Init %s polyline.' % name) + initState = r.getState() + assert len(r.getState()['points']) == 3 + + # hover over center + center = r.mapToScene(pg.Point(3, 3)) + mouseMove(plt, center) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_roi', 'Hover mouse over center of ROI.') + + # drag ROI + mouseDrag(plt, center, center + pg.Point(10, -10), QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_roi', 'Drag mouse over center of ROI.') + + # hover over handle + pt = r.mapToScene(pg.Point(r.getState()['points'][2])) + mouseMove(plt, pt) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_handle', 'Hover mouse over handle.') + + # drag handle + mouseDrag(plt, pt, pt + pg.Point(5, 20), QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_handle', 'Drag mouse over handle.') + + # hover over segment + pt = r.mapToScene((pg.Point(r.getState()['points'][2]) + pg.Point(r.getState()['points'][1])) * 0.5) + mouseMove(plt, pt+pg.Point(0, 2)) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_segment', 'Hover mouse over diagonal segment.') + + # click segment + mouseClick(plt, pt, QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.') + + r.clearPoints() + assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.') + assert len(r.getState()['points']) == 0 + + r.setPoints(initState['points']) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.') + assert len(r.getState()['points']) == 3 + + r.setState(initState) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') + assert len(r.getState()['points']) == 3 + \ No newline at end of file diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 18f06297..8c46c789 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -41,7 +41,7 @@ Procedure for unit-testing with images: # This is the name of a tag in the test-data repository that this version of # pyqtgraph should be tested against. When adding or changing test images, # create and push a new tag and update this variable. -testDataTag = 'test-data-3' +testDataTag = 'test-data-4' import time @@ -306,7 +306,7 @@ class ImageTester(QtGui.QWidget): QtGui.QWidget.__init__(self) self.resize(1200, 800) - self.showFullScreen() + #self.showFullScreen() self.layout = QtGui.QGridLayout() self.setLayout(self.layout) @@ -324,6 +324,8 @@ class ImageTester(QtGui.QWidget): self.failBtn = QtGui.QPushButton('Fail') self.layout.addWidget(self.passBtn, 2, 0) self.layout.addWidget(self.failBtn, 2, 1) + self.passBtn.clicked.connect(self.passTest) + self.failBtn.clicked.connect(self.failTest) self.views = (self.view.addViewBox(row=0, col=0), self.view.addViewBox(row=0, col=1), @@ -386,6 +388,12 @@ class ImageTester(QtGui.QWidget): else: self.lastKey = str(event.text()).lower() + def passTest(self): + self.lastKey = 'p' + + def failTest(self): + self.lastKey = 'f' + def getTestDataRepo(): """Return the path to a git repository with the required commit checked From 49d5543fa5914288660e2119e2614bba83b3dff0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 30 May 2016 20:28:59 -0700 Subject: [PATCH 08/15] travis fix --- pyqtgraph/tests/image_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 8c46c789..fc4961e2 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -234,7 +234,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., def saveFailedTest(data, expect, filename): """Upload failed test images to web server to allow CI test debugging. """ - commit, error = runSubprocess(['git', 'rev-parse', 'HEAD']) + commit = runSubprocess(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) From 2e59cd63cba2ef1b9fa461385052d92d19633ea3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 5 Jun 2016 00:15:51 -0700 Subject: [PATCH 09/15] Fix image test makePng function --- pyqtgraph/tests/image_testing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index fc4961e2..c8e108df 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -276,8 +276,8 @@ def makePng(img): """ io = QtCore.QBuffer() qim = fn.makeQImage(img, alpha=False) - qim.save(io, format='png') - png = io.data().data().encode() + qim.save(io, 'PNG') + png = bytes(io.data().data()) return png From 230659a4dbc29f451745b1f635ae0e5a991c648e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 5 Jun 2016 00:48:13 -0700 Subject: [PATCH 10/15] Allow Qt lib selection from environment variable for testing Cover up some QtTest differences between PyQt4 / PyQt5 --- pyqtgraph/Qt.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index c9700784..92defc84 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -9,7 +9,7 @@ This module exists to smooth out some of the differences between PySide and PyQt """ -import sys, re, time +import os, sys, re, time from .python2_3 import asUnicode @@ -17,17 +17,19 @@ PYSIDE = 'PySide' PYQT4 = 'PyQt4' PYQT5 = 'PyQt5' -QT_LIB = None +QT_LIB = os.getenv('PYQTGRAPH_QT_LIB') -## Automatically determine whether to use PyQt or PySide. +## Automatically determine whether to use PyQt or PySide (unless specified by +## environment variable). ## This is done by first checking to see whether one of the libraries ## is already imported. If not, then attempt to import PyQt4, then PySide. -libOrder = [PYQT4, PYSIDE, PYQT5] +if QT_LIB is None: + libOrder = [PYQT4, PYSIDE, PYQT5] -for lib in libOrder: - if lib in sys.modules: - QT_LIB = lib - break + for lib in libOrder: + if lib in sys.modules: + QT_LIB = lib + break if QT_LIB is None: for lib in libOrder: @@ -38,7 +40,7 @@ if QT_LIB is None: except ImportError: pass -if QT_LIB == None: +if QT_LIB is None: raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") if QT_LIB == PYSIDE: @@ -157,6 +159,11 @@ elif QT_LIB == PYQT5: from PyQt5 import QtOpenGL except ImportError: pass + try: + from PyQt5 import QtTest + QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed + except ImportError: + pass # Re-implement deprecated APIs def scale(self, sx, sy): @@ -200,6 +207,9 @@ elif QT_LIB == PYQT5: VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR +else: + raise ValueError("Invalid Qt lib '%s'" % QT_LIB) + # Common to PyQt4 and 5 if QT_LIB.startswith('PyQt'): import sip From 637eab8359375885bae989f7ed8bb95878b9b8ce Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 14 Jun 2016 21:56:25 -0700 Subject: [PATCH 11/15] Add debugging output for image testing --- pyqtgraph/tests/image_testing.py | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c8e108df..c1ac4dd7 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -40,7 +40,8 @@ Procedure for unit-testing with images: # This is the name of a tag in the test-data repository that this version of # pyqtgraph should be tested against. When adding or changing test images, -# create and push a new tag and update this variable. +# create and push a new tag and update this variable. To test locally, begin +# by creating the tag in your ~/.pyqtgraph/test-data repository. testDataTag = 'test-data-4' @@ -105,6 +106,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): """ if isinstance(image, QtGui.QWidget): w = image + graphstate = scenegraphState(w, standardFile) image = np.zeros((w.height(), w.width(), 4), dtype=np.ubyte) qimg = fn.makeQImage(image, alpha=True, copy=False, transpose=False) painter = QtGui.QPainter(qimg) @@ -150,6 +152,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) assertImageMatch(image, stdImage, **kwargs) + + if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)): + print graphstate except Exception: if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " @@ -171,6 +176,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): else: if os.getenv('TRAVIS') is not None: saveFailedTest(image, stdImage, standardFile) + print graphstate raise @@ -542,3 +548,35 @@ def runSubprocess(command, return_code=False, **kwargs): raise sp.CalledProcessError(p.returncode, command) return output + + +def scenegraphState(view, name): + """Return information about the scenegraph for debugging test failures. + """ + state = "====== Scenegraph state for %s ======\n" % name + state += "view size: %dx%d\n" % (view.width(), view.height()) + state += "view transform:\n" + indent(transformStr(view.transform()), " ") + for item in view.scene().items(): + if item.parentItem() is None: + state += itemState(item) + '\n' + return state + + +def itemState(root): + state = str(root) + '\n' + from .. import ViewBox + state += 'bounding rect: ' + str(root.boundingRect()) + '\n' + if isinstance(root, ViewBox): + state += "view range: " + str(root.viewRange()) + '\n' + state += "transform:\n" + indent(transformStr(root.transform()).strip(), " ") + '\n' + for item in root.childItems(): + state += indent(itemState(item).strip(), " ") + '\n' + return state + + +def transformStr(t): + return ("[%0.2f %0.2f %0.2f]\n"*3) % (t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), t.m31(), t.m32(), t.m33()) + + +def indent(s, pfx): + return '\n'.join([pfx+line for line in s.split('\n')]) From f0071a09dc2eb5c69fdf0b5253498c29219adf79 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 14 Jun 2016 22:02:05 -0700 Subject: [PATCH 12/15] docstring update --- pyqtgraph/graphicsItems/ROI.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 4cee274e..51853c61 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -215,15 +215,20 @@ class ROI(GraphicsObject): def setPos(self, pos, y=None, update=True, finish=True): """Set the position of the ROI (in the parent's coordinate system). - By default, this will cause both sigRegionChanged and sigRegionChangeFinished to be emitted. - If finish is False, then sigRegionChangeFinished will not be emitted. You can then use - stateChangeFinished() to cause the signal to be emitted after a series of state changes. + Accepts either separate (x, y) arguments or a single :class:`Point` or + ``QPointF`` argument. - If update is False, the state change will be remembered but not processed and no signals + By default, this method causes both ``sigRegionChanged`` and + ``sigRegionChangeFinished`` to be emitted. If *finish* is False, then + ``sigRegionChangeFinished`` will not be emitted. You can then use + stateChangeFinished() to cause the signal to be emitted after a series + of state changes. + + If *update* is False, the state change will be remembered but not processed and no signals will be emitted. You can then use stateChanged() to complete the state change. This allows multiple change functions to be called sequentially while minimizing processing overhead - and repeated signals. Setting update=False also forces finish=False. + and repeated signals. Setting ``update=False`` also forces ``finish=False``. """ if y is None: pos = Point(pos) From f32dce7908f6eee38944e398ab7b13b5e9c2f6e0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 16 Jun 2016 17:34:39 -0700 Subject: [PATCH 13/15] Avoid using QGraphicsLayout for tests; this produces unreliable results --- pyqtgraph/graphicsItems/tests/test_ROI.py | 36 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index a23cd86b..1fdf5bfb 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -27,13 +27,27 @@ def test_getArrayRegion(): def check_getArrayRegion(roi, name, testResize=True): initState = roi.getState() - win = pg.GraphicsLayoutWidget() + #win = pg.GraphicsLayoutWidget() + win = pg.GraphicsView() win.show() win.resize(200, 400) - vb1 = win.addViewBox() - win.nextRow() - vb2 = win.addViewBox() + # Don't use Qt's layouts for testing--these generate unpredictable results. + #vb1 = win.addViewBox() + #win.nextRow() + #vb2 = win.addViewBox() + + # Instead, place the viewboxes manually + vb1 = pg.ViewBox() + win.scene().addItem(vb1) + vb1.setPos(6, 6) + vb1.resize(188, 191) + + vb2 = pg.ViewBox() + win.scene().addItem(vb2) + vb2.setPos(6, 203) + vb2.resize(188, 191) + img1 = pg.ImageItem(border='w') img2 = pg.ImageItem(border='w') vb1.addItem(img1) @@ -115,8 +129,14 @@ def test_PolyLineROI(): (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), 'closed'), (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=False, pen=0.3), 'open') ] - plt = pg.plot() + + #plt = pg.plot() + plt = pg.GraphicsView() + plt.show() plt.resize(200, 200) + plt.plotItem = pg.PlotItem() + plt.scene().addItem(plt.plotItem) + plt.plotItem.resize(200, 200) plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. @@ -125,9 +145,9 @@ def test_PolyLineROI(): QtTest.QTest.qWait(100) for r, name in rois: - plt.clear() - plt.addItem(r) - plt.autoRange() + plt.plotItem.clear() + plt.plotItem.addItem(r) + plt.plotItem.autoRange() app.processEvents() assertImageApproved(plt, 'roi/polylineroi/'+name+'_init', 'Init %s polyline.' % name) From 0d131e4be496053ba85e86c2f0c1aad8d860dbd5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 18 Jul 2016 08:13:25 -0700 Subject: [PATCH 14/15] Remove axes in ROI tests (these cause travis failures) --- pyqtgraph/graphicsItems/tests/test_ROI.py | 16 ++++++++++------ pyqtgraph/tests/image_testing.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 1fdf5bfb..973d8f1a 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -134,9 +134,13 @@ def test_PolyLineROI(): plt = pg.GraphicsView() plt.show() plt.resize(200, 200) - plt.plotItem = pg.PlotItem() - plt.scene().addItem(plt.plotItem) - plt.plotItem.resize(200, 200) + vb = pg.ViewBox() + plt.scene().addItem(vb) + vb.resize(200, 200) + #plt.plotItem = pg.PlotItem() + #plt.scene().addItem(plt.plotItem) + #plt.plotItem.resize(200, 200) + plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. @@ -145,9 +149,9 @@ def test_PolyLineROI(): QtTest.QTest.qWait(100) for r, name in rois: - plt.plotItem.clear() - plt.plotItem.addItem(r) - plt.plotItem.autoRange() + vb.clear() + vb.addItem(r) + vb.autoRange() app.processEvents() assertImageApproved(plt, 'roi/polylineroi/'+name+'_init', 'Init %s polyline.' % name) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c1ac4dd7..018896c2 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -281,7 +281,7 @@ def makePng(img): """Given an array like (H, W, 4), return a PNG-encoded byte string. """ io = QtCore.QBuffer() - qim = fn.makeQImage(img, alpha=False) + qim = fn.makeQImage(img.transpose(1, 0, 2), alpha=False) qim.save(io, 'PNG') png = bytes(io.data().data()) return png From 08b93dce822abec1a049f85a5e1db7e7b1727ffe Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 18 Jul 2016 09:13:06 -0700 Subject: [PATCH 15/15] minor corrections --- pyqtgraph/tests/image_testing.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 018896c2..a2b20ee7 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -59,7 +59,7 @@ if sys.version[0] >= '3': else: import httplib import urllib -from ..Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore, QtTest from .. import functions as fn from .. import GraphicsLayoutWidget from .. import ImageItem, TextItem @@ -106,6 +106,10 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): """ if isinstance(image, QtGui.QWidget): w = image + + # just to be sure the widget size is correct (new window may be resized): + QtGui.QApplication.processEvents() + graphstate = scenegraphState(w, standardFile) image = np.zeros((w.height(), w.width(), 4), dtype=np.ubyte) qimg = fn.makeQImage(image, alpha=True, copy=False, transpose=False) @@ -154,7 +158,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): assertImageMatch(image, stdImage, **kwargs) if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)): - print graphstate + print(graphstate) except Exception: if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " @@ -176,7 +180,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): else: if os.getenv('TRAVIS') is not None: saveFailedTest(image, stdImage, standardFile) - print graphstate + print(graphstate) raise