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 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 testSetAllLayersInvisible( self ): tiling = Tiling((900,400), blockSize=100) tp = TileProvider(tiling, self.sims) 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.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.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] == 255)) # all white self.assertTrue(np.all(aimg[:,:,3] == 255)) 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.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.GRAY2)) self.assertTrue(np.all(aimg[:,:,3] == 255)) finally: tp.notifyThreadsToStop() tp.joinThreads()
def testSetAllLayersInvisible(self): tiling = Tiling((900, 400), blockSize=100) tp = TileProvider(tiling, self.sims) 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.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.join() 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.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.GRAY2)) self.assertTrue(np.all(aimg[:, :, 3] == 255)) finally: tp.notifyThreadsToStop() tp.joinThreads()
def testOutOfViewDirtyPropagation( self ): self.lsm.append(self.layer1) tiling = Tiling((900,400), blockSize=100) tp = TileProvider(tiling, self.pump.stackedImageSources) try: # Navigate down to the second z-slice self.pump.syncedSliceSources.through = [0,1,0] tp.requestRefresh(QRectF(100,100,200,200)) tp.join() # 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.join() # 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.join() 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.join() # 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.) tiles = tp.getTiles(QRectF(100,100,200,200)) 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)) finally: tp.notifyThreadsToStop() tp.joinThreads()
def testOutOfViewDirtyPropagation(self): self.lsm.append(self.layer1) tiling = Tiling((900, 400), blockSize=100) tp = TileProvider(tiling, self.pump.stackedImageSources) try: # Navigate down to the second z-slice self.pump.syncedSliceSources.through = [0, 1, 0] tp.requestRefresh(QRectF(100, 100, 200, 200)) tp.join() # 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.join() # 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.join() 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.join() # 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)) finally: tp.notifyThreadsToStop() tp.joinThreads()
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