From f258c3d87cd4326e00b2e0cc0b050ad3336d0d47 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 29 May 2012 23:18:34 -0400 Subject: [PATCH] minor bugfixes / features: - optional context menu for ImageItem - inverted y-axis in Canvas (+y now points upward) - extra __init__ arguments for Dock - Transform can be constructed from Matrix4x4 - many others --- GraphicsScene/mouseEvents.py | 3 +- Transform.py | 21 ++- __init__.py | 2 + canvas/Canvas.py | 2 +- dockarea/Dock.py | 12 +- examples/Draw.py | 18 +- graphicsItems/ImageItem.py | 291 ++++++++----------------------- graphicsItems/PlotCurveItem.py | 2 +- graphicsItems/ScaleBar.py | 2 +- graphicsItems/ViewBox/ViewBox.py | 24 ++- imageview/ImageView.py | 6 +- widgets/CheckTable.py | 2 +- widgets/JoystickButton.py | 2 + widgets/ValueLabel.py | 2 + 14 files changed, 144 insertions(+), 245 deletions(-) diff --git a/GraphicsScene/mouseEvents.py b/GraphicsScene/mouseEvents.py index ce991c84..bb24b758 100644 --- a/GraphicsScene/mouseEvents.py +++ b/GraphicsScene/mouseEvents.py @@ -32,6 +32,7 @@ class MouseDragEvent: self._buttons = moveEvent.buttons() self._button = pressEvent.button() self._modifiers = moveEvent.modifiers() + self.acceptedItem = None def accept(self): """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" @@ -160,7 +161,7 @@ class MouseClickEvent: self._buttons = pressEvent.buttons() self._modifiers = pressEvent.modifiers() self._time = ptime.time() - + self.acceptedItem = None def accept(self): """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" diff --git a/Transform.py b/Transform.py index 616b28a0..cdc9ceae 100644 --- a/Transform.py +++ b/Transform.py @@ -2,6 +2,7 @@ from .Qt import QtCore, QtGui from .Point import Point import numpy as np +import pyqtgraph as pg class Transform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate @@ -11,7 +12,9 @@ class Transform(QtGui.QTransform): QtGui.QTransform.__init__(self) self.reset() - if isinstance(init, dict): + if init is None: + return + elif isinstance(init, dict): self.restoreState(init) elif isinstance(init, Transform): self._state = { @@ -22,6 +25,10 @@ class Transform(QtGui.QTransform): self.update() elif isinstance(init, QtGui.QTransform): self.setFromQTransform(init) + elif isinstance(init, QtGui.QMatrix4x4): + self.setFromMatrix4x4(init) + else: + raise Exception("Cannot create Transform from input type: %s" % str(type(init))) def getScale(self): @@ -65,6 +72,18 @@ class Transform(QtGui.QTransform): } self.update() + def setFromMatrix4x4(self, m): + m = pg.Transform3D(m) + angle, axis = m.getRotation() + if angle != 0 and (axis[0] != 0 or axis[1] != 0 or axis[2] != 1): + raise Exception("Can only convert 4x4 matrix to 3x3 if rotation is around Z-axis.") + self._state = { + 'pos': Point(m.getTranslation()), + 'scale': Point(m.getScale()), + 'angle': angle + } + self.update() + def translate(self, *args): """Acceptable arguments are: x, y diff --git a/__init__.py b/__init__.py index 6cdb82af..3dd80d75 100644 --- a/__init__.py +++ b/__init__.py @@ -110,7 +110,9 @@ importAll('widgets', excludes=['MatplotlibWidget']) from .imageview import * from .WidgetGroup import * from .Point import Point +from .Vector import Vector from .Transform import Transform +from .Transform3D import Transform3D from .functions import * from .graphicsWindows import * from .SignalProxy import * diff --git a/canvas/Canvas.py b/canvas/Canvas.py index b7a27105..95293515 100644 --- a/canvas/Canvas.py +++ b/canvas/Canvas.py @@ -51,7 +51,7 @@ class Canvas(QtGui.QWidget): #self.view.enableMouse() self.view.setAspectLocked(True) - self.view.invertY() + #self.view.invertY() grid = GridItem() self.grid = CanvasItem(grid, name='Grid', movable=False) diff --git a/dockarea/Dock.py b/dockarea/Dock.py index 9c3c1605..63fafca8 100644 --- a/dockarea/Dock.py +++ b/dockarea/Dock.py @@ -7,14 +7,14 @@ class Dock(QtGui.QWidget, DockDrop): sigStretchChanged = QtCore.Signal() - def __init__(self, name, area=None, size=(10, 10)): + def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True): QtGui.QWidget.__init__(self) DockDrop.__init__(self) self.area = area self.label = DockLabel(name, self) self.labelHidden = False self.moveLabel = True ## If false, the dock is no longer allowed to move the label. - self.autoOrient = True + self.autoOrient = autoOrientation self.orientation = 'horizontal' #self.label.setAlignment(QtCore.Qt.AlignHCenter) self.topLayout = QtGui.QGridLayout() @@ -63,6 +63,12 @@ class Dock(QtGui.QWidget, DockDrop): self.widgetArea.setStyleSheet(self.hStyle) self.setStretch(*size) + + if widget is not None: + self.addWidget(widget) + + if hideTitle: + self.hideTitleBar() def implements(self, name=None): if name is None: @@ -108,7 +114,7 @@ class Dock(QtGui.QWidget, DockDrop): def setOrientation(self, o='auto', force=False): #print self.name(), "setOrientation", o, force - if o == 'auto': + if o == 'auto' and self.autoOrient: if self.container().type() == 'tab': o = 'horizontal' elif self.width() > self.height()*1.5: diff --git a/examples/Draw.py b/examples/Draw.py index e64b76b6..2e6d4297 100644 --- a/examples/Draw.py +++ b/examples/Draw.py @@ -9,21 +9,19 @@ import pyqtgraph as pg app = QtGui.QApplication([]) ## Create window with GraphicsView widget -win = QtGui.QMainWindow() -win.resize(800,800) -view = pg.GraphicsView() -#view.useOpenGL(True) -win.setCentralWidget(view) -win.show() +w = pg.GraphicsView() +w.show() +w.resize(800,800) -## Allow mouse scale/pan -view.enableMouse() -## ..But lock the aspect ratio +view = pg.ViewBox() +w.setCentralItem(view) + +## lock the aspect ratio view.setAspectLocked(True) ## Create image item img = pg.ImageItem(np.zeros((200,200))) -view.scene().addItem(img) +view.addItem(img) ## Set initial view bounds view.setRange(QtCore.QRectF(0, 0, 200, 200)) diff --git a/graphicsItems/ImageItem.py b/graphicsItems/ImageItem.py index d5555d4d..2a0cb336 100644 --- a/graphicsItems/ImageItem.py +++ b/graphicsItems/ImageItem.py @@ -30,9 +30,7 @@ class ImageItem(GraphicsObject): sigImageChanged = QtCore.Signal() - - ## performance gains from this are marginal, and it's rather unreliable. - useWeave = False + sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu def __init__(self, image=None, **kargs): """ @@ -48,7 +46,6 @@ class ImageItem(GraphicsObject): #self.clipMask = None self.paintMode = None - #self.useWeave = True self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None @@ -56,6 +53,7 @@ class ImageItem(GraphicsObject): #self.clipLevel = None self.drawKernel = None self.border = None + self.removable = False if image is not None: self.setImage(image, **kargs) @@ -160,6 +158,9 @@ class ImageItem(GraphicsObject): self.setCompositionMode(kargs['compositionMode']) if 'border' in kargs: self.setBorder(kargs['border']) + if 'removable' in kargs: + self.removable = kargs['removable'] + self.menu = None def setRect(self, rect): """Scale and translate the image to fit within rect (must be a QRect or QRectF).""" @@ -264,7 +265,6 @@ class ImageItem(GraphicsObject): argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) self.qimage = fn.makeQImage(argb, alpha) - #self.pixmap = QtGui.QPixmap.fromImage(self.qimage) prof.finish() @@ -324,21 +324,72 @@ class ImageItem(GraphicsObject): return 1,1 return br.width()/self.width(), br.height()/self.height() - def mousePressEvent(self, ev): + #def mousePressEvent(self, ev): + #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: + #self.drawAt(ev.pos(), ev) + #ev.accept() + #else: + #ev.ignore() + + #def mouseMoveEvent(self, ev): + ##print "mouse move", ev.pos() + #if self.drawKernel is not None: + #self.drawAt(ev.pos(), ev) + + #def mouseReleaseEvent(self, ev): + #pass + + def mouseDragEvent(self, ev): + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + return + + ev.accept() + self.drawAt(ev.pos(), ev) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + if self.raiseContextMenu(ev): + ev.accept() if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: self.drawAt(ev.pos(), ev) - ev.accept() - else: - ev.ignore() + + def raiseContextMenu(self, ev): + ## only raise menu if this terminal is removable + menu = self.getMenu() + if menu is None: + return False + menu = self.scene().addParentContextMenus(self, menu, ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + return True + + def getMenu(self): + if self.menu is None: + if not self.removable: + return None + self.menu = QtGui.QMenu() + self.menu.setTitle("Image") + remAct = QtGui.QAction("Remove image", self.menu) + remAct.triggered.connect(self.removeClicked) + self.menu.addAction(remAct) + self.menu.remAct = remAct + return self.menu + + + def hoverEvent(self, ev): + if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. + ev.acceptClicks(QtCore.Qt.RightButton) + #self.box.setBrush(fn.mkBrush('w')) + elif not ev.isExit() and self.removable: + ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks + #else: + #self.box.setBrush(self.brush) + #self.update() + + - def mouseMoveEvent(self, ev): - #print "mouse move", ev.pos() - if self.drawKernel is not None: - self.drawAt(ev.pos(), ev) - - def mouseReleaseEvent(self, ev): - pass - def tabletEvent(self, ev): print(ev.device()) print(ev.pointerType()) @@ -364,20 +415,10 @@ class ImageItem(GraphicsObject): ty[i] += dy1+dy2 sy[i] += dy1+dy2 - #print sx - #print sy - #print tx - #print ty - #print self.image.shape - #print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape - #print dk[sx[0]:sx[1], sy[0]:sy[1]].shape ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) - #src = dk[sx[0]:sx[1], sy[0]:sy[1]] - #mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]] mask = self.drawMask src = dk - #print self.image[ts].shape, src.shape if isinstance(self.drawMode, collections.Callable): self.drawMode(dk, self.image, mask, ss, ts, ev) @@ -401,197 +442,5 @@ class ImageItem(GraphicsObject): self.drawMode = mode self.drawMask = mask - - - - - #def setImage(self, image=None, copy=True, autoRange=True, clipMask=None, white=None, black=None, axes=None): - #prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self), disabled=True) - ##debug.printTrace() - #if axes is None: - #axh = {'x': 0, 'y': 1, 'c': 2} - #else: - #axh = axes - ##print "Update image", black, white - #if white is not None: - #self.whiteLevel = white - #if black is not None: - #self.blackLevel = black - - #gotNewData = False - #if image is None: - #if self.image is None: - #return - #else: - #gotNewData = True - #if self.image is None or image.shape != self.image.shape: - #self.prepareGeometryChange() - #if copy: - #self.image = image.view(np.ndarray).copy() - #else: - #self.image = image.view(np.ndarray) - ##print " image max:", self.image.max(), "min:", self.image.min() - #prof.mark('1') - - ## Determine scale factors - #if autoRange or self.blackLevel is None: - #if self.image.dtype is np.ubyte: - #self.blackLevel = 0 - #self.whiteLevel = 255 - #else: - #self.blackLevel = self.image.min() - #self.whiteLevel = self.image.max() - ##print "Image item using", self.blackLevel, self.whiteLevel - - #if self.blackLevel != self.whiteLevel: - #scale = 255. / (self.whiteLevel - self.blackLevel) - #else: - #scale = 0. - - #prof.mark('2') - - ### Recolor and convert to 8 bit per channel - ## Try using weave, then fall back to python - #shape = self.image.shape - #black = float(self.blackLevel) - #white = float(self.whiteLevel) - - #if black == 0 and white == 255 and self.image.dtype == np.ubyte: - #im = self.image - #elif self.image.dtype in [np.ubyte, np.uint16]: - ## use lookup table instead - #npts = 2**(self.image.itemsize * 8) - #lut = self.getLookupTable(npts, black, white) - #im = lut[self.image] - #else: - #im = self.applyColorScaling(self.image, black, scale) - - #prof.mark('3') - - #try: - #im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) - #except: - #print im.shape, axh - #raise - #alpha = np.clip(int(255 * self.alpha), 0, 255) - #prof.mark('4') - ## Fill image - #if im.ndim == 2: - #im2 = im.transpose(axh['y'], axh['x']) - #im1[..., 0] = im2 - #im1[..., 1] = im2 - #im1[..., 2] = im2 - #im1[..., 3] = alpha - #elif im.ndim == 3: #color image - #im2 = im.transpose(axh['y'], axh['x'], axh['c']) - #if im2.shape[2] > 4: - #raise Exception("ImageItem got image with more than 4 color channels (shape is %s; axes are %s)" % (str(im.shape), str(axh))) - ### [B G R A] Reorder colors - #order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - - #for i in range(0, im.shape[axh['c']]): - #im1[..., order[i]] = im2[..., i] - - ### fill in unused channels with 0 or alpha - #for i in range(im.shape[axh['c']], 3): - #im1[..., i] = 0 - #if im.shape[axh['c']] < 4: - #im1[..., 3] = alpha - - #else: - #raise Exception("Image must be 2 or 3 dimensions") - ##self.im1 = im1 - ## Display image - #prof.mark('5') - #if self.clipLevel is not None or clipMask is not None: - #if clipMask is not None: - #mask = clipMask.transpose() - #else: - #mask = (self.image < self.clipLevel).transpose() - #im1[..., 0][mask] *= 0.5 - #im1[..., 1][mask] *= 0.5 - #im1[..., 2][mask] = 255 - #prof.mark('6') - ##print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape - ##self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( - #prof.mark('7') - #try: - #buf = im1.data - #except AttributeError: - #im1 = np.ascontiguousarray(im1) - #buf = im1.data - - #qimage = QtGui.QImage(buf, im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) - #self.qimage = qimage - #self.qimage.data = im1 - #self._pixmap = None - #prof.mark('8') - - ##self.pixmap = QtGui.QPixmap.fromImage(qimage) - #prof.mark('9') - ###del self.ims - ##self.item.setPixmap(self.pixmap) - - #self.update() - #prof.mark('10') - - #if gotNewData: - ##self.emit(QtCore.SIGNAL('imageChanged')) - #self.sigImageChanged.emit() - - #prof.finish() - - #def getLookupTable(self, num, black, white): - #num = int(num) - #black = int(black) - #white = int(white) - #if white < black: - #b = black - #black = white - #white = b - #key = (num, black, white) - #lut = np.empty(num, dtype=np.ubyte) - #lut[:black] = 0 - #rng = lut[black:white] - #try: - #rng[:] = np.linspace(0, 255, white-black)[:len(rng)] - #except: - #print key, rng.shape - #lut[white:] = 255 - #return lut - - - #def applyColorScaling(self, img, offset, scale): - #try: - #if not ImageItem.useWeave: - #raise Exception('Skipping weave compile') - ##sim = np.ascontiguousarray(self.image) ## should not be needed - #sim = img.reshape(img.size) - ##sim.shape = sim.size - #im = np.empty(sim.shape, dtype=np.ubyte) - #n = im.size - - #code = """ - #for( int i=0; i 255.0 ) - #a = 255.0; - #else if( a < 0.0 ) - #a = 0.0; - #im(i) = a; - #} - #""" - - #weave.inline(code, ['sim', 'im', 'n', 'offset', 'scale'], type_converters=converters.blitz, compiler = 'gcc') - ##sim.shape = shape - #im.shape = img.shape - #except: - #if ImageItem.useWeave: - #ImageItem.useWeave = False - ##sys.excepthook(*sys.exc_info()) - ##print "==============================================================================" - ##print "Weave compile failed, falling back to slower version." - ##img.shape = shape - #im = ((img - offset) * scale).clip(0.,255.).astype(np.ubyte) - #return im - + def removeClicked(self): + self.sigRemoveRequested.emit(self) \ No newline at end of file diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index db4b677c..e141f96a 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -322,7 +322,7 @@ class PlotCurveItem(GraphicsObject): pixels = self.pixelVectors() - if pixels is None: + if pixels == (None, None): pixels = [Point(0,0), Point(0,0)] xmin = x.min() - pixels[0].x() * lineWidth xmax = x.max() + pixels[0].x() * lineWidth diff --git a/graphicsItems/ScaleBar.py b/graphicsItems/ScaleBar.py index 08fa94c3..961f07d7 100644 --- a/graphicsItems/ScaleBar.py +++ b/graphicsItems/ScaleBar.py @@ -22,7 +22,7 @@ class ScaleBar(UIGraphicsItem): rect = self.boundingRect() unit = self.pixelSize() - y = rect.bottom() + (rect.top()-rect.bottom()) * 0.02 + y = rect.top() + (rect.bottom()-rect.top()) * 0.02 y1 = y + unit[1]*self._width x = rect.right() + (rect.left()-rect.right()) * 0.02 x1 = x - self.size diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 8278326a..2b1d2dcc 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -327,7 +327,8 @@ class ViewBox(GraphicsWidget): changes[1] = yRange if len(changes) == 0: - raise Exception("Must specify at least one of rect, xRange, or yRange.") + print rect + raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect))) changed = [False, False] for ax, range in changes.items(): @@ -390,13 +391,17 @@ class ViewBox(GraphicsWidget): """ self.setRange(xRange=[min, max], update=update, padding=padding) - def autoRange(self, padding=0.02): + def autoRange(self, padding=0.02, item=None): """ Set the range of the view box to make all children visible. Note that this is not the same as enableAutoRange, which causes the view to automatically auto-range whenever its contents are changed. """ - bounds = self.childrenBoundingRect() + if item is None: + bounds = self.childrenBoundingRect() + else: + bounds = self.mapFromItemToView(item, item.boundingRect()).boundingRect() + if bounds is not None: self.setRange(bounds, padding=padding) @@ -738,6 +743,19 @@ class ViewBox(GraphicsWidget): return self.childGroup.mapToItem(item, obj) #return item.mapFromScene(self.mapViewToScene(obj)) + def mapViewToDevice(self, obj): + return self.mapToDevice(self.mapFromView(obj)) + + def mapDeviceToView(self, obj): + return self.mapToView(self.mapFromDevice(obj)) + + def viewPixelSize(self): + """Return the (width, height) of a screen pixel in view coordinates.""" + o = self.mapToView(Point(0,0)) + px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] + return (px.length(), py.length()) + + def itemBoundingRect(self, item): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() diff --git a/imageview/ImageView.py b/imageview/ImageView.py index e7b109f9..a847716d 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -175,7 +175,7 @@ class ImageView(QtGui.QWidget): self.roiClicked() ## initialize roi plot to correct shape / visibility - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None): """ Set the image to be displayed in the widget. @@ -284,6 +284,8 @@ class ImageView(QtGui.QWidget): self.imageItem.scale(*scale) if pos is not None: self.imageItem.setPos(*pos) + if transform is not None: + self.imageItem.setTransform(transform) prof.mark('6') if autoRange: @@ -325,7 +327,7 @@ class ImageView(QtGui.QWidget): image = self.getProcessedImage() #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) - self.view.setRange(self.imageItem.boundingRect(), padding=0.) + self.view.autoRange() ##setRange(self.imageItem.viewBoundingRect(), padding=0.) def getProcessedImage(self): """Returns the image data after it has been processed by any normalization options in use.""" diff --git a/widgets/CheckTable.py b/widgets/CheckTable.py index e133390d..3322113a 100644 --- a/widgets/CheckTable.py +++ b/widgets/CheckTable.py @@ -57,7 +57,7 @@ class CheckTable(QtGui.QWidget): def removeRow(self, name): row = self.rowNames.index(name) - self.oldRows[name] = self.saveState['rows'][name] ## save for later + self.oldRows[name] = self.saveState()['rows'][row] ## save for later self.rowNames.pop(row) for w in self.rowWidgets[row]: w.setParent(None) diff --git a/widgets/JoystickButton.py b/widgets/JoystickButton.py index 39326d14..201a957a 100644 --- a/widgets/JoystickButton.py +++ b/widgets/JoystickButton.py @@ -12,6 +12,8 @@ class JoystickButton(QtGui.QPushButton): self.setCheckable(True) self.state = None self.setState(0,0) + self.setFixedWidth(50) + self.setFixedHeight(50) def mousePressEvent(self, ev): diff --git a/widgets/ValueLabel.py b/widgets/ValueLabel.py index 9c6885ce..e32e6b14 100644 --- a/widgets/ValueLabel.py +++ b/widgets/ValueLabel.py @@ -47,6 +47,8 @@ class ValueLabel(QtGui.QLabel): self.formatStr = text self.update() + def setAverageTime(self, t): + self.averageTime = t def averageValue(self): return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values))