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)
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
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 )
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))
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()
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)
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 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 testLen( self ): for i in xrange(5): t = Tiling((100*i, 100), blockSize = 50) self.assertEqual(len(t), (100*i*2)//50)
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))
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()
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
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
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
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()