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
This commit is contained in:
Luke Campagnola 2011-04-25 08:51:18 -04:00
parent 349561e133
commit 6783f4fa26
19 changed files with 418 additions and 54 deletions

View File

@ -9,8 +9,8 @@ class ColorButton(QtGui.QPushButton):
sigColorChanging = QtCore.Signal(object) ## emitted whenever a new color is picked in the color dialog 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) sigColorChanged = QtCore.Signal(object) ## emitted when the selected color is accepted (user clicks OK)
def __init__(self, color=(128,128,128)): def __init__(self, parent=None, color=(128,128,128)):
QtGui.QPushButton.__init__(self) QtGui.QPushButton.__init__(self, parent)
self.setColor(color) self.setColor(color)
self.colorDialog = QtGui.QColorDialog() self.colorDialog = QtGui.QColorDialog()
self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True)

View File

@ -91,6 +91,11 @@ class GraphicsView(QtGui.QGraphicsView):
#prof.finish() #prof.finish()
def close(self): def close(self):
self.centralWidget = None
self.scene().clear()
#print " ", self.scene().itemCount()
self.currentItem = None
self.sceneObj = None
self.closed = True self.closed = True
def useOpenGL(self, b=True): def useOpenGL(self, b=True):

View File

@ -152,6 +152,7 @@ class ImageView(QtGui.QWidget):
#QtGui.QWidget.__dtor__(self) #QtGui.QWidget.__dtor__(self)
def close(self): def close(self):
self.ui.roiPlot.close()
self.ui.graphicsView.close() self.ui.graphicsView.close()
self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage)
self.scene.clear() self.scene.clear()
@ -159,7 +160,6 @@ class ImageView(QtGui.QWidget):
del self.imageDisp del self.imageDisp
#self.image = None #self.image = None
#self.imageDisp = None #self.imageDisp = None
self.ui.roiPlot.close()
self.setParent(None) self.setParent(None)
def keyPressEvent(self, ev): def keyPressEvent(self, ev):

View File

@ -66,4 +66,6 @@ class MultiPlotItem(QtGui.QGraphicsWidget):
def close(self): def close(self):
for p in self.plots: for p in self.plots:
p[0].close() p[0].close()
self.plots = None
for i in range(self.layout.count()):
self.layout.removeAt(i)

View File

@ -40,4 +40,6 @@ class MultiPlotWidget(GraphicsView):
def close(self): def close(self):
self.mPlotItem.close() self.mPlotItem.close()
self.mPlotItem = None
self.setParent(None) self.setParent(None)
GraphicsView.close(self)

0
PIL_Fix/Image.py-1.6 Executable file → Normal file
View File

View File

@ -60,12 +60,13 @@ class PlotItem(QtGui.QGraphicsWidget):
self.autoBtn = QtGui.QToolButton() self.autoBtn = QtGui.QToolButton()
self.autoBtn.setText('A') self.autoBtn.setText('A')
self.autoBtn.hide() self.autoBtn.hide()
self.proxies = []
for b in [self.ctrlBtn, self.autoBtn]: for b in [self.ctrlBtn, self.autoBtn]:
proxy = QtGui.QGraphicsProxyWidget(self) proxy = QtGui.QGraphicsProxyWidget(self)
proxy.setWidget(b) proxy.setWidget(b)
proxy.setAcceptHoverEvents(False) proxy.setAcceptHoverEvents(False)
b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt") b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt")
self.proxies.append(proxy)
#QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked) #QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked)
self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) self.ctrlBtn.clicked.connect(self.ctrlBtnClicked)
#QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale) #QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale)
@ -155,9 +156,9 @@ class PlotItem(QtGui.QGraphicsWidget):
c.setupUi(w) c.setupUi(w)
dv = QtGui.QDoubleValidator(self) dv = QtGui.QDoubleValidator(self)
self.ctrlMenu = QtGui.QMenu() self.ctrlMenu = QtGui.QMenu()
ac = QtGui.QWidgetAction(self) self.menuAction = QtGui.QWidgetAction(self)
ac.setDefaultWidget(w) self.menuAction.setDefaultWidget(w)
self.ctrlMenu.addAction(ac) self.ctrlMenu.addAction(self.menuAction)
if HAVE_WIDGETGROUP: if HAVE_WIDGETGROUP:
self.stateGroup = WidgetGroup(self.ctrlMenu) self.stateGroup = WidgetGroup(self.ctrlMenu)
@ -284,9 +285,46 @@ class PlotItem(QtGui.QGraphicsWidget):
def close(self): def close(self):
#print "delete", 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: if self.manager is not None:
self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) self.manager.sigWidgetListChanged.disconnect(self.updatePlotList)
self.manager.removeWidget(self.name) self.manager.removeWidget(self.name)
#else:
#print "no manager"
def registerPlot(self, name): def registerPlot(self, name):
self.name = name self.name = name
@ -1042,6 +1080,8 @@ class PlotItem(QtGui.QGraphicsWidget):
#ev.accept() #ev.accept()
def resizeEvent(self, ev): 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.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height())
self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.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] del self.widgets[name]
#self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) #self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys())
self.sigWidgetListChanged.emit(self.widgets.keys()) self.sigWidgetListChanged.emit(self.widgets.keys())
else:
print "plot %s not managed" % name
def listWidgets(self): def listWidgets(self):

