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.scale(rect.width() / self.width(), rect.height() / self.height()) self.translate(rect.left(), rect.top()) 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 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