Merge remote-tracking branch 'pyqtgraph/develop' into u55-u55-patch-1

This commit is contained in:
Luke Campagnola 2016-01-30 00:54:29 -08:00
commit 586de3176e
8 changed files with 357 additions and 251 deletions

View File

@ -91,6 +91,8 @@ Mesh Generation Functions
Miscellaneous Functions Miscellaneous Functions
----------------------- -----------------------
.. autofunction:: pyqtgraph.eq
.. autofunction:: pyqtgraph.arrayToQPath .. autofunction:: pyqtgraph.arrayToQPath
.. autofunction:: pyqtgraph.pseudoScatter .. autofunction:: pyqtgraph.pseudoScatter

View File

@ -4,72 +4,27 @@ from ..widgets.GraphicsView import GraphicsView
from ..GraphicsScene import GraphicsScene from ..GraphicsScene import GraphicsScene
from ..graphicsItems.ViewBox import ViewBox from ..graphicsItems.ViewBox import ViewBox
#class FlowchartGraphicsView(QtGui.QGraphicsView):
class FlowchartGraphicsView(GraphicsView): class FlowchartGraphicsView(GraphicsView):
sigHoverOver = QtCore.Signal(object) sigHoverOver = QtCore.Signal(object)
sigClicked = QtCore.Signal(object) sigClicked = QtCore.Signal(object)
def __init__(self, widget, *args): def __init__(self, widget, *args):
#QtGui.QGraphicsView.__init__(self, *args)
GraphicsView.__init__(self, *args, useOpenGL=False) GraphicsView.__init__(self, *args, useOpenGL=False)
#self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255)))
self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True) self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True)
self.setCentralItem(self._vb) self.setCentralItem(self._vb)
#self.scene().addItem(self.vb)
#self.setMouseTracking(True)
#self.lastPos = None
#self.setTransformationAnchor(self.AnchorViewCenter)
#self.setRenderHints(QtGui.QPainter.Antialiasing)
self.setRenderHint(QtGui.QPainter.Antialiasing, True) self.setRenderHint(QtGui.QPainter.Antialiasing, True)
#self.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
#self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect)
def viewBox(self): def viewBox(self):
return self._vb return self._vb
#def mousePressEvent(self, ev):
#self.moved = False
#self.lastPos = ev.pos()
#return QtGui.QGraphicsView.mousePressEvent(self, ev)
#def mouseMoveEvent(self, ev):
#self.moved = True
#callSuper = False
#if ev.buttons() & QtCore.Qt.RightButton:
#if self.lastPos is not None:
#dif = ev.pos() - self.lastPos
#self.scale(1.01**-dif.y(), 1.01**-dif.y())
#elif ev.buttons() & QtCore.Qt.MidButton:
#if self.lastPos is not None:
#dif = ev.pos() - self.lastPos
#self.translate(dif.x(), -dif.y())
#else:
##self.emit(QtCore.SIGNAL('hoverOver'), self.items(ev.pos()))
#self.sigHoverOver.emit(self.items(ev.pos()))
#callSuper = True
#self.lastPos = ev.pos()
#if callSuper:
#QtGui.QGraphicsView.mouseMoveEvent(self, ev)
#def mouseReleaseEvent(self, ev):
#if not self.moved:
##self.emit(QtCore.SIGNAL('clicked'), ev)
#self.sigClicked.emit(ev)
#return QtGui.QGraphicsView.mouseReleaseEvent(self, ev)
class FlowchartViewBox(ViewBox): class FlowchartViewBox(ViewBox):
def __init__(self, widget, *args, **kwargs): def __init__(self, widget, *args, **kwargs):
ViewBox.__init__(self, *args, **kwargs) ViewBox.__init__(self, *args, **kwargs)
self.widget = widget self.widget = widget
#self.menu = None
#self._subMenus = None ## need a place to store the menus otherwise they dissappear (even though they've been added to other menus) ((yes, it doesn't make sense))
def getMenu(self, ev): def getMenu(self, ev):
## called by ViewBox to create a new context menu ## called by ViewBox to create a new context menu
@ -84,26 +39,3 @@ class FlowchartViewBox(ViewBox):
menu = self.widget.buildMenu(ev.scenePos()) menu = self.widget.buildMenu(ev.scenePos())
menu.setTitle("Add node") menu.setTitle("Add node")
return [menu, ViewBox.getMenu(self, ev)] return [menu, ViewBox.getMenu(self, ev)]
##class FlowchartGraphicsScene(QtGui.QGraphicsScene):
#class FlowchartGraphicsScene(GraphicsScene):
#sigContextMenuEvent = QtCore.Signal(object)
#def __init__(self, *args):
##QtGui.QGraphicsScene.__init__(self, *args)
#GraphicsScene.__init__(self, *args)
#def mouseClickEvent(self, ev):
##QtGui.QGraphicsScene.contextMenuEvent(self, ev)
#if not ev.button() in [QtCore.Qt.RightButton]:
#self.sigContextMenuEvent.emit(ev)

View File

