ImageItem.py 19.6 KB
Newer Older
1 2
from __future__ import division

3
from ..Qt import QtGui, QtCore
4
import numpy as np
5
import collections
6 7
from .. import functions as fn
from .. import debug as debug
8
from .GraphicsObject import GraphicsObject
9
from ..Point import Point
10 11

__all__ = ['ImageItem']
12 13


14 15
class ImageItem(GraphicsObject):
    """
Luke Campagnola's avatar
Luke Campagnola committed
16
    **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
17
    
Luke Campagnola's avatar
Luke Campagnola committed
18 19 20 21 22 23 24 25 26 27 28
    GraphicsObject displaying an image. Optimized for rapid update (ie video display).
    This item displays either a 2D numpy array (height, width) or
    a 3D array (height, width, RGBa). This array is optionally scaled (see 
    :func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
    with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
    before being displayed.
    
    ImageItem is frequently used in conjunction with 
    :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or 
    :class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
    for controlling the levels and lookup table used to display the image.
29 30 31 32
    """
    
    
    sigImageChanged = QtCore.Signal()
Luke Campagnola's avatar
Luke Campagnola committed
33
    sigRemoveRequested = QtCore.Signal(object)  # self; emitted when 'remove' is selected from context menu
34 35 36
    
    def __init__(self, image=None, **kargs):
        """
Luke Campagnola's avatar
Luke Campagnola committed
37
        See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
38 39
        """
        GraphicsObject.__init__(self)
Luke Campagnola's avatar
Luke Campagnola committed
40
        self.menu = None
41 42 43 44 45 46 47
        self.image = None   ## original image data
        self.qimage = None  ## rendered image for display
        
        self.paintMode = None
        
        self.levels = None  ## [min, max] or [[redMin, redMax], ...]
        self.lut = None
48
        self.autoDownsample = False
49 50 51
        
        self.drawKernel = None
        self.border = None
Luke Campagnola's avatar
Luke Campagnola committed
52
        self.removable = False
53 54 55 56 57 58 59
        
        if image is not None:
            self.setImage(image, **kargs)
        else:
            self.setOpts(**kargs)

    def setCompositionMode(self, mode):
Luke Campagnola's avatar
Luke Campagnola committed
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
        """Change the composition mode of the item (see QPainter::CompositionMode
        in the Qt documentation). This is useful when overlaying multiple ImageItems.
        
        ============================================  ============================================================
        **Most common arguments:**
        QtGui.QPainter.CompositionMode_SourceOver     Default; image replaces the background if it
                                                      is opaque. Otherwise, it uses the alpha channel to blend
                                                      the image with the background.
        QtGui.QPainter.CompositionMode_Overlay        The image color is mixed with the background color to 
                                                      reflect the lightness or darkness of the background.
        QtGui.QPainter.CompositionMode_Plus           Both the alpha and color of the image and background pixels 
                                                      are added together.
        QtGui.QPainter.CompositionMode_Multiply       The output is the image color multiplied by the background.
        ============================================  ============================================================
        """
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
        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):
        """
Luke Campagnola's avatar
Luke Campagnola committed
114 115 116 117 118 119 120
        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. See :func:`makeARGB <pyqtgraph.makeARGB>`
        for more details on how levels are applied.
121 122 123 124 125 126 127 128 129 130 131
        """
        self.levels = levels
        if update:
            self.updateImage()
        
    def getLevels(self):
        return self.levels
        #return self.whiteLevel, self.blackLevel

    def setLookupTable(self, lut, update=True):
        """
Luke Campagnola's avatar
Luke Campagnola committed
132 133 134 135 136 137 138 139
        Set the lookup table (numpy array) to use for this image. (see 
        :func:`makeARGB <pyqtgraph.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.
        
        Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
        or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
        """
140 141 142 143
        self.lut = lut
        if update:
            self.updateImage()

144
    def setAutoDownsample(self, ads):
Luke Campagnola's avatar
Luke Campagnola committed
145 146 147 148 149
        """
        Set the automatic downsampling mode for this ImageItem.
        
        Added in version 0.9.9
        """
150 151 152 153
        self.autoDownsample = ads
        self.qimage = None
        self.update()

154
    def setOpts(self, update=True, **kargs):
155
        
156 157 158 159 160 161 162 163 164 165 166 167
        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'])
Luke Campagnola's avatar
Luke Campagnola committed
168 169 170
        if 'removable' in kargs:
            self.removable = kargs['removable']
            self.menu = None
171 172 173 174
        if 'autoDownsample' in kargs:
            self.setAutoDownsample(kargs['autoDownsample'])
        if update:
            self.update()
175 176

    def setRect(self, rect):
