Commit 6783f4fa authored by Luke Campagnola's avatar Luke Campagnola
Browse files

sync changes from acq4:

 - numerous fixes in close() functions
 - added Transform class
 - ROI widgets now operate in degrees instead of radians for easier Qt compatibility
parent 349561e1
......@@ -9,8 +9,8 @@ class ColorButton(QtGui.QPushButton):
sigColorChanging = QtCore.Signal(object) ## emitted whenever a new color is picked in the color dialog
sigColorChanged = QtCore.Signal(object) ## emitted when the selected color is accepted (user clicks OK)
def __init__(self, color=(128,128,128)):
QtGui.QPushButton.__init__(self)
def __init__(self, parent=None, color=(128,128,128)):
QtGui.QPushButton.__init__(self, parent)
self.setColor(color)
self.colorDialog = QtGui.QColorDialog()
self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True)
......
......@@ -91,6 +91,11 @@ class GraphicsView(QtGui.QGraphicsView):
#prof.finish()
def close(self):
self.centralWidget = None
self.scene().clear()
#print " ", self.scene().itemCount()
self.currentItem = None
self.sceneObj = None
self.closed = True
def useOpenGL(self, b=True):
......
......@@ -152,6 +152,7 @@ class ImageView(QtGui.QWidget):
#QtGui.QWidget.__dtor__(self)
def close(self):
self.ui.roiPlot.close()
self.ui.graphicsView.close()
self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage)
self.scene.clear()
......@@ -159,7 +160,6 @@ class ImageView(QtGui.QWidget):
del self.imageDisp
#self.image = None
#self.imageDisp = None
self.ui.roiPlot.close()
self.setParent(None)
def keyPressEvent(self, ev):
......
......@@ -66,4 +66,6 @@ class MultiPlotItem(QtGui.QGraphicsWidget):
def close(self):
for p in self.plots:
p[0].close()
\ No newline at end of file
self.plots = None
for i in range(self.layout.count()):
self.layout.removeAt(i)
\ No newline at end of file
......@@ -40,4 +40,6 @@ class MultiPlotWidget(GraphicsView):
def close(self):
self.mPlotItem.close()
self.setParent(None)
\ No newline at end of file
self.mPlotItem = None
self.setParent(None)
GraphicsView.close(self)
\ No newline at end of file
File mode changed from 100755 to 100644
......@@ -60,12 +60,13 @@ class PlotItem(QtGui.QGraphicsWidget):
self.autoBtn = QtGui.QToolButton()
self.autoBtn.setText('A')
self.autoBtn.hide()
self.proxies = []
for b in [self.ctrlBtn, self.autoBtn]:
proxy = QtGui.QGraphicsProxyWidget(self)
proxy.setWidget(b)
proxy.setAcceptHoverEvents(False)
b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt")
self.proxies.append(proxy)
#QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked)
self.ctrlBtn.clicked.connect(self.ctrlBtnClicked)
#QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale)
......@@ -155,9 +156,9 @@ class PlotItem(QtGui.QGraphicsWidget):
c.setupUi(w)
dv = QtGui.QDoubleValidator(self)
self.ctrlMenu = QtGui.QMenu()
ac = QtGui.QWidgetAction(self)
ac.setDefaultWidget(w)
self.ctrlMenu.addAction(ac)
self.menuAction = QtGui.QWidgetAction(self)
self.menuAction.setDefaultWidget(w)
self.ctrlMenu.addAction(self.menuAction)
if HAVE_WIDGETGROUP:
self.stateGroup = WidgetGroup(self.ctrlMenu)
......@@ -284,9 +285,46 @@ class PlotItem(QtGui.QGraphicsWidget):
def close(self):
#print "delete", self
## All this crap is needed to avoid PySide trouble.
## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets)
## the solution is to manually remove all widgets before scene.clear() is called
if self.ctrlMenu is None: ## already shut down
return
self.ctrlMenu.setParent(None)
self.ctrlMenu = None
self.ctrlBtn.setParent(None)
self.ctrlBtn = None
self.autoBtn.setParent(None)
self.autoBtn = None
for k in self.scales:
i = self.scales[k]['item']
i.close()
self.scales = None
self.scene().removeItem(self.vb)
self.vb = None
for i in range(self.layout.count()):
self.layout.removeAt(i)
for p in self.proxies:
try:
p.setWidget(None)
except RuntimeError:
break
self.scene().removeItem(p)
self.proxies = []
self.menuAction.releaseWidget(self.menuAction.defaultWidget())
self.menuAction.setParent(None)
self.menuAction = None
if self.manager is not None:
self.manager.sigWidgetListChanged.disconnect(self.updatePlotList)
self.manager.removeWidget(self.name)
#else:
#print "no manager"
def registerPlot(self, name):
self.name = name
......@@ -1042,6 +1080,8 @@ class PlotItem(QtGui.QGraphicsWidget):
#ev.accept()
def resizeEvent(self, ev):
if self.ctrlBtn is None: ## already closed down
return
self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height())
self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height())
......@@ -1190,6 +1230,8 @@ class PlotWidgetManager(QtCore.QObject):
del self.widgets[name]
#self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys())
self.sigWidgetListChanged.emit(self.widgets.keys())
else:
print "plot %s not managed" % name
def listWidgets(self):
......
......@@ -39,6 +39,7 @@ class PlotWidget(GraphicsView):
#self.scene().clear()
#self.mPlotItem.close()
self.setParent(None)
GraphicsView.close(self)
def __getattr__(self, attr): ## implicitly wrap methods from plotItem
if hasattr(self.plotItem, attr):
......
......@@ -6,7 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from PyQt4 import QtCore
from math import acos
import numpy as np
def clip(x, mn, mx):
if x > mx:
......@@ -99,17 +99,17 @@ class Point(QtCore.QPointF):
return (self[0]**2 + self[1]**2) ** 0.5
def angle(self, a):
"""Returns the angle between this vector and the vector a."""
"""Returns the angle in degrees between this vector and the vector a."""
n1 = self.length()
n2 = a.length()
if n1 == 0. or n2 == 0.:
return None
## Probably this should be done with arctan2 instead..
ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians
ang = np.arccos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians
c = self.cross(a)
if c > 0:
ang *= -1.
return ang
return ang * 180. / np.pi
def dot(self, a):
"""Returns the dot product of a and this Point."""
......@@ -119,6 +119,11 @@ class Point(QtCore.QPointF):
def cross(self, a):
a = Point(a)
return self[0]*a[1] - self[1]*a[0]
def proj(self, b):
"""Return the projection of this vector onto the vector b"""
b1 = b / b.length()
return self.dot(b1) * b1
def __repr__(self):
return "Point(%f, %f)" % (self[0], self[1])
......@@ -128,4 +133,7 @@ class Point(QtCore.QPointF):
return min(self[0], self[1])
def max(self):
return max(self[0], self[1])
\ No newline at end of file
return max(self[0], self[1])
def copy(self):
return Point(self)
\ No newline at end of file
# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
from Point import Point
import numpy as np
class Transform(QtGui.QTransform):
"""Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate
This transform always has 0 shear."""
def __init__(self, init=None):
QtGui.QTransform.__init__(self)
self.reset()
if isinstance(init, dict):
self.restoreState(init)
elif isinstance(init, Transform):
self._state = {
'pos': Point(init._state['pos']),
'scale': Point(init._state['scale']),
'angle': init._state['angle']
}
self.update()
elif isinstance(init, QtGui.QTransform):
self.setFromQTransform(init)
def reset(self):
self._state = {
'pos': Point(0,0),
'scale': Point(1,1),
'angle': 0.0 ## in degrees
}
self.update()
def setFromQTransform(self, tr):
p1 = Point(tr.map(0., 0.))
p2 = Point(tr.map(1., 0.))
p3 = Point(tr.map(0., 1.))
dp2 = Point(p2-p1)
dp3 = Point(p3-p1)
## detect flipped axes
if dp2.angle(dp3) > 0:
da = 180
sy = -1.0
else:
da = 0
sy = 1.0
self._state = {
'pos': Point(p1),
'scale': Point(dp2.length(), dp3.length() * sy),
'angle': (np.arctan2(dp2[1], dp2[0]) * 180. / np.pi) + da
}
self.update()
def translate(self, *args):
"""Acceptable arguments are:
x, y
[x, y]
Point(x,y)"""
t = Point(*args)
self.setTranslate(self._state['pos']+t)
def setTranslate(self, *args):
"""Acceptable arguments are:
x, y
[x, y]
Point(x,y)"""
self._state['pos'] = Point(*args)
self.update()
def scale(self, *args):
"""Acceptable arguments are:
x, y
[x, y]
Point(x,y)"""
s = Point(*args)
self.setScale(self._state['scale'] * s)
def setScale(self, *args):
"""Acceptable arguments are:
x, y
[x, y]
Point(x,y)"""
self._state['scale'] = Point(*args)
self.update()
def rotate(self, angle):
"""Rotate the transformation by angle (in degrees)"""
self.setRotate(self._state['angle'] + angle)
def setRotate(self, angle):
"""Set the transformation rotation to angle (in degrees)"""
self._state['angle'] = angle
self.update()
def __div__(self, t):
"""A / B == B^-1 * A"""
dt = t.inverted()[0] * self
return Transform(dt)
def __mul__(self, t):
return Transform(QtGui.QTransform.__mul__(self, t))
def saveState(self):
p = self._state['pos']
s = self._state['scale']
if s[0] == 0:
raise Exception('Invalid scale')
return {'pos': (p[0], p[1]), 'scale': (s[0], s[1]), 'angle': self._state['angle']}
def restoreState(self, state):
self._state['pos'] = Point(state.get('pos', (0,0)))
self._state['scale'] = Point(state.get('scale', (1.,1.)))
self._state['angle'] = state.get('angle', 0)
self.update()
def update(self):
QtGui.QTransform.reset(self)
## modifications to the transform are multiplied on the right, so we need to reverse order here.
QtGui.QTransform.translate(self, *self._state['pos'])
QtGui.QTransform.rotate(self, self._state['angle'])
QtGui.QTransform.scale(self, *self._state['scale'])
def __repr__(self):
return str(self.saveState())
def matrix(self):
return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]])
if __name__ == '__main__':
import widgets
import GraphicsView
from functions import *
app = QtGui.QApplication([])
win = QtGui.QMainWindow()
win.show()
cw = GraphicsView.GraphicsView()
#cw.enableMouse()
win.setCentralWidget(cw)
s = QtGui.QGraphicsScene()
cw.setScene(s)
b = QtGui.QGraphicsRectItem(-5, -5, 10, 10)
b.setPen(QtGui.QPen(mkPen('y')))
t1 = QtGui.QGraphicsTextItem()
t1.setHtml('<span style="color: #F00">R</span>')
s.addItem(b)
s.addItem(t1)
tr1 = Transform()
tr2 = Transform()
tr3 = QtGui.QTransform()
tr3.translate(20, 0)
tr3.rotate(45)
print "QTransform -> Transform:", Transform(tr3)
print "tr1:", tr1
tr2.translate(20, 0)
tr2.rotate(45)
print "tr2:", tr2
dt = tr2/tr1
print "tr2 / tr1 = ", dt
print "tr2 * tr1 = ", tr2*tr1
tr4 = Transform()
tr4.scale(-1, 1)
tr4.rotate(30)
print "tr1 * tr4 = ", tr1*tr4
w1 = widgets.TestROI((0,0), (50, 50))
w2 = widgets.TestROI((0,0), (150, 150))
s.addItem(w1)
s.addItem(w2)
w1Base = w1.getState()
w2Base = w2.getState()
def update():
tr1 = w1.getGlobalTransform(w1Base)
tr2 = w2.getGlobalTransform(w2Base)
t1.setTransform(tr1 * tr2)
w1.setState(w1Base)
w1.applyGlobalTransform(tr2)
w1.sigRegionChanged.connect(update)
w2.sigRegionChanged.connect(update)
\ No newline at end of file
......@@ -7,6 +7,8 @@ from graphicsWindows import *
#import PlotWidget
#import ImageView
from PyQt4 import QtGui
from Point import Point
from Transform import Transform
plots = []
images = []
......
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
......@@ -256,8 +256,11 @@ class ImageItem(QtGui.QGraphicsObject):
def getLevels(self):
return self.whiteLevel, self.blackLevel
def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None):
axh = {'x': 0, 'y': 1, 'c': 2}
def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None, axes=None):
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
......@@ -280,8 +283,12 @@ class ImageItem(QtGui.QGraphicsObject):
# Determine scale factors
if autoRange or self.blackLevel is None:
self.blackLevel = self.image.min()
self.whiteLevel = self.image.max()
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:
......@@ -324,7 +331,6 @@ class ImageItem(QtGui.QGraphicsObject):
print "Weave compile failed, falling back to slower version."
self.image.shape = shape
im = ((self.image - black) * scale).clip(0.,255.).astype(np.ubyte)
try:
im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte)
......@@ -341,10 +347,13 @@ class ImageItem(QtGui.QGraphicsObject):
im1[..., 3] = alpha
elif im.ndim == 3: #color image
im2 = im.transpose(axh['y'], axh['x'], axh['c'])
## [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[..., 2-i] = im2[..., i] ## for some reason, the colors line up as BGR in the final image.
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:
......@@ -781,7 +790,7 @@ class PlotCurveItem(GraphicsObject):
def mouseMoveEvent(self, ev):
#GraphicsObject.mouseMoveEvent(self, ev)
self.mouseMoved = True
print "move"
#print "move"
def mouseReleaseEvent(self, ev):
#GraphicsObject.mouseReleaseEvent(self, ev)
......@@ -848,9 +857,9 @@ class CurvePoint(QtGui.QGraphicsObject):
p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1]))
p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2]))
ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x())
ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians
self.resetTransform()
self.rotate(180+ ang * 180 / np.pi)
self.rotate(180+ ang * 180 / np.pi) ## takes degrees
QtGui.QGraphicsItem.setPos(self, *newPos)
return True
......@@ -940,7 +949,7 @@ class CurveArrow(CurvePoint):
class ScatterPlotItem(QtGui.QGraphicsWidget):
sigPointClicked = QtCore.Signal(object)
sigPointClicked = QtCore.Signal(object, object)
def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5):
QtGui.QGraphicsWidget.__init__(self)
......@@ -1027,7 +1036,7 @@ class ScatterPlotItem(QtGui.QGraphicsWidget):
pass
def pointClicked(self, point):
self.sigPointClicked.emit(point)
self.sigPointClicked.emit(self, point)
def points(self):
return self.spots[:]
......@@ -1044,6 +1053,7 @@ class SpotItem(QtGui.QGraphicsWidget):
self.pen = pen
self.brush = brush
self.path = QtGui.QPainterPath()
self.size = size
#s2 = size/2.
self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
self.scale(size, size)
......@@ -1236,7 +1246,7 @@ class LabelItem(QtGui.QGraphicsWidget):
def setAngle(self, angle):
self.angle = angle
self.item.resetMatrix()
self.item.resetTransform()
self.item.rotate(angle)
self.updateMin()
......@@ -1310,6 +1320,11 @@ class ScaleItem(QtGui.QGraphicsWidget):
self.grid = False
def close(self):
self.scene().removeItem(self.label)
self.label = None
self.scene().removeItem(self)
def setGrid(self, grid):
"""Set the alpha value for the grid, or False to disable."""
......@@ -1722,7 +1737,7 @@ class ViewBox(QtGui.QGraphicsWidget):
m = QtGui.QTransform()
## First center the viewport at 0
self.childGroup.resetMatrix()
self.childGroup.resetTransform()
center = self.transform().inverted()[0].map(bounds.center())
#print " transform to center:", center
if self.yInverted:
......@@ -2009,6 +2024,7 @@ class InfiniteLine(GraphicsObject):
self.currentPen = self.pen
def setAngle(self, angle):
"""Takes angle argument in degrees."""
self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135
self.updateLine()
......
......@@ -9,12 +9,15 @@ for use as region-of-interest markers. ROI class automatically handles extractio
of array data from ImageItems.
"""
from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg
from PyQt4 import QtCore, QtGui
if not hasattr(QtCore, 'Signal'):
QtCore.Signal = QtCore.pyqtSignal
#from numpy import array, arccos, dot, pi, zeros, vstack, ubyte, fromfunction, ceil, floor, arctan2
import numpy as np
from numpy.linalg import norm
import scipy.ndimage as ndimage
from Point import *
from Transform import Transform
from math import cos, sin
import functions as fn
#from ObjectWorkaround import *
......@@ -35,6 +38,8 @@ def rectStr(r):
class ROI(QtGui.QGraphicsObject):
"""Generic region-of-interest widget.
Can be used for implementing many types of selection box with rotate/translate/scale handles."""
sigRegionChangeFinished = QtCore.Signal(object)
sigRegionChangeStarted = QtCore.Signal(object)
......@@ -54,10 +59,11 @@ class ROI(QtGui.QGraphicsObject):
self.pen = fn.mkPen(pen)
self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255))
self.handles = []
self.state = {'pos': pos, 'size': size, 'angle': angle}
self.state = {'pos': pos, 'size': size, 'angle': angle} ## angle is in degrees for ease of Qt integration
self.lastState = None
self.setPos(pos)
self.rotate(-angle * 180. / np.pi)
#self.rotate(-angle * 180. / np.pi)
self.rotate(-angle)
self.setZValue(10)
self.isMoving = False
......@@ -114,7 +120,8 @@ class ROI(QtGui.QGraphicsObject):
def setAngle(self, angle, update=True):
self.state['angle'] = angle
tr = QtGui.QTransform()
tr.rotate(-angle * 180 / np.pi)
#tr.rotate(-angle * 180 / np.pi)
tr.rotate(angle)
self.setTransform(tr)
if update:
self.updateHandles()
......@@ -129,10 +136,10 @@ class ROI(QtGui.QGraphicsObject):
pos = Point(pos)
return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item})
def addScaleHandle(self, pos, center, axes=None, item=None, name=None):
def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False):
pos = Point(pos)
center = Point(center)
info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item}
info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect}
if pos.x() == center.x():
info['xoff'] = True