pyqtgraph/graphicsItems/ImageItem.py
Luke Campagnola cd24530eb1 Bugfixes:
- Corrected ImageItem.setRect transformation order  
  - PlotCurveItem uses nkPen for interpreting shadowPen arguments
  - PlotItem and PlotWidget wrap a few more missing methods from ViewBox
2012-03-17 12:10:51 -04:00

538 lines
17 KiB
Python

from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
try:
import scipy.weave as weave
from scipy.weave import converters
except:
pass
import pyqtgraph.functions as fn
import pyqtgraph.debug as debug
from GraphicsObject import GraphicsObject
__all__ = ['ImageItem']
class ImageItem(GraphicsObject):
"""
GraphicsObject displaying an image. Optimized for rapid update (ie video display)
"""
sigImageChanged = QtCore.Signal()
## performance gains from this are marginal, and it's rather unreliable.
useWeave = False
def __init__(self, image=None, **kargs):
"""
See setImage for all allowed arguments.
"""
GraphicsObject.__init__(self)
#self.pixmapItem = QtGui.QGraphicsPixmapItem(self)
#self.qimage = QtGui.QImage()
#self._pixmap = None
self.image = None ## original image data
self.qimage = None ## rendered image for display
#self.clipMask = None
self.paintMode = None
#self.useWeave = True
self.levels = None ## [min, max] or [[redMin, redMax], ...]
self.lut = None
#self.clipLevel = None
self.drawKernel = None
self.border = None
if image is not None:
self.setImage(image, **kargs)
else:
self.setOpts(**kargs)
def setCompositionMode(self, mode):
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()
def width(self):
if self.image is None:
return None
return self.image.shape[0]
def height(self):
if self.image is None:
return None
return self.image.shape[1]
def boundingRect(self):
if self.image is None:
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:
[blackLevel, whiteLevel]
[[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
Only the first format is compatible with lookup tables.
"""
self.levels = levels
if update:
self.updateImage()
def getLevels(self):
return self.levels
#return self.whiteLevel, self.blackLevel
def setLookupTable(self, lut, update=True):
"""
Set the lookup table to use for this image. (see functions.makeARGB for more information on how this is used)
Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use."""
self.lut = lut
if update:
self.updateImage()
def setOpts(self, update=True, **kargs):
if 'lut' in kargs:
self.setLookupTable(kargs['lut'], update=update)
if 'levels' in kargs:
self.setLevels(kargs['levels'], update=update)
#if 'clipLevel' in kargs:
#self.setClipLevel(kargs['clipLevel'])
if 'opacity' in kargs:
self.setOpacity(kargs['opacity'])
if 'compositionMode' in kargs:
self.setCompositionMode(kargs['compositionMode'])
if 'border' in kargs:
self.setBorder(kargs['border'])
def setRect(self, rect):
"""Scale and translate the image to fit within rect."""
self.resetTransform()
self.translate(rect.left(), rect.top())
self.scale(rect.width() / self.width(), rect.height() / self.height())
def setImage(self, image=None, autoLevels=None, **kargs):
"""
Update the image displayed by this item.
Arguments:
image
autoLevels
lut
levels
opacity
compositionMode
border
"""
prof = debug.Profiler('ImageItem.setImage', disabled=True)
gotNewData = False
if image is None:
if self.image is None:
return
else:
gotNewData = True
if self.image is None or image.shape != self.image.shape:
self.prepareGeometryChange()
self.image = image.view(np.ndarray)
prof.mark('1')
if autoLevels is None:
if 'levels' in kargs:
autoLevels = False
else:
autoLevels = True
if autoLevels:
img = self.image
while img.size > 2**16:
img = img[::2, ::2]
mn, mx = img.min(), img.max()
if mn == mx:
mn = 0
mx = 255
kargs['levels'] = [mn,mx]
prof.mark('2')
self.setOpts(update=False, **kargs)
prof.mark('3')
self.qimage = None
self.update()
prof.mark('4')
if gotNewData:
self.sigImageChanged.emit()
prof.finish()
def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image.
## can we make any assumptions here that speed things up?
## dtype, range, size are all the same?
defaults = {
'autoLevels': False,
}
defaults.update(kargs)
return self.setImage(*args, **defaults)
def render(self):
prof = debug.Profiler('ImageItem.render', disabled=True)
if self.image is None:
return
if callable(self.lut):
lut = self.lut(self.image)
else:
lut = self.lut
argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels)
self.qimage = fn.makeQImage(argb, alpha)
#self.pixmap = QtGui.QPixmap.fromImage(self.qimage)
prof.finish()
def paint(self, p, *args):
prof = debug.Profiler('ImageItem.paint', disabled=True)
if self.image is None:
return
if self.qimage is None:
self.render()
if self.paintMode is not None:
p.setCompositionMode(self.paintMode)
p.drawImage(QtCore.QPointF(0,0), self.qimage)
if self.border is not None:
p.setPen(self.border)
p.drawRect(self.boundingRect())
prof.finish()
def getHistogram(self, bins=500, step=3):
"""returns x and y arrays containing the histogram values for the current image.
The step argument causes pixels to be skipped when computing the histogram to save time."""
if self.image is None:
return None,None
stepData = self.image[::step, ::step]
hist = np.histogram(stepData, bins=bins)
return hist[1][:-1], hist[0]
def setPxMode(self, b):
"""Set whether the item ignores transformations and draws directly to screen pixels."""
self.setFlag(self.ItemIgnoresTransformations, b)
def setScaledMode(self):
self.setPxMode(False)
def getPixmap(self):
if self.qimage is None:
self.render()
if self.qimage is None:
return None
return QtGui.QPixmap.fromImage(self.qimage)
def pixelSize(self):
"""return scene-size of a single pixel in the image"""
br = self.sceneBoundingRect()
if self.image is None:
return 1,1
return br.width()/self.width(), br.height()/self.height()
def mousePressEvent(self, ev):
if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
self.drawAt(ev.pos(), ev)
ev.accept()
else:
ev.ignore()
def mouseMoveEvent(self, ev):
#print "mouse move", ev.pos()
if self.drawKernel is not None:
self.drawAt(ev.pos(), ev)
def mouseReleaseEvent(self, ev):
pass
def tabletEvent(self, ev):
print ev.device()
print ev.pointerType()
print ev.pressure()
def drawAt(self, pos, ev=None):
pos = [int(pos.x()), int(pos.y())]
dk = self.drawKernel
kc = self.drawKernelCenter
sx = [0,dk.shape[0]]
sy = [0,dk.shape[1]]
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
for i in [0,1]:
dx1 = -min(0, tx[i])
dx2 = min(0, self.image.shape[0]-tx[i])
tx[i] += dx1+dx2
sx[i] += dx1+dx2
dy1 = -min(0, ty[i])
dy2 = min(0, self.image.shape[1]-ty[i])
ty[i] += dy1+dy2
sy[i] += dy1+dy2
#print sx
#print sy
#print tx
#print ty
#print self.image.shape
#print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape
#print dk[sx[0]:sx[1], sy[0]:sy[1]].shape
ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1]))
ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
#src = dk[sx[0]:sx[1], sy[0]:sy[1]]
#mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]]
mask = self.drawMask
src = dk
#print self.image[ts].shape, src.shape
if callable(self.drawMode):
self.drawMode(dk, self.image, mask, ss, ts, ev)
else:
src = src[ss]
if self.drawMode == 'set':
if mask is not None:
mask = mask[ss]
self.image[ts] = self.image[ts] * (1-mask) + src * mask
else:
self.image[ts] = src
elif self.drawMode == 'add':
self.image[ts] += src
else:
raise Exception("Unknown draw mode '%s'" % self.drawMode)
self.updateImage()
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
self.drawKernel = kernel
self.drawKernelCenter = center
self.drawMode = mode
self.drawMask = mask
#def setImage(self, image=None, copy=True, autoRange=True, clipMask=None, white=None, black=None, axes=None):
#prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self), disabled=True)
##debug.printTrace()
#if axes is None:
#axh = {'x': 0, 'y': 1, 'c': 2}
#else:
#axh = axes
##print "Update image", black, white
#if white is not None:
#self.whiteLevel = white
#if black is not None:
#self.blackLevel = black
#gotNewData = False
#if image is None:
#if self.image is None:
#return
#else:
#gotNewData = True
#if self.image is None or image.shape != self.image.shape:
#self.prepareGeometryChange()
#if copy:
#self.image = image.view(np.ndarray).copy()
#else:
#self.image = image.view(np.ndarray)
##print " image max:", self.image.max(), "min:", self.image.min()
#prof.mark('1')
## Determine scale factors
#if autoRange or self.blackLevel is None:
#if self.image.dtype is np.ubyte:
#self.blackLevel = 0
#self.whiteLevel = 255
#else:
#self.blackLevel = self.image.min()
#self.whiteLevel = self.image.max()
##print "Image item using", self.blackLevel, self.whiteLevel
#if self.blackLevel != self.whiteLevel:
#scale = 255. / (self.whiteLevel - self.blackLevel)
#else:
#scale = 0.
#prof.mark('2')
### Recolor and convert to 8 bit per channel
## Try using weave, then fall back to python
#shape = self.image.shape
#black = float(self.blackLevel)
#white = float(self.whiteLevel)
#if black == 0 and white == 255 and self.image.dtype == np.ubyte:
#im = self.image
#elif self.image.dtype in [np.ubyte, np.uint16]:
## use lookup table instead
#npts = 2**(self.image.itemsize * 8)
#lut = self.getLookupTable(npts, black, white)
#im = lut[self.image]
#else:
#im = self.applyColorScaling(self.image, black, scale)
#prof.mark('3')
#try:
#im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte)
#except:
#print im.shape, axh
#raise
#alpha = np.clip(int(255 * self.alpha), 0, 255)
#prof.mark('4')
## Fill image
#if im.ndim == 2:
#im2 = im.transpose(axh['y'], axh['x'])
#im1[..., 0] = im2
#im1[..., 1] = im2
#im1[..., 2] = im2
#im1[..., 3] = alpha
#elif im.ndim == 3: #color image
#im2 = im.transpose(axh['y'], axh['x'], axh['c'])
#if im2.shape[2] > 4:
#raise Exception("ImageItem got image with more than 4 color channels (shape is %s; axes are %s)" % (str(im.shape), str(axh)))
### [B G R A] Reorder colors
#order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image.
#for i in range(0, im.shape[axh['c']]):
#im1[..., order[i]] = im2[..., i]
### fill in unused channels with 0 or alpha
#for i in range(im.shape[axh['c']], 3):
#im1[..., i] = 0
#if im.shape[axh['c']] < 4:
#im1[..., 3] = alpha
#else:
#raise Exception("Image must be 2 or 3 dimensions")
##self.im1 = im1
## Display image
#prof.mark('5')
#if self.clipLevel is not None or clipMask is not None:
#if clipMask is not None:
#mask = clipMask.transpose()
#else:
#mask = (self.image < self.clipLevel).transpose()
#im1[..., 0][mask] *= 0.5
#im1[..., 1][mask] *= 0.5
#im1[..., 2][mask] = 255
#prof.mark('6')
##print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape
##self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :(
#prof.mark('7')
#try:
#buf = im1.data
#except AttributeError:
#im1 = np.ascontiguousarray(im1)
#buf = im1.data
#qimage = QtGui.QImage(buf, im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32)
#self.qimage = qimage
#self.qimage.data = im1
#self._pixmap = None
#prof.mark('8')
##self.pixmap = QtGui.QPixmap.fromImage(qimage)
#prof.mark('9')
###del self.ims
##self.item.setPixmap(self.pixmap)
#self.update()
#prof.mark('10')
#if gotNewData:
##self.emit(QtCore.SIGNAL('imageChanged'))
#self.sigImageChanged.emit()
#prof.finish()
#def getLookupTable(self, num, black, white):
#num = int(num)
#black = int(black)
#white = int(white)
#if white < black:
#b = black
#black = white
#white = b
#key = (num, black, white)
#lut = np.empty(num, dtype=np.ubyte)
#lut[:black] = 0
#rng = lut[black:white]
#try:
#rng[:] = np.linspace(0, 255, white-black)[:len(rng)]
#except:
#print key, rng.shape
#lut[white:] = 255
#return lut
#def applyColorScaling(self, img, offset, scale):
#try:
#if not ImageItem.useWeave:
#raise Exception('Skipping weave compile')
##sim = np.ascontiguousarray(self.image) ## should not be needed
#sim = img.reshape(img.size)
##sim.shape = sim.size
#im = np.empty(sim.shape, dtype=np.ubyte)
#n = im.size
#code = """
#for( int i=0; i<n; i++ ) {
#float a = (sim(i)-offset) * (float)scale;
#if( a > 255.0 )
#a = 255.0;
#else if( a < 0.0 )
#a = 0.0;
#im(i) = a;
#}
#"""
#weave.inline(code, ['sim', 'im', 'n', 'offset', 'scale'], type_converters=converters.blitz, compiler = 'gcc')
##sim.shape = shape
#im.shape = img.shape
#except:
#if ImageItem.useWeave:
#ImageItem.useWeave = False
##sys.excepthook(*sys.exc_info())
##print "=============================================================================="
##print "Weave compile failed, falling back to slower version."
##img.shape = shape
#im = ((img - offset) * scale).clip(0.,255.).astype(np.ubyte)
#return im