Commit f258c3d8 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

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
parent 2c80098c
......@@ -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."""
......
......@@ -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
......
......@@ -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 *
......
......@@ -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)
......
......@@ -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:
......
......@@ -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()
## Allow mouse scale/pan
view.enableMouse()
## ..But lock the aspect ratio
w = pg.GraphicsView()
w.show()
w.resize(800,800)
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))
......
......@@ -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<n; i++ ) {
#float a = (sim(i)-offset) * (float)scale;
#if( a > 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
......@@ -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
......
......@@ -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
......
......@@ -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()
......
......@@ -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."""
......
......@@ -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)
......
......@@ -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):
......