Example #1
0
    def reset(self):
        """Reset rotations, tiling, etc. Called when first initialized
        and when the underlying data changes.

        """
        DEFAULT_TILE_WIDTH = 512

        self.resetAxes(finish=False)

        tileWidth = self.tileWidth
        if self.tileWidth is None:
            tileWidth = DEFAULT_TILE_WIDTH

        self._tiling = Tiling(self._dataShape,
                              self.data2scene,
                              name=self.name,
                              blockSize=tileWidth)

        self._tileProvider = TileProvider(self._tiling,
                                          self._stackedImageSources)
        self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)
        self._dirtyIndicator.setVisible(False)
Example #2
0
    def testData2Scene(self):
        t = Tiling((0, 0))
        trans = QTransform()
        t.data2scene = trans
        self.assertEquals(trans, t.data2scene)

        # try using transformation that is not invertible
        trans = QTransform(1, 1, 1, 1, 1, 1)
        with self.assertRaises(AssertionError):
            t.data2scene = trans
Example #3
0
 def testNoneShape( self ):
     t = Tiling((0,0))
     self.assertEqual( t.imageRectFs, [] )
     self.assertEqual( t.tileRectFs, [] )
     self.assertEqual( t.imageRects, [] )
     self.assertEqual( t.tileRects, [] )
     self.assertEqual( t.sliceShape, (0,0) )
     self.assertEqual( t.boundingRectF(), QRectF(0,0,0,0) )
     self.assertEqual( t.containsF(QPoint(0,0)), None )
     self.assertEqual( t.intersected( QRect(0,0,1,1) ), [])
     self.assertEqual( len(t), 0 )
Example #4
0
    def testSetAllLayersInvisible( self ):
        tiling = Tiling((900,400), blockSize=100)
        tp = TileProvider(tiling, self.sims)

        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()
        tiles = tp.getTiles(QRectF(100,100,200,200))
        for tile in tiles:
            aimg = byte_view(tile.qimg)
            self.assertTrue(np.all(aimg[:,:,0:3] == self.GRAY3))
            self.assertTrue(np.all(aimg[:,:,3] == 255))

        self.layer1.visible = False
        self.layer2.visible = False
        self.layer3.visible = False
        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()
        tiles = tp.getTiles(QRectF(100,100,200,200))
        for tile in tiles:
            # If all tiles are invisible, then no tile is even rendered at all.
            assert tile.qimg is None

        self.layer1.visible = False
        self.layer2.visible = True
        self.layer2.opacity = 1.0
        self.layer3.visible = False
        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()
        tiles = tp.getTiles(QRectF(100,100,200,200))
        for tile in tiles:
            aimg = byte_view(tile.qimg)
            self.assertTrue(np.all(aimg[:,:,0:3] == self.GRAY2))
            self.assertTrue(np.all(aimg[:,:,3] == 255))
Example #5
0
    def testEverythingDirtyPropagation(self):
        self.lsm.append(self.layer2)
        tiling = Tiling((900, 400), blockSize=100)
        tp = TileProvider(tiling, self.pump.stackedImageSources)
        try:
            tp.requestRefresh(QRectF(100, 100, 200, 200))
            tp.join()
            tiles = tp.getTiles(QRectF(100, 100, 200, 200))
            for tile in tiles:
                aimg = byte_view(tile.qimg)
                self.assertTrue(np.all(aimg[:, :, 0:3] == self.CONSTANT))
                self.assertTrue(np.all(aimg[:, :, 3] == 255))

            NEW_CONSTANT = self.CONSTANT + 1
            self.ds2.constant = NEW_CONSTANT
            tp.requestRefresh(QRectF(100, 100, 200, 200))
            tp.join()
            tiles = tp.getTiles(QRectF(100, 100, 200, 200))
            for tile in tiles:
                aimg = byte_view(tile.qimg)
                self.assertTrue(np.all(aimg[:, :, 0:3] == NEW_CONSTANT))
                self.assertTrue(np.all(aimg[:, :, 3] == 255))

        finally:
            tp.notifyThreadsToStop()
            tp.joinThreads()
Example #6
0
    def reset(self):
        """Reset rotations, tiling, etc. Called when first initialized
        and when the underlying data changes.

        """
        self.resetAxes(finish=False)

        self._tiling = Tiling(self._dataShape, self.data2scene, name=self.name)
        self._brushingLayer  = TiledImageLayer(self._tiling)

        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)
Example #7
0
    def reset(self):
        """Reset rotations, tiling, etc. Called when first initialized
        and when the underlying data changes.

        """
        self.resetAxes(finish=False)

        self._tiling = Tiling(self._dataShape, self.data2scene, name=self.name)

        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)
        self._dirtyIndicator.setVisible(False)
Example #8
0
    def sceneShape(self, sceneShape):
        """
        Set the size of the scene in QGraphicsView's coordinate system.
        sceneShape -- (widthX, widthY),
        where the origin of the coordinate system is in the upper left corner
        of the screen and 'x' points right and 'y' points down
        """   
        assert len(sceneShape) == 2
        self.setSceneRect(0,0, *sceneShape)
        
        #The scene shape is in Qt's QGraphicsScene coordinate system,
        #that is the origin is in the top left of the screen, and the
        #'x' axis points to the right and the 'y' axis down.
        
        #The coordinate system of the data handles things differently.
        #The x axis points down and the y axis points to the right.

        r = self.scene2data.mapRect(QRect(0,0,sceneShape[0], sceneShape[1]))
        sliceShape = (r.width(), r.height())
        
        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = None

        self._tiling = Tiling(sliceShape, self.data2scene)

        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)

        self._onSizeChanged()
        if self._tileProvider:
            self._tileProvider.notifyThreadsToStop() # prevent ref cycle

        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.changed.connect(self.invalidateViewports)