@ -6,7 +6,6 @@ from .Terminal import *
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
from ..debug import * from ..debug import *
import numpy as np import numpy as np
from .eq import *
def strDict(d): def strDict(d):
@ -261,7 +260,7 @@ class Node(QtCore.QObject):
for k, v in args.items(): for k, v in args.items():
term = self._inputs[k] term = self._inputs[k]
oldVal = term.value() oldVal = term.value()
if not eq(oldVal, v): if not fn.eq(oldVal, v):
changed = True changed = True
term.setValue(v, process=False) term.setValue(v, process=False)
if changed and '_updatesHandled_' not in args: if changed and '_updatesHandled_' not in args:

View File

@ -4,8 +4,7 @@ import weakref
from ..graphicsItems.GraphicsObject import GraphicsObject from ..graphicsItems.GraphicsObject import GraphicsObject
from .. import functions as fn from .. import functions as fn
from ..Point import Point from ..Point import Point
#from PySide import QtCore, QtGui
from .eq import *
class Terminal(object): class Terminal(object):
def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None): def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None):
@ -29,9 +28,6 @@ class Terminal(object):
============== ================================================================================= ============== =================================================================================
""" """
self._io = io self._io = io
#self._isOutput = opts[0] in ['out', 'io']
#self._isInput = opts[0]] in ['in', 'io']
#self._isIO = opts[0]=='io'
self._optional = optional self._optional = optional
self._multi = multi self._multi = multi
self._node = weakref.ref(node) self._node = weakref.ref(node)
@ -68,7 +64,7 @@ class Terminal(object):
"""If this is a single-value terminal, val should be a single value. """If this is a single-value terminal, val should be a single value.
If this is a multi-value terminal, val should be a dict of terminal:value pairs""" If this is a multi-value terminal, val should be a dict of terminal:value pairs"""
if not self.isMultiValue(): if not self.isMultiValue():
if eq(val, self._value): if fn.eq(val, self._value):
return return
self._value = val self._value = val
else: else:
@ -81,11 +77,6 @@ class Terminal(object):
if self.isInput() and process: if self.isInput() and process:
self.node().update() self.node().update()
## Let the flowchart handle this.
#if self.isOutput():
#for c in self.connections():
#if c.isInput():
#c.inputChanged(self)
self.recolor() self.recolor()
def setOpts(self, **opts): def setOpts(self, **opts):
@ -94,7 +85,6 @@ class Terminal(object):
self._multiable = opts.get('multiable', self._multiable) self._multiable = opts.get('multiable', self._multiable)
if 'multi' in opts: if 'multi' in opts:
self.setMultiValue(opts['multi']) self.setMultiValue(opts['multi'])
def connected(self, term): def connected(self, term):
"""Called whenever this terminal has been connected to another. (note--this function is called on both terminals)""" """Called whenever this terminal has been connected to another. (note--this function is called on both terminals)"""
@ -109,12 +99,10 @@ class Terminal(object):
if self.isMultiValue() and term in self._value: if self.isMultiValue() and term in self._value:
del self._value[term] del self._value[term]
self.node().update() self.node().update()
#self.recolor()
else: else:
if self.isInput(): if self.isInput():
self.setValue(None) self.setValue(None)
self.node().disconnected(self, term) self.node().disconnected(self, term)
#self.node().update()
def inputChanged(self, term, process=True): def inputChanged(self, term, process=True):
"""Called whenever there is a change to the input value to this terminal. """Called whenever there is a change to the input value to this terminal.
@ -178,7 +166,6 @@ class Terminal(object):
return term in self.connections() return term in self.connections()
def hasInput(self): def hasInput(self):
#conn = self.extendedConnections()
for t in self.connections(): for t in self.connections():
if t.isOutput(): if t.isOutput():
return True return True
@ -186,17 +173,10 @@ class Terminal(object):
def inputTerminals(self): def inputTerminals(self):
"""Return the terminal(s) that give input to this one.""" """Return the terminal(s) that give input to this one."""
#terms = self.extendedConnections()
#for t in terms:
#if t.isOutput():
#return t
return [t for t in self.connections() if t.isOutput()] return [t for t in self.connections() if t.isOutput()]
def dependentNodes(self): def dependentNodes(self):
"""Return the list of nodes which receive input from this terminal.""" """Return the list of nodes which receive input from this terminal."""
#conn = self.extendedConnections()
#del conn[self]
return set([t.node() for t in self.connections() if t.isInput()]) return set([t.node() for t in self.connections() if t.isInput()])
def connectTo(self, term, connectionItem=None): def connectTo(self, term, connectionItem=None):
@ -210,12 +190,6 @@ class Terminal(object):
for t in [self, term]: for t in [self, term]:
if t.isInput() and not t._multi and len(t.connections()) > 0: if t.isInput() and not t._multi and len(t.connections()) > 0:
raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, list(t.connections().keys()))) raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, list(t.connections().keys())))
#if self.hasInput() and term.hasInput():
#raise Exception('Target terminal already has input')
#if term in self.node().terminals.values():
#if self.isOutput() or term.isOutput():
#raise Exception('Can not connect an output back to the same node.')
except: except:
if connectionItem is not None: if connectionItem is not None:
connectionItem.close() connectionItem.close()
@ -223,18 +197,12 @@ class Terminal(object):
if connectionItem is None: if connectionItem is None:
connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem()) connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem())
#self.graphicsItem().scene().addItem(connectionItem)
self.graphicsItem().getViewBox().addItem(connectionItem) self.graphicsItem().getViewBox().addItem(connectionItem)
#connectionItem.setParentItem(self.graphicsItem().parent().parent())
self._connections[term] = connectionItem self._connections[term] = connectionItem
term._connections[self] = connectionItem term._connections[self] = connectionItem
self.recolor() self.recolor()
#if self.isOutput() and term.isInput():
#term.inputChanged(self)
#if term.isInput() and term.isOutput():
#self.inputChanged(term)
self.connected(term) self.connected(term)
term.connected(self) term.connected(self)
@ -244,8 +212,6 @@ class Terminal(object):
if not self.connectedTo(term): if not self.connectedTo(term):
return return
item = self._connections[term] item = self._connections[term]
#print "removing connection", item
#item.scene().removeItem(item)
item.close() item.close()
del self._connections[term] del self._connections[term]
del term._connections[self] del term._connections[self]
@ -254,10 +220,6 @@ class Terminal(object):
self.disconnected(term) self.disconnected(term)
term.disconnected(self) term.disconnected(self)
#if self.isOutput() and term.isInput():
#term.inputChanged(self)
#if term.isInput() and term.isOutput():
#self.inputChanged(term)
def disconnectAll(self): def disconnectAll(self):
@ -270,7 +232,7 @@ class Terminal(object):
color = QtGui.QColor(0,0,0) color = QtGui.QColor(0,0,0)
elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals
color = QtGui.QColor(200,200,0) color = QtGui.QColor(200,200,0)
elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) elif self._value is None or fn.eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error)
color = QtGui.QColor(255,255,255) color = QtGui.QColor(255,255,255)
elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok
color = QtGui.QColor(200, 200, 0) color = QtGui.QColor(200, 200, 0)
@ -283,7 +245,6 @@ class Terminal(object):
if recurse: if recurse:
for t in self.connections(): for t in self.connections():
t.recolor(color, recurse=False) t.recolor(color, recurse=False)
def rename(self, name): def rename(self, name):
oldName = self._name oldName = self._name
@ -294,17 +255,6 @@ class Terminal(object):
def __repr__(self): def __repr__(self):
return "<Terminal %s.%s>" % (str(self.node().name()), str(self.name())) return "<Terminal %s.%s>" % (str(self.node().name()), str(self.name()))
#def extendedConnections(self, terms=None):
#"""Return list of terminals (including this one) that are directly or indirectly wired to this."""
#if terms is None:
#terms = {}
#terms[self] = None
#for t in self._connections:
#if t in terms:
#continue
#terms.update(t.extendedConnections(terms))
#return terms
def __hash__(self): def __hash__(self):
return id(self) return id(self)
@ -318,18 +268,15 @@ class Terminal(object):
return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable} return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable}
#class TerminalGraphicsItem(QtGui.QGraphicsItem):
class TerminalGraphicsItem(GraphicsObject): class TerminalGraphicsItem(GraphicsObject):
def __init__(self, term, parent=None): def __init__(self, term, parent=None):
self.term = term self.term = term
#QtGui.QGraphicsItem.__init__(self, parent)
GraphicsObject.__init__(self, parent) GraphicsObject.__init__(self, parent)
self.brush = fn.mkBrush(0,0,0) self.brush = fn.mkBrush(0,0,0)
self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self) self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self)
self.label = QtGui.QGraphicsTextItem(self.term.name(), self) self.label = QtGui.QGraphicsTextItem(self.term.name(), self)
self.label.scale(0.7, 0.7) self.label.scale(0.7, 0.7)
#self.setAcceptHoverEvents(True)
self.newConnection = None self.newConnection = None
self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem
if self.term.isRenamable(): if self.term.isRenamable():
@ -338,7 +285,6 @@ class TerminalGraphicsItem(GraphicsObject):
self.label.keyPressEvent = self.labelKeyPress self.label.keyPressEvent = self.labelKeyPress
self.setZValue(1) self.setZValue(1)
self.menu = None self.menu = None
def labelFocusOut(self, ev): def labelFocusOut(self, ev):
QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev) QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev)
@ -471,8 +417,6 @@ class TerminalGraphicsItem(GraphicsObject):
break break
if not gotTarget: if not gotTarget:
#print "remove unused connection"
#self.scene().removeItem(self.newConnection)
self.newConnection.close() self.newConnection.close()
self.newConnection = None self.newConnection = None
else: else:
@ -488,12 +432,6 @@ class TerminalGraphicsItem(GraphicsObject):
self.box.setBrush(self.brush) self.box.setBrush(self.brush)
self.update() self.update()
#def hoverEnterEvent(self, ev):
#self.hover = True
#def hoverLeaveEvent(self, ev):
#self.hover = False
def connectPoint(self): def connectPoint(self):
## return the connect position of this terminal in view coords ## return the connect position of this terminal in view coords
return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center())) return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center()))
@ -503,11 +441,9 @@ class TerminalGraphicsItem(GraphicsObject):
item.updateLine() item.updateLine()
#class ConnectionItem(QtGui.QGraphicsItem):
class ConnectionItem(GraphicsObject): class ConnectionItem(GraphicsObject):
def __init__(self, source, target=None): def __init__(self, source, target=None):
#QtGui.QGraphicsItem.__init__(self)
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.setFlags( self.setFlags(
self.ItemIsSelectable | self.ItemIsSelectable |
@ -528,14 +464,12 @@ class ConnectionItem(GraphicsObject):
'selectedColor': (200, 200, 0), 'selectedColor': (200, 200, 0),
'selectedWidth': 3.0, 'selectedWidth': 3.0,
} }
#self.line = QtGui.QGraphicsLineItem(self)
self.source.getViewBox().addItem(self) self.source.getViewBox().addItem(self)
self.updateLine() self.updateLine()
self.setZValue(0) self.setZValue(0)
def close(self): def close(self):
if self.scene() is not None: if self.scene() is not None:
#self.scene().removeItem(self.line)
self.scene().removeItem(self) self.scene().removeItem(self)
def setTarget(self, target): def setTarget(self, target):
@ -575,8 +509,11 @@ class ConnectionItem(GraphicsObject):
return path return path
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
if not self.isSelected():
ev.ignore()
return
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
#if isinstance(self.target, TerminalGraphicsItem):
self.source.disconnect(self.target) self.source.disconnect(self.target)
ev.accept() ev.accept()
else: else:
@ -590,6 +527,7 @@ class ConnectionItem(GraphicsObject):
ev.accept() ev.accept()
sel = self.isSelected() sel = self.isSelected()
self.setSelected(True) self.setSelected(True)
self.setFocus()
if not sel and self.isSelected(): if not sel and self.isSelected():
self.update() self.update()
@ -600,12 +538,9 @@ class ConnectionItem(GraphicsObject):
self.hovered = False self.hovered = False
self.update() self.update()
def boundingRect(self): def boundingRect(self):
return self.shape().boundingRect() return self.shape().boundingRect()
##return self.line.boundingRect()
#px = self.pixelWidth()
#return QtCore.QRectF(-5*px, 0, 10*px, self.length)
def viewRangeChanged(self): def viewRangeChanged(self):
self.shapePath = None self.shapePath = None
self.prepareGeometryChange() self.prepareGeometryChange()
@ -628,7 +563,5 @@ class ConnectionItem(GraphicsObject):
p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth'])) p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth']))
else: else:
p.setPen(fn.mkPen(self.style['color'], width=self.style['width'])) p.setPen(fn.mkPen(self.style['color'], width=self.style['width']))
#p.drawLine(0, 0, 0, self.length)
p.drawPath(self.path) p.drawPath(self.path)

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
from numpy import ndarray, bool_
from ..metaarray import MetaArray
def eq(a, b):
"""The great missing equivalence function: Guaranteed evaluation to a single bool value."""
if a is b:
return True
try:
e = a==b
except ValueError:
return False
except AttributeError:
return False
except:
print("a:", str(type(a)), str(a))
print("b:", str(type(b)), str(b))
raise
t = type(e)
if t is bool:
return e
elif t is bool_:
return bool(e)
elif isinstance(e, ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')):
try: ## disaster: if a is an empty array and b is not, then e.all() is True
if a.shape != b.shape:
return False
except:
return False
if (hasattr(e, 'implements') and e.implements('MetaArray')):
return e.asarray().all()
else:
return e.all()
else:
raise Exception("== operator returned type %s" % str(type(e)))

View File

@ -243,6 +243,7 @@ def mkBrush(*args, **kwds):
color = args color = args
return QtGui.QBrush(mkColor(color)) return QtGui.QBrush(mkColor(color))
def mkPen(*args, **kargs): def mkPen(*args, **kargs):
""" """
Convenience function for constructing QPen. Convenience function for constructing QPen.
@ -292,6 +293,7 @@ def mkPen(*args, **kargs):
pen.setDashPattern(dash) pen.setDashPattern(dash)
return pen return pen
def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0):
"""Generate a QColor from HSVa values. (all arguments are float 0.0-1.0)""" """Generate a QColor from HSVa values. (all arguments are float 0.0-1.0)"""
c = QtGui.QColor() c = QtGui.QColor()
@ -303,10 +305,12 @@ def colorTuple(c):
"""Return a tuple (R,G,B,A) from a QColor""" """Return a tuple (R,G,B,A) from a QColor"""
return (c.red(), c.green(), c.blue(), c.alpha()) return (c.red(), c.green(), c.blue(), c.alpha())
def colorStr(c): def colorStr(c):
"""Generate a hex string code from a QColor""" """Generate a hex string code from a QColor"""
return ('%02x'*4) % colorTuple(c) return ('%02x'*4) % colorTuple(c)
def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs):
""" """
Creates a QColor from a single index. Useful for stepping through a predefined list of colors. Creates a QColor from a single index. Useful for stepping through a predefined list of colors.
@ -331,6 +335,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi
c.setAlpha(alpha) c.setAlpha(alpha)
return c return c
def glColor(*args, **kargs): def glColor(*args, **kargs):
""" """
Convert a color to OpenGL color format (r,g,b,a) floats 0.0-1.0 Convert a color to OpenGL color format (r,g,b,a) floats 0.0-1.0
@ -367,6 +372,40 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0)
return path return path
def eq(a, b):
"""The great missing equivalence function: Guaranteed evaluation to a single bool value."""
if a is b:
return True
try:
e = a==b
except ValueError:
return False
except AttributeError:
return False
except:
print('failed to evaluate equivalence for:')
print(" a:", str(type(a)), str(a))
print(" b:", str(type(b)), str(b))
raise
t = type(e)
if t is bool:
return e
elif t is np.bool_:
return bool(e)
elif isinstance(e, np.ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')):
try: ## disaster: if a is an empty array and b is not, then e.all() is True
if a.shape != b.shape:
return False
except:
return False
if (hasattr(e, 'implements') and e.implements('MetaArray')):
return e.asarray().all()
else:
return e.all()
else:
raise Exception("== operator returned type %s" % str(type(e)))
def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs):
""" """
@ -774,12 +813,11 @@ def solveBilinearTransform(points1, points2):
return matrix return matrix
def rescaleData(data, scale, offset, dtype=None): def rescaleData(data, scale, offset, dtype=None, clip=None):
"""Return data rescaled and optionally cast to a new dtype:: """Return data rescaled and optionally cast to a new dtype::
data => (data-offset) * scale data => (data-offset) * scale
Uses scipy.weave (if available) to improve performance.
""" """
if dtype is None: if dtype is None:
dtype = data.dtype dtype = data.dtype
@ -824,9 +862,21 @@ def rescaleData(data, scale, offset, dtype=None):
setConfigOptions(useWeave=False) setConfigOptions(useWeave=False)
#p = np.poly1d([scale, -offset*scale]) #p = np.poly1d([scale, -offset*scale])
#data = p(data).astype(dtype) #d2 = p(data)
d2 = data-offset d2 = data - float(offset)
np.multiply(d2, scale, out=d2, casting="unsafe") d2 *= scale
# Clip before converting dtype to avoid overflow
if dtype.kind in 'ui':
lim = np.iinfo(dtype)
if clip is None:
# don't let rescale cause integer overflow
d2 = np.clip(d2, lim.min, lim.max)
else:
d2 = np.clip(d2, max(clip[0], lim.min), min(clip[1], lim.max))
else:
if clip is not None:
d2 = np.clip(d2, *clip)
data = d2.astype(dtype) data = d2.astype(dtype)
return data return data
@ -848,15 +898,18 @@ def makeRGBA(*args, **kwds):
kwds['useRGBA'] = True kwds['useRGBA'] = True
return makeARGB(*args, **kwds) return makeARGB(*args, **kwds)
def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
""" """
Convert an array of values into an ARGB array suitable for building QImages, OpenGL textures, etc. Convert an array of values into an ARGB array suitable for building QImages,
OpenGL textures, etc.
Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. Returns the ARGB array (unsigned byte) and a boolean indicating whether
This is a two stage process: there is alpha channel data. This is a two stage process:
1) Rescale the data based on the values in the *levels* argument (min, max). 1) Rescale the data based on the values in the *levels* argument (min, max).
2) Determine the final output by passing the rescaled values through a lookup table. 2) Determine the final output by passing the rescaled values through a
lookup table.
Both stages are optional. Both stages are optional.
@ -875,55 +928,68 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. channel). The use of this feature requires that levels.shape[0] == data.shape[-1].
scale The maximum value to which data will be rescaled before being passed through the scale The maximum value to which data will be rescaled before being passed through the
lookup table (or returned if there is no lookup table). By default this will lookup table (or returned if there is no lookup table). By default this will
be set to the length of the lookup table, or 256 is no lookup table is provided. be set to the length of the lookup table, or 255 if no lookup table is provided.
For OpenGL color specifications (as in GLColor4f) use scale=1.0
lut Optional lookup table (array with dtype=ubyte). lut Optional lookup table (array with dtype=ubyte).
Values in data will be converted to color by indexing directly from lut. Values in data will be converted to color by indexing directly from lut.
The output data shape will be input.shape + lut.shape[1:]. The output data shape will be input.shape + lut.shape[1:].
Lookup tables can be built using ColorMap or GradientWidget.
Note: the output of makeARGB will have the same dtype as the lookup table, so
for conversion to QImage, the dtype must be ubyte.
Lookup tables can be built using GradientWidget.
useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures).
The default is False, which returns in ARGB order for use with QImage The default is False, which returns in ARGB order for use with QImage
(Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order (Note that 'ARGB' is a term used by the Qt documentation; the *actual* order
is BGRA). is BGRA).
============== ================================================================================== ============== ==================================================================================
""" """
profile = debug.Profiler() profile = debug.Profiler()
if data.ndim not in (2, 3):
raise TypeError("data must be 2D or 3D")
if data.ndim == 3 and data.shape[2] > 4:
raise TypeError("data.shape[2] must be <= 4")
if lut is not None and not isinstance(lut, np.ndarray): if lut is not None and not isinstance(lut, np.ndarray):
lut = np.array(lut) lut = np.array(lut)
if levels is not None and not isinstance(levels, np.ndarray):
levels = np.array(levels)
if levels is not None: if levels is None:
if levels.ndim == 1: # automatically decide levels based on data dtype
if len(levels) != 2: if data.dtype.kind == 'u':
raise Exception('levels argument must have length 2') levels = np.array([0, 2**(data.itemsize*8)-1])
elif levels.ndim == 2: elif data.dtype.kind == 'i':
if lut is not None and lut.ndim > 1: s = 2**(data.itemsize*8 - 1)
raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2') levels = np.array([-s, s-1])
if levels.shape != (data.shape[-1], 2):
raise Exception('levels must have shape (data.shape[-1], 2)')
else: else:
print(levels) raise Exception('levels argument is required for float input types')
raise Exception("levels argument must be 1D or 2D.") if not isinstance(levels, np.ndarray):
levels = np.array(levels)
if levels.ndim == 1:
if levels.shape[0] != 2:
raise Exception('levels argument must have length 2')
elif levels.ndim == 2:
if lut is not None and lut.ndim > 1:
raise Exception('Cannot make ARGB data when both levels and lut have ndim > 2')
if levels.shape != (data.shape[-1], 2):
raise Exception('levels must have shape (data.shape[-1], 2)')
else:
raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape))
profile() profile()
# Decide on maximum scaled value
if scale is None: if scale is None:
if lut is not None: if lut is not None:
scale = lut.shape[0] scale = lut.shape[0] - 1
else: else:
scale = 255. scale = 255.
## Apply levels if given # Decide on the dtype we want after scaling
if lut is None:
dtype = np.ubyte
else:
dtype = np.min_scalar_type(lut.shape[0]-1)
# Apply levels if given
if levels is not None: if levels is not None:
if isinstance(levels, np.ndarray) and levels.ndim == 2: if isinstance(levels, np.ndarray) and levels.ndim == 2:
## we are going to rescale each channel independently # we are going to rescale each channel independently
if levels.shape[0] != data.shape[-1]: if levels.shape[0] != data.shape[-1]:
raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])") raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])")
newData = np.empty(data.shape, dtype=int) newData = np.empty(data.shape, dtype=int)
@ -931,20 +997,20 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
minVal, maxVal = levels[i] minVal, maxVal = levels[i]
if minVal == maxVal: if minVal == maxVal:
maxVal += 1e-16 maxVal += 1e-16
newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int) newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype)
data = newData data = newData
else: else:
# Apply level scaling unless it would have no effect on the data
minVal, maxVal = levels minVal, maxVal = levels
if minVal == maxVal: if minVal != 0 or maxVal != scale:
maxVal += 1e-16 if minVal == maxVal:
if maxVal == minVal: maxVal += 1e-16
data = rescaleData(data, 1, minVal, dtype=int) data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
else:
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
profile() profile()
## apply LUT if given # apply LUT if given
if lut is not None: if lut is not None:
data = applyLookupTable(data, lut) data = applyLookupTable(data, lut)
else: else:
@ -953,16 +1019,18 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
profile() profile()
## copy data into ARGB ordered array # this will be the final image array
imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte)
profile() profile()
# decide channel order
if useRGBA: if useRGBA:
order = [0,1,2,3] ## array comes out RGBA order = [0,1,2,3] # array comes out RGBA
else: else:
order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. order = [2,1,0,3] # for some reason, the colors line up as BGR in the final image.
# copy data into image array
if data.ndim == 2: if data.ndim == 2:
# This is tempting: # This is tempting:
# imgData[..., :3] = data[..., np.newaxis] # imgData[..., :3] = data[..., np.newaxis]
@ -977,7 +1045,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
imgData[..., i] = data[..., order[i]] imgData[..., i] = data[..., order[i]]
profile() profile()
# add opaque alpha channel if needed
if data.ndim == 2 or data.shape[2] == 3: if data.ndim == 2 or data.shape[2] == 3:
alpha = False alpha = False
imgData[..., 3] = 255 imgData[..., 3] = 255

