commit
95b2acb027
@ -135,7 +135,6 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||||||
self._moveDistance = d
|
self._moveDistance = d
|
||||||
|
|
||||||
def mousePressEvent(self, ev):
|
def mousePressEvent(self, ev):
|
||||||
#print 'scenePress'
|
|
||||||
QtGui.QGraphicsScene.mousePressEvent(self, ev)
|
QtGui.QGraphicsScene.mousePressEvent(self, ev)
|
||||||
if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events
|
if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events
|
||||||
if self.lastHoverEvent is not None:
|
if self.lastHoverEvent is not None:
|
||||||
@ -173,8 +172,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||||||
continue
|
continue
|
||||||
if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet
|
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]
|
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0]
|
||||||
dist = Point(ev.screenPos() - cev.screenPos())
|
dist = Point(ev.scenePos() - cev.scenePos()).length()
|
||||||
if dist.length() < self._moveDistance and now - cev.time() < self.minDragTime:
|
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
|
||||||
continue
|
continue
|
||||||
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
|
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
|
||||||
self.dragButtons.append(int(btn))
|
self.dragButtons.append(int(btn))
|
||||||
@ -231,8 +230,6 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||||||
|
|
||||||
prevItems = list(self.hoverItems.keys())
|
prevItems = list(self.hoverItems.keys())
|
||||||
|
|
||||||
#print "hover prev items:", prevItems
|
|
||||||
#print "hover test items:", items
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if hasattr(item, 'hoverEvent'):
|
if hasattr(item, 'hoverEvent'):
|
||||||
event.currentItem = item
|
event.currentItem = item
|
||||||
|
@ -277,8 +277,6 @@ class HoverEvent(object):
|
|||||||
else:
|
else:
|
||||||
self.exit = True
|
self.exit = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def isEnter(self):
|
def isEnter(self):
|
||||||
"""Returns True if the mouse has just entered the item's shape"""
|
"""Returns True if the mouse has just entered the item's shape"""
|
||||||
return self.enter
|
return self.enter
|
||||||
|
@ -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
|
from .python2_3 import asUnicode
|
||||||
|
|
||||||
@ -17,17 +17,19 @@ PYSIDE = 'PySide'
|
|||||||
PYQT4 = 'PyQt4'
|
PYQT4 = 'PyQt4'
|
||||||
PYQT5 = 'PyQt5'
|
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
|
## 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.
|
## 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:
|
for lib in libOrder:
|
||||||
if lib in sys.modules:
|
if lib in sys.modules:
|
||||||
QT_LIB = lib
|
QT_LIB = lib
|
||||||
break
|
break
|
||||||
|
|
||||||
if QT_LIB is None:
|
if QT_LIB is None:
|
||||||
for lib in libOrder:
|
for lib in libOrder:
|
||||||
@ -38,7 +40,7 @@ if QT_LIB is None:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
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.")
|
raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.")
|
||||||
|
|
||||||
if QT_LIB == PYSIDE:
|
if QT_LIB == PYSIDE:
|
||||||
@ -157,6 +159,11 @@ elif QT_LIB == PYQT5:
|
|||||||
from PyQt5 import QtOpenGL
|
from PyQt5 import QtOpenGL
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
try:
|
||||||
|
from PyQt5 import QtTest
|
||||||
|
QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Re-implement deprecated APIs
|
# Re-implement deprecated APIs
|
||||||
|
|
||||||
@ -208,6 +215,9 @@ elif QT_LIB == PYQT5:
|
|||||||
|
|
||||||
VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
|
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
|
# Common to PyQt4 and 5
|
||||||
if QT_LIB.startswith('PyQt'):
|
if QT_LIB.startswith('PyQt'):
|
||||||
import sip
|
import sip
|
||||||
|
@ -213,20 +213,30 @@ class ROI(GraphicsObject):
|
|||||||
"""Return the angle of the ROI in degrees."""
|
"""Return the angle of the ROI in degrees."""
|
||||||
return self.getState()['angle']
|
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).
|
"""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
|
Accepts either separate (x, y) arguments or a single :class:`Point` or
|
||||||
stateChangeFinished() to cause the signal to be emitted after a series of state changes.
|
``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
|
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
|
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)
|
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
|
self.state['pos'] = pos
|
||||||
QtGui.QGraphicsItem.setPos(self, pos)
|
QtGui.QGraphicsItem.setPos(self, pos)
|
||||||
if update:
|
if update:
|
||||||
@ -526,7 +536,7 @@ class ROI(GraphicsObject):
|
|||||||
if isinstance(handle, Handle):
|
if isinstance(handle, Handle):
|
||||||
index = [i for i, info in enumerate(self.handles) if info['item'] is handle]
|
index = [i for i, info in enumerate(self.handles) if info['item'] is handle]
|
||||||
if len(index) == 0:
|
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]
|
return index[0]
|
||||||
else:
|
else:
|
||||||
return handle
|
return handle
|
||||||
@ -636,11 +646,20 @@ class ROI(GraphicsObject):
|
|||||||
if self.mouseHovering == hover:
|
if self.mouseHovering == hover:
|
||||||
return
|
return
|
||||||
self.mouseHovering = hover
|
self.mouseHovering = hover
|
||||||
if hover:
|
self._updateHoverColor()
|
||||||
self.currentPen = fn.mkPen(255, 255, 0)
|
|
||||||
|
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:
|
else:
|
||||||
self.currentPen = self.pen
|
return self.pen
|
||||||
self.update()
|
|
||||||
|
|
||||||
def contextMenuEnabled(self):
|
def contextMenuEnabled(self):
|
||||||
return self.removable
|
return self.removable
|
||||||
@ -919,8 +938,9 @@ class ROI(GraphicsObject):
|
|||||||
if self.lastState is None:
|
if self.lastState is None:
|
||||||
changed = True
|
changed = True
|
||||||
else:
|
else:
|
||||||
for k in list(self.state.keys()):
|
state = self.getState()
|
||||||
if self.state[k] != self.lastState[k]:
|
for k in list(state.keys()):
|
||||||
|
if state[k] != self.lastState[k]:
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
@ -940,10 +960,11 @@ class ROI(GraphicsObject):
|
|||||||
self.sigRegionChanged.emit(self)
|
self.sigRegionChanged.emit(self)
|
||||||
|
|
||||||
self.freeHandleMoved = False
|
self.freeHandleMoved = False
|
||||||
self.lastState = self.stateCopy()
|
self.lastState = self.getState()
|
||||||
|
|
||||||
if finish:
|
if finish:
|
||||||
self.stateChangeFinished()
|
self.stateChangeFinished()
|
||||||
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
def stateChangeFinished(self):
|
def stateChangeFinished(self):
|
||||||
self.sigRegionChangeFinished.emit(self)
|
self.sigRegionChangeFinished.emit(self)
|
||||||
@ -988,8 +1009,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))
|
||||||
@ -1072,8 +1094,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:
|
||||||
@ -1084,7 +1108,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
|
||||||
@ -1095,8 +1119,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
|
||||||
@ -1106,18 +1128,47 @@ 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.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
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
|
||||||
from relative state to current state. If relative state isn't specified,
|
from relative state to current state. If relative state isn't specified,
|
||||||
@ -1576,10 +1627,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
|
||||||
@ -1650,6 +1701,7 @@ class MultiLineROI(MultiRectROI):
|
|||||||
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):
|
||||||
"""
|
"""
|
||||||
Elliptical ROI subclass with one scale handle and one rotation handle.
|
Elliptical ROI subclass with one scale handle and one rotation handle.
|
||||||
@ -1679,19 +1731,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):
|
||||||
@ -1772,6 +1832,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.
|
||||||
@ -1801,12 +1862,6 @@ class PolyLineROI(ROI):
|
|||||||
ROI.__init__(self, pos, size=[1,1], **args)
|
ROI.__init__(self, pos, size=[1,1], **args)
|
||||||
|
|
||||||
self.setPoints(positions)
|
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):
|
def setPoints(self, points, closed=None):
|
||||||
"""
|
"""
|
||||||
@ -1824,6 +1879,8 @@ class PolyLineROI(ROI):
|
|||||||
if closed is not None:
|
if closed is not None:
|
||||||
self.closed = closed
|
self.closed = closed
|
||||||
|
|
||||||
|
self.clearPoints()
|
||||||
|
|
||||||
for p in points:
|
for p in points:
|
||||||
self.addFreeHandle(p)
|
self.addFreeHandle(p)
|
||||||
|
|
||||||
@ -1831,7 +1888,6 @@ class PolyLineROI(ROI):
|
|||||||
for i in range(start, len(self.handles)-1):
|
for i in range(start, len(self.handles)-1):
|
||||||
self.addSegment(self.handles[i]['item'], self.handles[i+1]['item'])
|
self.addSegment(self.handles[i]['item'], self.handles[i+1]['item'])
|
||||||
|
|
||||||
|
|
||||||
def clearPoints(self):
|
def clearPoints(self):
|
||||||
"""
|
"""
|
||||||
Remove all handles and segments.
|
Remove all handles and segments.
|
||||||
@ -1839,6 +1895,12 @@ class PolyLineROI(ROI):
|
|||||||
while len(self.handles) > 0:
|
while len(self.handles) > 0:
|
||||||
self.removeHandle(self.handles[0]['item'])
|
self.removeHandle(self.handles[0]['item'])
|
||||||
|
|
||||||
|
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):
|
def saveState(self):
|
||||||
state = ROI.saveState(self)
|
state = ROI.saveState(self)
|
||||||
state['closed'] = self.closed
|
state['closed'] = self.closed
|
||||||
@ -1847,11 +1909,10 @@ class PolyLineROI(ROI):
|
|||||||
|
|
||||||
def setState(self, state):
|
def setState(self, state):
|
||||||
ROI.setState(self, state)
|
ROI.setState(self, state)
|
||||||
self.clearPoints()
|
|
||||||
self.setPoints(state['points'], closed=state['closed'])
|
self.setPoints(state['points'], closed=state['closed'])
|
||||||
|
|
||||||
def addSegment(self, h1, h2, index=None):
|
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:
|
if index is None:
|
||||||
self.segments.append(seg)
|
self.segments.append(seg)
|
||||||
else:
|
else:
|
||||||
@ -1867,11 +1928,12 @@ class PolyLineROI(ROI):
|
|||||||
## Inform all the ROI's segments that the mouse is(not) hovering over it
|
## Inform all the ROI's segments that the mouse is(not) hovering over it
|
||||||
ROI.setMouseHover(self, hover)
|
ROI.setMouseHover(self, hover)
|
||||||
for s in self.segments:
|
for s in self.segments:
|
||||||
s.setMouseHover(hover)
|
s.setParentHover(hover)
|
||||||
|
|
||||||
def addHandle(self, info, index=None):
|
def addHandle(self, info, index=None):
|
||||||
h = ROI.addHandle(self, info, index=index)
|
h = ROI.addHandle(self, info, index=index)
|
||||||
h.sigRemoveRequested.connect(self.removeHandle)
|
h.sigRemoveRequested.connect(self.removeHandle)
|
||||||
|
self.stateChanged(finish=True)
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def segmentClicked(self, segment, ev=None, pos=None): ## pos should be in this item's coordinate system
|
def segmentClicked(self, segment, ev=None, pos=None): ## pos should be in this item's coordinate system
|
||||||
@ -1899,11 +1961,12 @@ class PolyLineROI(ROI):
|
|||||||
|
|
||||||
if len(segments) == 1:
|
if len(segments) == 1:
|
||||||
self.removeSegment(segments[0])
|
self.removeSegment(segments[0])
|
||||||
else:
|
elif len(segments) > 1:
|
||||||
handles = [h['item'] for h in segments[1].handles]
|
handles = [h['item'] for h in segments[1].handles]
|
||||||
handles.remove(handle)
|
handles.remove(handle)
|
||||||
segments[0].replaceHandle(handle, handles[0])
|
segments[0].replaceHandle(handle, handles[0])
|
||||||
self.removeSegment(segments[1])
|
self.removeSegment(segments[1])
|
||||||
|
self.stateChanged(finish=True)
|
||||||
|
|
||||||
def removeSegment(self, seg):
|
def removeSegment(self, seg):
|
||||||
for handle in seg.handles[:]:
|
for handle in seg.handles[:]:
|
||||||
@ -1920,20 +1983,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()
|
||||||
@ -1945,30 +1998,22 @@ 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))
|
br = self.boundingRect()
|
||||||
if sl is None:
|
if br.width() > 1000:
|
||||||
return None
|
raise Exception()
|
||||||
sliced = data[sl[0]]
|
sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True)
|
||||||
im = QtGui.QImage(sliced.shape[axes[0]], sliced.shape[axes[1]], QtGui.QImage.Format_ARGB32)
|
mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]])
|
||||||
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)
|
||||||
@ -2062,6 +2107,32 @@ class LineSegmentROI(ROI):
|
|||||||
return np.concatenate(rgns, axis=axes[0])
|
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):
|
class SpiralROI(ROI):
|
||||||
def __init__(self, pos=None, size=None, **args):
|
def __init__(self, pos=None, size=None, **args):
|
||||||
if size == None:
|
if size == None:
|
||||||
|
@ -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()
|
||||||
@ -1054,8 +1053,6 @@ class ViewBox(GraphicsWidget):
|
|||||||
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
|
||||||
self.updateAutoRange()
|
self.updateAutoRange()
|
||||||
@ -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']
|
||||||
|
200
pyqtgraph/graphicsItems/tests/test_ROI.py
Normal file
200
pyqtgraph/graphicsItems/tests/test_ROI.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
import pyqtgraph as pg
|
||||||
|
from pyqtgraph.Qt import QtCore, QtTest
|
||||||
|
from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick
|
||||||
|
|
||||||
|
|
||||||
|
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'),
|
||||||
|
(pr, 'polylineroi'),
|
||||||
|
]
|
||||||
|
for roi, name in rois:
|
||||||
|
# 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, testResize=True):
|
||||||
|
initState = roi.getState()
|
||||||
|
|
||||||
|
#win = pg.GraphicsLayoutWidget()
|
||||||
|
win = pg.GraphicsView()
|
||||||
|
win.show()
|
||||||
|
win.resize(200, 400)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
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, :]) | (rgn == 0))
|
||||||
|
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, False)
|
||||||
|
|
||||||
|
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.')
|
||||||
|
|
||||||
|
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())
|
||||||
|
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.setState(initState)
|
||||||
|
img1.resetTransform()
|
||||||
|
img1.setPos(0, 0)
|
||||||
|
img1.scale(1, 0.5)
|
||||||
|
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.')
|
||||||
|
|
||||||
|
|
||||||
|
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 = pg.GraphicsView()
|
||||||
|
plt.show()
|
||||||
|
plt.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.
|
||||||
|
|
||||||
|
# seemingly arbitrary requirements; might need longer wait time for some platforms..
|
||||||
|
QtTest.QTest.qWaitForWindowShown(plt)
|
||||||
|
QtTest.QTest.qWait(100)
|
||||||
|
|
||||||
|
for r, name in rois:
|
||||||
|
vb.clear()
|
||||||
|
vb.addItem(r)
|
||||||
|
vb.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
|
||||||
|
|
||||||
|
|
@ -40,8 +40,9 @@ Procedure for unit-testing with images:
|
|||||||
|
|
||||||
# This is the name of a tag in the test-data repository that this version of
|
# 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,
|
# 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
|
||||||
testDataTag = 'test-data-3'
|
# by creating the tag in your ~/.pyqtgraph/test-data repository.
|
||||||
|
testDataTag = 'test-data-4'
|
||||||
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@ -58,7 +59,7 @@ if sys.version[0] >= '3':
|
|||||||
else:
|
else:
|
||||||
import httplib
|
import httplib
|
||||||
import urllib
|
import urllib
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore, QtTest
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
from .. import GraphicsLayoutWidget
|
from .. import GraphicsLayoutWidget
|
||||||
from .. import ImageItem, TextItem
|
from .. import ImageItem, TextItem
|
||||||
@ -105,12 +106,20 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
|||||||
"""
|
"""
|
||||||
if isinstance(image, QtGui.QWidget):
|
if isinstance(image, QtGui.QWidget):
|
||||||
w = image
|
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)
|
image = np.zeros((w.height(), w.width(), 4), dtype=np.ubyte)
|
||||||
qimg = fn.makeQImage(image, alpha=True, copy=False, transpose=False)
|
qimg = fn.makeQImage(image, alpha=True, copy=False, transpose=False)
|
||||||
painter = QtGui.QPainter(qimg)
|
painter = QtGui.QPainter(qimg)
|
||||||
w.render(painter)
|
w.render(painter)
|
||||||
painter.end()
|
painter.end()
|
||||||
|
|
||||||
|
# transpose BGRA to RGBA
|
||||||
|
image = image[..., [2, 1, 0, 3]]
|
||||||
|
|
||||||
if message is None:
|
if message is None:
|
||||||
code = inspect.currentframe().f_back.f_code
|
code = inspect.currentframe().f_back.f_code
|
||||||
message = "%s::%s" % (code.co_filename, code.co_name)
|
message = "%s::%s" % (code.co_filename, code.co_name)
|
||||||
@ -144,9 +153,12 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
|||||||
" different than standard image shape %s." %
|
" different than standard image shape %s." %
|
||||||
(ims1, ims2))
|
(ims1, ims2))
|
||||||
sr = np.round(sr).astype(int)
|
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)
|
assertImageMatch(image, stdImage, **kwargs)
|
||||||
|
|
||||||
|
if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)):
|
||||||
|
print(graphstate)
|
||||||
except Exception:
|
except Exception:
|
||||||
if stdFileName in gitStatus(dataPath):
|
if stdFileName in gitStatus(dataPath):
|
||||||
print("\n\nWARNING: unit test failed against modified standard "
|
print("\n\nWARNING: unit test failed against modified standard "
|
||||||
@ -159,7 +171,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
|||||||
print('Saving new standard image to "%s"' % stdFileName)
|
print('Saving new standard image to "%s"' % stdFileName)
|
||||||
if not os.path.isdir(stdPath):
|
if not os.path.isdir(stdPath):
|
||||||
os.makedirs(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)
|
img.save(stdFileName)
|
||||||
else:
|
else:
|
||||||
if stdImage is None:
|
if stdImage is None:
|
||||||
@ -168,6 +180,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
|||||||
else:
|
else:
|
||||||
if os.getenv('TRAVIS') is not None:
|
if os.getenv('TRAVIS') is not None:
|
||||||
saveFailedTest(image, stdImage, standardFile)
|
saveFailedTest(image, stdImage, standardFile)
|
||||||
|
print(graphstate)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
@ -231,7 +244,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
|||||||
def saveFailedTest(data, expect, filename):
|
def saveFailedTest(data, expect, filename):
|
||||||
"""Upload failed test images to web server to allow CI test debugging.
|
"""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 = filename.split('/')
|
||||||
name.insert(-1, commit.strip())
|
name.insert(-1, commit.strip())
|
||||||
filename = '/'.join(name)
|
filename = '/'.join(name)
|
||||||
@ -272,9 +285,9 @@ def makePng(img):
|
|||||||
"""Given an array like (H, W, 4), return a PNG-encoded byte string.
|
"""Given an array like (H, W, 4), return a PNG-encoded byte string.
|
||||||
"""
|
"""
|
||||||
io = QtCore.QBuffer()
|
io = QtCore.QBuffer()
|
||||||
qim = fn.makeQImage(img, alpha=False)
|
qim = fn.makeQImage(img.transpose(1, 0, 2), alpha=False)
|
||||||
qim.save(io, format='png')
|
qim.save(io, 'PNG')
|
||||||
png = io.data().data().encode()
|
png = bytes(io.data().data())
|
||||||
return png
|
return png
|
||||||
|
|
||||||
|
|
||||||
@ -303,7 +316,7 @@ class ImageTester(QtGui.QWidget):
|
|||||||
|
|
||||||
QtGui.QWidget.__init__(self)
|
QtGui.QWidget.__init__(self)
|
||||||
self.resize(1200, 800)
|
self.resize(1200, 800)
|
||||||
self.showFullScreen()
|
#self.showFullScreen()
|
||||||
|
|
||||||
self.layout = QtGui.QGridLayout()
|
self.layout = QtGui.QGridLayout()
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
@ -321,6 +334,8 @@ class ImageTester(QtGui.QWidget):
|
|||||||
self.failBtn = QtGui.QPushButton('Fail')
|
self.failBtn = QtGui.QPushButton('Fail')
|
||||||
self.layout.addWidget(self.passBtn, 2, 0)
|
self.layout.addWidget(self.passBtn, 2, 0)
|
||||||
self.layout.addWidget(self.failBtn, 2, 1)
|
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.views = (self.view.addViewBox(row=0, col=0),
|
||||||
self.view.addViewBox(row=0, col=1),
|
self.view.addViewBox(row=0, col=1),
|
||||||
@ -383,6 +398,12 @@ class ImageTester(QtGui.QWidget):
|
|||||||
else:
|
else:
|
||||||
self.lastKey = str(event.text()).lower()
|
self.lastKey = str(event.text()).lower()
|
||||||
|
|
||||||
|
def passTest(self):
|
||||||
|
self.lastKey = 'p'
|
||||||
|
|
||||||
|
def failTest(self):
|
||||||
|
self.lastKey = 'f'
|
||||||
|
|
||||||
|
|
||||||
def getTestDataRepo():
|
def getTestDataRepo():
|
||||||
"""Return the path to a git repository with the required commit checked
|
"""Return the path to a git repository with the required commit checked
|
||||||
@ -531,3 +552,35 @@ def runSubprocess(command, return_code=False, **kwargs):
|
|||||||
raise sp.CalledProcessError(p.returncode, command)
|
raise sp.CalledProcessError(p.returncode, command)
|
||||||
|
|
||||||
return output
|
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')])
|
||||||
|
Loading…
Reference in New Issue
Block a user