Example #9
0
 def testLen( self ):
     for i in xrange(5):
         t = Tiling((100*i, 100), blockSize = 50)
         self.assertEqual(len(t), (100*i*2)//50)
Example #10
0
    def testOutOfViewDirtyPropagation( self ):
        self.lsm.append(self.layer1)
        tiling = Tiling((900,400), blockSize=100)
        tp = TileProvider(tiling, self.pump.stackedImageSources)

        # Navigate down to the second z-slice
        self.pump.syncedSliceSources.through = [0,1,0]
        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()

        # Sanity check: Do we see the right data on the second
        # slice? (should be all 1s)
        tiles = tp.getTiles(QRectF(100,100,200,200))
        for tile in tiles:
            aimg = byte_view(tile.qimg)
            self.assertTrue(np.all(aimg[:,:,0:3] == 1))
            self.assertTrue(np.all(aimg[:,:,3] == 255))

        # Navigate down to the third z-slice
        self.pump.syncedSliceSources.through = [0,2,0]
        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()

        # Sanity check: Do we see the right data on the third
        # slice?(should be all 2s)
        tiles = tp.getTiles(QRectF(100,100,200,200))
        for tile in tiles:
            aimg = byte_view(tile.qimg)
            self.assertTrue(np.all(aimg[:,:,0:3] == 2))
            self.assertTrue(np.all(aimg[:,:,3] == 255))

        # Navigate back up to the second z-slice
        self.pump.syncedSliceSources.through = [0,1,0]
        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()
        for tile in tiles:
            aimg = byte_view(tile.qimg)
            self.assertTrue(np.all(aimg[:,:,0:3] == 1))
            self.assertTrue(np.all(aimg[:,:,3] == 255))

        # Change some of the data in the (out-of-view) third z-slice
        slicing = (slice(None), slice(100,300), slice(100,300),
                   slice(2,3), slice(None))
        slicing = tuple(slicing)
        self.ds1._array[slicing] = 99
        self.ds1.setDirty( slicing )

        # Navigate back down to the third z-slice
        self.pump.syncedSliceSources.through = [0,2,0]
        tp.requestRefresh(QRectF(100,100,200,200))
        tp.waitForTiles()

        # Even though the data was out-of-view when it was
        # changed, it should still have new values. If dirtiness
        # wasn't propagated correctly, the cache's old values will
        # be used. (For example, this fails if you comment out the
        # call to setDirty, above.)

        # Shrink accessed rect by 1 pixel on each side (Otherwise,
        # tiling overlap_draw causes getTiles() to return
        # surrounding tiles that we haven't actually touched in
        # this test)
        tiles = tp.getTiles(QRectF(101,101,198,198))

        for tile in tiles:
            aimg = byte_view(tile.qimg)
            # Use any() because the tile borders may not be
            # perfectly aligned with the data we changed.
            self.assertTrue(np.any(aimg[:,:,0:3] == 99))
Example #11
0
class ImageScene2D(QGraphicsScene):
    """
    The 2D scene description of a tiled image generated by evaluating
    an overlay stack, together with a 2D cursor.
    """

    @property
    def stackedImageSources(self):
        return self._stackedImageSources
    
    @stackedImageSources.setter
    def stackedImageSources(self, s):
        self._stackedImageSources = s
        s.sizeChanged.connect(self._onSizeChanged)

    @property
    def showTileOutlines(self):
        return self._showTileOutlines
    @showTileOutlines.setter
    def showTileOutlines(self, show):
        self._showTileOutlines = show
        self.invalidate()

    @property
    def sceneShape(self):
        """
        The shape of the scene in QGraphicsView's coordinate system.
        """
        return (self.sceneRect().width(), self.sceneRect().height())
    @sceneShape.setter
    def sceneShape(self, sceneShape):
        """
        Set the size of the scene in QGraphicsView's coordinate system.
        sceneShape -- (widthX, widthY),
        where the origin of the coordinate system is in the upper left corner
        of the screen and 'x' points right and 'y' points down
        """   
        assert len(sceneShape) == 2
        self.setSceneRect(0,0, *sceneShape)
        
        #The scene shape is in Qt's QGraphicsScene coordinate system,
        #that is the origin is in the top left of the screen, and the
        #'x' axis points to the right and the 'y' axis down.
        
        #The coordinate system of the data handles things differently.
        #The x axis points down and the y axis points to the right.

        r = self.scene2data.mapRect(QRect(0,0,sceneShape[0], sceneShape[1]))
        sliceShape = (r.width(), r.height())
        
        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = None

        self._tiling = Tiling(sliceShape, self.data2scene)

        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)

        self._onSizeChanged()
        if self._tileProvider:
            self._tileProvider.notifyThreadsToStop() # prevent ref cycle

        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.changed.connect(self.invalidateViewports)

    def invalidateViewports( self, rectF ):
        '''Call invalidate on the intersection of all observing viewport-rects and rectF.'''
        rectF = rectF if rectF.isValid() else self.sceneRect()
        for view in self.views():
            QGraphicsScene.invalidate( self, rectF.intersected(view.viewportRect()) )        

    def __init__( self, parent=None ):
        QGraphicsScene.__init__( self, parent=parent )

        # tiled rendering of patches
        self._tiling         = None
        self._brushingLayer  = None
        # indicates the dirtyness of each tile
        self._dirtyIndicator = None

        self._tileProvider = None
        self._stackedImageSources = None
        self._showTileOutlines = False
    
        self.data2scene = QTransform(0,1,1,0,0,0) 
        self.scene2data = self.data2scene.transposed()
        
        self._slicingPositionSettled = True

        self._redrawIndicator = False
    
    def drawLine(self, fromPoint, toPoint, pen):
        tileId = self._tiling.containsF(toPoint)
        if tileId is None:
            return
       
        p = self._brushingLayer[tileId] 
        p.lock()
        painter = QPainter(p.image)
        painter.setPen(pen)
        
        tL = self._tiling.imageRectFs[tileId].topLeft()
        painter.drawLine(fromPoint-tL, toPoint-tL)
        painter.end()
        p.dataVer += 1
        p.unlock()
        self.scheduleRedraw(self._tiling.imageRectFs[tileId])

    def _onSizeChanged(self):
        self._brushingLayer  = TiledImageLayer(self._tiling)
                
    def drawForeground(self, painter, rect):
        if self._tiling is None:
            return

        tile_nos = self._tiling.intersectedF(rect)

        for tileId in tile_nos:
            p = self._brushingLayer[tileId]
            if p.dataVer == p.imgVer:
                continue

            p.paint(painter) #access to the underlying image patch is serialized

            ## draw tile outlines
            if self._showTileOutlines:
                painter.drawRect(self._tiling.imageRects[tileId])
    
    def indicateSlicingPositionSettled(self, settled):
        self._dirtyIndicator.setVisible(settled)
        self._slicingPositionSettled = settled
   
    def drawBackground(self, painter, rectF):
        if self._tileProvider is None:
            return

        tiles = self._tileProvider.getTiles(rectF)
        for tile in tiles:
            # prevent flickering
            if not tile.progress < 1.0:
                painter.drawImage(tile.rectF, tile.qimg)
            self._dirtyIndicator.setTileProgress(tile.id, tile.progress) 

    def joinRendering( self ):
        return self._tileProvider.join()
