Merge remote-tracking branch 'pyqtgraph/develop' into u55-u55-patch-1
This commit is contained in:
commit
586de3176e
@ -91,6 +91,8 @@ Mesh Generation Functions
|
||||
Miscellaneous Functions
|
||||
-----------------------
|
||||
|
||||
.. autofunction:: pyqtgraph.eq
|
||||
|
||||
.. autofunction:: pyqtgraph.arrayToQPath
|
||||
|
||||
.. autofunction:: pyqtgraph.pseudoScatter
|
||||
|
@ -4,72 +4,27 @@ from ..widgets.GraphicsView import GraphicsView
|
||||
from ..GraphicsScene import GraphicsScene
|
||||
from ..graphicsItems.ViewBox import ViewBox
|
||||
|
||||
#class FlowchartGraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
class FlowchartGraphicsView(GraphicsView):
|
||||
|
||||
sigHoverOver = QtCore.Signal(object)
|
||||
sigClicked = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, widget, *args):
|
||||
#QtGui.QGraphicsView.__init__(self, *args)
|
||||
GraphicsView.__init__(self, *args, useOpenGL=False)
|
||||
#self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255)))
|
||||
self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True)
|
||||
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.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
|
||||
#self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect)
|
||||
|
||||
def viewBox(self):
|
||||
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):
|
||||
|
||||
def __init__(self, widget, *args, **kwargs):
|
||||
ViewBox.__init__(self, *args, **kwargs)
|
||||
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):
|
||||
## called by ViewBox to create a new context menu
|
||||
@ -84,26 +39,3 @@ class FlowchartViewBox(ViewBox):
|
||||
menu = self.widget.buildMenu(ev.scenePos())
|
||||
menu.setTitle("Add node")
|
||||
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)
|
@ -6,7 +6,6 @@ from .Terminal import *
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..debug import *
|
||||
import numpy as np
|
||||
from .eq import *
|
||||
|
||||
|
||||
def strDict(d):
|
||||
@ -261,7 +260,7 @@ class Node(QtCore.QObject):
|
||||
for k, v in args.items():
|
||||
term = self._inputs[k]
|
||||
oldVal = term.value()
|
||||
if not eq(oldVal, v):
|
||||
if not fn.eq(oldVal, v):
|
||||
changed = True
|
||||
term.setValue(v, process=False)
|
||||
if changed and '_updatesHandled_' not in args:
|
||||
|
@ -4,8 +4,7 @@ import weakref
|
||||
from ..graphicsItems.GraphicsObject import GraphicsObject
|
||||
from .. import functions as fn
|
||||
from ..Point import Point
|
||||
#from PySide import QtCore, QtGui
|
||||
from .eq import *
|
||||
|
||||
|
||||
class Terminal(object):
|
||||
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._isOutput = opts[0] in ['out', 'io']
|
||||
#self._isInput = opts[0]] in ['in', 'io']
|
||||
#self._isIO = opts[0]=='io'
|
||||
self._optional = optional
|
||||
self._multi = multi
|
||||
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 multi-value terminal, val should be a dict of terminal:value pairs"""
|
||||
if not self.isMultiValue():
|
||||
if eq(val, self._value):
|
||||
if fn.eq(val, self._value):
|
||||
return
|
||||
self._value = val
|
||||
else:
|
||||
@ -81,11 +77,6 @@ class Terminal(object):
|
||||
if self.isInput() and process:
|
||||
self.node().update()
|
||||
|
||||
## Let the flowchart handle this.
|
||||
#if self.isOutput():
|
||||
#for c in self.connections():
|
||||
#if c.isInput():
|
||||
#c.inputChanged(self)
|
||||
self.recolor()
|
||||
|
||||
def setOpts(self, **opts):
|
||||
@ -95,7 +86,6 @@ class Terminal(object):
|
||||
if 'multi' in opts:
|
||||
self.setMultiValue(opts['multi'])
|
||||
|
||||
|
||||
def connected(self, term):
|
||||
"""Called whenever this terminal has been connected to another. (note--this function is called on both terminals)"""
|
||||
if self.isInput() and term.isOutput():
|
||||
@ -109,12 +99,10 @@ class Terminal(object):
|
||||
if self.isMultiValue() and term in self._value:
|
||||
del self._value[term]
|
||||
self.node().update()
|
||||
#self.recolor()
|
||||
else:
|
||||
if self.isInput():
|
||||
self.setValue(None)
|
||||
self.node().disconnected(self, term)
|
||||
#self.node().update()
|
||||
|
||||
def inputChanged(self, term, process=True):
|
||||
"""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()
|
||||
|
||||
def hasInput(self):
|
||||
#conn = self.extendedConnections()
|
||||
for t in self.connections():
|
||||
if t.isOutput():
|
||||
return True
|
||||
@ -186,17 +173,10 @@ class Terminal(object):
|
||||
|
||||
def inputTerminals(self):
|
||||
"""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()]
|
||||
|
||||
|
||||
def dependentNodes(self):
|
||||
"""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()])
|
||||
|
||||
def connectTo(self, term, connectionItem=None):
|
||||
@ -210,12 +190,6 @@ class Terminal(object):
|
||||
for t in [self, term]:
|
||||
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())))
|
||||
#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:
|
||||
if connectionItem is not None:
|
||||
connectionItem.close()
|
||||
@ -223,18 +197,12 @@ class Terminal(object):
|
||||
|
||||
if connectionItem is None:
|
||||
connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem())
|
||||
#self.graphicsItem().scene().addItem(connectionItem)
|
||||
self.graphicsItem().getViewBox().addItem(connectionItem)
|
||||
#connectionItem.setParentItem(self.graphicsItem().parent().parent())
|
||||
self._connections[term] = connectionItem
|
||||
term._connections[self] = connectionItem
|
||||
|
||||
self.recolor()
|
||||
|
||||
#if self.isOutput() and term.isInput():
|
||||
#term.inputChanged(self)
|
||||
#if term.isInput() and term.isOutput():
|
||||
#self.inputChanged(term)
|
||||
self.connected(term)
|
||||
term.connected(self)
|
||||
|
||||
@ -244,8 +212,6 @@ class Terminal(object):
|
||||
if not self.connectedTo(term):
|
||||
return
|
||||
item = self._connections[term]
|
||||
#print "removing connection", item
|
||||
#item.scene().removeItem(item)
|
||||
item.close()
|
||||
del self._connections[term]
|
||||
del term._connections[self]
|
||||
@ -254,10 +220,6 @@ class Terminal(object):
|
||||
|
||||
self.disconnected(term)
|
||||
term.disconnected(self)
|
||||
#if self.isOutput() and term.isInput():
|
||||
#term.inputChanged(self)
|
||||
#if term.isInput() and term.isOutput():
|
||||
#self.inputChanged(term)
|
||||
|
||||
|
||||
def disconnectAll(self):
|
||||
@ -270,7 +232,7 @@ class Terminal(object):
|
||||
color = QtGui.QColor(0,0,0)
|
||||
elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals
|
||||
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)
|
||||
elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok
|
||||
color = QtGui.QColor(200, 200, 0)
|
||||
@ -284,7 +246,6 @@ class Terminal(object):
|
||||
for t in self.connections():
|
||||
t.recolor(color, recurse=False)
|
||||
|
||||
|
||||
def rename(self, name):
|
||||
oldName = self._name
|
||||
self._name = name
|
||||
@ -294,17 +255,6 @@ class Terminal(object):
|
||||
def __repr__(self):
|
||||
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):
|
||||
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}
|
||||
|
||||
|
||||
#class TerminalGraphicsItem(QtGui.QGraphicsItem):
|
||||
class TerminalGraphicsItem(GraphicsObject):
|
||||
|
||||
def __init__(self, term, parent=None):
|
||||
self.term = term
|
||||
#QtGui.QGraphicsItem.__init__(self, parent)
|
||||
GraphicsObject.__init__(self, parent)
|
||||
self.brush = fn.mkBrush(0,0,0)
|
||||
self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self)
|
||||
self.label = QtGui.QGraphicsTextItem(self.term.name(), self)
|
||||
self.label.scale(0.7, 0.7)
|
||||
#self.setAcceptHoverEvents(True)
|
||||
self.newConnection = None
|
||||
self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem
|
||||
if self.term.isRenamable():
|
||||
@ -339,7 +286,6 @@ class TerminalGraphicsItem(GraphicsObject):
|
||||
self.setZValue(1)
|
||||
self.menu = None
|
||||
|
||||
|
||||
def labelFocusOut(self, ev):
|
||||
QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev)
|
||||
self.labelChanged()
|
||||
@ -471,8 +417,6 @@ class TerminalGraphicsItem(GraphicsObject):
|
||||
break
|
||||
|
||||
if not gotTarget:
|
||||
#print "remove unused connection"
|
||||
#self.scene().removeItem(self.newConnection)
|
||||
self.newConnection.close()
|
||||
self.newConnection = None
|
||||
else:
|
||||
@ -488,12 +432,6 @@ class TerminalGraphicsItem(GraphicsObject):
|
||||
self.box.setBrush(self.brush)
|
||||
self.update()
|
||||
|
||||
#def hoverEnterEvent(self, ev):
|
||||
#self.hover = True
|
||||
|
||||
#def hoverLeaveEvent(self, ev):
|
||||
#self.hover = False
|
||||
|
||||
def connectPoint(self):
|
||||
## return the connect position of this terminal in view coords
|
||||
return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center()))
|
||||
@ -503,11 +441,9 @@ class TerminalGraphicsItem(GraphicsObject):
|
||||
item.updateLine()
|
||||
|
||||
|
||||
#class ConnectionItem(QtGui.QGraphicsItem):
|
||||
class ConnectionItem(GraphicsObject):
|
||||
|
||||
def __init__(self, source, target=None):
|
||||
#QtGui.QGraphicsItem.__init__(self)
|
||||
GraphicsObject.__init__(self)
|
||||
self.setFlags(
|
||||
self.ItemIsSelectable |
|
||||
@ -528,14 +464,12 @@ class ConnectionItem(GraphicsObject):
|
||||
'selectedColor': (200, 200, 0),
|
||||
'selectedWidth': 3.0,
|
||||
}
|
||||
#self.line = QtGui.QGraphicsLineItem(self)
|
||||
self.source.getViewBox().addItem(self)
|
||||
self.updateLine()
|
||||
self.setZValue(0)
|
||||
|
||||
def close(self):
|
||||
if self.scene() is not None:
|
||||
#self.scene().removeItem(self.line)
|
||||
self.scene().removeItem(self)
|
||||
|
||||
def setTarget(self, target):
|
||||
@ -575,8 +509,11 @@ class ConnectionItem(GraphicsObject):
|
||||
return path
|
||||
|
||||
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 isinstance(self.target, TerminalGraphicsItem):
|
||||
self.source.disconnect(self.target)
|
||||
ev.accept()
|
||||
else:
|
||||
@ -590,6 +527,7 @@ class ConnectionItem(GraphicsObject):
|
||||
ev.accept()
|
||||
sel = self.isSelected()
|
||||
self.setSelected(True)
|
||||
self.setFocus()
|
||||
if not sel and self.isSelected():
|
||||
self.update()
|
||||
|
||||
@ -600,12 +538,9 @@ class ConnectionItem(GraphicsObject):
|
||||
self.hovered = False
|
||||
self.update()
|
||||
|
||||
|
||||
def boundingRect(self):
|
||||
return self.shape().boundingRect()
|
||||
##return self.line.boundingRect()
|
||||
#px = self.pixelWidth()
|
||||
#return QtCore.QRectF(-5*px, 0, 10*px, self.length)
|
||||
|
||||
def viewRangeChanged(self):
|
||||
self.shapePath = None
|
||||
self.prepareGeometryChange()
|
||||
@ -629,6 +564,4 @@ class ConnectionItem(GraphicsObject):
|
||||
else:
|
||||
p.setPen(fn.mkPen(self.style['color'], width=self.style['width']))
|
||||
|
||||
#p.drawLine(0, 0, 0, self.length)
|
||||
|
||||
p.drawPath(self.path)
|
||||
|
@ -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)))
|
@ -243,6 +243,7 @@ def mkBrush(*args, **kwds):
|
||||
color = args
|
||||
return QtGui.QBrush(mkColor(color))
|
||||
|
||||
|
||||
def mkPen(*args, **kargs):
|
||||
"""
|
||||
Convenience function for constructing QPen.
|
||||
@ -292,6 +293,7 @@ def mkPen(*args, **kargs):
|
||||
pen.setDashPattern(dash)
|
||||
return pen
|
||||
|
||||
|
||||
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)"""
|
||||
c = QtGui.QColor()
|
||||
@ -303,10 +305,12 @@ def colorTuple(c):
|
||||
"""Return a tuple (R,G,B,A) from a QColor"""
|
||||
return (c.red(), c.green(), c.blue(), c.alpha())
|
||||
|
||||
|
||||
def colorStr(c):
|
||||
"""Generate a hex string code from a QColor"""
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
return c
|
||||
|
||||
|
||||
def glColor(*args, **kargs):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -774,12 +813,11 @@ def solveBilinearTransform(points1, points2):
|
||||
|
||||
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::
|
||||
|
||||
data => (data-offset) * scale
|
||||
|
||||
Uses scipy.weave (if available) to improve performance.
|
||||
"""
|
||||
if dtype is None:
|
||||
dtype = data.dtype
|
||||
@ -824,9 +862,21 @@ def rescaleData(data, scale, offset, dtype=None):
|
||||
setConfigOptions(useWeave=False)
|
||||
|
||||
#p = np.poly1d([scale, -offset*scale])
|
||||
#data = p(data).astype(dtype)
|
||||
d2 = data-offset
|
||||
np.multiply(d2, scale, out=d2, casting="unsafe")
|
||||
#d2 = p(data)
|
||||
d2 = data - float(offset)
|
||||
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)
|
||||
return data
|
||||
|
||||
@ -848,15 +898,18 @@ def makeRGBA(*args, **kwds):
|
||||
kwds['useRGBA'] = True
|
||||
return makeARGB(*args, **kwds)
|
||||
|
||||
|
||||
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.
|
||||
This is a two stage process:
|
||||
Returns the ARGB array (unsigned byte) and a boolean indicating whether
|
||||
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).
|
||||
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.
|
||||
|
||||
@ -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].
|
||||
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
|
||||
be set to the length of the lookup table, or 256 is no lookup table is provided.
|
||||
For OpenGL color specifications (as in GLColor4f) use scale=1.0
|
||||
be set to the length of the lookup table, or 255 if no lookup table is provided.
|
||||
lut Optional lookup table (array with dtype=ubyte).
|
||||
Values in data will be converted to color by indexing directly from lut.
|
||||
The output data shape will be input.shape + lut.shape[1:].
|
||||
|
||||
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.
|
||||
Lookup tables can be built using ColorMap or GradientWidget.
|
||||
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
|
||||
(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).
|
||||
============== ==================================================================================
|
||||
"""
|
||||
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):
|
||||
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:
|
||||
# automatically decide levels based on data dtype
|
||||
if data.dtype.kind == 'u':
|
||||
levels = np.array([0, 2**(data.itemsize*8)-1])
|
||||
elif data.dtype.kind == 'i':
|
||||
s = 2**(data.itemsize*8 - 1)
|
||||
levels = np.array([-s, s-1])
|
||||
else:
|
||||
raise Exception('levels argument is required for float input types')
|
||||
if not isinstance(levels, np.ndarray):
|
||||
levels = np.array(levels)
|
||||
if levels.ndim == 1:
|
||||
if len(levels) != 2:
|
||||
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 bot levels and lut have ndim > 2')
|
||||
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:
|
||||
print(levels)
|
||||
raise Exception("levels argument must be 1D or 2D.")
|
||||
raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape))
|
||||
|
||||
profile()
|
||||
|
||||
# Decide on maximum scaled value
|
||||
if scale is None:
|
||||
if lut is not None:
|
||||
scale = lut.shape[0]
|
||||
scale = lut.shape[0] - 1
|
||||
else:
|
||||
scale = 255.
|
||||
|
||||
## Apply levels if given
|
||||
if levels is not None:
|
||||
# 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 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]:
|
||||
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)
|
||||
@ -931,20 +997,20 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
minVal, maxVal = levels[i]
|
||||
if minVal == maxVal:
|
||||
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
|
||||
else:
|
||||
# Apply level scaling unless it would have no effect on the data
|
||||
minVal, maxVal = levels
|
||||
if minVal != 0 or maxVal != scale:
|
||||
if minVal == maxVal:
|
||||
maxVal += 1e-16
|
||||
if maxVal == minVal:
|
||||
data = rescaleData(data, 1, minVal, dtype=int)
|
||||
else:
|
||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
|
||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
|
||||
|
||||
|
||||
profile()
|
||||
|
||||
## apply LUT if given
|
||||
# apply LUT if given
|
||||
if lut is not None:
|
||||
data = applyLookupTable(data, lut)
|
||||
else:
|
||||
@ -953,16 +1019,18 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
|
||||
profile()
|
||||
|
||||
## copy data into ARGB ordered array
|
||||
# this will be the final image array
|
||||
imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte)
|
||||
|
||||
profile()
|
||||
|
||||
# decide channel order
|
||||
if useRGBA:
|
||||
order = [0,1,2,3] ## array comes out RGBA
|
||||
order = [0,1,2,3] # array comes out RGBA
|
||||
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:
|
||||
# This is tempting:
|
||||
# imgData[..., :3] = data[..., np.newaxis]
|
||||
@ -978,6 +1046,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
|
||||
profile()
|
||||
|
||||
# add opaque alpha channel if needed
|
||||
if data.ndim == 2 or data.shape[2] == 3:
|
||||
alpha = False
|
||||
imgData[..., 3] = 255
|
||||
|
@ -47,6 +47,10 @@ class ImageItem(GraphicsObject):
|
||||
self.lut = None
|
||||
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.border = None
|
||||
self.removable = False
|
||||
@ -75,11 +79,6 @@ class ImageItem(GraphicsObject):
|
||||
self.paintMode = mode
|
||||
self.update()
|
||||
|
||||
## use setOpacity instead.
|
||||
#def setAlpha(self, alpha):
|
||||
#self.setOpacity(alpha)
|
||||
#self.updateImage()
|
||||
|
||||
def setBorder(self, b):
|
||||
self.border = fn.mkPen(b)
|
||||
self.update()
|
||||
@ -99,16 +98,6 @@ class ImageItem(GraphicsObject):
|
||||
return QtCore.QRectF(0., 0., 0., 0.)
|
||||
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):
|
||||
"""
|
||||
Set image scaling levels. Can be one of:
|
||||
@ -119,7 +108,11 @@ class ImageItem(GraphicsObject):
|
||||
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
|
||||
for more details on how levels are applied.
|
||||
"""
|
||||
if levels is not None:
|
||||
levels = np.asarray(levels)
|
||||
if not fn.eq(levels, self.levels):
|
||||
self.levels = levels
|
||||
self._effectiveLut = None
|
||||
if update:
|
||||
self.updateImage()
|
||||
|
||||
@ -137,7 +130,9 @@ class ImageItem(GraphicsObject):
|
||||
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
|
||||
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
|
||||
"""
|
||||
if lut is not self.lut:
|
||||
self.lut = lut
|
||||
self._effectiveLut = None
|
||||
if update:
|
||||
self.updateImage()
|
||||
|
||||
@ -222,7 +217,10 @@ class ImageItem(GraphicsObject):
|
||||
else:
|
||||
gotNewData = True
|
||||
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 'autoDownsample' not in kargs:
|
||||
kargs['autoDownsample'] = True
|
||||
@ -261,6 +259,17 @@ class ImageItem(GraphicsObject):
|
||||
if gotNewData:
|
||||
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):
|
||||
## used for re-rendering qimage from self.image.
|
||||
@ -298,6 +307,27 @@ class ImageItem(GraphicsObject):
|
||||
else:
|
||||
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)
|
||||
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
||||
|
||||
|
@ -111,6 +111,183 @@ def test_subArray():
|
||||
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__':
|
||||
test_interpolateArray()
|
Loading…
Reference in New Issue
Block a user