View File

@ -47,6 +47,10 @@ class ImageItem(GraphicsObject):
self.lut = None self.lut = None
self.autoDownsample = False self.autoDownsample = False
# In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently
self._effectiveLut = None
self.drawKernel = None self.drawKernel = None
self.border = None self.border = None
self.removable = False self.removable = False
@ -74,11 +78,6 @@ class ImageItem(GraphicsObject):
""" """
self.paintMode = mode self.paintMode = mode
self.update() self.update()
## use setOpacity instead.
#def setAlpha(self, alpha):
#self.setOpacity(alpha)
#self.updateImage()
def setBorder(self, b): def setBorder(self, b):
self.border = fn.mkPen(b) self.border = fn.mkPen(b)
@ -99,16 +98,6 @@ class ImageItem(GraphicsObject):
return QtCore.QRectF(0., 0., 0., 0.) return QtCore.QRectF(0., 0., 0., 0.)
return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) return QtCore.QRectF(0., 0., float(self.width()), float(self.height()))
#def setClipLevel(self, level=None):
#self.clipLevel = level
#self.updateImage()
#def paint(self, p, opt, widget):
#pass
#if self.pixmap is not None:
#p.drawPixmap(0, 0, self.pixmap)
#print "paint"
def setLevels(self, levels, update=True): def setLevels(self, levels, update=True):
""" """
Set image scaling levels. Can be one of: Set image scaling levels. Can be one of:
@ -119,9 +108,13 @@ class ImageItem(GraphicsObject):
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>` Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
for more details on how levels are applied. for more details on how levels are applied.
""" """
self.levels = levels if levels is not None:
if update: levels = np.asarray(levels)
self.updateImage() if not fn.eq(levels, self.levels):
self.levels = levels
self._effectiveLut = None
if update:
self.updateImage()
def getLevels(self): def getLevels(self):
return self.levels return self.levels
@ -137,9 +130,11 @@ class ImageItem(GraphicsObject):
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`. or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
""" """
self.lut = lut if lut is not self.lut:
if update: self.lut = lut
self.updateImage() self._effectiveLut = None
if update:
self.updateImage()
def setAutoDownsample(self, ads): def setAutoDownsample(self, ads):
""" """
@ -222,7 +217,10 @@ class ImageItem(GraphicsObject):
else: else:
gotNewData = True gotNewData = True
shapeChanged = (self.image is None or image.shape != self.image.shape) shapeChanged = (self.image is None or image.shape != self.image.shape)
self.image = image.view(np.ndarray) image = image.view(np.ndarray)
if self.image is None or image.dtype != self.image.dtype:
self._effectiveLut = None
self.image = image
if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1: if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1:
if 'autoDownsample' not in kargs: if 'autoDownsample' not in kargs:
kargs['autoDownsample'] = True kargs['autoDownsample'] = True
@ -261,6 +259,17 @@ class ImageItem(GraphicsObject):
if gotNewData: if gotNewData:
self.sigImageChanged.emit() self.sigImageChanged.emit()
def quickMinMax(self, targetSize=1e6):
"""
Estimate the min/max values of the image data by subsampling.
"""
data = self.image
while data.size > targetSize:
ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
return nanmin(data), nanmax(data)
def updateImage(self, *args, **kargs): def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image. ## used for re-rendering qimage from self.image.
@ -297,6 +306,27 @@ class ImageItem(GraphicsObject):
image = fn.downsample(image, yds, axis=1) image = fn.downsample(image, yds, axis=1)
else: else:
image = self.image image = self.image
# if the image data is a small int, then we can combine levels + lut
# into a single lut for better performance
if self.levels is not None and self.levels.ndim == 1 and image.dtype in (np.ubyte, np.uint16):
if self._effectiveLut is None:
eflsize = 2**(image.itemsize*8)
ind = np.arange(eflsize)
minlev, maxlev = self.levels
if lut is None:
efflut = fn.rescaleData(ind, scale=255./(maxlev-minlev),
offset=minlev, dtype=np.ubyte)
else:
lutdtype = np.min_scalar_type(lut.shape[0]-1)
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/(maxlev-minlev),
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
efflut = lut[efflut]
self._effectiveLut = efflut
lut = self._effectiveLut
levels = None
argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels) argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels)
self.qimage = fn.makeQImage(argb, alpha, transpose=False) self.qimage = fn.makeQImage(argb, alpha, transpose=False)