View File

@ -39,6 +39,7 @@ class PlotWidget(GraphicsView):
#self.scene().clear() #self.scene().clear()
#self.mPlotItem.close() #self.mPlotItem.close()
self.setParent(None) self.setParent(None)
GraphicsView.close(self)
def __getattr__(self, attr): ## implicitly wrap methods from plotItem def __getattr__(self, attr): ## implicitly wrap methods from plotItem
if hasattr(self.plotItem, attr): if hasattr(self.plotItem, attr):

View File

@ -6,7 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
""" """
from PyQt4 import QtCore from PyQt4 import QtCore
from math import acos import numpy as np
def clip(x, mn, mx): def clip(x, mn, mx):
if x > mx: if x > mx:
@ -99,17 +99,17 @@ class Point(QtCore.QPointF):
return (self[0]**2 + self[1]**2) ** 0.5 return (self[0]**2 + self[1]**2) ** 0.5
def angle(self, a): 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() n1 = self.length()
n2 = a.length() n2 = a.length()
if n1 == 0. or n2 == 0.: if n1 == 0. or n2 == 0.:
return None return None
## Probably this should be done with arctan2 instead.. ## 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) c = self.cross(a)
if c > 0: if c > 0:
ang *= -1. ang *= -1.
return ang return ang * 180. / np.pi
def dot(self, a): def dot(self, a):
"""Returns the dot product of a and this Point.""" """Returns the dot product of a and this Point."""
@ -120,6 +120,11 @@ class Point(QtCore.QPointF):
a = Point(a) a = Point(a)
return self[0]*a[1] - self[1]*a[0] 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): def __repr__(self):
return "Point(%f, %f)" % (self[0], self[1]) return "Point(%f, %f)" % (self[0], self[1])
@ -129,3 +134,6 @@ class Point(QtCore.QPointF):
def max(self): def max(self):
return max(self[0], self[1]) return max(self[0], self[1])
def copy(self):
return Point(self)

190
Transform.py Normal file
View File

@ -0,0 +1,190 @@
# -*- 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)

View File

@ -7,6 +7,8 @@ from graphicsWindows import *
#import PlotWidget #import PlotWidget
#import ImageView #import ImageView
from PyQt4 import QtGui from PyQt4 import QtGui
from Point import Point
from Transform import Transform
plots = [] plots = []
images = [] images = []

0
examples/test_Arrow.py Normal file → Executable file
View File

0
examples/test_ImageItem.py Normal file → Executable file
View File

0
examples/test_ImageView.py Normal file → Executable file
View File

0
examples/test_PlotWidget.py Normal file → Executable file
View File

0
examples/test_draw.py Normal file → Executable file
View File

