Merge pull request #272 from campagnola/image-performance

Image performance
This commit is contained in:
Luke Campagnola 2016-01-30 00:36:30 -08:00
commit dcf45b03e2
8 changed files with 357 additions and 251 deletions

View File

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

View File

@ -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)

View File

@ -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:

View File

@ -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)

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
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

View File

@ -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)

View File

@ -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()