Example #12
0
class ImageScene2D(QGraphicsScene):
    """
    The 2D scene description of a tiled image generated by evaluating
    an overlay stack, together with a 2D cursor.
    """
    axesChanged = pyqtSignal(int, bool)
    dirtyChanged = pyqtSignal()

    @property
    def is_swapped(self):
        """
        Indicates whether the dimensions are swapped
        swapping the axis will swap the dimensions and rotating the roi will swap the dimensions
        :return: bool
        """
        return bool(self._swapped) != bool(self._rotation % 2)  # xor

    @property
    def stackedImageSources(self):
        return self._stackedImageSources

    @stackedImageSources.setter
    def stackedImageSources(self, s):
        self._stackedImageSources = s

    @property
    def showTileOutlines(self):
        return self._showTileOutlines
    @showTileOutlines.setter
    def showTileOutlines(self, show):
        self._showTileOutlines = show
        self.invalidate()

    @property
    def showTileProgress(self):
        return self._showTileProgress
    
    @showTileProgress.setter
    def showTileProgress(self, show):
        self._showTileProgress = show
        self._dirtyIndicator.setVisible(show)

    def resetAxes(self, finish=True):
        # rotation is in range(4) and indicates in which corner of the
        # view the origin lies. 0 = top left, 1 = top right, etc.
        self._rotation = 0
        self._swapped = self._swappedDefault # whether axes are swapped
        self._newAxes()
        self._setSceneRect()
        self.scene2data, isInvertible = self.data2scene.inverted()
        assert isInvertible
        if finish:
            self._finishViewMatrixChange()

    def _newAxes(self):
        """Given self._rotation and self._swapped, calculates and sets
        the appropriate data2scene transformation.

        """
        # TODO: this function works, but it is not elegant. There must
        # be a simpler way to calculate the appropriate transformation.

        w, h = self.dataShape
        assert self._rotation in range(0, 4)

        # unlike self._rotation, the local variable 'rotation'
        # indicates how many times to rotate clockwise after swapping
        # axes.

        # t1 : do axis swap
        t1 = QTransform()
        if self._swapped:
            t1 = QTransform(0, 1, 0,
                            1, 0, 0,
                            0, 0, 1)
            h, w = w, h

        # t2 : do rotation
        t2 = QTransform()
        t2.rotate(self._rotation * 90)

        # t3: shift to re-center
        rot2trans = {0 : (0, 0),
                     1 : (h, 0),
                     2 : (w, h),
                     3 : (0, w)}

        trans = rot2trans[self._rotation]
        t3 = QTransform.fromTranslate(*trans)

        self.data2scene = t1 * t2 * t3
        if self._tileProvider:
            self._tileProvider.axesSwapped = self._swapped
        self.axesChanged.emit(self._rotation, self._swapped)

    def rot90(self, direction):
        """ direction: left ==> -1, right ==> +1"""
        assert direction in [-1, 1]
        self._rotation = (self._rotation + direction) % 4
        self._newAxes()

    def swapAxes(self, transform):
        self._swapped = not self._swapped
        self._newAxes()

    def _onRotateLeft(self):
        self.rot90(-1)
        self._finishViewMatrixChange()

    def _onRotateRight(self):
        self.rot90(1)
        self._finishViewMatrixChange()

    def _onSwapAxes(self):
        self.swapAxes(self.data2scene)
        self._finishViewMatrixChange()

    def _finishViewMatrixChange(self):
        self.scene2data, isInvertible = self.data2scene.inverted()
        self._setSceneRect()
        self._tiling.data2scene = self.data2scene
        self._tileProvider._onSizeChanged()
        QGraphicsScene.invalidate(self, self.sceneRect())

    @property
    def sceneShape(self):
        return (self.sceneRect().width(), self.sceneRect().height())

    def _setSceneRect(self):
        w, h = self.dataShape
        rect = self.data2scene.mapRect(QRect(0, 0, w, h))
        sw, sh = rect.width(), rect.height()
        self.setSceneRect(0, 0, sw, sh)
        
        if self._dataRectItem is not None:
            self.removeItem( self._dataRectItem )
        
        #this property represent a parent to QGraphicsItems which should
        #be clipped to the data, such as temporary capped lines for brushing.
        #This works around ilastik issue #516.
        self._dataRectItem = QGraphicsRectItem(0,0,sw,sh)
        self._dataRectItem.setPen(QPen(QColor(0,0,0,0)))
        self._dataRectItem.setFlag(QGraphicsItem.ItemClipsChildrenToShape)
        self.addItem(self._dataRectItem)

    @property
    def dataRectItem(self):
        return self._dataRectItem

    @property
    def dataShape(self):
        """
        The shape of the scene in QGraphicsView's coordinate system.
        """
        return self._dataShape

    @dataShape.setter
    def dataShape(self, value):
        """
        Set the size of the scene in QGraphicsView's coordinate system.
        dataShape -- (widthX, widthY),
        where the origin of the coordinate system is in the upper left corner
        of the screen and 'x' points right and 'y' points down
        """
        assert len(value) == 2
        self._dataShape = value
        self.reset()
        self._finishViewMatrixChange()

    def setCacheSize(self, cache_size):
        self._tileProvider.set_cache_size(cache_size)

    def cacheSize(self):
        return self._tileProvider.cache_size

    def setPrefetchingEnabled(self, enable):
        self._prefetching_enabled = enable

    def setPreemptiveFetchNumber(self, n):
        if n > self.cacheSize() - 1:
            self._n_preemptive = self.cacheSize() - 1
        else:
            self._n_preemptive = n
    def preemptiveFetchNumber(self):
        return self._n_preemptive

    def invalidateViewports(self, sceneRectF):
        '''Call invalidate on the intersection of all observing viewport-rects and rectF.'''
        sceneRectF = sceneRectF if sceneRectF.isValid() else self.sceneRect()
        for view in self.views():
            QGraphicsScene.invalidate(self, sceneRectF.intersected(view.viewportRect()))

    def reset(self):
        """Reset rotations, tiling, etc. Called when first initialized
        and when the underlying data changes.

        """
        self.resetAxes(finish=False)

        self._tiling = Tiling(self._dataShape, self.data2scene, name=self.name)

        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)
        self._dirtyIndicator.setVisible(False)


    def __init__(self, posModel, along, preemptive_fetch_number=5,
                 parent=None, name="Unnamed Scene",
                 swapped_default=False):
        """
        * preemptive_fetch_number -- number of prefetched slices; 0 turns the feature off
        * swapped_default -- whether axes should be swapped by default.

        """
        QGraphicsScene.__init__(self, parent=parent)

        self._along = along
        self._posModel = posModel

        # QGraphicsItems can change this if they are in a state that should temporarily forbid brushing
        # (For example, when the slice intersection marker is in 'draggable' state.)
        self.allow_brushing = True

        self._dataShape = (0, 0)
        self._dataRectItem = None #A QGraphicsRectItem (or None)
        self._offsetX = 0
        self._offsetY = 0
        self.name = name

        self._stackedImageSources = StackedImageSources(LayerStackModel())
        self._showTileOutlines = False

        # FIXME: We don't show the red 'progress pies' because they look terrible.  
        #        If we could fix their timing, maybe it would be worth it.
        self._showTileProgress = False

        self._tileProvider = None
        self._dirtyIndicator = None
        self._prefetching_enabled = False
        
        self._swappedDefault = swapped_default
        self.reset()

        # BowWave preemptive caching
        self.setPreemptiveFetchNumber(preemptive_fetch_number)
        self._course = (1,1) # (along, pos or neg direction)
        self._time = self._posModel.time
        self._channel = self._posModel.channel
        self._posModel.timeChanged.connect(self._onTimeChanged)
        self._posModel.channelChanged.connect(self._onChannelChanged)
        self._posModel.slicingPositionChanged.connect(self._onSlicingPositionChanged)
        
        self._allTilesCompleteEvent = threading.Event()
        self.dirty = False
        
        # We manually keep track of the tile-wise QGraphicsItems that
        # we've added to the scene in this dict, otherwise we would need
        # to use O(N) lookups for every tile by calling QGraphicsScene.items()
        self.tile_graphicsitems = defaultdict(set) # [Tile.id] -> set(QGraphicsItems)

    def drawForeground(self, painter, rect):
        if self._tiling is None:
            return

        if self._showTileOutlines:
            tile_nos = self._tiling.intersected(rect)

            for tileId in tile_nos:
            ## draw tile outlines
                # Dashed black line
                pen = QPen()
                pen.setDashPattern([5,5])
                painter.setPen(pen)
                painter.drawRect(self._tiling.imageRects[tileId])

                # Dashed white line
                # (offset to occupy the spaces in the dashed black line)
                pen = QPen()
                pen.setDashPattern([5,5])
                pen.setDashOffset(5)
                pen.setColor(QColor(Qt.white))
                painter.setPen(pen)
                painter.drawRect(self._tiling.imageRects[tileId])

    def indicateSlicingPositionSettled(self, settled):
        if self._showTileProgress:
            self._dirtyIndicator.setVisible(settled)

    def drawBackground(self, painter, sceneRectF):
        if self._tileProvider is None:
            return

        tiles = self._tileProvider.getTiles(sceneRectF)
        allComplete = True
        for tile in tiles:
            #We always draw the tile, even though it might not be up-to-date
            #In ilastik's live mode, the user sees the old result while adding
            #new brush strokes on top
            #See also ilastik issue #132 and tests/lazy_test.py
            if tile.qimg is not None:
                painter.drawImage(tile.rectF, tile.qimg)

            # The tile also contains a list of any QGraphicsItems that were produced by the layers.
            # If there are any new ones, add them to the scene.
            new_items = set(tile.qgraphicsitems) - self.tile_graphicsitems[tile.id]
            obsolete_items = self.tile_graphicsitems[tile.id] - set(tile.qgraphicsitems)
            for g_item in obsolete_items:
                self.tile_graphicsitems[tile.id].remove(g_item)
                self.removeItem(g_item)
            for g_item in new_items:
                self.tile_graphicsitems[tile.id].add(g_item)
                self.addItem(g_item)

            if tile.progress < 1.0:
                allComplete = False
            if self._showTileProgress:
                self._dirtyIndicator.setTileProgress(tile.id, tile.progress)

        if allComplete:
            if self.dirty:
                self.dirty = False
                self.dirtyChanged.emit()
            self._allTilesCompleteEvent.set()
        else:
            if not self.dirty:
                self.dirty = True
                self.dirtyChanged.emit()
            self._allTilesCompleteEvent.clear()

        # preemptive fetching
        if self._prefetching_enabled:
            upcoming_through_slices = self._bowWave(self._n_preemptive)
            for through in upcoming_through_slices:
                self._tileProvider.prefetch(sceneRectF, through, layer_indexes=None)

    def triggerPrefetch(self, layer_indexes, time_range='current', spatial_axis_range='current', sceneRectF=None ):
        """
        Trigger a one-time prefetch for the given set of layers.
        
        TODO: I'm not 100% sure what happens here for layers with multiple channels.
        
        layer_indexes: list-of-ints, or None, which means 'all visible'.
        time_range: (start_time, stop_time)
        spatial_axis_range: (start_slice, stop_slice), meaning Z/Y/X depending on our projection (self.along)
        sceneRectF: Used to determine which tiles to request.
                    An invalid QRectF results in all tiles getting refreshed (visible or not).
        """
        # Process parameters
        sceneRectF = sceneRectF or QRectF()

        if time_range == 'current':
            time_range = (self._posModel.slicingPos5D[0],
                          self._posModel.slicingPos5D[0]+1)
        elif time_range == 'all':
            time_range = (0, self._posModel.shape5D[0])
        else:
            assert len(time_range) == 2
            assert time_range[0] >= 0 and time_range[1] < self._posModel.shape5D[0]

        spatial_axis = self._along[1]
        if spatial_axis_range == 'current':
            spatial_axis_range = (self._posModel.slicingPos5D[spatial_axis],
                                  self._posModel.slicingPos5D[spatial_axis]+1)
        elif spatial_axis_range == 'all':
            spatial_axis_range = (0, self._posModel.shape5D[spatial_axis])
        else:
            assert len(spatial_axis_range) == 2
            assert 0 <= spatial_axis_range[0] <  self._posModel.shape5D[spatial_axis]
            assert 0 <  spatial_axis_range[1] <= self._posModel.shape5D[spatial_axis]

        # Construct list of 'through' coordinates
        through_list = []
        for t in range( *time_range ):
            for s in range( *spatial_axis_range ):
                through_list.append( (t, s) )

        # Make sure the tile cache is big enough to hold the prefetched data.
        if self._tileProvider.cache_size < len(through_list):
            self._tileProvider.set_cache_size( len(through_list) )

        # Trigger prefetches
        for through in through_list:
            self._tileProvider.prefetch(sceneRectF, through, layer_indexes)
        

    def joinRenderingAllTiles(self, viewport_only=True, rect=None):
        """
        Wait until all tiles in the scene have been 100% rendered.
        If sceneRectF is None, use the viewport rect.
        If sceneRectF is an invalid QRectF(), then wait for all tiles.
        Note: If called from the GUI thread, the GUI thread will block until all tiles are rendered!
        """
        # If this is the main thread, keep repainting (otherwise we'll deadlock).
        if threading.current_thread().name == "MainThread":
            if viewport_only:
                sceneRectF = self.views()[0].viewportRect()
            else:
                if rect is None or not isinstance(rect, QRectF):
                    sceneRectF = QRectF() # invalid QRectF means 'get all tiles'
                else:
                    sceneRectF = rect
            self._tileProvider.waitForTiles(sceneRectF)
        else:
            self._allTilesCompleteEvent.wait()


    def _bowWave(self, n):
        through = [ self._posModel.slicingPos5D[axis] for axis in self._along[:-1] ]
        t_max = [ self._posModel.shape5D[axis] for axis in self._along[:-1] ]

        BowWave = []

        a = self._course[0]
        for d in xrange(1,n+1):
            m = through[a] + d * self._course[1]
            if m < t_max[a] and m >= 0:
                t = list(through)
                t[a] = m
                BowWave.append(tuple(t))
        return BowWave

    def _onSlicingPositionChanged(self, new, old):
        if (new[self._along[1] - 1] - old[self._along[1] - 1]) < 0:
            self._course = (1, -1)
        else:
            self._course = (1, 1)

    def _onChannelChanged(self, new):
        if (new - self._channel) < 0:
            self._course = (2, -1)
        else:
            self._course = (2, 1)
        self._channel = new

    def _onTimeChanged(self, new):
        if (new - self._time) < 0:
            self._course = (0, -1)
        else:
            self._course = (0, 1)
        self._time = new