Luke Campagnola's avatar
Luke Campagnola committed
177
        """Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
178 179
        self.resetTransform()
        self.translate(rect.left(), rect.top())
Luke Campagnola's avatar
Luke Campagnola committed
180
        self.scale(rect.width() / self.width(), rect.height() / self.height())
181

182 183 184 185 186 187
    def clear(self):
        self.image = None
        self.prepareGeometryChange()
        self.informViewBoundsChanged()
        self.update()

188 189
    def setImage(self, image=None, autoLevels=None, **kargs):
        """
Luke Campagnola's avatar
Luke Campagnola committed
190 191
        Update the image displayed by this item. For more information on how the image
        is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
192
        
Luke Campagnola's avatar
Luke Campagnola committed
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
        =================  =========================================================================
        **Arguments:**
        image              (numpy array) Specifies the image data. May be 2D (width, height) or 
                           3D (width, height, RGBa). The array dtype must be integer or floating
                           point of any bit depth. For 3D arrays, the third dimension must
                           be of length 3 (RGB) or 4 (RGBA).
        autoLevels         (bool) If True, this forces the image to automatically select 
                           levels based on the maximum and minimum values in the data.
                           By default, this argument is true unless the levels argument is
                           given.
        lut                (numpy array) The color lookup table to use when displaying the image.
                           See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
        levels             (min, max) The minimum and maximum values to use when rescaling the image
                           data. By default, this will be set to the minimum and maximum values 
                           in the image. If the image array has dtype uint8, no rescaling is necessary.
        opacity            (float 0.0-1.0)
        compositionMode    see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
        border             Sets the pen used when drawing the image border. Default is None.
211 212 213
        autoDownsample     (bool) If True, the image is automatically downsampled to match the
                           screen resolution. This improves performance for large images and 
                           reduces aliasing.
Luke Campagnola's avatar
Luke Campagnola committed
214
        =================  =========================================================================
215
        """
216 217
        profile = debug.Profiler()

218 219 220 221 222 223
        gotNewData = False
        if image is None:
            if self.image is None:
                return
        else:
            gotNewData = True
224
            shapeChanged = (self.image is None or image.shape != self.image.shape)
225
            self.image = image.view(np.ndarray)
226 227 228
            if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1:
                if 'autoDownsample' not in kargs:
                    kargs['autoDownsample'] = True
229 230 231
            if shapeChanged:
                self.prepareGeometryChange()
                self.informViewBoundsChanged()
232 233 234

        profile()

235 236 237 238 239 240 241 242 243 244 245 246 247 248
        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]
249 250 251

        profile()

252
        self.setOpts(update=False, **kargs)
253 254 255

        profile()

256 257
        self.qimage = None
        self.update()
258 259

        profile()
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276

        if gotNewData:
            self.sigImageChanged.emit()


    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):
277 278
        # Convert data to QImage for display.
        
279
        profile = debug.Profiler()
280
        if self.image is None or self.image.size == 0:
281
            return
282
        if isinstance(self.lut, collections.Callable):
283 284 285
            lut = self.lut(self.image)
        else:
            lut = self.lut
286

287 288 289 290 291
        if self.autoDownsample:
            # reduce dimensions of image based on screen resolution
            o = self.mapToDevice(QtCore.QPointF(0,0))
            x = self.mapToDevice(QtCore.QPointF(1,0))
            y = self.mapToDevice(QtCore.QPointF(0,1))
292 293
            w = Point(x-o).length()
            h = Point(y-o).length()
294 295 296 297 298 299 300
            xds = max(1, int(1/w))
            yds = max(1, int(1/h))
            image = fn.downsample(self.image, xds, axis=0)
            image = fn.downsample(image, yds, axis=1)
        else:
            image = self.image
        
301
        argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels)
302
        self.qimage = fn.makeQImage(argb, alpha, transpose=False)
303 304

    def paint(self, p, *args):
305
        profile = debug.Profiler()
306 307 308 309
        if self.image is None:
            return
        if self.qimage is None:
            self.render()
310 311
            if self.qimage is None:
                return
312
            profile('render QImage')
313 314
        if self.paintMode is not None:
            p.setCompositionMode(self.paintMode)
315 316
            profile('set comp mode')

317
        p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage)
318
        profile('p.drawImage')
319 320 321 322
        if self.border is not None:
            p.setPen(self.border)
            p.drawRect(self.boundingRect())

Luke Campagnola's avatar
Luke Campagnola committed
323 324 325 326 327
    def save(self, fileName, *args):
        """Save this image to file. Note that this saves the visible image (after scale/color changes), not the original data."""
        if self.qimage is None:
            self.render()
        self.qimage.save(fileName, *args)
328

329
    def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds):
Luke Campagnola's avatar
Luke Campagnola committed
330
        """Returns x and y arrays containing the histogram values for the current image.