View File

@ -111,6 +111,183 @@ def test_subArray():
assert np.all(bb == cc) assert np.all(bb == cc)
def test_rescaleData():
dtypes = map(np.dtype, ('ubyte', 'uint16', 'byte', 'int16', 'int', 'float'))
for dtype1 in dtypes:
for dtype2 in dtypes:
data = (np.random.random(size=10) * 2**32 - 2**31).astype(dtype1)
for scale, offset in [(10, 0), (10., 0.), (1, -50), (0.2, 0.5), (0.001, 0)]:
if dtype2.kind in 'iu':
lim = np.iinfo(dtype2)
lim = lim.min, lim.max
else:
lim = (-np.inf, np.inf)
s1 = np.clip(float(scale) * (data-float(offset)), *lim).astype(dtype2)
s2 = pg.rescaleData(data, scale, offset, dtype2)
assert s1.dtype == s2.dtype
if dtype2.kind in 'iu':
assert np.all(s1 == s2)
else:
assert np.allclose(s1, s2)
def test_makeARGB():
# Many parameters to test here:
# * data dtype (ubyte, uint16, float, others)
# * data ndim (2 or 3)
# * levels (None, 1D, or 2D)
# * lut dtype
# * lut size
# * lut ndim (1 or 2)
# * useRGBA argument
# Need to check that all input values map to the correct output values, especially
# at and beyond the edges of the level range.
def checkArrays(a, b):
# because py.test output is difficult to read for arrays
if not np.all(a == b):
comp = []
for i in range(a.shape[0]):
if a.shape[1] > 1:
comp.append('[')
for j in range(a.shape[1]):
m = a[i,j] == b[i,j]
comp.append('%d,%d %s %s %s%s' %
(i, j, str(a[i,j]).ljust(15), str(b[i,j]).ljust(15),
m, ' ********' if not np.all(m) else ''))
if a.shape[1] > 1:
comp.append(']')
raise Exception("arrays do not match:\n%s" % '\n'.join(comp))
def checkImage(img, check, alpha, alphaCheck):
assert img.dtype == np.ubyte
assert alpha is alphaCheck
if alpha is False:
checkArrays(img[..., 3], 255)
if np.isscalar(check) or check.ndim == 3:
checkArrays(img[..., :3], check)
elif check.ndim == 2:
checkArrays(img[..., :3], check[..., np.newaxis])
elif check.ndim == 1:
checkArrays(img[..., :3], check[..., np.newaxis, np.newaxis])
else:
raise Exception('invalid check array ndim')
# uint8 data tests
im1 = np.arange(256).astype('ubyte').reshape(256, 1)
im2, alpha = pg.makeARGB(im1, levels=(0, 255))
checkImage(im2, im1, alpha, False)
im3, alpha = pg.makeARGB(im1, levels=(0.0, 255.0))
checkImage(im3, im1, alpha, False)
im4, alpha = pg.makeARGB(im1, levels=(255, 0))
checkImage(im4, 255-im1, alpha, False)
im5, alpha = pg.makeARGB(np.concatenate([im1]*3, axis=1), levels=[(0, 255), (0.0, 255.0), (255, 0)])
checkImage(im5, np.concatenate([im1, im1, 255-im1], axis=1), alpha, False)
im2, alpha = pg.makeARGB(im1, levels=(128,383))
checkImage(im2[:128], 0, alpha, False)
checkImage(im2[128:], im1[:128], alpha, False)
# uint8 data + uint8 LUT
lut = np.arange(256)[::-1].astype(np.uint8)
im2, alpha = pg.makeARGB(im1, lut=lut)
checkImage(im2, lut, alpha, False)
# lut larger than maxint
lut = np.arange(511).astype(np.uint8)
im2, alpha = pg.makeARGB(im1, lut=lut)
checkImage(im2, lut[::2], alpha, False)
# lut smaller than maxint
lut = np.arange(128).astype(np.uint8)
im2, alpha = pg.makeARGB(im1, lut=lut)
checkImage(im2, np.linspace(0, 127, 256).astype('ubyte'), alpha, False)
# lut + levels
lut = np.arange(256)[::-1].astype(np.uint8)
im2, alpha = pg.makeARGB(im1, lut=lut, levels=[-128, 384])
checkImage(im2, np.linspace(192, 65.5, 256).astype('ubyte'), alpha, False)
im2, alpha = pg.makeARGB(im1, lut=lut, levels=[64, 192])
checkImage(im2, np.clip(np.linspace(385.5, -126.5, 256), 0, 255).astype('ubyte'), alpha, False)
# uint8 data + uint16 LUT
lut = np.arange(4096)[::-1].astype(np.uint16) // 16
im2, alpha = pg.makeARGB(im1, lut=lut)
checkImage(im2, np.arange(256)[::-1].astype('ubyte'), alpha, False)
# uint8 data + float LUT
lut = np.linspace(10., 137., 256)
im2, alpha = pg.makeARGB(im1, lut=lut)
checkImage(im2, lut.astype('ubyte'), alpha, False)
# uint8 data + 2D LUT
lut = np.zeros((256, 3), dtype='ubyte')
lut[:,0] = np.arange(256)
lut[:,1] = np.arange(256)[::-1]
lut[:,2] = 7
im2, alpha = pg.makeARGB(im1, lut=lut)
checkImage(im2, lut[:,None,::-1], alpha, False)
# check useRGBA
im2, alpha = pg.makeARGB(im1, lut=lut, useRGBA=True)
checkImage(im2, lut[:,None,:], alpha, False)
# uint16 data tests
im1 = np.arange(0, 2**16, 256).astype('uint16')[:, None]
im2, alpha = pg.makeARGB(im1, levels=(512, 2**16))
checkImage(im2, np.clip(np.linspace(-2, 253, 256), 0, 255).astype('ubyte'), alpha, False)
lut = (np.arange(512, 2**16)[::-1] // 256).astype('ubyte')
im2, alpha = pg.makeARGB(im1, lut=lut, levels=(512, 2**16-256))
checkImage(im2, np.clip(np.linspace(257, 2, 256), 0, 255).astype('ubyte'), alpha, False)
# float data tests
im1 = np.linspace(1.0, 17.0, 256)[:, None]
im2, alpha = pg.makeARGB(im1, levels=(5.0, 13.0))
checkImage(im2, np.clip(np.linspace(-128, 383, 256), 0, 255).astype('ubyte'), alpha, False)
lut = (np.arange(1280)[::-1] // 10).astype('ubyte')
im2, alpha = pg.makeARGB(im1, lut=lut, levels=(1, 17))
checkImage(im2, np.linspace(127.5, 0, 256).astype('ubyte'), alpha, False)
# test sanity checks
class AssertExc(object):
def __init__(self, exc=Exception):
self.exc = exc
def __enter__(self):
return self
def __exit__(self, *args):
assert args[0] is self.exc, "Should have raised %s (got %s)" % (self.exc, args[0])
return True
with AssertExc(TypeError): # invalid image shape
pg.makeARGB(np.zeros((2,), dtype='float'))
with AssertExc(TypeError): # invalid image shape
pg.makeARGB(np.zeros((2,2,7), dtype='float'))
with AssertExc(): # float images require levels arg
pg.makeARGB(np.zeros((2,2), dtype='float'))
with AssertExc(): # bad levels arg
pg.makeARGB(np.zeros((2,2), dtype='float'), levels=[1])
with AssertExc(): # bad levels arg
pg.makeARGB(np.zeros((2,2), dtype='float'), levels=[1,2,3])
with AssertExc(): # can't mix 3-channel levels and LUT
pg.makeARGB(np.zeros((2,2)), lut=np.zeros((10,3), dtype='ubyte'), levels=[(0,1)]*3)
with AssertExc(): # multichannel levels must have same number of channels as image
pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=[(1,2)]*4)
with AssertExc(): # 3d levels not allowed
pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2]))
if __name__ == '__main__': if __name__ == '__main__':
test_interpolateArray() test_interpolateArray()