0
examples/test_scatterPlot.py Normal file → Executable file
View File

View File

@ -256,8 +256,11 @@ class ImageItem(QtGui.QGraphicsObject):
def getLevels(self): def getLevels(self):
return self.whiteLevel, self.blackLevel return self.whiteLevel, self.blackLevel
def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None): def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None, axes=None):
axh = {'x': 0, 'y': 1, 'c': 2} if axes is None:
axh = {'x': 0, 'y': 1, 'c': 2}
else:
axh = axes
#print "Update image", black, white #print "Update image", black, white
if white is not None: if white is not None:
self.whiteLevel = white self.whiteLevel = white
@ -280,8 +283,12 @@ class ImageItem(QtGui.QGraphicsObject):
# Determine scale factors # Determine scale factors
if autoRange or self.blackLevel is None: if autoRange or self.blackLevel is None:
self.blackLevel = self.image.min() if self.image.dtype is np.ubyte:
self.whiteLevel = self.image.max() 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 #print "Image item using", self.blackLevel, self.whiteLevel
if self.blackLevel != self.whiteLevel: if self.blackLevel != self.whiteLevel:
@ -325,7 +332,6 @@ class ImageItem(QtGui.QGraphicsObject):
self.image.shape = shape self.image.shape = shape
im = ((self.image - black) * scale).clip(0.,255.).astype(np.ubyte) im = ((self.image - black) * scale).clip(0.,255.).astype(np.ubyte)
try: try:
im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte)
except: except:
@ -341,10 +347,13 @@ class ImageItem(QtGui.QGraphicsObject):
im1[..., 3] = alpha im1[..., 3] = alpha
elif im.ndim == 3: #color image elif im.ndim == 3: #color image
im2 = im.transpose(axh['y'], axh['x'], axh['c']) 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']]): 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): for i in range(im.shape[axh['c']], 3):
im1[..., i] = 0 im1[..., i] = 0
if im.shape[axh['c']] < 4: if im.shape[axh['c']] < 4:
@ -781,7 +790,7 @@ class PlotCurveItem(GraphicsObject):
def mouseMoveEvent(self, ev): def mouseMoveEvent(self, ev):
#GraphicsObject.mouseMoveEvent(self, ev) #GraphicsObject.mouseMoveEvent(self, ev)
self.mouseMoved = True self.mouseMoved = True
print "move" #print "move"
def mouseReleaseEvent(self, ev): def mouseReleaseEvent(self, ev):
#GraphicsObject.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])) p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1]))
p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) 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.resetTransform()
self.rotate(180+ ang * 180 / np.pi) self.rotate(180+ ang * 180 / np.pi) ## takes degrees
QtGui.QGraphicsItem.setPos(self, *newPos) QtGui.QGraphicsItem.setPos(self, *newPos)
return True return True
@ -940,7 +949,7 @@ class CurveArrow(CurvePoint):
class ScatterPlotItem(QtGui.QGraphicsWidget): 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): def __init__(self, spots=None, pxMode=True, pen=None, brush=None, size=5):
QtGui.QGraphicsWidget.__init__(self) QtGui.QGraphicsWidget.__init__(self)
@ -1027,7 +1036,7 @@ class ScatterPlotItem(QtGui.QGraphicsWidget):
pass pass
def pointClicked(self, point): def pointClicked(self, point):
self.sigPointClicked.emit(point) self.sigPointClicked.emit(self, point)
def points(self): def points(self):
return self.spots[:] return self.spots[:]
@ -1044,6 +1053,7 @@ class SpotItem(QtGui.QGraphicsWidget):
self.pen = pen self.pen = pen
self.brush = brush self.brush = brush
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
self.size = size
#s2 = size/2. #s2 = size/2.
self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
self.scale(size, size) self.scale(size, size)
@ -1236,7 +1246,7 @@ class LabelItem(QtGui.QGraphicsWidget):
def setAngle(self, angle): def setAngle(self, angle):
self.angle = angle self.angle = angle
self.item.resetMatrix() self.item.resetTransform()
self.item.rotate(angle) self.item.rotate(angle)
self.updateMin() self.updateMin()
@ -1311,6 +1321,11 @@ class ScaleItem(QtGui.QGraphicsWidget):
self.grid = False self.grid = False
def close(self):
self.scene().removeItem(self.label)
self.label = None
self.scene().removeItem(self)
def setGrid(self, grid): def setGrid(self, grid):
"""Set the alpha value for the grid, or False to disable.""" """Set the alpha value for the grid, or False to disable."""
self.grid = grid self.grid = grid
@ -1722,7 +1737,7 @@ class ViewBox(QtGui.QGraphicsWidget):
m = QtGui.QTransform() m = QtGui.QTransform()
## First center the viewport at 0 ## First center the viewport at 0
self.childGroup.resetMatrix() self.childGroup.resetTransform()
center = self.transform().inverted()[0].map(bounds.center()) center = self.transform().inverted()[0].map(bounds.center())
#print " transform to center:", center #print " transform to center:", center
if self.yInverted: if self.yInverted:
@ -2009,6 +2024,7 @@ class InfiniteLine(GraphicsObject):
self.currentPen = self.pen self.currentPen = self.pen
def setAngle(self, angle): def setAngle(self, angle):
"""Takes angle argument in degrees."""
self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135
self.updateLine() self.updateLine()