Example #13
0
class ImageScene2D(QGraphicsScene):
    """
    The 2D scene description of a tiled image generated by evaluating
    an overlay stack, together with a 2D cursor.
    """

    axesChanged = pyqtSignal(int, bool)
    dirtyChanged = pyqtSignal()

    @property
    def is_swapped(self):
        """
        Indicates whether the dimensions are swapped
        swapping the axis will swap the dimensions and rotating the roi will swap the dimensions
        :return: bool
        """
        return bool(self._swapped) != bool(self._rotation % 2)  # xor

    @property
    def stackedImageSources(self):
        return self._stackedImageSources

    @stackedImageSources.setter
    def stackedImageSources(self, s):
        self._stackedImageSources = s

    @property
    def showTileOutlines(self):
        return self._showTileOutlines

    @showTileOutlines.setter
    def showTileOutlines(self, show):
        self._showTileOutlines = show
        self.invalidate()

    @property
    def showTileProgress(self):
        return self._showTileProgress

    @showTileProgress.setter
    def showTileProgress(self, show):
        self._showTileProgress = show
        self._dirtyIndicator.setVisible(show)

    def resetAxes(self, finish=True):
        # rotation is in range(4) and indicates in which corner of the
        # view the origin lies. 0 = top left, 1 = top right, etc.
        self._rotation = 0
        self._swapped = self._swappedDefault  # whether axes are swapped
        self._newAxes()
        self._setSceneRect()
        self.scene2data, isInvertible = self.data2scene.inverted()
        assert isInvertible
        if finish:
            self._finishViewMatrixChange()

    def _newAxes(self):
        """Given self._rotation and self._swapped, calculates and sets
        the appropriate data2scene transformation.

        """
        # TODO: this function works, but it is not elegant. There must
        # be a simpler way to calculate the appropriate transformation.

        w, h = self.dataShape
        assert self._rotation in range(0, 4)

        # unlike self._rotation, the local variable 'rotation'
        # indicates how many times to rotate clockwise after swapping
        # axes.

        # t1 : do axis swap
        t1 = QTransform()
        if self._swapped:
            t1 = QTransform(0, 1, 0, 1, 0, 0, 0, 0, 1)
            h, w = w, h

        # t2 : do rotation
        t2 = QTransform()
        t2.rotate(self._rotation * 90)

        # t3: shift to re-center
        rot2trans = {0: (0, 0), 1: (h, 0), 2: (w, h), 3: (0, w)}

        trans = rot2trans[self._rotation]
        t3 = QTransform.fromTranslate(*trans)

        self.data2scene = t1 * t2 * t3
        if self._tileProvider:
            self._tileProvider.axesSwapped = self._swapped
        self.axesChanged.emit(self._rotation, self._swapped)

    def rot90(self, direction):
        """ direction: left ==> -1, right ==> +1"""
        assert direction in [-1, 1]
        self._rotation = (self._rotation + direction) % 4
        self._newAxes()

    def swapAxes(self, transform):
        self._swapped = not self._swapped
        self._newAxes()

    def _onRotateLeft(self):
        self.rot90(-1)
        self._finishViewMatrixChange()

    def _onRotateRight(self):
        self.rot90(1)
        self._finishViewMatrixChange()

    def _onSwapAxes(self):
        self.swapAxes(self.data2scene)
        self._finishViewMatrixChange()

    def _finishViewMatrixChange(self):
        self.scene2data, isInvertible = self.data2scene.inverted()
        self._setSceneRect()
        self._tiling.data2scene = self.data2scene
        self._tileProvider._onSizeChanged()
        QGraphicsScene.invalidate(self, self.sceneRect())

    @property
    def sceneShape(self):
        return (self.sceneRect().width(), self.sceneRect().height())

    def _setSceneRect(self):
        w, h = self.dataShape
        rect = self.data2scene.mapRect(QRect(0, 0, w, h))
        sw, sh = rect.width(), rect.height()
        self.setSceneRect(0, 0, sw, sh)

        if self._dataRectItem is not None:
            self.removeItem(self._dataRectItem)

        # this property represent a parent to QGraphicsItems which should
        # be clipped to the data, such as temporary capped lines for brushing.
        # This works around ilastik issue #516.
        self._dataRectItem = QGraphicsRectItem(0, 0, sw, sh)
        self._dataRectItem.setPen(QPen(QColor(0, 0, 0, 0)))
        self._dataRectItem.setFlag(QGraphicsItem.ItemClipsChildrenToShape)
        self.addItem(self._dataRectItem)

    @property
    def dataRectItem(self):
        return self._dataRectItem

    @property
    def dataShape(self):
        """
        The shape of the scene in QGraphicsView's coordinate system.
        """
        return self._dataShape

    @dataShape.setter
    def dataShape(self, value):
        """
        Set the size of the scene in QGraphicsView's coordinate system.
        dataShape -- (widthX, widthY),
        where the origin of the coordinate system is in the upper left corner
        of the screen and 'x' points right and 'y' points down
        """
        assert len(value) == 2
        self._dataShape = value
        self.reset()
        self._finishViewMatrixChange()

    def setCacheSize(self, cache_size):
        self._tileProvider.set_cache_size(cache_size)

    def cacheSize(self):
        return self._tileProvider.cache_size

    def setTileWidth(self, tileWidth):
        self._tileWidth = tileWidth
        PreferencesManager().set("ImageScene2D", "tileWidth", tileWidth)

    def tileWidth(self):
        return self._tileWidth

    def setPrefetchingEnabled(self, enable):
        self._prefetching_enabled = enable

    def setPreemptiveFetchNumber(self, n):
        if n > self.cacheSize() - 1:
            self._n_preemptive = self.cacheSize() - 1
        else:
            self._n_preemptive = n

    def preemptiveFetchNumber(self):
        return self._n_preemptive

    def invalidateViewports(self, sceneRectF):
        """Call invalidate on the intersection of all observing viewport-rects and rectF."""
        sceneRectF = sceneRectF if sceneRectF.isValid() else self.sceneRect()
        for view in self.views():
            QGraphicsScene.invalidate(self, sceneRectF.intersected(view.viewportRect()))

    def reset(self):
        """Reset rotations, tiling, etc. Called when first initialized
        and when the underlying data changes.

        """
        self.resetAxes(finish=False)

        self._tiling = Tiling(self._dataShape, self.data2scene, name=self.name, blockSize=self.tileWidth())

        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)
        self._dirtyIndicator.setVisible(False)

    def mouseMoveEvent(self, event):
        """
        Normally our base class (QGraphicsScene) distributes mouse events to the
        various QGraphicsItems in the scene. But when the mouse is being dragged,
        it only sends events to the one object that was under the mouse when the
        button was first pressed.

        Here, we forward all events to QGraphicsItems on the drag path, even if
        they're just brushed by the mouse incidentally.
        """
        super(ImageScene2D, self).mouseMoveEvent(event)

        if not event.isAccepted() and event.buttons() != Qt.NoButton:
            if self.last_drag_pos is None:
                self.last_drag_pos = event.scenePos()

            # As a special feature, find the item and send it this event.
            path = QPainterPath(self.last_drag_pos)
            path.lineTo(event.scenePos())
            items = self.items(path)
            for item in items:
                item.mouseMoveEvent(event)
            self.last_drag_pos = event.scenePos()
        else:
            self.last_drag_pos = None

    def mousePressEvent(self, event):
        """
        By default, our base class (QGraphicsScene) only sends mouse press events to the top-most item under the mouse.
        When labeling edges, we want the edge label layer to accept mouse events, even if it isn't on top.
        Therefore, we send events to all items under the mouse, until the event is accepted.
        """
        super(ImageScene2D, self).mousePressEvent(event)
        if not event.isAccepted():
            items = self.items(event.scenePos())
            for item in items:
                item.mousePressEvent(event)
                if event.isAccepted():
                    break

    def __init__(
        self, posModel, along, preemptive_fetch_number=5, parent=None, name="Unnamed Scene", swapped_default=False
    ):
        """
        * preemptive_fetch_number -- number of prefetched slices; 0 turns the feature off
        * swapped_default -- whether axes should be swapped by default.

        """
        QGraphicsScene.__init__(self, parent=parent)

        self._along = along
        self._posModel = posModel

        # QGraphicsItems can change this if they are in a state that should temporarily forbid brushing
        # (For example, when the slice intersection marker is in 'draggable' state.)
        self.allow_brushing = True

        self._dataShape = (0, 0)
        self._dataRectItem = None  # A QGraphicsRectItem (or None)
        self._offsetX = 0
        self._offsetY = 0
        self.name = name
        self._tileWidth = PreferencesManager().get("ImageScene2D", "tileWidth", default=512)

        self._stackedImageSources = StackedImageSources(LayerStackModel())
        self._showTileOutlines = False

        # FIXME: We don't show the red 'progress pies' because they look terrible.
        #        If we could fix their timing, maybe it would be worth it.
        self._showTileProgress = False

        self._tileProvider = None
        self._dirtyIndicator = None
        self._prefetching_enabled = False

        self._swappedDefault = swapped_default
        self.reset()

        # BowWave preemptive caching
        self.setPreemptiveFetchNumber(preemptive_fetch_number)
        self._course = (1, 1)  # (along, pos or neg direction)
        self._time = self._posModel.time
        self._channel = self._posModel.channel
        self._posModel.timeChanged.connect(self._onTimeChanged)
        self._posModel.channelChanged.connect(self._onChannelChanged)
        self._posModel.slicingPositionChanged.connect(self._onSlicingPositionChanged)

        self._allTilesCompleteEvent = threading.Event()
        self.dirty = False

        # We manually keep track of the tile-wise QGraphicsItems that
        # we've added to the scene in this dict, otherwise we would need
        # to use O(N) lookups for every tile by calling QGraphicsScene.items()
        self.tile_graphicsitems = defaultdict(set)  # [Tile.id] -> set(QGraphicsItems)

        self.last_drag_pos = None  # See mouseMoveEvent()

    def drawForeground(self, painter, rect):
        if self._tiling is None:
            return

        if self._showTileOutlines:
            tile_nos = self._tiling.intersected(rect)

            for tileId in tile_nos:
                ## draw tile outlines
                # Dashed black line
                pen = QPen()
                pen.setWidth(0)
                pen.setDashPattern([5, 5])
                painter.setPen(pen)
                painter.drawRect(self._tiling.imageRects[tileId])

                # Dashed white line
                # (offset to occupy the spaces in the dashed black line)
                pen = QPen()
                pen.setWidth(0)
                pen.setDashPattern([5, 5])
                pen.setDashOffset(5)
                pen.setColor(QColor(Qt.white))
                painter.setPen(pen)
                painter.drawRect(self._tiling.imageRects[tileId])

    def indicateSlicingPositionSettled(self, settled):
        if self._showTileProgress:
            self._dirtyIndicator.setVisible(settled)

    def drawBackground(self, painter, sceneRectF):
        if self._tileProvider is None:
            return

        # FIXME: For some strange reason, drawBackground is called with
        #        a much larger sceneRectF than necessasry sometimes.
        #        This can happen after panSlicingViews(), for instance.
        #        Somehow, the QGraphicsScene gets confused about how much area
        #        it needs to draw immediately after the ImageView's scrollbar is panned.
        #        As a workaround, we manually check the amount of the scene that needs to be drawn,
        #        instead of relying on the above sceneRectF parameter to be correct.
        if self.views():
            sceneRectF = self.views()[0].viewportRect().intersected(sceneRectF)

        if not sceneRectF.isValid():
            return

        tiles = self._tileProvider.getTiles(sceneRectF)
        allComplete = True
        for tile in tiles:
            # We always draw the tile, even though it might not be up-to-date
            # In ilastik's live mode, the user sees the old result while adding
            # new brush strokes on top
            # See also ilastik issue #132 and tests/lazy_test.py
            if tile.qimg is not None:
                painter.drawImage(tile.rectF, tile.qimg)

            # The tile also contains a list of any QGraphicsItems that were produced by the layers.
            # If there are any new ones, add them to the scene.
            new_items = set(tile.qgraphicsitems) - self.tile_graphicsitems[tile.id]
            obsolete_items = self.tile_graphicsitems[tile.id] - set(tile.qgraphicsitems)
            for g_item in obsolete_items:
                self.tile_graphicsitems[tile.id].remove(g_item)
                self.removeItem(g_item)
            for g_item in new_items:
                self.tile_graphicsitems[tile.id].add(g_item)
                self.addItem(g_item)

            if tile.progress < 1.0:
                allComplete = False
            if self._showTileProgress:
                self._dirtyIndicator.setTileProgress(tile.id, tile.progress)

        if allComplete:
            if self.dirty:
                self.dirty = False
                self.dirtyChanged.emit()
            self._allTilesCompleteEvent.set()
        else:
            if not self.dirty:
                self.dirty = True
                self.dirtyChanged.emit()
            self._allTilesCompleteEvent.clear()

        # preemptive fetching
        if self._prefetching_enabled:
            upcoming_through_slices = self._bowWave(self._n_preemptive)
            for through in upcoming_through_slices:
                self._tileProvider.prefetch(sceneRectF, through, layer_indexes=None)

    def triggerPrefetch(self, layer_indexes, time_range="current", spatial_axis_range="current", sceneRectF=None):
        """
        Trigger a one-time prefetch for the given set of layers.

        TODO: I'm not 100% sure what happens here for layers with multiple channels.

        layer_indexes: list-of-ints, or None, which means 'all visible'.
        time_range: (start_time, stop_time)
        spatial_axis_range: (start_slice, stop_slice), meaning Z/Y/X depending on our projection (self.along)
        sceneRectF: Used to determine which tiles to request.
                    An invalid QRectF results in all tiles getting refreshed (visible or not).
        """
        # Process parameters
        sceneRectF = sceneRectF or QRectF()

        if time_range == "current":
            time_range = (self._posModel.slicingPos5D[0], self._posModel.slicingPos5D[0] + 1)
        elif time_range == "all":
            time_range = (0, self._posModel.shape5D[0])
        else:
            assert len(time_range) == 2
            assert time_range[0] >= 0 and time_range[1] < self._posModel.shape5D[0]

        spatial_axis = self._along[1]
        if spatial_axis_range == "current":
            spatial_axis_range = (
                self._posModel.slicingPos5D[spatial_axis],
                self._posModel.slicingPos5D[spatial_axis] + 1,
            )
        elif spatial_axis_range == "all":
            spatial_axis_range = (0, self._posModel.shape5D[spatial_axis])
        else:
            assert len(spatial_axis_range) == 2
            assert 0 <= spatial_axis_range[0] < self._posModel.shape5D[spatial_axis]
            assert 0 < spatial_axis_range[1] <= self._posModel.shape5D[spatial_axis]

        # Construct list of 'through' coordinates
        through_list = []
        for t in range(*time_range):
            for s in range(*spatial_axis_range):
                through_list.append((t, s))

        # Make sure the tile cache is big enough to hold the prefetched data.
        if self._tileProvider.cache_size < len(through_list):
            self._tileProvider.set_cache_size(len(through_list))

        # Trigger prefetches
        for through in through_list:
            self._tileProvider.prefetch(sceneRectF, through, layer_indexes)

    def joinRenderingAllTiles(self, viewport_only=True, rect=None):
        """
        Wait until all tiles in the scene have been 100% rendered.
        If sceneRectF is None, use the viewport rect.
        If sceneRectF is an invalid QRectF(), then wait for all tiles.
        Note: If called from the GUI thread, the GUI thread will block until all tiles are rendered!
        """
        # If this is the main thread, keep repainting (otherwise we'll deadlock).
        if threading.current_thread().name == "MainThread":
            if viewport_only:
                sceneRectF = self.views()[0].viewportRect()
            else:
                if rect is None or not isinstance(rect, QRectF):
                    sceneRectF = QRectF()  # invalid QRectF means 'get all tiles'
                else:
                    sceneRectF = rect
            self._tileProvider.waitForTiles(sceneRectF)
        else:
            self._allTilesCompleteEvent.wait()

    def _bowWave(self, n):
        through = [self._posModel.slicingPos5D[axis] for axis in self._along[:-1]]
        t_max = [self._posModel.shape5D[axis] for axis in self._along[:-1]]

        BowWave = []

        a = self._course[0]
        for d in range(1, n + 1):
            m = through[a] + d * self._course[1]
            if m < t_max[a] and m >= 0:
                t = list(through)
                t[a] = m
                BowWave.append(tuple(t))
        return BowWave

    def _onSlicingPositionChanged(self, new, old):
        if (new[self._along[1] - 1] - old[self._along[1] - 1]) < 0:
            self._course = (1, -1)
        else:
            self._course = (1, 1)

    def _onChannelChanged(self, new):
        if (new - self._channel) < 0:
            self._course = (2, -1)
        else:
            self._course = (2, 1)
        self._channel = new

    def _onTimeChanged(self, new):
        if (new - self._time) < 0:
            self._course = (0, -1)
        else:
            self._course = (0, 1)
        self._time = new
