Beispiel #1
0
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)
Beispiel #2
0
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)
Beispiel #3
0
class LayerViewerGui(QWidget):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """
    __metaclass__ = LayerViewerGuiMetaclass
    
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus( self ):
        return [self.menuView] # From the .ui file

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Stop rendering
        for scene in self.editor.imageScenes:
            if scene._tileProvider:
                scene._tileProvider.notifyThreadsToStop()
            scene.joinRendering()
            
        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    @traceLogged(traceLogger)
    def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False

        self.threadRouter = ThreadRouter(self) # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in topLevelOperatorView.inputs.values() + topLevelOperatorView.outputs.values():
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)
        
        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = Op1ToMulti(graph=slot.operator.graph)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Outputs
                self._orphanOperators.append( opPromoteInput )

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append( slot )
            slot.notifyInserted( bind(self._handleLayerInsertion) )
            slot.notifyRemoved( bind(self._handleLayerRemoval) )
            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)
 
        self.layerstack = LayerStackModel()

        self._initCentralUic()
        self._initEditor(crosshair=crosshair)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui.
        
    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setupLayers( self ):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                if slot.ready() and slot.meta.axistags is not None:
                    layer = self.createStandardLayerFromSlot(slot)
                    
                    # Name the layer after the slot name.
                    if isinstance( multiLayerSlot.getRealOperator(), Op1ToMulti ):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator().Input.partner.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    #layers.append(layer)
        return layers

    @traceLogged(traceLogger)
    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady( bind(self.updateAllLayers) )
        slot[slotIndex].notifyUnready( bind(self.updateAllLayers) )

    @traceLogged(traceLogger)
    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.updateAllLayers()

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.
        Chooses between grayscale or RGB depending on the number of channels in the slot.

        * If *slot* has 1 channel, a GrayscaleLayer is created.
        * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels.
        * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels.
        * If *slot* has 4 channels, an RGBA layer is created

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
                                   If slot has 4 channels, this parameter has no effect.
        """
        def getRange(meta):
            if 'drange' in meta:
                return meta.drange
            if meta.dtype == numpy.uint8:
                return (0, 255)
            else:
                # If we don't know the range of the data, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                #
                # Even in the case of integer data, which has more than 255 possible values,
                # (like uint16), it seems reasonable to use this setting as default
                return 'autoPercentiles'

        # Examine channel dimension to determine Grayscale vs. RGB
        shape = slot.meta.shape
        normalize = getRange(slot.meta)
        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
        except:
            numChannels = 1

        if numChannels == 4:
            lastChannelIsAlpha = True
            
        if lastChannelIsAlpha:
            assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(numChannels)
        else:
            assert numChannels <= 3, "Can't display a standard layer with more than three channels (with no alpha).  Your image has {} channels.".format(numChannels)

        if numChannels == 1:
            assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
            source = LazyflowSource(slot)
            normSource = NormalizingSource( source, bounds=normalize )
            return GrayscaleLayer(normSource)

        assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
            "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags )
        redProvider = OpSingleChannelSelector(graph=slot.graph)
        redProvider.Input.connect(slot)
        redProvider.Index.setValue( 0 )
        redSource = LazyflowSource( redProvider.Output )
        redNormSource = NormalizingSource( redSource, bounds=normalize )

        greenProvider = OpSingleChannelSelector(graph=slot.graph)
        greenProvider.Input.connect(slot)
        greenProvider.Index.setValue( 1 )
        greenSource = LazyflowSource( greenProvider.Output )
        greenNormSource = NormalizingSource( greenSource, bounds=normalize )

        blueNormSource = None
        if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
            blueProvider = OpSingleChannelSelector(graph=slot.graph)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue( 2 )
            blueSource = LazyflowSource( blueProvider.Output )
            blueNormSource = NormalizingSource( blueSource, bounds=normalize )

        alphaNormSource = None
        if lastChannelIsAlpha:
            alphaProvider = OpSingleChannelSelector(graph=slot.graph)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue( numChannels-1 )
            alphaSource = LazyflowSource( alphaProvider.Output )
            alphaNormSource = NormalizingSource( alphaSource, bounds=normalize )

        layer = RGBALayer( red=redNormSource, green=greenNormSource, blue=blueNormSource, alpha=alphaNormSource )
        return layer

    @traceLogged(traceLogger)
    @threadRouted
    def updateAllLayers(self):
        if self._stopped or not self._initialized:
            return

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str( [l.name for l in newGuiLayers] )
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
           
            #if  the image is 2D, do not show the HUD action (issue #190) 
            is2D = numpy.sum(numpy.asarray(newDataShape[1:4]) == 1) == 1
            self.menuGui.actionToggleSelectedHud.setVisible(not is2D)
            self.menuGui.actionToggleAllHuds.setVisible(not is2D)
            
            # Find the xyz midpoint
            midpos5d = [x/2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] )

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0]
                needDelete = (newLayer.datasources != oldLayer.datasources)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister( obsoleteShortcut )
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert( index, layer )

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register( *layer.shortcutRegistration )
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    @traceLogged(traceLogger)
    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None and slot.ready() and slot.meta.axistags is not None:
                    # Use an Op5ifyer adapter to transpose the shape for us.
                    op5 = Op5ifyer( graph=slot.graph )
                    op5.input.connect( slot )
                    newDataShape = op5.output.meta.shape

                    # We just needed the operator to determine the transposed shape.
                    # Disconnect it so it can be garbage collected.
                    op5.input.disconnect()

        if newDataShape is not None:
            # For now, this base class combines multi-channel images into a single layer,
            # So, we want the volume editor to behave as though there is only one channel
            newDataShape = newDataShape[:-1] + (1,)
        return newDataShape

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir+"/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    @traceLogged(traceLogger)
    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/centralWidget.ui", self)

        # Menu is specified in a separate ui file with a dummy window
        self.menuGui = uic.loadUi(localDir+"/menu.ui") # Save as member so it doesn't get picked up by GC
        self.menuBar = self.menuGui.menuBar
        self.menuView = self.menuGui.menuView

        def toggleDebugPatches(show):
            self.editor.showDebugPatches = show

        def setCacheSize( cache_size ):
            dlg = QDialog(self)
            layout = QHBoxLayout()
            layout.addWidget( QLabel("Cached Slices Per View:") )

            cache_size = [self.editor.cacheSize]
            def parseCacheSize( strSize ):
                try:
                    cache_size[0] = int(strSize)
                except:
                    pass

            edit = QLineEdit( str(cache_size[0]), parent=dlg )
            edit.textChanged.connect( parseCacheSize )
            layout.addWidget( edit )
            okButton = QPushButton( "OK", parent=dlg )
            okButton.clicked.connect( dlg.accept )
            layout.addWidget( okButton )
            dlg.setLayout( layout )
            dlg.setModal(True)
            dlg.exec_()
            self.editor.cacheSize = cache_size[0]

        def enablePrefetching( enable ):
            for scene in self.editor.imageScenes:
                scene.setPrefetchingEnabled( enable )

        def fitToScreen():
            shape = self.editor.posModel.shape
            for i, v in enumerate(self.editor.imageViews):
                s = list(shape)
                del s[i]
                v.changeViewPort(v.scene().data2scene.mapRect(QRectF(0,0,*s)))

        def fitImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].fitImage()

        def restoreImageToOriginalSize():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].doScaleTo()

        '''
        def rubberBandZoom():
            if hasattr(self.editor, '_lastImageViewFocus'):
                if not self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = True
                    self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup = self.editor.imageViews[self.editor._lastImageViewFocus].cursor()
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(Qt.CrossCursor)
                else:
                    self.editor.imageViews[self.editor._lastImageViewFocus]._isRubberBandZoom = False
                    self.editor.imageViews[self.editor._lastImageViewFocus].setCursor(self.editor.imageViews[self.editor._lastImageViewFocus]._cursorBackup)
        '''

        def hideHud():
            hide = not self.editor.imageViews[0]._hud.isVisible()
            for i, v in enumerate(self.editor.imageViews):
                v.setHudVisible(hide)

        def toggleSelectedHud():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].toggleHud()

        def centerAllImages():
            for i, v in enumerate(self.editor.imageViews):
                v.centerImage()

        def centerImage():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageViews[self.editor._lastImageViewFocus].centerImage()
                self.menuGui.actionOnly_for_current_view.setEnabled(True)

        def resetAxes():
            if hasattr(self.editor, '_lastImageViewFocus'):
                self.editor.imageScenes[self.editor._lastImageViewFocus].resetAxes()
                self.menuGui.actionOnly_for_current_view.setEnabled(True)

        def resetAllAxes():
            for i, s in enumerate(self.editor.imageScenes):
                s.resetAxes()

        self.menuGui.actionCenterAllImages.triggered.connect(centerAllImages)
        self.menuGui.actionCenterImage.triggered.connect(centerImage)
        self.menuGui.actionToggleAllHuds.triggered.connect(hideHud)
        self.menuGui.actionResetAllAxes.triggered.connect(resetAllAxes)
        self.menuGui.actionToggleSelectedHud.triggered.connect(toggleSelectedHud)
        self.menuGui.actionResetAxes.triggered.connect(resetAxes)
        self.menuGui.actionShowDebugPatches.toggled.connect(toggleDebugPatches)
        self.menuGui.actionFitToScreen.triggered.connect(fitToScreen)
        self.menuGui.actionFitImage.triggered.connect(fitImage)
        self.menuGui.actionReset_zoom.triggered.connect(restoreImageToOriginalSize)
        #FIXME: this needs bug fixing
        #self.menuGui.actionRubberBandZoom.triggered.connect(rubberBandZoom)
        self.menuGui.actionSetCacheSize.triggered.connect(setCacheSize)
        self.menuGui.actionUsePrefetching.toggled.connect(enablePrefetching)

    @traceLogged(traceLogger)
    def _initEditor(self, crosshair):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, crosshair=crosshair)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel )
        self.editor.setNavigationInterpreter( self.clickReporter )
        self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick )
        self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick )

        self.editor.newImageView2DFocus.connect(self._setIconToViewMenu)
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

    @traceLogged(traceLogger)
    def _setIconToViewMenu(self):
        """
        In the "Only for Current View" menu item of the View menu,
        show the user which axis is the current one by changing the menu item icon.
        """
        self.menuGui.actionOnly_for_current_view.setIcon(QIcon(self.editor.imageViews[self.editor._lastImageViewFocus]._hud.axisLabel.pixmap()))

    @traceLogged(traceLogger)
    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                if isinstance( datasource, NormalizingSource ):
                    datasource = datasource._rawSource
                if isinstance(datasource, LazyflowSource):
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break

        assert dataTags is not None, "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key],)

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass
Beispiel #4
0
class LayerViewerGui(QWidget):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """
    __metaclass__ = LayerViewerGuiMetaclass
    
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus( self ):
        debug_mode = ilastik_config.getboolean("ilastik", "debug")
        return [ self.volumeEditorWidget.getViewMenu(debug_mode) ]

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Stop rendering
        for scene in self.editor.imageScenes:
            if scene._tileProvider:
                scene._tileProvider.notifyThreadsToStop()
            scene.joinRendering()
            
        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    @traceLogged(traceLogger)
    def __init__(self, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False, crosshair=True):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False

        self.threadRouter = ThreadRouter(self) # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in topLevelOperatorView.inputs.values() + topLevelOperatorView.outputs.values():
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)
        
        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = OpWrapSlot(parent=slot.getRealOperator().parent)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Output
                self._orphanOperators.append( opPromoteInput )

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append( slot )
            slot.notifyInserted( bind(self._handleLayerInsertion) )
            slot.notifyRemoved( bind(self._handleLayerRemoval) )
            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)
 
        self.layerstack = LayerStackModel()

        self._initCentralUic()
        self._initEditor(crosshair=crosshair)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui.
        
    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setupLayers( self ):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                if slot.ready() and slot.meta.axistags is not None:
                    layer = self.createStandardLayerFromSlot(slot)
                    
                    # Name the layer after the slot name.
                    if isinstance( multiLayerSlot.getRealOperator(), OpWrapSlot ):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator().Input.partner.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    layers.append(layer)
        return layers

    @traceLogged(traceLogger)
    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady( bind(self.updateAllLayers) )
        slot[slotIndex].notifyUnready( bind(self.updateAllLayers) )

    @traceLogged(traceLogger)
    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.updateAllLayers(slot)

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.
        Chooses between grayscale or RGB depending on the number of channels in the slot.

        * If *slot* has 1 channel or more than 4 channels, a GrayscaleLayer is created.
        * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels.
        * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels.
        * If *slot* has 4 channels, an RGBA layer is created

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
                                   If slot has 4 channels, this parameter has no effect.
        """
        
        def getRange(meta):
            if meta.drange is not None and meta.normalizeDisplay is False:
                # do not normalize if the user provided a range and set normalization to False
                return meta.drange
            else:
                # If we don't know the range of the data and normalization is allowed
                # by the user, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                #
                # Even in the case of integer data, which has more than 255 possible values,
                # (like uint16), it seems reasonable to use this setting as default
                return None # means autoNormalize
                   
        shape = slot.meta.shape
        
        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
            axisinfo = slot.meta.axistags["c"].description
        except:
            numChannels = 1
            axisinfo = "" # == no info on channels given
        
        rindex = None
        bindex = None
        gindex = None
        aindex = None
        
        if axisinfo == "" or axisinfo == "default":
            # Examine channel dimension to determine Grayscale vs. RGB

            if numChannels == 4:
                lastChannelIsAlpha = True
                
            if lastChannelIsAlpha:
                assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(numChannels)

            if numChannels == 1 or (numChannels > 4):
                assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
                source = LazyflowSource(slot)
                layer = GrayscaleLayer(source)
                layer.numberOfChannels = numChannels
                normalize = getRange(slot.meta)
                layer.set_normalize(0,normalize)
                return layer

            assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
                "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags )
            
            rindex = 0
            gindex = 1
            if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
                bindex = 2
            if lastChannelIsAlpha:
                aindex = numChannels-1
        
        elif axisinfo == "grayscale":
            source = LazyflowSource(slot)
            layer = GrayscaleLayer(source)
            layer.numberOfChannels = numChannels
            normalize = getRange(slot.meta)
            layer.set_normalize(0,normalize)            
            return layer
        
        elif axisinfo == "rgba":
            rindex = 0
            if numChannels>=2:
                gindex = 1
            if numChannels>=3:
                bindex = 2
            if numChannels>=4:
                aindex = numChannels-1
        else:
            raise RuntimeError("unknown channel display mode")

        redSource = None
        if rindex is not None:
            redProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            redProvider.Input.connect(slot)
            redProvider.Index.setValue( rindex )
            redSource = LazyflowSource( redProvider.Output )
        
        greenSource = None
        if gindex is not None:
            greenProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            greenProvider.Input.connect(slot)
            greenProvider.Index.setValue( gindex )
            greenSource = LazyflowSource( greenProvider.Output )
        
        blueSource = None
        if bindex is not None:
                blueProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
                blueProvider.Input.connect(slot)
                blueProvider.Index.setValue( bindex )
                blueSource = LazyflowSource( blueProvider.Output )

        alphaSource = None
        if aindex is not None:
            alphaProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue( aindex )
            alphaSource = LazyflowSource( alphaProvider.Output )
        
        layer = RGBALayer( red=redSource, green=greenSource, blue=blueSource, alpha=alphaSource)
        

        
        normalize = getRange(slot.meta)
        print "createLayer normalize", normalize
        for i in xrange(4):
            if [redSource,greenSource,blueSource,alphaSource][i]:
                layer.set_normalize(i,normalize)
        return layer

    @traceLogged(traceLogger)
    @threadRouted
    def updateAllLayers(self, slot=None):
        if self._stopped or not self._initialized:
            return
        if slot is not None and slot.ready() and slot.meta.axistags is None:
            # Don't update in response to value slots.
            return

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str( [l.name for l in newGuiLayers] )
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
                       
            # Find the xyz midpoint
            midpos5d = [x/2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews( midpos3d, [0,1,2] )

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name, newGuiLayers)[0]
                needDelete = (newLayer.datasources != oldLayer.datasources)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister( obsoleteShortcut )
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert( index, layer )

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register( *layer.shortcutRegistration )
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    @traceLogged(traceLogger)
    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None and slot.ready() and slot.meta.axistags is not None:
                    # Use an OpReorderAxes adapter to transpose the shape for us.
                    op5 = OpReorderAxes( parent=slot.getRealOperator().parent )
                    op5.Input.connect( slot )
                    newDataShape = op5.Output.meta.shape

                    # We just needed the operator to determine the transposed shape.
                    # Disconnect it so it can be garbage collected.
                    op5.Input.disconnect()
        return newDataShape

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir+"/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    @traceLogged(traceLogger)
    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/centralWidget.ui", self)

    @traceLogged(traceLogger)
    def _initEditor(self, crosshair):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, crosshair=crosshair)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel )
        self.editor.setNavigationInterpreter( self.clickReporter )
        self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick )
        self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick )
        
        clickReporter2 = ClickReportingInterpreter( self.editor.brushingInterpreter, self.editor.posModel )
        clickReporter2.rightClickReceived.connect( self._handleEditorRightClick )
        self.editor.brushingInterpreter = clickReporter2
        
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

    @traceLogged(traceLogger)
    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                try: # not all datasources have the dataSlot property, find out by trying
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break
                except AttributeError:
                    pass

        if(dataTags is None):
            raise RuntimeError("Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer.")
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key],)

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass
Beispiel #5
0
class LayerViewerGui(with_metaclass(LayerViewerGuiMetaclass, QWidget)):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a superclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus(self):
        debug_mode = ilastik_config.getboolean("ilastik", "debug")
        return [self.volumeEditorWidget.getViewMenu(debug_mode)]

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Unsubscribe to all signals
        for fn in self.__cleanup_fns:
            fn()

        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    def __init__(self,
                 parentApplet,
                 topLevelOperatorView,
                 additionalMonitoredSlots=[],
                 centralWidgetOnly=False,
                 crosshair=True,
                 is_3d_widget_visible=False):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False
        self.__cleanup_fns = []

        self.threadRouter = ThreadRouter(self)  # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in list(topLevelOperatorView.inputs.values()) + list(
                topLevelOperatorView.outputs.values()):
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)

        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [
        ]  # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = OpWrapSlot(
                    parent=slot.getRealOperator().parent)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Output
                self._orphanOperators.append(opPromoteInput)

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append(slot)
            slot.notifyInserted(bind(self._handleLayerInsertion))
            self.__cleanup_fns.append(
                partial(slot.unregisterInserted,
                        bind(self._handleLayerInsertion)))

            slot.notifyRemoved(bind(self._handleLayerRemoval))
            self.__cleanup_fns.append(
                partial(slot.unregisterRemoved,
                        bind(self._handleLayerRemoval)))

            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)

        self.layerstack = LayerStackModel()
        self.saved_layer_visibilities = None

        self._initCentralUic()

        self._initEditor(crosshair=crosshair,
                         is_3d_widget_visible=is_3d_widget_visible)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi(
            )  # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi(
            )  # Default implementation loads a blank drawer from drawer.ui.

        self._up_to_date = False

        # By default, we start out disabled until we have at least one layer.
        self.centralWidget().setEnabled(False)

    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setNeedUpdate(self, slot=None):
        self._need_update = True
        if self.isVisible():
            if slot.graph:
                slot.graph.call_when_setup_finished(self.updateAllLayers)
            else:
                self.updateAllLayers()

    def showEvent(self, event):
        if self._need_update:
            self.updateAllLayers()
        super(LayerViewerGui, self).showEvent(event)

    def setupLayers(self):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                has_space = slot.meta.axistags and slot.meta.axistags.axisTypeCount(
                    vigra.AxisType.Space) > 2
                if slot.ready() and has_space:
                    layer = self.createStandardLayerFromSlot(slot)

                    # Name the layer after the slot name.
                    if isinstance(multiLayerSlot.getRealOperator(),
                                  OpWrapSlot):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator(
                        ).Input.upstream_slot.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    layers.append(layer)
        return layers

    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady(bind(self.setNeedUpdate))
        slot[slotIndex].notifyUnready(bind(self.setNeedUpdate))

        self.__cleanup_fns.append(
            partial(slot[slotIndex].unregisterReady, bind(self.setNeedUpdate)))
        self.__cleanup_fns.append(
            partial(slot[slotIndex].unregisterUnready,
                    bind(self.setNeedUpdate)))

    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.setNeedUpdate(slot)

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.  
        Will be either a GrayscaleLayer or RGBALayer, depending on the channel metadata.

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
        """
        numChannels = 1
        display_mode = "default"
        c_index = slot.meta.axistags.index('c')
        if c_index < len(slot.meta.axistags):
            numChannels = slot.meta.shape[c_index]
            display_mode = slot.meta.display_mode

        if display_mode == "" or display_mode == "default":
            ## Figure out whether the default should be rgba or grayscale
            if lastChannelIsAlpha:
                assert numChannels <= 4, \
                    "This function doesn't support alpha for slots with more than 4 channels.  "\
                    "Your image has {} channels.".format(numChannels)

            # Automatically select Grayscale or RGBA based on number of channels
            if numChannels == 2 or numChannels == 3:
                display_mode = "rgba"
            elif slot.meta.dtype == numpy.uint64:
                display_mode = "random-colortable"
            else:
                display_mode = "grayscale"

        # Override RGBA --> Grayscale if there's only 1 channel.
        if display_mode == "rgba" and numChannels == 1:
            display_mode = "grayscale"

        if display_mode == "grayscale":
            assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
            return cls._create_grayscale_layer_from_slot(slot, numChannels)
        elif display_mode == "rgba":
            assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
                "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}"\
                .format( numChannels, lastChannelIsAlpha, slot.meta.axistags )
            return cls._create_rgba_layer_from_slot(slot, numChannels,
                                                    lastChannelIsAlpha)
        elif display_mode == "random-colortable":
            return cls._create_random_colortable_layer_from_slot(slot)
        elif display_mode == "alpha-modulated":
            return cls._create_alpha_modulated_layer_from_slot(slot)
        elif display_mode == "binary-mask":
            return cls._create_binary_mask_layer_from_slot(slot)
        else:
            raise RuntimeError("unknown channel display mode: " + display_mode)

    @classmethod
    def _create_grayscale_layer_from_slot(cls, slot, n_channels):
        source = LazyflowSource(slot)
        layer = GrayscaleLayer(source)
        layer.numberOfChannels = n_channels
        layer.set_range(0, slot.meta.drange)
        normalize = cls._should_normalize_display(slot)
        layer.set_normalize(0, normalize)
        return layer

    @classmethod
    def _create_random_colortable_layer_from_slot(cls, slot, num_colors=256):
        colortable = generateRandomColors(num_colors,
                                          clamp={
                                              'v': 1.0,
                                              's': 0.5
                                          },
                                          zeroIsTransparent=True)
        layer = ColortableLayer(LazyflowSource(slot), colortable)
        layer.colortableIsRandom = True
        return layer

    @classmethod
    def _create_alpha_modulated_layer_from_slot(cls, slot):
        layer = AlphaModulatedLayer(LazyflowSource(slot),
                                    tintColor=QColor(Qt.cyan),
                                    range=(0.0, 1.0),
                                    normalize=(0.0, 1.0))
        return layer

    @classmethod
    def _create_binary_mask_layer_from_slot(cls, slot):
        # 0: black, 1-255: transparent
        # This works perfectly for uint8.
        # For uint32, etc., values of 256,512, etc. will be appear 'off'.
        # But why would you use uint32 for a binary mask anyway?
        colortable = [QColor(0, 0, 0, 255).rgba()]
        colortable += 255 * [QColor(0, 0, 0, 0).rgba()]
        layer = ColortableLayer(LazyflowSource(slot), colortable)
        return layer

    @classmethod
    def _create_rgba_layer_from_slot(cls, slot, numChannels,
                                     lastChannelIsAlpha):
        bindex = aindex = None
        rindex, gindex = 0, 1
        if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
            bindex = 2
        if lastChannelIsAlpha:
            aindex = numChannels - 1

        if numChannels >= 2:
            gindex = 1
        if numChannels >= 3:
            bindex = 2
        if numChannels >= 4:
            aindex = numChannels - 1

        redSource = None
        if rindex is not None:
            redProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            redProvider.Input.connect(slot)
            redProvider.Index.setValue(rindex)
            redSource = LazyflowSource(redProvider.Output)
            redSource.additional_owned_ops.append(redProvider)

        greenSource = None
        if gindex is not None:
            greenProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            greenProvider.Input.connect(slot)
            greenProvider.Index.setValue(gindex)
            greenSource = LazyflowSource(greenProvider.Output)
            greenSource.additional_owned_ops.append(greenProvider)

        blueSource = None
        if bindex is not None:
            blueProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue(bindex)
            blueSource = LazyflowSource(blueProvider.Output)
            blueSource.additional_owned_ops.append(blueProvider)

        alphaSource = None
        if aindex is not None:
            alphaProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue(aindex)
            alphaSource = LazyflowSource(alphaProvider.Output)
            alphaSource.additional_owned_ops.append(alphaProvider)

        layer = RGBALayer(red=redSource,
                          green=greenSource,
                          blue=blueSource,
                          alpha=alphaSource)
        normalize = cls._should_normalize_display(slot)
        for i in range(4):
            if [redSource, greenSource, blueSource, alphaSource][i]:
                layer.set_range(i, slot.meta.drange)
                layer.set_normalize(i, normalize)

        return layer

    def _get_numchannels(self, slot, n_channels):
        pass

    @classmethod
    def _should_normalize_display(cls, slot):
        if slot.meta.drange is not None and slot.meta.normalizeDisplay is False:
            # do not normalize if the user provided a range and set normalization to False
            return False
        else:
            # If we don't know the range of the data and normalization is allowed
            # by the user, create a layer that is auto-normalized.
            # See volumina.pixelpipeline.datasources for details.
            #
            # Even in the case of integer data, which has more than 255 possible values,
            # (like uint16), it seems reasonable to use this setting as default
            return None  # means autoNormalize

    @threadRouted
    def updateAllLayers(self, slot=None):
        if self._stopped or not self._initialized:
            return
        if slot is not None and slot.ready() and slot.meta.axistags is None:
            # Don't update in response to value slots.
            return

        self._need_update = False

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        # The order of the initial layerstack has to be static, where the "Raw Input" layer is at the stacks last position
        for i in range(len(newGuiLayers)):
            if newGuiLayers[i].name == "Raw Input":
                rlayer = newGuiLayers[i]
                newGuiLayers.remove(rlayer)
                newGuiLayers.append(rlayer)
                break

        for layer in newGuiLayers:
            assert not [l for l in self.layerstack if l is layer], \
                "You are attempting to re-use a layer ({}).  " \
                "Your setupOutputs() function may not re-use layer objects.  " \
                "The layerstack retains ownership of the layers you provide and " \
                "may choose to clean and delete them without your knowledge.".format( layer.name )

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str([l.name for l in newGuiLayers])
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape

            # Find the xyz midpoint
            midpos5d = [x // 2 for x in newDataShape]

            # center viewer there
            self.setViewerPos(midpos5d)

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = list(
                    filter(lambda l: l.name == oldLayer.name, newGuiLayers))[0]
                needDelete = newLayer.isDifferentEnough(oldLayer)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    action_info = layer.shortcutRegistration[1]
                    ShortcutManager().unregister(action_info)
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert(index, layer)

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register(*layer.shortcutRegistration)
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                layer.clean_up()

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(
                    lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

        if len(self.layerstack) > 0:
            self.centralWidget().setEnabled(True)

    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None:
                    newDataShape = self.getVoluminaShapeForSlot(slot)
        return newDataShape

    def getLayerByName(self, name):
        matches = [l for l in list(self.layerstack) if l.name == name]
        if not matches:
            return None
        if len(matches) == 1:
            return matches[0]
        assert False, "Found more than one matching layer with name {}".format(
            name)

    def toggle_show_raw(self, raw_layer_name="Raw Data"):
        """
        Convenience function.
        Hide all layers except for the raw data layer specified by the given layer name.
        The next time this function is called, restore previous layer visibility states.
        """
        if self.saved_layer_visibilities:
            for layer in self.layerstack:
                layer.visible = self.saved_layer_visibilities[layer.name]
            self.saved_layer_visibilities = None
        else:
            self.saved_layer_visibilities = {
                layer.name: layer.visible
                for layer in self.layerstack
            }
            for layer in self.layerstack:
                layer.visible = False
            self.getLayerByName(raw_layer_name).visible = True

    @threadRouted
    def setViewerPos(self, pos, setTime=False, setChannel=False):
        try:
            pos5d = self.validatePos(pos, dims=5)

            # set xyz position
            pos3d = pos5d[1:4]
            self.editor.posModel.slicingPos = pos3d

            # set time and channel if requested
            if setTime:
                self.editor.posModel.time = pos5d[0]
            if setChannel:
                self.editor.posModel.channel = pos5d[4]

            self.editor.navCtrl.panSlicingViews(pos3d, [0, 1, 2])
            for i in range(3):
                self.editor.navCtrl.changeSliceAbsolute(pos3d[i], i)

        except Exception as e:
            logger.warning("Failed to navigate to position (%s): %s" %
                           (pos, e))
        return

    def validatePos(self, pos, dims=5):
        if not isinstance(pos, list):
            raise Exception("Wrong data format")
        if not len(pos) == dims:
            raise Exception("Wrong data format")
        ds = self.editor.dataShape
        for i in range(dims):
            try:
                pos[i] = max(0, min(int(pos[i]), ds[i] - 1))
            except:
                pos[i] = 0
        return pos

    @classmethod
    def getVoluminaShapeForSlot(self, slot):
        shape = None
        if slot.ready() and slot.meta.axistags is not None:
            # Use an OpReorderAxes adapter to transpose the shape for us.
            op5 = OpReorderAxes(parent=slot.getRealOperator().parent)
            op5.Input.connect(slot)
            op5.AxisOrder.setValue('txyzc')
            shape = op5.Output.meta.shape

            # We just needed the operator to determine the transposed shape.
            # Disconnect it so it can be garbage collected.
            op5.Input.disconnect()
            op5.cleanUp()
        return shape

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir + "/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir + "/centralWidget.ui", self)

    def _initEditor(self, crosshair, is_3d_widget_visible):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack,
                                   parent=self,
                                   crosshair=crosshair,
                                   is_3d_widget_visible=is_3d_widget_visible)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter(
            self.editor.navInterpret, self.editor.posModel)
        self.editor.setNavigationInterpreter(self.clickReporter)
        self.clickReporter.rightClickReceived.connect(
            self._handleEditorRightClick)
        self.clickReporter.leftClickReceived.connect(
            self._handleEditorLeftClick)
        self.clickReporter.toolTipReceived.connect(self._handleEditorToolTip)

        clickReporter2 = ClickReportingInterpreter(
            self.editor.brushingInterpreter, self.editor.posModel)
        clickReporter2.rightClickReceived.connect(self._handleEditorRightClick)
        self.editor.brushingInterpreter = clickReporter2

        self.editor.setInteractionMode('navigation')
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

        # Should we default to 2D?
        prefer_2d = False
        for multislot in self.observedSlots:
            for slot in multislot:
                if slot.ready() and slot.meta.prefer_2d:
                    prefer_2d = True
                    break
        if prefer_2d:
            # Default to Z (axis 2 in the editor)
            self.volumeEditorWidget.quadview.ensureMaximized(2)

    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k: p for k, p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                try:  # not all datasources have the dataSlot property, find out by trying
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break
                except AttributeError:
                    pass

        if (dataTags is None):
            raise RuntimeError(
                "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
            )
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key], )

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        if len(self.layerstack) > 0:
            dataPosition = self._convertPositionToDataSpace(position5d)
            self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        if len(self.layerstack) > 0:
            dataPosition = self._convertPositionToDataSpace(position5d)
            self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass

    def _handleEditorToolTip(self, position5d, globalWindowCoordinate):
        if len(self.layerstack) > 0:
            dataPosition = self._convertPositionToDataSpace(position5d)
            self.handleEditorToolTip(dataPosition, globalWindowCoordinate)

    def handleEditorToolTip(self, position5d, globalWindowCoordinate):
        # Override me
        pass
Beispiel #6
0
class LayerViewerGui(with_metaclass(LayerViewerGuiMetaclass, QWidget)):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a superclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus( self ):
        debug_mode = ilastik_config.getboolean("ilastik", "debug")
        return [ self.volumeEditorWidget.getViewMenu(debug_mode) ]

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Unsubscribe to all signals
        for fn in self.__cleanup_fns:
            fn()
            
        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    def __init__(self, parentApplet, topLevelOperatorView, additionalMonitoredSlots=[], centralWidgetOnly=False,
                 crosshair=True, is_3d_widget_visible=False):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False
        self.__cleanup_fns = []

        self.threadRouter = ThreadRouter(self) # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in list(topLevelOperatorView.inputs.values()) + list(topLevelOperatorView.outputs.values()):
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)
        
        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [] # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = OpWrapSlot(parent=slot.getRealOperator().parent)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Output
                self._orphanOperators.append( opPromoteInput )

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append( slot )
            slot.notifyInserted( bind(self._handleLayerInsertion) )
            self.__cleanup_fns.append( partial( slot.unregisterInserted, bind(self._handleLayerInsertion) ) )

            slot.notifyRemoved( bind(self._handleLayerRemoval) )
            self.__cleanup_fns.append( partial( slot.unregisterRemoved, bind(self._handleLayerRemoval) ) )

            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)
 
        self.layerstack = LayerStackModel()
        self.saved_layer_visibilities = None

        self._initCentralUic()

        self._initEditor(crosshair=crosshair, is_3d_widget_visible=is_3d_widget_visible)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi() # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi() # Default implementation loads a blank drawer from drawer.ui.

        self._up_to_date = False
        
        # By default, we start out disabled until we have at least one layer.
        self.centralWidget().setEnabled(False)

    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setNeedUpdate(self, slot=None):
        self._need_update = True
        if self.isVisible():
            if slot.graph:
                slot.graph.call_when_setup_finished( self.updateAllLayers )
            else:
                self.updateAllLayers()

    def showEvent(self, event):
        if self._need_update:
            self.updateAllLayers()
        super( LayerViewerGui, self ).showEvent(event)

    def setupLayers( self ):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):                
                has_space = slot.meta.axistags and slot.meta.axistags.axisTypeCount(vigra.AxisType.Space) > 2
                if slot.ready() and has_space:
                    layer = self.createStandardLayerFromSlot(slot)
                    
                    # Name the layer after the slot name.
                    if isinstance( multiLayerSlot.getRealOperator(), OpWrapSlot ):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator().Input.upstream_slot.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    layers.append(layer)
        return layers

    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady( bind(self.setNeedUpdate) )
        slot[slotIndex].notifyUnready( bind(self.setNeedUpdate) )

        self.__cleanup_fns.append( partial( slot[slotIndex].unregisterReady, bind(self.setNeedUpdate) ) )
        self.__cleanup_fns.append( partial( slot[slotIndex].unregisterUnready, bind(self.setNeedUpdate) ) )


    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.setNeedUpdate(slot)

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False, name=None, opacity=1.0, visible=True):
        """
        Convenience function.
        Generates a volumina layer using the given slot.  
        Will be either a GrayscaleLayer or RGBALayer, depending on the channel metadata.

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
        """
        numChannels = 1
        display_mode = "default"
        c_index = slot.meta.axistags.index('c')
        if c_index < len(slot.meta.axistags):
            numChannels = slot.meta.shape[c_index]
            display_mode = slot.meta.display_mode
                
        if display_mode == "" or display_mode == "default":
            ## Figure out whether the default should be rgba or grayscale
            if lastChannelIsAlpha:
                assert numChannels <= 4, \
                    "This function doesn't support alpha for slots with more than 4 channels.  "\
                    "Your image has {} channels.".format(numChannels)

            # Automatically select Grayscale or RGBA based on number of channels
            if numChannels == 2 or numChannels == 3:
                display_mode = "rgba"
            elif slot.meta.dtype == numpy.uint64:
                display_mode = "random-colortable"
            else:
                display_mode = "grayscale"

        # Override RGBA --> Grayscale if there's only 1 channel.
        if display_mode == "rgba" and numChannels == 1:
            display_mode = "grayscale"
                
        if display_mode == "grayscale":
            assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
            layer = cls._create_grayscale_layer_from_slot( slot, numChannels )
        elif display_mode == "rgba":
            assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
                "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}"\
                .format( numChannels, lastChannelIsAlpha, slot.meta.axistags )
            layer =  cls._create_rgba_layer_from_slot(slot, numChannels, lastChannelIsAlpha)
        elif display_mode == "random-colortable":
            layer =  cls._create_random_colortable_layer_from_slot(slot)
        elif display_mode == "alpha-modulated":
            layer = cls._create_alpha_modulated_layer_from_slot(slot)
        elif display_mode == "binary-mask":
            layer =  cls._create_binary_mask_layer_from_slot(slot)
        else:
            raise RuntimeError("unknown channel display mode: " + display_mode )

        layer.name = name or slot.name
        layer.visible = visible
        layer.opacity = opacity

        return layer

    @classmethod
    def _create_grayscale_layer_from_slot(cls, slot, n_channels):
        #FIXME: move all of this stuff into the class constructor. Same for all
        # _create_*layer_from_slot methods.
        source = LazyflowSource(slot)
        layer = GrayscaleLayer(source, window_leveling=True)
        layer.numberOfChannels = n_channels
        layer.set_range(0, slot.meta.drange)
        normalize = cls._should_normalize_display(slot)
        layer.set_normalize( 0, normalize )
        return layer

    @classmethod
    def _create_random_colortable_layer_from_slot(cls, slot, num_colors=256):
        colortable = generateRandomColors(num_colors, clamp={'v': 1.0, 's' : 0.5}, zeroIsTransparent=True)
        layer = ColortableLayer(LazyflowSource(slot), colortable)
        layer.colortableIsRandom = True
        return layer

    @classmethod
    def _create_alpha_modulated_layer_from_slot(cls, slot):
        layer = AlphaModulatedLayer( LazyflowSource(slot),
                                     tintColor=QColor( Qt.cyan ),
                                     range=(0.0, 1.0),
                                     normalize=(0.0, 1.0) )
        return layer

    @classmethod
    def _create_binary_mask_layer_from_slot(cls, slot):
        # 0: black, 1-255: transparent
        # This works perfectly for uint8.  
        # For uint32, etc., values of 256,512, etc. will be appear 'off'.
        # But why would you use uint32 for a binary mask anyway? 
        colortable = [QColor(0,0,0,255).rgba()]
        colortable += 255*[QColor(0,0,0,0).rgba()]
        layer = ColortableLayer(LazyflowSource(slot), colortable)
        return layer
        
    @classmethod
    def _create_rgba_layer_from_slot(cls, slot, numChannels, lastChannelIsAlpha):
        bindex = aindex = None
        rindex, gindex = 0,1
        if numChannels > 3 or (numChannels == 3 and not lastChannelIsAlpha):
            bindex = 2
        if lastChannelIsAlpha:
            aindex = numChannels-1

        if numChannels>=2:
            gindex = 1
        if numChannels>=3:
            bindex = 2
        if numChannels>=4:
            aindex = numChannels-1

        redSource = None
        if rindex is not None:
            redProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            redProvider.Input.connect(slot)
            redProvider.Index.setValue( rindex )
            redSource = LazyflowSource( redProvider.Output )
            redSource.additional_owned_ops.append( redProvider )
        
        greenSource = None
        if gindex is not None:
            greenProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            greenProvider.Input.connect(slot)
            greenProvider.Index.setValue( gindex )
            greenSource = LazyflowSource( greenProvider.Output )
            greenSource.additional_owned_ops.append( greenProvider )
        
        blueSource = None
        if bindex is not None:
            blueProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue( bindex )
            blueSource = LazyflowSource( blueProvider.Output )
            blueSource.additional_owned_ops.append( blueProvider )

        alphaSource = None
        if aindex is not None:
            alphaProvider = OpSingleChannelSelector(parent=slot.getRealOperator().parent)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue( aindex )
            alphaSource = LazyflowSource( alphaProvider.Output )
            alphaSource.additional_owned_ops.append( alphaProvider )
        
        layer = RGBALayer( red=redSource, green=greenSource, blue=blueSource, alpha=alphaSource)
        normalize = cls._should_normalize_display(slot)
        for i in range(4):
            if [redSource,greenSource,blueSource,alphaSource][i]:
                layer.set_range(i, slot.meta.drange)
                layer.set_normalize(i, normalize)

        return layer
    
    def _get_numchannels(self, slot, n_channels):
        pass
    
    @classmethod
    def _should_normalize_display(cls, slot):
        if slot.meta.drange is not None and slot.meta.normalizeDisplay is False:
            # do not normalize if the user provided a range and set normalization to False
            return False
        else:
            # If we don't know the range of the data and normalization is allowed
            # by the user, create a layer that is auto-normalized.
            # See volumina.pixelpipeline.datasources for details.
            #
            # Even in the case of integer data, which has more than 255 possible values,
            # (like uint16), it seems reasonable to use this setting as default
            return None # means autoNormalize

    @threadRouted
    def updateAllLayers(self, slot=None):
        if self._stopped or not self._initialized:
            return
        if slot is not None and slot.ready() and slot.meta.axistags is None:
            # Don't update in response to value slots.
            return

        self._need_update = False

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        # The order of the initial layerstack has to be static, where the "Raw Input" layer is at the stacks last position
        for i in range(len(newGuiLayers)):
            if newGuiLayers[i].name == "Raw Input":
                rlayer = newGuiLayers[i]
                newGuiLayers.remove(rlayer)
                newGuiLayers.append(rlayer)
                break

        for layer in newGuiLayers:
            assert not [l for l in self.layerstack if l is layer], \
                "You are attempting to re-use a layer ({}).  " \
                "Your setupOutputs() function may not re-use layer objects.  " \
                "The layerstack retains ownership of the layers you provide and " \
                "may choose to clean and delete them without your knowledge.".format( layer.name )

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str( [l.name for l in newGuiLayers] )
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape
                       
            # Find the xyz midpoint
            midpos5d = [x//2 for x in newDataShape]

            # center viewer there
            self.setViewerPos(midpos5d)

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = list(filter(lambda l: l.name == oldLayer.name, newGuiLayers))[0]
                needDelete = newLayer.isDifferentEnough(oldLayer)
                
            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    action_info = layer.shortcutRegistration[1]
                    ShortcutManager().unregister( action_info )
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert( index, layer )

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register( *layer.shortcutRegistration )
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                layer.clean_up()

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

        if len(self.layerstack) > 0:
            self.centralWidget().setEnabled( True )

    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None:
                    newDataShape = self.getVoluminaShapeForSlot(slot)
        return newDataShape

    def getLayerByName(self, name):
        matches = [l for l in list(self.layerstack) if l.name == name]
        if not matches:
            return None
        if len(matches) == 1:
            return matches[0]
        assert False, "Found more than one matching layer with name {}".format( name )

    def toggle_show_raw(self, raw_layer_name="Raw Data"):
        """
        Convenience function.
        Hide all layers except for the raw data layer specified by the given layer name.
        The next time this function is called, restore previous layer visibility states.
        """
        if self.saved_layer_visibilities:
            for layer in self.layerstack:
                layer.visible = self.saved_layer_visibilities[layer.name]
            self.saved_layer_visibilities = None
        else:
            self.saved_layer_visibilities = {layer.name : layer.visible for layer in self.layerstack}
            for layer in self.layerstack:
                layer.visible = False
            self.getLayerByName(raw_layer_name).visible = True

    @threadRouted
    def setViewerPos(self, pos, setTime=False, setChannel=False):
        try:
            pos5d = self.validatePos(pos, dims=5)
            
            # set xyz position
            pos3d = pos5d[1:4]
            self.editor.posModel.slicingPos = pos3d
            
            # set time and channel if requested
            if setTime:
                self.editor.posModel.time = pos5d[0]
            if setChannel:
                self.editor.posModel.channel = pos5d[4]

            self.editor.navCtrl.panSlicingViews( pos3d, [0,1,2] )
            for i in range(3):
                self.editor.navCtrl.changeSliceAbsolute(pos3d[i],i)

        except Exception as e:
            logger.warning("Failed to navigate to position (%s): %s" % (pos, e))
        return
    
    def validatePos(self, pos, dims=5):
        if not isinstance(pos, list):
            raise Exception("Wrong data format")
        if not len(pos) == dims:
            raise Exception("Wrong data format")
        ds = self.editor.dataShape
        for i in range(dims):
            try:
                pos[i] = max(0, min(int(pos[i]), ds[i]-1))
            except:
                pos[i] = 0                
        return pos

    @classmethod
    def getVoluminaShapeForSlot(self, slot):
        shape = None
        if slot.ready() and slot.meta.axistags is not None:
            # Use an OpReorderAxes adapter to transpose the shape for us.
            op5 = OpReorderAxes( parent=slot.getRealOperator().parent )
            op5.Input.connect( slot )
            op5.AxisOrder.setValue('txyzc')
            shape = op5.Output.meta.shape

            # We just needed the operator to determine the transposed shape.
            # Disconnect it so it can be garbage collected.
            op5.Input.disconnect()
            op5.cleanUp()
        return shape

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir+"/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir+"/centralWidget.ui", self)

    def _initEditor(self, crosshair, is_3d_widget_visible):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, parent=self, crosshair=crosshair, is_3d_widget_visible=is_3d_widget_visible)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter( self.editor.navInterpret, self.editor.posModel )
        self.editor.setNavigationInterpreter( self.clickReporter )
        self.clickReporter.rightClickReceived.connect( self._handleEditorRightClick )
        self.clickReporter.leftClickReceived.connect( self._handleEditorLeftClick )
        self.clickReporter.toolTipReceived.connect( self._handleEditorToolTip )

        clickReporter2 = ClickReportingInterpreter( self.editor.brushingInterpreter, self.editor.posModel )
        clickReporter2.rightClickReceived.connect( self._handleEditorRightClick )
        self.editor.brushingInterpreter = clickReporter2
        
        self.editor.setInteractionMode( 'navigation' )
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

        # Should we default to 2D?
        prefer_2d = False
        for multislot in self.observedSlots:
            for slot in multislot:
                if slot.ready() and slot.meta.prefer_2d:
                    prefer_2d = True
                    break
        if prefer_2d:
            # Default to Z (axis 2 in the editor)
            self.volumeEditorWidget.quadview.ensureMaximized(2)

    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k:p for k,p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                try: # not all datasources have the dataSlot property, find out by trying
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break
                except AttributeError:
                    pass

        if(dataTags is None):
            raise RuntimeError("Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer.")
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key],)

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        if len(self.layerstack) > 0:
            dataPosition = self._convertPositionToDataSpace(position5d)
            self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        if len(self.layerstack) > 0:
            dataPosition = self._convertPositionToDataSpace(position5d)
            self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass

    def _handleEditorToolTip(self, position5d, globalWindowCoordinate):
        if len(self.layerstack) > 0:
            dataPosition = self._convertPositionToDataSpace(position5d)
            self.handleEditorToolTip(dataPosition, globalWindowCoordinate)

    def handleEditorToolTip(self, position5d, globalWindowCoordinate):
        # Override me
        pass
class LayerViewerGui(QWidget):
    """
    Implements an applet GUI whose central widget is a VolumeEditor
    and whose layer controls simply contains a layer list widget.
    Intended to be used as a subclass for applet GUI objects.

    Provides: Central widget (viewer), View Menu, and Layer controls
    Provides an EMPTY applet drawer widget.  Subclasses should replace it with their own applet drawer.
    """
    __metaclass__ = LayerViewerGuiMetaclass

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._drawer

    def menus(self):
        debug_mode = ilastik_config.getboolean("ilastik", "debug")
        return [self.volumeEditorWidget.getViewMenu(debug_mode)]

    def viewerControlWidget(self):
        return self.__viewerControlWidget

    def stopAndCleanUp(self):
        self._stopped = True

        # Remove all layers
        self.layerstack.clear()

        # Unsubscribe to all signals
        for fn in self.__cleanup_fns:
            fn()

        # Stop rendering
        for scene in self.editor.imageScenes:
            if scene._tileProvider:
                scene._tileProvider.notifyThreadsToStop()
            scene.joinRendering()

        for op in self._orphanOperators:
            op.cleanUp()

    ###########################################
    ###########################################

    def __init__(self,
                 topLevelOperatorView,
                 additionalMonitoredSlots=[],
                 centralWidgetOnly=False,
                 crosshair=True):
        """
        Constructor.  **All** slots of the provided *topLevelOperatorView* will be monitored for changes.
        Changes include slot resize events, and slot ready/unready status changes.
        When a change is detected, the `setupLayers()` function is called, and the result is used to update the list of layers shown in the central widget.

        :param topLevelOperatorView: The top-level operator for the applet this GUI belongs to.
        :param additionalMonitoredSlots: Optional.  Can be used to add additional slots to the set of viewable layers (all slots from the top-level operator are already monitored).
        :param centralWidgetOnly: If True, provide only a central widget without drawer or viewer controls.
        """
        super(LayerViewerGui, self).__init__()

        self._stopped = False
        self._initialized = False
        self.__cleanup_fns = []

        self.threadRouter = ThreadRouter(self)  # For using @threadRouted

        self.topLevelOperatorView = topLevelOperatorView

        observedSlots = []

        for slot in topLevelOperatorView.inputs.values(
        ) + topLevelOperatorView.outputs.values():
            if slot.level == 0 or slot.level == 1:
                observedSlots.append(slot)

        observedSlots += additionalMonitoredSlots

        self._orphanOperators = [
        ]  # Operators that are owned by this GUI directly (not owned by the top-level operator)
        self.observedSlots = []
        for slot in observedSlots:
            if slot.level == 0:
                if not isinstance(slot.stype, ArrayLike):
                    # We don't support visualization of non-Array slots.
                    continue
                # To be monitored and updated correctly by this GUI, slots must have level=1, but this slot is of level 0.
                # Pass it through a trivial "up-leveling" operator so it will have level 1 for our purposes.
                opPromoteInput = OpWrapSlot(
                    parent=slot.getRealOperator().parent)
                opPromoteInput.Input.connect(slot)
                slot = opPromoteInput.Output
                self._orphanOperators.append(opPromoteInput)

            # Each slot should now be indexed as slot[layer_index]
            assert slot.level == 1
            self.observedSlots.append(slot)
            slot.notifyInserted(bind(self._handleLayerInsertion))
            self.__cleanup_fns.append(
                partial(slot.unregisterInserted,
                        bind(self._handleLayerInsertion)))

            slot.notifyRemoved(bind(self._handleLayerRemoval))
            self.__cleanup_fns.append(
                partial(slot.unregisterRemoved,
                        bind(self._handleLayerRemoval)))

            for i in range(len(slot)):
                self._handleLayerInsertion(slot, i)

        self.layerstack = LayerStackModel()

        self._initCentralUic()
        self._initEditor(crosshair=crosshair)
        self.__viewerControlWidget = None
        if not centralWidgetOnly:
            self.initViewerControlUi(
            )  # Might be overridden in a subclass. Default implementation loads a standard layer widget.
            #self._drawer = QWidget( self )
            self.initAppletDrawerUi(
            )  # Default implementation loads a blank drawer from drawer.ui.

    def _after_init(self):
        self._initialized = True
        self.updateAllLayers()

    def setupLayers(self):
        """
        Create a list of layers to be displayed in the central widget.
        Subclasses should override this method to create the list of layers that can be displayed.
        For debug and development purposes, the base class implementation simply generates layers for all topLevelOperatorView slots.
        """
        layers = []
        for multiLayerSlot in self.observedSlots:
            for j, slot in enumerate(multiLayerSlot):
                if slot.ready() and slot.meta.axistags is not None:
                    layer = self.createStandardLayerFromSlot(slot)

                    # Name the layer after the slot name.
                    if isinstance(multiLayerSlot.getRealOperator(),
                                  OpWrapSlot):
                        # We attached an 'upleveling' operator, so look upstream for the real slot.
                        layer.name = multiLayerSlot.getRealOperator(
                        ).Input.partner.name
                    else:
                        layer.name = multiLayerSlot.name + " " + str(j)
                    layers.append(layer)
        return layers

    def _handleLayerInsertion(self, slot, slotIndex):
        """
        The multislot providing our layers has a new item.
        Make room for it in the layer GUI and subscribe to updates.
        """
        # When the slot is ready, we'll replace the blank layer with real data
        slot[slotIndex].notifyReady(bind(self.updateAllLayers))
        slot[slotIndex].notifyUnready(bind(self.updateAllLayers))

        self.__cleanup_fns.append(
            partial(slot[slotIndex].unregisterReady,
                    bind(self.updateAllLayers)))
        self.__cleanup_fns.append(
            partial(slot[slotIndex].unregisterUnready,
                    bind(self.updateAllLayers)))

    def _handleLayerRemoval(self, slot, slotIndex):
        """
        An item is about to be removed from the multislot that is providing our layers.
        Remove the layer from the GUI.
        """
        self.updateAllLayers(slot)

    def generateAlphaModulatedLayersFromChannels(self, slot):
        # TODO
        assert False

    @classmethod
    def createStandardLayerFromSlot(cls, slot, lastChannelIsAlpha=False):
        """
        Convenience function.
        Generates a volumina layer using the given slot.
        Chooses between grayscale or RGB depending on the number of channels in the slot.

        * If *slot* has 1 channel or more than 4 channels, a GrayscaleLayer is created.
        * If *slot* has 2 non-alpha channels, an RGBALayer is created with R and G channels.
        * If *slot* has 3 non-alpha channels, an RGBALayer is created with R,G, and B channels.
        * If *slot* has 4 channels, an RGBA layer is created

        :param slot: The slot to generate a layer from
        :param lastChannelIsAlpha: If True, the last channel in the slot is assumed to be an alpha channel.
                                   If slot has 4 channels, this parameter has no effect.
        """
        def getRange(meta):
            return meta.drange

        def getNormalize(meta):
            if meta.drange is not None and meta.normalizeDisplay is False:
                # do not normalize if the user provided a range and set normalization to False
                return False
            else:
                # If we don't know the range of the data and normalization is allowed
                # by the user, create a layer that is auto-normalized.
                # See volumina.pixelpipeline.datasources for details.
                #
                # Even in the case of integer data, which has more than 255 possible values,
                # (like uint16), it seems reasonable to use this setting as default
                return None  # means autoNormalize

        shape = slot.meta.shape

        try:
            channelAxisIndex = slot.meta.axistags.index('c')
            #assert channelAxisIndex < len(slot.meta.axistags), \
            #    "slot %s has shape = %r, axistags = %r, but no channel dimension" \
            #    % (slot.name, slot.meta.shape, slot.meta.axistags)
            numChannels = shape[channelAxisIndex]
            axisinfo = slot.meta.axistags["c"].description
        except:
            numChannels = 1
            axisinfo = ""  # == no info on channels given

        rindex = None
        bindex = None
        gindex = None
        aindex = None

        if axisinfo == "" or axisinfo == "default":
            # Examine channel dimension to determine Grayscale vs. RGB

            if numChannels == 4:
                lastChannelIsAlpha = True

            if lastChannelIsAlpha:
                assert numChannels <= 4, "Can't display a standard layer with more than four channels (with alpha).  Your image has {} channels.".format(
                    numChannels)

            if numChannels == 1 or (numChannels > 4):
                assert not lastChannelIsAlpha, "Can't have an alpha channel if there is no color channel"
                source = LazyflowSource(slot)
                layer = GrayscaleLayer(source)
                layer.numberOfChannels = numChannels
                normalize = getNormalize(slot.meta)
                range = getRange(slot.meta)
                layer.set_range(0, range)
                layer.set_normalize(0, normalize)
                return layer

            assert numChannels > 2 or (numChannels == 2 and not lastChannelIsAlpha), \
                "Unhandled combination of channels.  numChannels={}, lastChannelIsAlpha={}, axistags={}".format( numChannels, lastChannelIsAlpha, slot.meta.axistags )

            rindex = 0
            gindex = 1
            if numChannels > 3 or (numChannels == 3
                                   and not lastChannelIsAlpha):
                bindex = 2
            if lastChannelIsAlpha:
                aindex = numChannels - 1

        elif axisinfo == "grayscale":
            source = LazyflowSource(slot)
            layer = GrayscaleLayer(source)
            layer.numberOfChannels = numChannels
            normalize = getNormalize(slot.meta)
            range = getRange(slot.meta)
            layer.set_range(0, range)
            layer.set_normalize(0, normalize)
            return layer

        elif axisinfo == "rgba":
            rindex = 0
            if numChannels >= 2:
                gindex = 1
            if numChannels >= 3:
                bindex = 2
            if numChannels >= 4:
                aindex = numChannels - 1
        else:
            raise RuntimeError("unknown channel display mode")

        redSource = None
        if rindex is not None:
            redProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            redProvider.Input.connect(slot)
            redProvider.Index.setValue(rindex)
            redSource = LazyflowSource(redProvider.Output)
            redSource.additional_owned_ops.append(redProvider)

        greenSource = None
        if gindex is not None:
            greenProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            greenProvider.Input.connect(slot)
            greenProvider.Index.setValue(gindex)
            greenSource = LazyflowSource(greenProvider.Output)
            greenSource.additional_owned_ops.append(greenProvider)

        blueSource = None
        if bindex is not None:
            blueProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            blueProvider.Input.connect(slot)
            blueProvider.Index.setValue(bindex)
            blueSource = LazyflowSource(blueProvider.Output)
            blueSource.additional_owned_ops.append(blueProvider)

        alphaSource = None
        if aindex is not None:
            alphaProvider = OpSingleChannelSelector(
                parent=slot.getRealOperator().parent)
            alphaProvider.Input.connect(slot)
            alphaProvider.Index.setValue(aindex)
            alphaSource = LazyflowSource(alphaProvider.Output)
            alphaSource.additional_owned_ops.append(alphaProvider)

        layer = RGBALayer(red=redSource,
                          green=greenSource,
                          blue=blueSource,
                          alpha=alphaSource)

        normalize = getNormalize(slot.meta)
        range = getRange(slot.meta)
        for i in xrange(4):
            if [redSource, greenSource, blueSource, alphaSource][i]:
                layer.set_range(i, range)
                layer.set_normalize(i, normalize)

        return layer

    @threadRouted
    def updateAllLayers(self, slot=None):
        if self._stopped or not self._initialized:
            return
        if slot is not None and slot.ready() and slot.meta.axistags is None:
            # Don't update in response to value slots.
            return

        # Ask for the updated layer list (usually provided by the subclass)
        newGuiLayers = self.setupLayers()

        for layer in newGuiLayers:
            assert not filter( lambda l: l is layer, self.layerstack ), \
                "You are attempting to re-use a layer ({}).  " \
                "Your setupOutputs() function may not re-use layer objects.  " \
                "The layerstack retains ownership of the layers you provide and " \
                "may choose to clean and delete them without your knowledge.".format( layer.name )

        newNames = set(l.name for l in newGuiLayers)
        if len(newNames) != len(newGuiLayers):
            msg = "All layers must have unique names.\n"
            msg += "You're attempting to use these layer names:\n"
            msg += str([l.name for l in newGuiLayers])
            raise RuntimeError(msg)

        # If the datashape changed, tell the editor
        # FIXME: This may not be necessary now that this gui doesn't handle the multi-image case...
        newDataShape = self.determineDatashape()
        if newDataShape is not None and self.editor.dataShape != newDataShape:
            self.editor.dataShape = newDataShape

            # Find the xyz midpoint
            midpos5d = [x / 2 for x in newDataShape]
            midpos3d = midpos5d[1:4]

            # Start in the center of the volume
            self.editor.posModel.slicingPos = midpos3d
            self.editor.navCtrl.panSlicingViews(midpos3d, [0, 1, 2])

        # Old layers are deleted if
        # (1) They are not in the new set or
        # (2) Their data has changed
        for index, oldLayer in reversed(list(enumerate(self.layerstack))):
            if oldLayer.name not in newNames:
                needDelete = True
            else:
                newLayer = filter(lambda l: l.name == oldLayer.name,
                                  newGuiLayers)[0]
                needDelete = newLayer.isDifferentEnough(oldLayer)

            if needDelete:
                layer = self.layerstack[index]
                if hasattr(layer, 'shortcutRegistration'):
                    obsoleteShortcut = layer.shortcutRegistration[2]
                    obsoleteShortcut.setEnabled(False)
                    ShortcutManager().unregister(obsoleteShortcut)
                self.layerstack.selectRow(index)
                self.layerstack.deleteSelected()

        # Insert all layers that aren't already in the layerstack
        # (Identified by the name attribute)
        existingNames = set(l.name for l in self.layerstack)
        for index, layer in enumerate(newGuiLayers):
            if layer.name not in existingNames:
                # Insert new
                self.layerstack.insert(index, layer)

                # If this layer has an associated shortcut, register it with the shortcut manager
                if hasattr(layer, 'shortcutRegistration'):
                    ShortcutManager().register(*layer.shortcutRegistration)
            else:
                # Clean up the layer instance that the client just gave us.
                # We don't want to use it.
                if hasattr(layer, 'shortcutRegistration'):
                    shortcut = layer.shortcutRegistration[2]
                    shortcut.setEnabled(False)
                    layer.clean_up()

                # Move existing layer to the correct position
                stackIndex = self.layerstack.findMatchingIndex(
                    lambda l: l.name == layer.name)
                self.layerstack.selectRow(stackIndex)
                while stackIndex > index:
                    self.layerstack.moveSelectedUp()
                    stackIndex -= 1
                while stackIndex < index:
                    self.layerstack.moveSelectedDown()
                    stackIndex += 1

    def determineDatashape(self):
        newDataShape = None
        for provider in self.observedSlots:
            for i, slot in enumerate(provider):
                if newDataShape is None:
                    newDataShape = self.getVoluminaShapeForSlot(slot)
        return newDataShape

    @classmethod
    def getVoluminaShapeForSlot(self, slot):
        shape = None
        if slot.ready() and slot.meta.axistags is not None:
            # Use an OpReorderAxes adapter to transpose the shape for us.
            op5 = OpReorderAxes(parent=slot.getRealOperator().parent)
            op5.Input.connect(slot)
            shape = op5.Output.meta.shape

            # We just needed the operator to determine the transposed shape.
            # Disconnect it so it can be garbage collected.
            op5.Input.disconnect()
            op5.cleanUp()
        return shape

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        Subclasses should override this if they provide their own viewer control widget.
        """
        localDir = os.path.split(__file__)[0]
        self.__viewerControlWidget = ViewerControls()

        # The editor's layerstack is in charge of which layer movement buttons are enabled
        model = self.editor.layerStack

        if self.__viewerControlWidget is not None:
            self.__viewerControlWidget.setupConnections(model)

    def initAppletDrawerUi(self):
        """
        By default, this base class provides a blank applet drawer.
        Override this in a subclass to get a real applet drawer.
        """
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir + "/drawer.ui")

    def getAppletDrawerUi(self):
        return self._drawer

    def _initCentralUic(self):
        """
        Load the GUI from the ui file into this class and connect it with event handlers.
        """
        # Load the ui file into this class (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        uic.loadUi(localDir + "/centralWidget.ui", self)

    def _initEditor(self, crosshair):
        """
        Initialize the Volume Editor GUI.
        """
        self.editor = VolumeEditor(self.layerstack, crosshair=crosshair)

        # Replace the editor's navigation interpreter with one that has extra functionality
        self.clickReporter = ClickReportingInterpreter(
            self.editor.navInterpret, self.editor.posModel)
        self.editor.setNavigationInterpreter(self.clickReporter)
        self.clickReporter.rightClickReceived.connect(
            self._handleEditorRightClick)
        self.clickReporter.leftClickReceived.connect(
            self._handleEditorLeftClick)

        clickReporter2 = ClickReportingInterpreter(
            self.editor.brushingInterpreter, self.editor.posModel)
        clickReporter2.rightClickReceived.connect(self._handleEditorRightClick)
        self.editor.brushingInterpreter = clickReporter2

        self.editor.setInteractionMode('navigation')
        self.volumeEditorWidget.init(self.editor)

        self.editor._lastImageViewFocus = 0

        # Zoom at a 1-1 scale to avoid loading big datasets entirely...
        for view in self.editor.imageViews:
            view.doScaleTo(1)

    def _convertPositionToDataSpace(self, voluminaPosition):
        taggedPosition = {k: p for k, p in zip('txyzc', voluminaPosition)}

        # Find the first lazyflow layer in the stack
        # We assume that all lazyflow layers have the same axistags
        dataTags = None
        for layer in self.layerstack:
            for datasource in layer.datasources:
                try:  # not all datasources have the dataSlot property, find out by trying
                    dataTags = datasource.dataSlot.meta.axistags
                    if dataTags is not None:
                        break
                except AttributeError:
                    pass

        if (dataTags is None):
            raise RuntimeError(
                "Can't convert mouse click coordinates from volumina-5d: Could not find a lazyflow data source in any layer."
            )
        position = ()
        for tag in dataTags:
            position += (taggedPosition[tag.key], )

        return position

    def _handleEditorRightClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorRightClick(dataPosition, globalWindowCoordinate)

    def _handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        dataPosition = self._convertPositionToDataSpace(position5d)
        self.handleEditorLeftClick(dataPosition, globalWindowCoordinate)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        # Override me
        pass

    def handleEditorLeftClick(self, position5d, globalWindowCoordiante):
        # Override me
        pass