View File

@ -9,12 +9,15 @@ for use as region-of-interest markers. ROI class automatically handles extractio
of array data from ImageItems. 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 #from numpy import array, arccos, dot, pi, zeros, vstack, ubyte, fromfunction, ceil, floor, arctan2
import numpy as np import numpy as np
from numpy.linalg import norm from numpy.linalg import norm
import scipy.ndimage as ndimage import scipy.ndimage as ndimage
from Point import * from Point import *
from Transform import Transform
from math import cos, sin from math import cos, sin
import functions as fn import functions as fn
#from ObjectWorkaround import * #from ObjectWorkaround import *
@ -35,6 +38,8 @@ def rectStr(r):
class ROI(QtGui.QGraphicsObject): 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) sigRegionChangeFinished = QtCore.Signal(object)
sigRegionChangeStarted = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object)
@ -54,10 +59,11 @@ class ROI(QtGui.QGraphicsObject):
self.pen = fn.mkPen(pen) self.pen = fn.mkPen(pen)
self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255))
self.handles = [] 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.lastState = None
self.setPos(pos) self.setPos(pos)
self.rotate(-angle * 180. / np.pi) #self.rotate(-angle * 180. / np.pi)
self.rotate(-angle)
self.setZValue(10) self.setZValue(10)
self.isMoving = False self.isMoving = False
@ -114,7 +120,8 @@ class ROI(QtGui.QGraphicsObject):
def setAngle(self, angle, update=True): def setAngle(self, angle, update=True):
self.state['angle'] = angle self.state['angle'] = angle
tr = QtGui.QTransform() tr = QtGui.QTransform()
tr.rotate(-angle * 180 / np.pi) #tr.rotate(-angle * 180 / np.pi)
tr.rotate(angle)
self.setTransform(tr) self.setTransform(tr)
if update: if update:
self.updateHandles() self.updateHandles()
@ -129,10 +136,10 @@ class ROI(QtGui.QGraphicsObject):
pos = Point(pos) pos = Point(pos)
return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}) 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) pos = Point(pos)
center = Point(center) 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(): if pos.x() == center.x():
info['xoff'] = True info['xoff'] = True
if pos.y() == center.y(): if pos.y() == center.y():
@ -212,6 +219,11 @@ class ROI(QtGui.QGraphicsObject):
h['item'].hide() h['item'].hide()
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
## Bug: sometimes we get events we shouldn't.
p = ev.pos()
if not self.isMoving and not self.shape().contains(p):
ev.ignore()
return
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
self.setSelected(True) self.setSelected(True)
if self.translatable: if self.translatable:
@ -345,6 +357,11 @@ class ROI(QtGui.QGraphicsObject):
lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize
lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize
## preserve aspect ratio (this can override snapping)
if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier):
#arv = Point(self.preMoveState['size']) -
lp1 = lp1.proj(lp0)
## determine scale factors and new size of ROI ## determine scale factors and new size of ROI
hs = h['pos'] - c hs = h['pos'] - c
if hs[0] == 0: if hs[0] == 0:
@ -392,15 +409,16 @@ class ROI(QtGui.QGraphicsObject):
return return
## determine new rotation angle, constrained if necessary ## determine new rotation angle, constrained if necessary
ang = newState['angle'] + lp0.angle(lp1) ang = newState['angle'] - lp0.angle(lp1)
if ang is None: ## this should never happen.. if ang is None: ## this should never happen..
return return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
ang = round(ang / (np.pi/12.)) * (np.pi/12.) ang = round(ang / 15.) * 15. ## 180/12 = 15
## create rotation transform ## create rotation transform
tr = QtGui.QTransform() tr = QtGui.QTransform()
tr.rotate(-ang * 180. / np.pi) #tr.rotate(-ang * 180. / np.pi)
tr.rotate(ang)
## mvoe ROI so that center point remains stationary after rotate ## mvoe ROI so that center point remains stationary after rotate
cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos'])
@ -473,7 +491,8 @@ class ROI(QtGui.QGraphicsObject):
if ang is None: if ang is None:
return return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
ang = round(ang / (np.pi/12.)) * (np.pi/12.) #ang = round(ang / (np.pi/12.)) * (np.pi/12.)
ang = round(ang / 15.) * 15.
hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) hs = abs(h['pos'][scaleAxis] - c[scaleAxis])
newState['size'][scaleAxis] = lp1.length() / hs newState['size'][scaleAxis] = lp1.length() / hs
@ -484,7 +503,8 @@ class ROI(QtGui.QGraphicsObject):
c1 = c * newState['size'] c1 = c * newState['size']
tr = QtGui.QTransform() tr = QtGui.QTransform()
tr.rotate(-ang * 180. / np.pi) #tr.rotate(-ang * 180. / np.pi)
tr.rotate(-ang)
cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos'])
newState['angle'] = ang newState['angle'] = ang
@ -576,7 +596,8 @@ class ROI(QtGui.QGraphicsObject):
def stateRect(self, state): def stateRect(self, state):
r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1])
tr = QtGui.QTransform() tr = QtGui.QTransform()
tr.rotate(-state['angle'] * 180 / np.pi) #tr.rotate(-state['angle'] * 180 / np.pi)
tr.rotate(-state['angle'])
r = tr.mapRect(r) r = tr.mapRect(r)
return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1])
@ -751,11 +772,60 @@ class ROI(QtGui.QGraphicsObject):
### Untranspose array before returning ### Untranspose array before returning
#return arr5.transpose(tr2) #return arr5.transpose(tr2)
def getGlobalTransform(self, relativeTo=None):
"""Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn't specified,
then we use the state of the ROI when mouse is pressed."""
if relativeTo == None:
relativeTo = self.preMoveState
st = self.getState()
## this is only allowed because we will be comparing the two
relativeTo['scale'] = relativeTo['size']
st['scale'] = st['size']
t1 = Transform(relativeTo)
t2 = Transform(st)
return t2/t1
#st = self.getState()
### rotation
#ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358
#rot = QtGui.QTransform()
#rot.rotate(-ang)
### We need to come up with a universal transformation--one that can be applied to other objects
### such that all maintain alignment.
### More specifically, we need to turn the ROI's position and angle into
### a rotation _around the origin_ and a translation.
#p0 = Point(relativeTo['pos'])
### base position, rotated
#p1 = rot.map(p0)
#trans = Point(st['pos']) - p1
#return trans, ang
def applyGlobalTransform(self, tr):
st = self.getState()
st['scale'] = st['size']
st = Transform(st)
#trans = QtGui.QTransform()
#trans.translate(*translate)
#trans.rotate(-rotate)
#x2, y2 = trans.map(*st['pos'])
#self.setAngle(st['angle']+rotate*np.pi/180.)
#self.setPos([x2, y2])
st = (st * tr).saveState()
st['size'] = st['scale']
self.setState(st)
class Handle(QtGui.QGraphicsItem): class Handle(QtGui.QGraphicsItem):
@ -783,6 +853,7 @@ class Handle(QtGui.QGraphicsItem):
self.pen.setCosmetic(True) self.pen.setCosmetic(True)
self.isMoving = False self.isMoving = False
self.sides, self.startAng = self.types[typ] self.sides, self.startAng = self.types[typ]
self.buildPath()
def connectROI(self, roi, i): def connectROI(self, roi, i):
self.roi.append((roi, i)) self.roi.append((roi, i))
@ -791,6 +862,12 @@ class Handle(QtGui.QGraphicsItem):
return self.bounds return self.bounds
def mousePressEvent(self, ev): def mousePressEvent(self, ev):
# Bug: sometimes we get events not meant for us!
p = ev.pos()
if not self.isMoving and not self.path.contains(p):
ev.ignore()
return
#print "handle press" #print "handle press"
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
self.isMoving = True self.isMoving = True
@ -833,6 +910,20 @@ class Handle(QtGui.QGraphicsItem):
for r in self.roi: for r in self.roi:
r[0].movePoint(r[1], pos, modifiers) r[0].movePoint(r[1], pos, modifiers)
def buildPath(self):
size = self.radius
self.path = QtGui.QPainterPath()
ang = self.startAng
dt = 2*np.pi / self.sides
for i in range(0, self.sides+1):
x = size * cos(ang)
y = size * sin(ang)
ang += dt
if i == 0:
self.path.moveTo(x, y)
else:
self.path.lineTo(x, y)
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
## determine rotation of transform ## determine rotation of transform
m = self.sceneTransform() m = self.sceneTransform()
@ -851,15 +942,19 @@ class Handle(QtGui.QGraphicsItem):
self.prepareGeometryChange() self.prepareGeometryChange()
p.setRenderHints(p.Antialiasing, True) p.setRenderHints(p.Antialiasing, True)
p.setPen(self.pen) p.setPen(self.pen)
ang = self.startAng + va
dt = 2*np.pi / self.sides p.rotate(va * 180. / 3.1415926)
for i in range(0, self.sides): p.drawPath(self.path)
x1 = size * cos(ang)
y1 = size * sin(ang) #ang = self.startAng + va
x2 = size * cos(ang+dt) #dt = 2*np.pi / self.sides
y2 = size * sin(ang+dt) #for i in range(0, self.sides):
ang += dt #x1 = size * cos(ang)
p.drawLine(Point(x1, y1), Point(x2, y2)) #y1 = size * sin(ang)
#x2 = size * cos(ang+dt)
#y2 = size * sin(ang+dt)
#ang += dt
#p.drawLine(Point(x1, y1), Point(x2, y2))
@ -902,10 +997,11 @@ class LineROI(ROI):
d = pos2-pos1 d = pos2-pos1
l = d.length() l = d.length()
ang = Point(1, 0).angle(d) ang = Point(1, 0).angle(d)
c = Point(-width/2. * sin(ang), -width/2. * cos(ang)) ra = ang * np.pi / 180.
c = Point(-width/2. * sin(ra), -width/2. * cos(ra))
pos1 = pos1 + c pos1 = pos1 + c
ROI.__init__(self, pos1, size=Point(l, width), angle=ang*180/np.pi, **args) ROI.__init__(self, pos1, size=Point(l, width), angle=ang, **args)
self.addScaleRotateHandle([0, 0.5], [1, 0.5]) self.addScaleRotateHandle([0, 0.5], [1, 0.5])
self.addScaleRotateHandle([1, 0.5], [0, 0.5]) self.addScaleRotateHandle([1, 0.5], [0, 0.5])
self.addScaleHandle([0.5, 1], [0.5, 0.5]) self.addScaleHandle([0.5, 1], [0.5, 0.5])