Example #14
0
class ImageScene2D(QGraphicsScene):
    """
    The 2D scene description of a tiled image generated by evaluating
    an overlay stack, together with a 2D cursor.
    """
    axesChanged = pyqtSignal(int, bool)

    @property
    def stackedImageSources(self):
        return self._stackedImageSources

    @stackedImageSources.setter
    def stackedImageSources(self, s):
        self._stackedImageSources = s
        s.sizeChanged.connect(self._onSizeChanged)

    @property
    def showTileOutlines(self):
        return self._showTileOutlines
    @showTileOutlines.setter
    def showTileOutlines(self, show):
        self._showTileOutlines = show
        self.invalidate()

    @property
    def showTileProgress(self):
        return self._showTileProgress
    
    @showTileProgress.setter
    def showTileProgress(self, show):
        self._showTileProgress = show
        self._dirtyIndicator.setVisible(show)

    def resetAxes(self, finish=True):
        # rotation is in range(4) and indicates in which corner of the
        # view the origin lies. 0 = top left, 1 = top right, etc.
        self._rotation = 0
        self._swapped = self._swappedDefault # whether axes are swapped
        self._newAxes()
        self._setSceneRect()
        self.scene2data, isInvertible = self.data2scene.inverted()
        assert isInvertible
        if finish:
            self._finishViewMatrixChange()

    def _newAxes(self):
        """Given self._rotation and self._swapped, calculates and sets
        the appropriate data2scene transformation.

        """
        # TODO: this function works, but it is not elegant. There must
        # be a simpler way to calculate the appropriate tranformation.

        w, h = self.dataShape
        assert self._rotation in range(0, 4)

        # unlike self._rotation, the local variable 'rotation'
        # indicates how many times to rotate clockwise after swapping
        # axes.

        # t1 : do axis swap
        t1 = QTransform()
        if self._swapped:
            t1 = QTransform(0, 1, 0, 1, 0, 0, 0, 0, 1)
            h, w = w, h

        # t2 : do rotation
        t2 = QTransform()
        t2.rotate(self._rotation * 90)

        # t3: shift to re-center
        rot2trans = {0 : (0, 0),
                     1 : (h, 0),
                     2 : (w, h),
                     3 : (0, w)}

        trans = rot2trans[self._rotation]
        t3 = QTransform.fromTranslate(*trans)

        self.data2scene = t1 * t2 * t3
        if self._tileProvider:
            self._tileProvider.axesSwapped = self._swapped
        self.axesChanged.emit(self._rotation, self._swapped)

    def rot90(self, transform, rect, direction):
        """ direction: left ==> -1, right ==> +1"""
        assert direction in [-1, 1]
        self._rotation = (self._rotation + direction) % 4
        self._newAxes()

    def swapAxes(self, transform):
        self._swapped = not self._swapped
        self._newAxes()

    def _onRotateLeft(self):
        self.rot90(self.data2scene, self.sceneRect(), -1)
        self._finishViewMatrixChange()

    def _onRotateRight(self):
        self.rot90(self.data2scene, self.sceneRect(), 1)
        self._finishViewMatrixChange()

    def _onSwapAxes(self):
        self.swapAxes(self.data2scene)
        self._finishViewMatrixChange()

    def _finishViewMatrixChange(self):
        self.scene2data, isInvertible = self.data2scene.inverted()
        self._setSceneRect()
        self._tiling.data2scene = self.data2scene
        self._tileProvider._onSizeChanged()
        QGraphicsScene.invalidate(self, self.sceneRect())

    @property
    def sceneShape(self):
        return (self.sceneRect().width(), self.sceneRect().height())

    def _setSceneRect(self):
        w, h = self.dataShape
        rect = self.data2scene.mapRect(QRect(0, 0, w, h))
        sw, sh = rect.width(), rect.height()
        self.setSceneRect(0, 0, sw, sh)

    @property
    def dataShape(self):
        """
        The shape of the scene in QGraphicsView's coordinate system.
        """
        return self._dataShape

    @dataShape.setter
    def dataShape(self, value):
        """
        Set the size of the scene in QGraphicsView's coordinate system.
        dataShape -- (widthX, widthY),
        where the origin of the coordinate system is in the upper left corner
        of the screen and 'x' points right and 'y' points down
        """
        assert len(value) == 2
        self._dataShape = value
        self.reset()
        self._finishViewMatrixChange()

    def setCacheSize(self, cache_size):
        if cache_size != self._tileProvider._cache_size:
            self._tileProvider = TileProvider(self._tiling, self._stackedImageSources, cache_size=cache_size)
            self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

    def cacheSize(self):
        return self._tileProvider._cache_size

    def setPrefetchingEnabled(self, enable):
        self._prefetching_enabled = enable

    def setPreemptiveFetchNumber(self, n):
        if n > self.cacheSize() - 1:
            self._n_preemptive = self.cacheSize() - 1
        else:
            self._n_preemptive = n
    def preemptiveFetchNumber(self):
        return self._n_preemptive

    def invalidateViewports(self, sceneRectF):
        '''Call invalidate on the intersection of all observing viewport-rects and rectF.'''
        sceneRectF = sceneRectF if sceneRectF.isValid() else self.sceneRect()
        for view in self.views():
            QGraphicsScene.invalidate(self, sceneRectF.intersected(view.viewportRect()))

    def reset(self):
        """Reset rotations, tiling, etc. Called when first initialized
        and when the underlying data changes.

        """
        self.resetAxes(finish=False)

        self._tiling = Tiling(self._dataShape, self.data2scene, name=self.name)
        self._brushingLayer  = TiledImageLayer(self._tiling)

        if self._tileProvider:
            self._tileProvider.notifyThreadsToStop() # prevent ref cycle
        self._tileProvider = TileProvider(self._tiling, self._stackedImageSources)
        self._tileProvider.sceneRectChanged.connect(self.invalidateViewports)

        if self._dirtyIndicator:
            self.removeItem(self._dirtyIndicator)
        del self._dirtyIndicator
        self._dirtyIndicator = DirtyIndicator(self._tiling)
        self.addItem(self._dirtyIndicator)


    def __init__(self, posModel, along, preemptive_fetch_number=5,
                 parent=None, name="Unnamed Scene",
                 swapped_default=False):
        """
        * preemptive_fetch_number -- number of prefetched slices; 0 turns the feature off
        * swapped_default -- whether axes should be swapped by default.

        """
        QGraphicsScene.__init__(self, parent=parent)

        self._along = along
        self._posModel = posModel

        self._dataShape = (0, 0)
        self._offsetX = 0
        self._offsetY = 0
        self.name = name

        self._stackedImageSources = StackedImageSources(LayerStackModel())
        self._showTileOutlines = False
        self._showTileProgress = True

        self._tileProvider = None
        self._dirtyIndicator = None
        self._prefetching_enabled = False
        
        self._swappedDefault = swapped_default
        self.reset()

        # BowWave preemptive caching
        self.setPreemptiveFetchNumber(preemptive_fetch_number)
        self._course = (1,1) # (along, pos or neg direction)
        self._time = self._posModel.time
        self._channel = self._posModel.channel
        self._posModel.timeChanged.connect(self._onTimeChanged)
        self._posModel.channelChanged.connect(self._onChannelChanged)
        self._posModel.slicingPositionChanged.connect(self._onSlicingPositionChanged)
        
        self._allTilesCompleteEvent = threading.Event()

    def __del__(self):
        if self._tileProvider:
            self._tileProvider.notifyThreadsToStop()
        self.joinRendering()

    def _onSizeChanged(self):
        self._brushingLayer  = TiledImageLayer(self._tiling)

    def drawForeground(self, painter, rect):
        if self._tiling is None:
            return

        tile_nos = self._tiling.intersected(rect)

        for tileId in tile_nos:
            p = self._brushingLayer[tileId]
            if p.dataVer == p.imgVer:
                continue

            p.paint(painter) #access to the underlying image patch is serialized

            ## draw tile outlines
            if self._showTileOutlines:
                # Dashed black line
                pen = QPen()
                pen.setDashPattern([5,5])
                painter.setPen(pen)
                painter.drawRect(self._tiling.imageRects[tileId])

                # Dashed white line
                # (offset to occupy the spaces in the dashed black line)
                pen = QPen()
                pen.setDashPattern([5,5])
                pen.setDashOffset(5)
                pen.setColor(QColor(Qt.white))
                painter.setPen(pen)
                painter.drawRect(self._tiling.imageRects[tileId])

    def indicateSlicingPositionSettled(self, settled):
        if self._showTileProgress:
            self._dirtyIndicator.setVisible(settled)

    def drawBackground(self, painter, sceneRectF):
        if self._tileProvider is None:
            return

        tiles = self._tileProvider.getTiles(sceneRectF)
        allComplete = True
        for tile in tiles:
            #We always draw the tile, even though it might not be up-to-date
            #In ilastik's live mode, the user sees the old result while adding
            #new brush strokes on top
            #See also ilastik issue #132 and tests/lazy_test.py
            if tile.qimg is not None:
                painter.drawImage(tile.rectF, tile.qimg)
            if tile.progress < 1.0:
                allComplete = False
            if self._showTileProgress:
                self._dirtyIndicator.setTileProgress(tile.id, tile.progress)

        if allComplete:
            self._allTilesCompleteEvent.set()
        else:
            self._allTilesCompleteEvent.clear()

        # preemptive fetching
        if self._prefetching_enabled:
            for through in self._bowWave(self._n_preemptive):
                self._tileProvider.prefetch(sceneRectF, through)

    def joinRendering(self):
        return self._tileProvider.join()

    def joinRenderingAllTiles(self):
        """
        Wait until all tiles in the scene have been 100% rendered.
        Note: This is useful for testing only.  If called from the GUI thread, the GUI thread will block until all tiles are rendered!
        """
        # If this is the main thread, keep repainting (otherwise we'll deadlock).
        if threading.current_thread().name == "MainThread":
            finished = False
            sceneRectF = self.views()[0].viewportRect()
            while not finished:
                finished = True
                tiles = self._tileProvider.getTiles(sceneRectF)
                for tile in tiles:
                    finished &= tile.progress >= 1.0
        else:
            self._allTilesCompleteEvent.wait()

    def _bowWave(self, n):
        shape5d = self._posModel.shape5D
        sl5d = self._posModel.slicingPos5D
        through = [sl5d[self._along[i]] for i in xrange(3)]
        t_max = [shape5d[self._along[i]] for i in xrange(3)]

        BowWave = []

        a = self._course[0]
        for d in xrange(1,n+1):
            m = through[a] + d * self._course[1]
            if m < t_max[a] and m >= 0:
                t = list(through)
                t[a] = m
                BowWave.append(tuple(t))
        return BowWave

    def _onSlicingPositionChanged(self, new, old):
        if (new[self._along[1] - 1] - old[self._along[1] - 1]) < 0:
            self._course = (1, -1)
        else:
            self._course = (1, 1)

    def _onChannelChanged(self, new):
        if (new - self._channel) < 0:
            self._course = (2, -1)
        else:
            self._course = (2, 1)
        self._channel = new

    def _onTimeChanged(self, new):
        if (new - self._time) < 0:
            self._course = (0, -1)
        else:
            self._course = (0, 1)
        self._time = new