331 332 333 334 335 336 337 338 339 340 341 342 343 344
        For an explanation of the return format, see numpy.histogram().
        
        The *step* argument causes pixels to be skipped when computing the histogram to save time.
        If *step* is 'auto', then a step is chosen such that the analyzed data has
        dimensions roughly *targetImageSize* for each axis.
        
        The *bins* argument and any extra keyword arguments are passed to 
        np.histogram(). If *bins* is 'auto', then a bin number is automatically
        chosen based on the image characteristics:
        
        * Integer images will have approximately *targetHistogramSize* bins, 
          with each bin having an integer width.
        * All other types will have *targetHistogramSize* bins.
        
Luke Campagnola's avatar
Luke Campagnola committed
345 346
        This method is also used when automatically computing levels.
        """
347 348
        if self.image is None:
            return None,None
349
        if step == 'auto':
350 351
            step = (np.ceil(self.image.shape[0] / targetImageSize),
                    np.ceil(self.image.shape[1] / targetImageSize))
352 353 354 355 356 357 358 359 360 361
        if np.isscalar(step):
            step = (step, step)
        stepData = self.image[::step[0], ::step[1]]
        
        if bins == 'auto':
            if stepData.dtype.kind in "ui":
                mn = stepData.min()
                mx = stepData.max()
                step = np.ceil((mx-mn) / 500.)
                bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
Luke Campagnola's avatar
Luke Campagnola committed
362 363
                if len(bins) == 0:
                    bins = [mn, mx]
364
            else:
365 366 367 368 369
                bins = 500

        kwds['bins'] = bins
        hist = np.histogram(stepData, **kwds)
        
370 371 372
        return hist[1][:-1], hist[0]

    def setPxMode(self, b):
Luke Campagnola's avatar
Luke Campagnola committed
373 374 375 376 377 378
        """
        Set whether the item ignores transformations and draws directly to screen pixels.
        If True, the item will not inherit any scale or rotation transformations from its
        parent items, but its position will be transformed as usual.
        (see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
        """
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
        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()
397 398 399 400 401
    
    def viewTransformChanged(self):
        if self.autoDownsample:
            self.qimage = None
            self.update()
402

Luke Campagnola's avatar
Luke Campagnola committed
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    #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 mouseDragEvent(self, ev):
        if ev.button() != QtCore.Qt.LeftButton:
            ev.ignore()
            return
Luke Campagnola's avatar
Luke Campagnola committed
422 423 424
        elif self.drawKernel is not None:
            ev.accept()
            self.drawAt(ev.pos(), ev)
Luke Campagnola's avatar
Luke Campagnola committed
425 426 427 428 429

    def mouseClickEvent(self, ev):
        if ev.button() == QtCore.Qt.RightButton:
            if self.raiseContextMenu(ev):
                ev.accept()
430 431
        if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
            self.drawAt(ev.pos(), ev)
Luke Campagnola's avatar
Luke Campagnola committed
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466

    def raiseContextMenu(self, ev):
        menu = self.getMenu()
        if menu is None:
            return False
        menu = self.scene().addParentContextMenus(self, menu, ev)
        pos = ev.screenPos()
        menu.popup(QtCore.QPoint(pos.x(), pos.y()))
        return True

    def getMenu(self):
        if self.menu is None:
            if not self.removable:
                return None
            self.menu = QtGui.QMenu()
            self.menu.setTitle("Image")
            remAct = QtGui.QAction("Remove image", self.menu)
            remAct.triggered.connect(self.removeClicked)
            self.menu.addAction(remAct)
            self.menu.remAct = remAct
        return self.menu
        
        
    def hoverEvent(self, ev):
        if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton):
            ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
            ev.acceptClicks(QtCore.Qt.RightButton)
            #self.box.setBrush(fn.mkBrush('w'))
        elif not ev.isExit() and self.removable:
            ev.acceptClicks(QtCore.Qt.RightButton)  ## accept context menu clicks
        #else:
            #self.box.setBrush(self.brush)
        #self.update()


467 468
        
    def tabletEvent(self, ev):
469 470 471
        print(ev.device())
        print(ev.pointerType())
        print(ev.pressure())
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
    
    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

        ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1]))
        ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
        mask = self.drawMask
        src = dk
        
498
        if isinstance(self.drawMode, collections.Callable):
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
            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

Luke Campagnola's avatar
Luke Campagnola committed
520
    def removeClicked(self):
521 522
        ## Send remove event only after we have exited the menu event handler
        self.removeTimer = QtCore.QTimer()
523
        self.removeTimer.timeout.connect(self.emitRemoveRequested)
524 525
        self.removeTimer.start(0)

526 527 528
    def emitRemoveRequested(self):
        self.removeTimer.timeout.disconnect(self.emitRemoveRequested)
        self.sigRemoveRequested.emit(self)