Example #15
0
    def testPaintDelay(self):
        t = Tiling((100, 100))
        assert len(t.tileRectFs) == 1

        delay = datetime.timedelta(milliseconds=300)
        # fudge should prevent hitting the delay time exactly
        # during the while loops below;
        # if your computer is verrry slow and the fudge too small
        # the test will fail...
        fudge = datetime.timedelta(milliseconds=50)
        d = DirtyIndicator(t, delay=delay)

        # make the image a little bit larger to accomodate the tile overlap
        img = QImage(110, 110, QImage.Format_ARGB32_Premultiplied)
        img.fill(0)
        img_saved = QImage(img)

        painter = QPainter()
        style = QStyleOptionGraphicsItem()
        style.exposedRect = t.tileRectFs[0]

        start = datetime.datetime.now()
        d.setTileProgress(0, 0)  # resets delay timer

        # 1. do not update the progress during the delay time
        actually_checked = False
        while datetime.datetime.now() - start < delay - fudge:
            # nothing should be painted
            self.assertEqual(img, img_saved)
            actually_checked = True
        self.assertTrue(actually_checked)
        time.sleep(fudge.total_seconds() * 2)
        # after the delay, the pie chart is painted
        painter.begin(img)
        d.paint(painter, style, None)
        self.assertNotEqual(img, img_saved)
        painter.end()

        # 2. update the progress during delay (this exposed a bug:
        #    the delay was ignored in that case and the pie chart
        #    painted nevertheless)
        d = DirtyIndicator(t, delay=delay)
        img.fill(0)
        start = datetime.datetime.now()
        d.setTileProgress(0, 0)  # resets delay timer

        actually_checked = False
        self.assertEqual(img, img_saved)  # precondition
        while datetime.datetime.now() - start < delay - fudge:
            # the painted during the delay time should have no effect
            painter.begin(img)
            d.setTileProgress(0, 0.5)
            d.paint(painter, style, None)
            painter.end()
            self.assertEqual(img, img_saved)
            actually_checked = True
        self.assertTrue(actually_checked)
        time.sleep(fudge.total_seconds() * 2)
        # now the pie should be painted
        painter.begin(img)
        d.paint(painter, style, None)
        self.assertNotEqual(img, img_saved)
        painter.end()