Beispiel #1
0
    def __init__(self,
                 parentApplet,
                 croppingSlots,
                 topLevelOperatorView,
                 drawerUiPath=None,
                 rawInputSlot=None,
                 crosshair=True):
        """
        Constructor.

        :param croppingSlots: Provides the slots needed for sourcing/sinking crop data.  See CroppingGui.CroppingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        # Do we have all the slots we need?
        assert isinstance(croppingSlots, CroppingGui.CroppingSlots)
        assert croppingSlots.cropInput is not None, "Missing a required slot."
        assert croppingSlots.cropOutput is not None, "Missing a required slot."
        assert croppingSlots.cropEraserValue is not None, "Missing a required slot."
        assert croppingSlots.cropDelete is not None, "Missing a required slot."
        assert croppingSlots.cropNames is not None, "Missing a required slot."
        assert croppingSlots.cropsAllowed is not None, "Missing a required slot."

        self.__cleanup_fns = []
        self._croppingSlots = croppingSlots
        self._minCropNumber = 0
        self._maxCropNumber = 99  #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self.topLevelOperatorView.Crops.notifyDirty(bind(self._updateCropList))
        self.topLevelOperatorView.Crops.notifyDirty(bind(self._updateCropList))
        self.__cleanup_fns.append(
            partial(self.topLevelOperatorView.Crops.unregisterDirty,
                    bind(self._updateCropList)))

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingCrops = False

        self._initCropUic(drawerUiPath)

        self._maxCropNumUsed = 0

        self._allowDeleteLastCropOnly = False
        self.__initShortcuts()
        # Init base class
        super(CroppingGui, self).__init__(
            parentApplet,
            topLevelOperatorView,
            [croppingSlots.cropInput, croppingSlots.cropOutput],
            crosshair=crosshair)
        self._croppingSlots.cropEraserValue.setValue(
            self.editor.brushingModel.erasingNumber)

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
Beispiel #2
0
    def __init__(self, parentApplet, topLevelOperatorView):
        drawerUiPath = os.path.join(
            os.path.split(__file__)[0], 'splitBodyCarvingDrawer.ui')
        super(SplitBodyCarvingGui, self).__init__(parentApplet,
                                                  topLevelOperatorView,
                                                  drawerUiPath=drawerUiPath)
        self._splitInfoWidget = BodySplitInfoWidget(self,
                                                    self.topLevelOperatorView)
        self._splitInfoWidget.navigationRequested.connect(
            self._handleNavigationRequest)
        self._labelControlUi.annotationWindowButton.pressed.connect(
            self._splitInfoWidget.show)

        # Hide all controls related to uncertainty; they aren't used in this applet
        self._labelControlUi.uncertaintyLabel.hide()
        self._labelControlUi.uncertaintyCombo.hide()
        self._labelControlUi.pushButtonUncertaintyFG.hide()
        self._labelControlUi.pushButtonUncertaintyBG.hide()

        # Hide manual save buttons; user must use the annotation window to save/load objects
        self._labelControlUi.saveControlLabel.hide()
        self._labelControlUi.save.hide()
        self._labelControlUi.saveAs.hide()
        self._labelControlUi.namesButton.hide()

        self.thunkEventHandler = ThunkEventHandler(self)

        fragmentColors = [
            QColor(0, 0, 0, 0),  # transparent (background)
            QColor(0, 255, 255),  # cyan
            QColor(255, 0, 255),  # magenta
            QColor(0, 0, 128),  # navy
            QColor(165, 42, 42),  # brown        
            QColor(255, 105, 180),  # hot pink
            QColor(255, 165, 0),  # orange
            QColor(173, 255, 47),  # green-yellow
            QColor(102, 205, 170),  # dark aquamarine
            QColor(128, 0, 128),  # purple
            QColor(240, 230, 140),  # khaki
            QColor(192, 192, 192),  # silver
            QColor(69, 69, 69)
        ]  # dark grey

        self._fragmentColors = fragmentColors

        # In this workflow, you aren't allowed to make brushstrokes unless there is a "current fragment"
        def handleEditingFragmentChange(slot, *args):
            if slot.value == "":
                self._changeInteractionMode(Tool.Navigation)
            else:
                self._changeInteractionMode(Tool.Paint)
            self._labelControlUi.paintToolButton.setEnabled(slot.value != "")
            self._labelControlUi.eraserToolButton.setEnabled(slot.value != "")
            self._labelControlUi.labelListView.setEnabled(slot.value != "")

        topLevelOperatorView.CurrentEditingFragment.notifyDirty(
            handleEditingFragmentChange)
        handleEditingFragmentChange(
            topLevelOperatorView.CurrentEditingFragment)
Beispiel #3
0
    def __init__(self,
                 workflow=[],
                 parent=None,
                 flags=QtCore.Qt.WindowFlags(0),
                 sideSplitterSizePolicy=SideSplitterSizePolicy.Manual):
        QMainWindow.__init__(self, parent=parent, flags=flags)
        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._sideSplitterSizePolicy = sideSplitterSizePolicy

        self.projectManager = ProjectManager()

        import inspect, os
        ilastikShellFilePath = os.path.dirname(
            inspect.getfile(inspect.currentframe()))
        uic.loadUi(ilastikShellFilePath + "/ui/ilastikShell.ui", self)
        self._applets = []
        self.appletBarMapping = {}

        self.setAttribute(Qt.WA_AlwaysShowToolTips)

        if 'Ubuntu' in platform.platform():
            # Native menus are prettier, but aren't working on Ubuntu at this time (Qt 4.7, Ubuntu 11)
            self.menuBar().setNativeMenuBar(False)

        (self._projectMenu, self._shellActions) = self._createProjectMenu()
        self._settingsMenu = self._createSettingsMenu()
        self.menuBar().addMenu(self._projectMenu)
        self.menuBar().addMenu(self._settingsMenu)

        self.updateShellProjectDisplay()

        self.progressDisplayManager = ProgressDisplayManager(self.statusBar)

        for applet in workflow:
            self.addApplet(applet)

        self.appletBar.expanded.connect(self.handleAppleBarItemExpanded)
        self.appletBar.clicked.connect(self.handleAppletBarClick)
        self.appletBar.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)

        # By default, make the splitter control expose a reasonable width of the applet bar
        self.mainSplitter.setSizes([300, 1])

        self.currentAppletIndex = 0

        self.currentImageIndex = -1
        self.populatingImageSelectionCombo = False
        self.imageSelectionCombo.currentIndexChanged.connect(
            self.changeCurrentInputImageIndex)

        self.enableWorkflow = False  # Global mask applied to all applets
        self._controlCmds = [
        ]  # Track the control commands that have been issued by each applet so they can be popped.
        self._disableCounts = [
        ]  # Controls for each applet can be disabled by his peers.
Beispiel #4
0
    def __init__(self, parentApplet, labelingSlots, topLevelOperatorView, drawerUiPath=None, rawInputSlot=None,
                 crosshair=True, is_3d_widget_visible=False):
        """
        Constructor.

        :param labelingSlots: Provides the slots needed for sourcing/sinking label data.  See LabelingGui.LabelingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        self._colorTable16 = list(colortables.default16_new)

        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert labelingSlots.labelInput is not None, "Missing a required slot."
        assert labelingSlots.labelOutput is not None, "Missing a required slot."
        assert labelingSlots.labelEraserValue is not None, "Missing a required slot."
        assert labelingSlots.labelDelete is not None, "Missing a required slot."
        assert labelingSlots.labelNames is not None, "Missing a required slot."

        self.__cleanup_fns = []

        self._labelingSlots = labelingSlots
        self._minLabelNumber = 0
        self._maxLabelNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self._labelingSlots.labelNames.notifyDirty(bind(self._updateLabelList))
        self.__cleanup_fns.append(partial(self._labelingSlots.labelNames.unregisterDirty, bind(self._updateLabelList)))
        self._colorTable16 = colortables.default16_new
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self._initLabelUic(drawerUiPath)

        # Init base class
        super(LabelingGui, self).__init__(parentApplet,
                                          topLevelOperatorView,
                                          [labelingSlots.labelInput, labelingSlots.labelOutput],
                                          crosshair=crosshair,
                                          is_3d_widget_visible=is_3d_widget_visible)

        self.__initShortcuts()
        self._labelingSlots.labelEraserValue.setValue(self.editor.brushingModel.erasingNumber)
        self._allowDeleteLastLabelOnly = False
        self._forceAtLeastTwoLabels = False

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
        self._changeInteractionMode(Tool.Navigation)
Beispiel #5
0
    def __init__(self, parentApplet, topLevelOperator):
        super(DataExportGui, self).__init__()

        self.drawer = None
        self.topLevelOperator = topLevelOperator

        self.threadRouter = ThreadRouter(self)
        self._thunkEventHandler = ThunkEventHandler(self)

        self._initAppletDrawerUic()
        self.initCentralUic()
        self.initViewerControls()

        self.parentApplet = parentApplet
        self.progressSignal = parentApplet.progressSignal

        self.overwrite = False

        @threadRoutedWithRouter(self.threadRouter)
        def handleNewDataset(multislot, index):
            # Make room in the GUI table
            self.batchOutputTableWidget.insertRow(index)

            # Update the table row data when this slot has new data
            # We can't bind in the row here because the row may change in the meantime.
            multislot[index].notifyReady(bind(self.updateTableForSlot))
            if multislot[index].ready():
                self.updateTableForSlot(multislot[index])

            multislot[index].notifyUnready(self._updateExportButtons)
            multislot[index].notifyReady(self._updateExportButtons)

        self.topLevelOperator.ExportPath.notifyInserted(bind(handleNewDataset))

        # For each dataset that already exists, update the GUI
        for i, subslot in enumerate(self.topLevelOperator.ExportPath):
            handleNewDataset(self.topLevelOperator.ExportPath, i)
            if subslot.ready():
                self.updateTableForSlot(subslot)

        @threadRoutedWithRouter(self.threadRouter)
        def handleLaneRemoved(multislot, index, finalLength):
            if self.batchOutputTableWidget.rowCount() <= finalLength:
                return

            # Remove the row we don't need any more
            self.batchOutputTableWidget.removeRow(index)

            # Remove the viewer for this dataset
            imageMultiSlot = self.topLevelOperator.Inputs[index]
            if imageMultiSlot in self.layerViewerGuis.keys():
                layerViewerGui = self.layerViewerGuis[imageMultiSlot]
                self.viewerStack.removeWidget(layerViewerGui)
                self._viewerControlWidgetStack.removeWidget(
                    layerViewerGui.viewerControlWidget())
                layerViewerGui.stopAndCleanUp()

        self.topLevelOperator.Inputs.notifyRemove(bind(handleLaneRemoved))
Beispiel #6
0
    def __init__(self,
                 labelingSlots,
                 topLevelOperator,
                 drawerUiPath=None,
                 rawInputSlot=None):
        """
        See LabelingSlots class (above) for expected type of labelingSlots parameter.
        
        observedSlots is the same as in the LayerViewer constructor.
        drawerUiPath can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        Data from the rawInputSlot parameter will be displayed directly underneatch the labels (if provided).
        """
        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert all([v is not None for v in labelingSlots.__dict__.values()])

        self._minLabelNumber = 0
        self._maxLabelNumber = 99  #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        # Init base class
        super(LabelingGui, self).__init__(topLevelOperator)

        self._labelingSlots = labelingSlots
        self._labelingSlots.labelEraserValue.setValue(
            self.editor.brushingModel.erasingNumber)
        self._labelingSlots.maxLabelValue.notifyDirty(
            bind(self.updateLabelList))

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self.initLabelUic(drawerUiPath)

        self.changeInteractionMode(Tool.Navigation)

        self.__initShortcuts()
Beispiel #7
0
    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        super(VigraWatershedViewerGui, self).__init__( parentApplet, topLevelOperatorView )
        self.topLevelOperatorView = topLevelOperatorView
        op = self.topLevelOperatorView
        
        # Init padding gui updates
        blockPadding = PreferencesManager().get( 'vigra watershed viewer', 'block padding', 10)
        op.WatershedPadding.notifyDirty( self.updatePaddingGui )
        op.WatershedPadding.setValue( blockPadding )
        self.updatePaddingGui()
        
        # Init block shape gui updates
        cacheBlockShape = PreferencesManager().get( 'vigra watershed viewer', 'cache block shape', (256, 10))
        op.CacheBlockShape.notifyDirty( self.updateCacheBlockGui )
        op.CacheBlockShape.setValue( tuple(cacheBlockShape) )
        self.updateCacheBlockGui()

        # Init seeds gui updates
        op.SeedThresholdValue.notifyDirty( self.updateSeedGui )
        op.SeedThresholdValue.notifyReady( self.updateSeedGui )
        op.SeedThresholdValue.notifyUnready( self.updateSeedGui )
        op.MinSeedSize.notifyDirty( self.updateSeedGui )
        self.updateSeedGui()
        
        # Init input channel gui updates
        op.InputChannelIndexes.notifyDirty( self.updateInputChannelGui )
        op.InputImage.notifyMetaChanged( bind(self.updateInputChannelGui) )
        self.updateInputChannelGui()

        self.thunkEventHandler = ThunkEventHandler(self)
        
        # Remember to unsubscribe during shutdown
        self.__cleanup_fns = []
        self.__cleanup_fns.append( partial( op.WatershedPadding.unregisterDirty, self.updatePaddingGui ) )
        self.__cleanup_fns.append( partial( op.CacheBlockShape.unregisterDirty, self.updateCacheBlockGui ) )
        self.__cleanup_fns.append( partial( op.SeedThresholdValue.unregisterDirty, self.updateSeedGui ) )
        self.__cleanup_fns.append( partial( op.SeedThresholdValue.unregisterReady, self.updateSeedGui ) )
        self.__cleanup_fns.append( partial( op.SeedThresholdValue.unregisterUnready, self.updateSeedGui ) )
        self.__cleanup_fns.append( partial( op.MinSeedSize.unregisterDirty, self.updateSeedGui ) )
        self.__cleanup_fns.append( partial( op.InputChannelIndexes.unregisterDirty, self.updateInputChannelGui ) )
        self.__cleanup_fns.append( partial( op.InputImage.unregisterDirty, self.updateInputChannelGui ) )
    def __init__(self, topLevelOperatorView):
        """
        """
        super(VigraWatershedViewerGui, self).__init__( topLevelOperatorView )
        self.topLevelOperatorView = topLevelOperatorView
        
        self.topLevelOperatorView.FreezeCache.setValue(True)
        self.topLevelOperatorView.OverrideLabels.setValue( { 0: (0,0,0,0) } )

        # Default settings (will be overwritten by serializer)
        self.topLevelOperatorView.InputChannelIndexes.setValue( [] )
        self.topLevelOperatorView.SeedThresholdValue.setValue( 0.0 )
        self.topLevelOperatorView.MinSeedSize.setValue( 0 )

        # Init padding gui updates
        blockPadding = PreferencesManager().get( 'vigra watershed viewer', 'block padding', 10)
        self.topLevelOperatorView.WatershedPadding.notifyDirty( self.updatePaddingGui )
        self.topLevelOperatorView.WatershedPadding.setValue(blockPadding)
        self.updatePaddingGui()
        
        # Init block shape gui updates
        cacheBlockShape = PreferencesManager().get( 'vigra watershed viewer', 'cache block shape', (256, 10))
        self.topLevelOperatorView.CacheBlockShape.notifyDirty( self.updateCacheBlockGui )
        self.topLevelOperatorView.CacheBlockShape.setValue( cacheBlockShape )
        self.updateCacheBlockGui()

        # Init seeds gui updates
        self.topLevelOperatorView.SeedThresholdValue.notifyDirty( self.updateSeedGui )
        self.topLevelOperatorView.SeedThresholdValue.notifyReady( self.updateSeedGui )
        self.topLevelOperatorView.SeedThresholdValue.notifyUnready( self.updateSeedGui )
        self.topLevelOperatorView.MinSeedSize.notifyDirty( self.updateSeedGui )
        self.updateSeedGui()
        
        # Init input channel gui updates
        self.topLevelOperatorView.InputChannelIndexes.notifyDirty( self.updateInputChannelGui )
        self.topLevelOperatorView.InputChannelIndexes.setValue( [0] )
        self.topLevelOperatorView.InputImage.notifyMetaChanged( bind(self.updateInputChannelGui) )
        self.updateInputChannelGui()

        self.thunkEventHandler = ThunkEventHandler(self)
    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        super(VigraWatershedViewerGui, self).__init__(parentApplet, topLevelOperatorView)
        self.topLevelOperatorView = topLevelOperatorView
        op = self.topLevelOperatorView

        op.FreezeCache.setValue(True)
        op.OverrideLabels.setValue({0: (0, 0, 0, 0)})

        # Default settings (will be overwritten by serializer)
        op.InputChannelIndexes.setValue([])
        op.SeedThresholdValue.setValue(0.0)
        op.MinSeedSize.setValue(0)

        # Init padding gui updates
        blockPadding = PreferencesManager().get("vigra watershed viewer", "block padding", 10)
        op.WatershedPadding.notifyDirty(self.updatePaddingGui)
        op.WatershedPadding.setValue(blockPadding)
        self.updatePaddingGui()

        # Init block shape gui updates
        cacheBlockShape = PreferencesManager().get("vigra watershed viewer", "cache block shape", (256, 10))
        op.CacheBlockShape.notifyDirty(self.updateCacheBlockGui)
        op.CacheBlockShape.setValue(tuple(cacheBlockShape))
        self.updateCacheBlockGui()

        # Init seeds gui updates
        op.SeedThresholdValue.notifyDirty(self.updateSeedGui)
        op.SeedThresholdValue.notifyReady(self.updateSeedGui)
        op.SeedThresholdValue.notifyUnready(self.updateSeedGui)
        op.MinSeedSize.notifyDirty(self.updateSeedGui)
        self.updateSeedGui()

        # Init input channel gui updates
        op.InputChannelIndexes.notifyDirty(self.updateInputChannelGui)
        op.InputChannelIndexes.setValue([0])
        op.InputImage.notifyMetaChanged(bind(self.updateInputChannelGui))
        self.updateInputChannelGui()

        self.thunkEventHandler = ThunkEventHandler(self)

        # Remember to unsubscribe during shutdown
        self.__cleanup_fns = []
        self.__cleanup_fns.append(partial(op.WatershedPadding.unregisterDirty, self.updatePaddingGui))
        self.__cleanup_fns.append(partial(op.CacheBlockShape.unregisterDirty, self.updateCacheBlockGui))
        self.__cleanup_fns.append(partial(op.SeedThresholdValue.unregisterDirty, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.SeedThresholdValue.unregisterReady, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.SeedThresholdValue.unregisterUnready, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.MinSeedSize.unregisterDirty, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.InputChannelIndexes.unregisterDirty, self.updateInputChannelGui))
        self.__cleanup_fns.append(partial(op.InputImage.unregisterDirty, self.updateInputChannelGui))
Beispiel #10
0
    def __init__( self, workflow = [], parent = None, flags = QtCore.Qt.WindowFlags(0), sideSplitterSizePolicy=SideSplitterSizePolicy.Manual ):
        QMainWindow.__init__(self, parent = parent, flags = flags )
        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._sideSplitterSizePolicy = sideSplitterSizePolicy

        self.projectManager = ProjectManager()
        
        import inspect, os
        ilastikShellFilePath = os.path.dirname(inspect.getfile(inspect.currentframe()))
        uic.loadUi( ilastikShellFilePath + "/ui/ilastikShell.ui", self )
        self._applets = []
        self.appletBarMapping = {}

        self.setAttribute(Qt.WA_AlwaysShowToolTips)
        
        if 'Ubuntu' in platform.platform():
            # Native menus are prettier, but aren't working on Ubuntu at this time (Qt 4.7, Ubuntu 11)
            self.menuBar().setNativeMenuBar(False)

        (self._projectMenu, self._shellActions) = self._createProjectMenu()
        self._settingsMenu = self._createSettingsMenu()
        self.menuBar().addMenu( self._projectMenu )
        self.menuBar().addMenu( self._settingsMenu )

        self.updateShellProjectDisplay()
        
        self.progressDisplayManager = ProgressDisplayManager(self.statusBar)

        self.appletBar.expanded.connect(self.handleAppleBarItemExpanded)
        self.appletBar.clicked.connect(self.handleAppletBarClick)
        self.appletBar.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel )
        
        # By default, make the splitter control expose a reasonable width of the applet bar
        self.mainSplitter.setSizes([300,1])
        
        self.currentAppletIndex = 0

        self.currentImageIndex = -1
        self.populatingImageSelectionCombo = False
        self.imageSelectionCombo.currentIndexChanged.connect( self.changeCurrentInputImageIndex )
        
        self.enableWorkflow = False # Global mask applied to all applets
        self._controlCmds = []      # Track the control commands that have been issued by each applet so they can be popped.
        self._disableCounts = []    # Controls for each applet can be disabled by his peers.
                                    # No applet can be enabled unless his disableCount == 0

        # Add all the applets from the workflow
        for app in workflow.applets:
            self.addApplet(app)
        self.workflow = workflow
Beispiel #11
0
    def __init__(self, parentApplet, croppingSlots, topLevelOperatorView, drawerUiPath=None, rawInputSlot=None, crosshair=True):
        """
        Constructor.

        :param croppingSlots: Provides the slots needed for sourcing/sinking crop data.  See CroppingGui.CroppingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        # Do we have all the slots we need?
        assert isinstance(croppingSlots, CroppingGui.CroppingSlots)
        assert croppingSlots.cropInput is not None, "Missing a required slot."
        assert croppingSlots.cropOutput is not None, "Missing a required slot."
        assert croppingSlots.cropEraserValue is not None, "Missing a required slot."
        assert croppingSlots.cropDelete is not None, "Missing a required slot."
        assert croppingSlots.cropNames is not None, "Missing a required slot."
        assert croppingSlots.cropsAllowed is not None, "Missing a required slot."

        self.__cleanup_fns = []
        self._croppingSlots = croppingSlots
        self._minCropNumber = 0
        self._maxCropNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self.topLevelOperatorView.Crops.notifyDirty( bind(self._updateCropList) )
        self.topLevelOperatorView.Crops.notifyDirty( bind(self._updateCropList) )
        self.__cleanup_fns.append( partial( self.topLevelOperatorView.Crops.unregisterDirty, bind(self._updateCropList) ) )
        
        self._colorTable16 = colortables.default16_new
        self._programmaticallyRemovingCrops = False

        self._initCropUic(drawerUiPath)

        self._maxCropNumUsed = 0

        self._allowDeleteLastCropOnly = False
        self.__initShortcuts()
        # Init base class
        super(CroppingGui, self).__init__(parentApplet,
                                          topLevelOperatorView,
                                          [croppingSlots.cropInput, croppingSlots.cropOutput],
                                          crosshair=crosshair)
        self._croppingSlots.cropEraserValue.setValue(self.editor.brushingModel.erasingNumber)

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
    def __init__(self, topLevelOperatorView):
        drawerUiPath = os.path.join( os.path.split(__file__)[0], 'splitBodyCarvingDrawer.ui' )
        super( SplitBodyCarvingGui, self ).__init__(topLevelOperatorView, drawerUiPath=drawerUiPath)
        self._splitInfoWidget = BodySplitInfoWidget(self, self.topLevelOperatorView)
        self._splitInfoWidget.navigationRequested.connect( self._handleNavigationRequest )
        self._labelControlUi.annotationWindowButton.pressed.connect( self._splitInfoWidget.show )
        
        # Hide all controls related to uncertainty; they aren't used in this applet
        self._labelControlUi.uncertaintyLabel.hide()
        self._labelControlUi.uncertaintyCombo.hide()
        self._labelControlUi.pushButtonUncertaintyFG.hide()
        self._labelControlUi.pushButtonUncertaintyBG.hide()
        
        # Hide manual save buttons; user must use the annotation window to save/load objects
        self._labelControlUi.saveControlLabel.hide()
        self._labelControlUi.save.hide()
        self._labelControlUi.saveAs.hide()
        self._labelControlUi.namesButton.hide()

        self.thunkEventHandler = ThunkEventHandler(self)

        fragmentColors = [ QColor(0,0,0,0), # transparent (background)
                           QColor(0, 255, 255),   # cyan
                           QColor(255, 0, 255),   # magenta
                           QColor(0, 0, 128),     # navy
                           QColor(165,  42,  42), # brown        
                           QColor(255, 105, 180), # hot pink
                           QColor(255, 165, 0),   # orange
                           QColor(173, 255,  47), # green-yellow
                           QColor(102, 205, 170), # dark aquamarine
                           QColor(128,0, 128),    # purple
                           QColor(240, 230, 140), # khaki
                           QColor(192, 192, 192), # silver
                           QColor(69, 69, 69) ]   # dark grey

        self._fragmentColors = fragmentColors

        # In this workflow, you aren't allowed to make brushstrokes unless there is a "current fragment"
        def handleEditingFragmentChange(slot, *args):
            if slot.value == "":
                self._changeInteractionMode(Tool.Navigation)
            else:
                self._changeInteractionMode(Tool.Paint)
            self._labelControlUi.paintToolButton.setEnabled( slot.value != "" )
            self._labelControlUi.eraserToolButton.setEnabled( slot.value != "" )
            self._labelControlUi.labelListView.setEnabled( slot.value != "" )
        topLevelOperatorView.CurrentEditingFragment.notifyDirty( handleEditingFragmentChange )
        handleEditingFragmentChange(topLevelOperatorView.CurrentEditingFragment)
Beispiel #13
0
    def __init__(self, labelingSlots, topLevelOperatorView, drawerUiPath=None, rawInputSlot=None, crosshair=True):
        """
        Constructor.

        :param labelingSlots: Provides the slots needed for sourcing/sinking label data.  See LabelingGui.LabelingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert all([v is not None for v in labelingSlots.__dict__.values()])

        self._labelingSlots = labelingSlots
        self._minLabelNumber = 0
        self._maxLabelNumber = 99  # 100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self._labelingSlots.maxLabelValue.notifyDirty(bind(self._updateLabelList))

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + "/labelingDrawer.ui"
        self._initLabelUic(drawerUiPath)

        # Init base class
        super(LabelingGui, self).__init__(
            topLevelOperatorView, [labelingSlots.labelInput, labelingSlots.labelOutput], crosshair=crosshair
        )

        self.__initShortcuts()
        self._labelingSlots.labelEraserValue.setValue(self.editor.brushingModel.erasingNumber)

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
        self._changeInteractionMode(Tool.Navigation)
    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        super(VigraWatershedViewerGui, self).__init__( parentApplet, topLevelOperatorView )
        self.topLevelOperatorView = topLevelOperatorView
        
        self.topLevelOperatorView.FreezeCache.setValue(True)
        self.topLevelOperatorView.OverrideLabels.setValue( { 0: (0,0,0,0) } )

        # Default settings (will be overwritten by serializer)
        self.topLevelOperatorView.InputChannelIndexes.setValue( [] )
        self.topLevelOperatorView.SeedThresholdValue.setValue( 0.0 )
        self.topLevelOperatorView.MinSeedSize.setValue( 0 )

        # Init padding gui updates
        blockPadding = PreferencesManager().get( 'vigra watershed viewer', 'block padding', 10)
        self.topLevelOperatorView.WatershedPadding.notifyDirty( self.updatePaddingGui )
        self.topLevelOperatorView.WatershedPadding.setValue( blockPadding )
        self.updatePaddingGui()
        
        # Init block shape gui updates
        cacheBlockShape = PreferencesManager().get( 'vigra watershed viewer', 'cache block shape', (256, 10))
        self.topLevelOperatorView.CacheBlockShape.notifyDirty( self.updateCacheBlockGui )
        self.topLevelOperatorView.CacheBlockShape.setValue( tuple(cacheBlockShape) )
        self.updateCacheBlockGui()

        # Init seeds gui updates
        self.topLevelOperatorView.SeedThresholdValue.notifyDirty( self.updateSeedGui )
        self.topLevelOperatorView.SeedThresholdValue.notifyReady( self.updateSeedGui )
        self.topLevelOperatorView.SeedThresholdValue.notifyUnready( self.updateSeedGui )
        self.topLevelOperatorView.MinSeedSize.notifyDirty( self.updateSeedGui )
        self.updateSeedGui()
        
        # Init input channel gui updates
        self.topLevelOperatorView.InputChannelIndexes.notifyDirty( self.updateInputChannelGui )
        self.topLevelOperatorView.InputChannelIndexes.setValue( [0] )
        self.topLevelOperatorView.InputImage.notifyMetaChanged( bind(self.updateInputChannelGui) )
        self.updateInputChannelGui()

        self.thunkEventHandler = ThunkEventHandler(self)
    def __init__(self, labelingSlots, topLevelOperator, drawerUiPath=None, rawInputSlot=None ):
        """
        See LabelingSlots class (above) for expected type of labelingSlots parameter.
        
        observedSlots is the same as in the LayerViewer constructor.
        drawerUiPath can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        Data from the rawInputSlot parameter will be displayed directly underneatch the labels (if provided).
        """
        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert all( [v is not None for v in labelingSlots.__dict__.values()] )
       
        self._minLabelNumber = 0
        self._maxLabelNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot
        
        # Init base class
        super(LabelingGui, self).__init__( topLevelOperator )

        self._labelingSlots = labelingSlots
        self._labelingSlots.labelEraserValue.setValue(self.editor.brushingModel.erasingNumber)
        self._labelingSlots.maxLabelValue.notifyDirty( bind(self.updateLabelList) )

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingLabels = False
        
        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self.initLabelUic(drawerUiPath)
        
        self.changeInteractionMode(Tool.Navigation)
        
        self.__initShortcuts()
Beispiel #16
0
class LabelingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer 
    applet with the added functionality of labeling.
    """

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

    def centralWidget(self):
        return self

    def appletDrawers(self):
        return [("Label Marking", self._labelControlUi)]

    def reset(self):
        # Clear the label list GUI
        self.clearLabelListGui()

        # Start in navigation mode (not painting)
        self.changeInteractionMode(Tool.Navigation)

    def setImageIndex(self, index):
        super(LabelingGui, self).setImageIndex(index)

        # Reset the GUI for "labels allowed" status
        self.changeInteractionMode(self._toolId)

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

    @property
    def minLabelNumber(self):
        return self._minLabelNumber

    @minLabelNumber.setter
    def minLabelNumber(self, n):
        self._minLabelNumer = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self.addNewLabel()

    @property
    def maxLabelNumber(self):
        return self._maxLabelNumber

    @maxLabelNumber.setter
    def maxLabelNumber(self, n):
        self._maxLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self.removeLastLabel()

    @property
    def labelingDrawerUi(self):
        return self._labelControlUi

    @property
    def labelListData(self):
        return self._labelControlUi.labelListModel

    def selectLabel(self, labelIndex):
        """Programmatically select the given labelIndex, which start from 0.
           Equivalent to clicking on the (labelIndex+1)'th position in the label widget."""
        self._labelControlUi.labelListModel.select(labelIndex)

    class LabelingSlots(object):
        def __init__(self):
            # Label slots are multi (level=1) and accessed as shown.
            # Slot to insert labels onto
            self.labelInput = None  # labelInput[image_index].setInSlot(xxx)
            # Slot to read labels from
            self.labelOutput = None  # labelOutput[image_index].get(roi)
            # Slot that determines which label value corresponds to erased values
            self.labelEraserValue = None  # labelEraserValue.setValue(xxx)
            # Slot that is used to request wholesale label deletion
            self.labelDelete = None  # labelDelete.setValue(xxx)
            # Slot that contains the maximum label value (for all images)
            self.maxLabelValue = None  # maxLabelValue.value

            # Slot to specify which images the user is allowed to label.
            self.labelsAllowed = None  # labelsAllowed[image_index].value == True

    @traceLogged(traceLogger)
    def __init__(self,
                 labelingSlots,
                 topLevelOperator,
                 drawerUiPath=None,
                 rawInputSlot=None):
        """
        See LabelingSlots class (above) for expected type of labelingSlots parameter.
        
        observedSlots is the same as in the LayerViewer constructor.
        drawerUiPath can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        Data from the rawInputSlot parameter will be displayed directly underneatch the labels (if provided).
        """
        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert all([v is not None for v in labelingSlots.__dict__.values()])

        self._minLabelNumber = 0
        self._maxLabelNumber = 99  #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        # Init base class
        super(LabelingGui, self).__init__(topLevelOperator)

        self._labelingSlots = labelingSlots
        self._labelingSlots.labelEraserValue.setValue(
            self.editor.brushingModel.erasingNumber)
        self._labelingSlots.maxLabelValue.notifyDirty(
            bind(self.updateLabelList))

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self.initLabelUic(drawerUiPath)

        self.changeInteractionMode(Tool.Navigation)

        self.__initShortcuts()

    @traceLogged(traceLogger)
    def initLabelUic(self, drawerUiPath):
        _labelControlUi = uic.loadUi(drawerUiPath)

        # We own the applet bar ui
        self._labelControlUi = _labelControlUi

        # Initialize the label list model
        model = LabelListModel()
        _labelControlUi.labelListView.setModel(model)
        _labelControlUi.labelListModel = model
        _labelControlUi.labelListModel.rowsRemoved.connect(self.onLabelRemoved)
        _labelControlUi.labelListModel.labelSelected.connect(
            self.onLabelSelected)

        @traceLogged(traceLogger)
        def onDataChanged(topLeft, bottomRight):
            """Handle changes to the label list selections."""
            firstRow = topLeft.row()
            lastRow = bottomRight.row()

            firstCol = topLeft.column()
            lastCol = bottomRight.column()

            if lastCol == firstCol == 0:
                assert (firstRow == lastRow
                        )  #only one data item changes at a time

                #in this case, the actual data (for example color) has changed
                color = _labelControlUi.labelListModel[firstRow].color
                self._colorTable16[firstRow + 1] = color.rgba()
                self.editor.brushingModel.setBrushColor(color)

                # Update the label layer colortable to match the list entry
                labellayer = self.getLabelLayer()
                labellayer.colorTable = self._colorTable16
            else:
                #this column is used for the 'delete' buttons, we don't care
                #about data changed here
                pass

        # Connect Applet GUI to our event handlers
        _labelControlUi.AddLabelButton.clicked.connect(bind(self.addNewLabel))
        _labelControlUi.labelListModel.dataChanged.connect(onDataChanged)

        # Initialize the arrow tool button with an icon and handler
        iconPath = os.path.split(__file__)[0] + "/icons/arrow.jpg"
        arrowIcon = QIcon(iconPath)
        _labelControlUi.arrowToolButton.setIcon(arrowIcon)
        _labelControlUi.arrowToolButton.setCheckable(True)
        _labelControlUi.arrowToolButton.clicked.connect(
            lambda checked: self.handleToolButtonClicked(
                checked, Tool.Navigation))

        # Initialize the paint tool button with an icon and handler
        paintBrushIconPath = os.path.split(
            __file__)[0] + "/icons/paintbrush.png"
        paintBrushIcon = QIcon(paintBrushIconPath)
        _labelControlUi.paintToolButton.setIcon(paintBrushIcon)
        _labelControlUi.paintToolButton.setCheckable(True)
        _labelControlUi.paintToolButton.clicked.connect(
            lambda checked: self.handleToolButtonClicked(checked, Tool.Paint))

        # Initialize the erase tool button with an icon and handler
        eraserIconPath = os.path.split(__file__)[0] + "/icons/eraser.png"
        eraserIcon = QIcon(eraserIconPath)
        _labelControlUi.eraserToolButton.setIcon(eraserIcon)
        _labelControlUi.eraserToolButton.setCheckable(True)
        _labelControlUi.eraserToolButton.clicked.connect(
            lambda checked: self.handleToolButtonClicked(checked, Tool.Erase))

        # This maps tool types to the buttons that enable them
        self.toolButtons = {
            Tool.Navigation: _labelControlUi.arrowToolButton,
            Tool.Paint: _labelControlUi.paintToolButton,
            Tool.Erase: _labelControlUi.eraserToolButton
        }

        self.brushSizes = [(1, ""), (3, "Tiny"), (5, "Small"), (7, "Medium"),
                           (11, "Large"), (23, "Huge"), (31, "Megahuge"),
                           (61, "Gigahuge")]

        for size, name in self.brushSizes:
            _labelControlUi.brushSizeComboBox.addItem(str(size) + " " + name)

        _labelControlUi.brushSizeComboBox.currentIndexChanged.connect(
            self.onBrushSizeChange)

        self.paintBrushSizeIndex = PreferencesManager().get('labeling',
                                                            'paint brush size',
                                                            default=0)
        self.eraserSizeIndex = PreferencesManager().get('labeling',
                                                        'eraser brush size',
                                                        default=4)

    def __initShortcuts(self):
        mgr = ShortcutManager()
        shortcutGroupName = "Labeling"

        addLabel = QShortcut(QKeySequence("a"),
                             self,
                             member=self.labelingDrawerUi.AddLabelButton.click)
        mgr.register(shortcutGroupName, "Add New Label Class", addLabel,
                     self.labelingDrawerUi.AddLabelButton)

        navMode = QShortcut(QKeySequence("n"),
                            self,
                            member=self.labelingDrawerUi.arrowToolButton.click)
        mgr.register(shortcutGroupName, "Navigation Cursor", navMode,
                     self.labelingDrawerUi.arrowToolButton)

        brushMode = QShortcut(
            QKeySequence("b"),
            self,
            member=self.labelingDrawerUi.paintToolButton.click)
        mgr.register(shortcutGroupName, "Brush Cursor", brushMode,
                     self.labelingDrawerUi.paintToolButton)

        eraserMode = QShortcut(
            QKeySequence("e"),
            self,
            member=self.labelingDrawerUi.eraserToolButton.click)
        mgr.register(shortcutGroupName, "Eraser Cursor", eraserMode,
                     self.labelingDrawerUi.eraserToolButton)

        changeBrushSize = QShortcut(
            QKeySequence("c"),
            self,
            member=self.labelingDrawerUi.brushSizeComboBox.showPopup)
        mgr.register(shortcutGroupName, "Change Brush Size", changeBrushSize,
                     self.labelingDrawerUi.brushSizeComboBox)

        self._labelShortcuts = []

    def _updateLabelShortcuts(self):
        numShortcuts = len(self._labelShortcuts)
        numRows = len(self._labelControlUi.labelListModel)

        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts, numRows):
            shortcut = QShortcut(
                QKeySequence(str(i + 1)),
                self,
                member=partial(self._labelControlUi.labelListView.selectRow,
                               i))
            self._labelShortcuts.append(shortcut)
            toolTipObject = LabelListModel.EntryToolTipAdapter(
                self._labelControlUi.labelListModel, i)
            ShortcutManager().register("Labeling", "", shortcut, toolTipObject)

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            shortcut = self._labelShortcuts[i]
            description = "Select " + self._labelControlUi.labelListModel[
                i].name
            ShortcutManager().setDescription(shortcut, description)

    def hideEvent(self, event):
        """
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        with PreferencesManager() as prefsMgr:
            prefsMgr.set('labeling', 'paint brush size',
                         self.paintBrushSizeIndex)
            prefsMgr.set('labeling', 'eraser brush size', self.eraserSizeIndex)
        super(LabelingGui, self).hideEvent(event)

    @traceLogged(traceLogger)
    def handleToolButtonClicked(self, checked, toolId):
        """
        Called when the user clicks any of the "tool" buttons in the label applet bar GUI.
        """
        if not checked:
            # Users can only *switch between* tools, not turn them off.
            # If they try to turn a button off, re-select it automatically.
            self.toolButtons[toolId].setChecked(True)
        else:
            # If the user is checking a new button
            self.changeInteractionMode(toolId)

    @threadRouted
    @traceLogged(traceLogger)
    def changeInteractionMode(self, toolId):
        """
        Implement the GUI's response to the user selecting a new tool.
        """
        # Uncheck all the other buttons
        for tool, button in self.toolButtons.items():
            if tool != toolId:
                button.setChecked(False)

        # If we have no editor, we can't do anything yet
        if self.editor is None:
            return

        # The volume editor expects one of two specific names
        modeNames = {
            Tool.Navigation: "navigation",
            Tool.Paint: "brushing",
            Tool.Erase: "brushing"
        }

        # Hide everything by default
        self._labelControlUi.arrowToolButton.hide()
        self._labelControlUi.paintToolButton.hide()
        self._labelControlUi.eraserToolButton.hide()
        self._labelControlUi.brushSizeComboBox.hide()
        self._labelControlUi.brushSizeCaption.hide()

        # If the user can't label this image, disable the button and say why its disabled
        labelsAllowed = False
        if 0 <= self.imageIndex < len(self._labelingSlots.labelsAllowed):
            labelsAllowedSlot = self._labelingSlots.labelsAllowed[
                self.imageIndex]
            if labelsAllowedSlot.ready():
                labelsAllowed = labelsAllowedSlot.value

                self._labelControlUi.AddLabelButton.setEnabled(
                    labelsAllowed and self.maxLabelNumber >
                    self._labelControlUi.labelListModel.rowCount())
                if labelsAllowed:
                    self._labelControlUi.AddLabelButton.setText("Add Label")
                else:
                    self._labelControlUi.AddLabelButton.setText(
                        "(Labeling Not Allowed)")

        if self.imageIndex != -1 and labelsAllowed:
            self._labelControlUi.arrowToolButton.show()
            self._labelControlUi.paintToolButton.show()
            self._labelControlUi.eraserToolButton.show()
            # Update the applet bar caption
            if toolId == Tool.Navigation:
                # Make sure the arrow button is pressed
                self._labelControlUi.arrowToolButton.setChecked(True)
                # Hide the brush size control
                self._labelControlUi.brushSizeCaption.hide()
                self._labelControlUi.brushSizeComboBox.hide()
            elif toolId == Tool.Paint:
                # Make sure the paint button is pressed
                self._labelControlUi.paintToolButton.setChecked(True)
                # Show the brush size control and set its caption
                self._labelControlUi.brushSizeCaption.show()
                self._labelControlUi.brushSizeComboBox.show()
                self._labelControlUi.brushSizeCaption.setText("Size:")

                # If necessary, tell the brushing model to stop erasing
                if self.editor.brushingModel.erasing:
                    self.editor.brushingModel.disableErasing()
                # Set the brushing size
                brushSize = self.brushSizes[self.paintBrushSizeIndex][0]
                self.editor.brushingModel.setBrushSize(brushSize)

                # Make sure the GUI reflects the correct size
                self._labelControlUi.brushSizeComboBox.setCurrentIndex(
                    self.paintBrushSizeIndex)

            elif toolId == Tool.Erase:
                # Make sure the erase button is pressed
                self._labelControlUi.eraserToolButton.setChecked(True)
                # Show the brush size control and set its caption
                self._labelControlUi.brushSizeCaption.show()
                self._labelControlUi.brushSizeComboBox.show()
                self._labelControlUi.brushSizeCaption.setText("Size:")

                # If necessary, tell the brushing model to start erasing
                if not self.editor.brushingModel.erasing:
                    self.editor.brushingModel.setErasing()
                # Set the brushing size
                eraserSize = self.brushSizes[self.eraserSizeIndex][0]
                self.editor.brushingModel.setBrushSize(eraserSize)

                # Make sure the GUI reflects the correct size
                self._labelControlUi.brushSizeComboBox.setCurrentIndex(
                    self.eraserSizeIndex)

        self.editor.setInteractionMode(modeNames[toolId])
        self._toolId = toolId

    @traceLogged(traceLogger)
    def onBrushSizeChange(self, index):
        """
        Handle the user's new brush size selection.
        Note: The editor's brushing model currently maintains only a single 
              brush size, which is used for both painting and erasing. 
              However, we maintain two different sizes for the user and swap 
              them depending on which tool is selected.
        """
        newSize = self.brushSizes[index][0]
        if self.editor.brushingModel.erasing:
            self.eraserSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)
        else:
            self.paintBrushSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)

    @traceLogged(traceLogger)
    def onLabelSelected(self, row):
        logger.debug("switching to label=%r" %
                     (self._labelControlUi.labelListModel[row]))

        # If the user is selecting a label, he probably wants to be in paint mode
        self.changeInteractionMode(Tool.Paint)

        #+1 because first is transparent
        #FIXME: shouldn't be just row+1 here
        self.editor.brushingModel.setDrawnNumber(row + 1)
        self.editor.brushingModel.setBrushColor(
            self._labelControlUi.labelListModel[row].color)

    @traceLogged(traceLogger)
    def resetLabelSelection(self):
        logger.debug("Resetting label selection")
        if len(self._labelControlUi.labelListModel) > 0:
            self._labelControlUi.labelListView.selectRow(0)
        else:
            self.changeInteractionMode(Tool.Navigation)
        return True

    @traceLogged(traceLogger)
    def updateLabelList(self):
        """
        This function is called when the number of labels has changed without our knowledge.
        We need to add/remove labels until we have the right number
        """
        # Get the number of labels in the label data
        # (Or the number of the labels the user has added.)
        numLabels = max(self._labelingSlots.maxLabelValue.value,
                        self._labelControlUi.labelListModel.rowCount())
        if numLabels == None:
            numLabels = 0

        # Add rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() < numLabels:
            self.addNewLabel()

        self._labelControlUi.AddLabelButton.setEnabled(
            numLabels < self.maxLabelNumber)

    @traceLogged(traceLogger)
    def addNewLabel(self):
        """
        Add a new label to the label list GUI control.
        Return the new number of labels in the control.
        """
        numLabels = len(self._labelControlUi.labelListModel)
        if numLabels >= len(self._colorTable16) - 1:
            # If the color table isn't large enough to handle all our labels,
            #  append a random color
            randomColor = QColor(numpy.random.randint(0, 255),
                                 numpy.random.randint(0, 255),
                                 numpy.random.randint(0, 255))
            self._colorTable16.append(randomColor.rgba())

        color = QColor()
        color.setRgba(self._colorTable16[
            numLabels + 1])  # First entry is transparent (for zero label)

        label = Label(self.getNextLabelName(), color)
        label.nameChanged.connect(self._updateLabelShortcuts)
        self._labelControlUi.labelListModel.insertRow(
            self._labelControlUi.labelListModel.rowCount(), label)
        nlabels = self._labelControlUi.labelListModel.rowCount()

        # Make the new label selected
        selectedRow = nlabels - 1
        self._labelControlUi.labelListModel.select(selectedRow)

        self._updateLabelShortcuts()

    def getNextLabelName(self):
        maxNum = 0
        for index, label in enumerate(self._labelControlUi.labelListModel):
            nums = re.findall("\d+", label.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Label {}".format(maxNum + 1)

    @traceLogged(traceLogger)
    def removeLastLabel(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the label list by one.
        """
        self._programmaticallyRemovingLabels = True
        numRows = self._labelControlUi.labelListModel.rowCount()
        # This will trigger the signal that calls onLabelRemoved()
        self._labelControlUi.labelListModel.removeRow(numRows - 1)
        self._updateLabelShortcuts()

        self._programmaticallyRemovingLabels = False

    @traceLogged(traceLogger)
    def clearLabelListGui(self):
        # Remove rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() > 0:
            self.removeLastLabel()

    @traceLogged(traceLogger)
    def onLabelRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingLabels:
            return

        assert start == end
        row = start

        oldcount = self._labelControlUi.labelListModel.rowCount() + 1
        logger.debug("removing label {} out of {}".format(row, oldcount))

        # Remove the deleted label's color from the color table so that renumbered labels keep their colors.
        oldColor = self._colorTable16.pop(row + 1)

        # Recycle the deleted color back into the table (for the next label to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the labellayer colortable with the new color mapping
        labellayer = self.getLabelLayer()
        labellayer.colorTable = self._colorTable16

        currentSelection = self._labelControlUi.labelListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post(self.resetLabelSelection)

        # Changing the deleteLabel input causes the operator (OpBlockedSparseArray)
        #  to search through the entire list of labels and delete the entries for the matching label.
        self._labelingSlots.labelDelete.setValue(row + 1)

        # We need to "reset" the deleteLabel input to -1 when we're finished.
        #  Otherwise, you can never delete the same label twice in a row.
        #  (Only *changes* to the input are acted upon.)
        self._labelingSlots.labelDelete.setValue(-1)

    def getLabelLayer(self):
        # Find the labellayer in the viewer stack
        try:
            labellayer = itertools.ifilter(lambda l: l.name == "Labels",
                                           self.layerstack).next()
        except StopIteration:
            raise RuntimeError(
                "Couldn't locate the label layer in the layer stack.  Does it have the expected name?"
            )
        return labellayer

    @traceLogged(traceLogger)
    def createLabelLayer(self, currentImageIndex, direct=False):
        """
        Return a colortable layer that displays the label slot data, along with its associated label source.
        direct: whether this layer is drawn synchronously by volumina
        """
        labelOutput = self._labelingSlots.labelOutput[currentImageIndex]
        if not labelOutput.ready():
            return (None, None)
        else:
            traceLogger.debug("Setting up labels for image index={}".format(
                currentImageIndex))
            # Add the layer to draw the labels, but don't add any labels
            labelsrc = LazyflowSinkSource(
                self._labelingSlots.labelOutput[currentImageIndex],
                self._labelingSlots.labelInput[currentImageIndex])

            labellayer = ColortableLayer(labelsrc,
                                         colorTable=self._colorTable16,
                                         direct=direct)
            labellayer.name = "Labels"
            labellayer.ref_object = None

            return labellayer, labelsrc

    @traceLogged(traceLogger)
    def setupLayers(self, currentImageIndex):
        """
        Sets up the label layer for display by our base class (layerviewer).
        If our subclass overrides this function to add his own layers,
        he must call this function explicitly.
        """
        layers = []

        # Labels
        labellayer, labelsrc = self.createLabelLayer(currentImageIndex)
        if labellayer is not None:
            layers.append(labellayer)

            # Tell the editor where to draw label data
            self.editor.setLabelSink(labelsrc)

        # Side effect 1: We want to guarantee that the label list
        #  is up-to-date before our subclass adds his layers
        self.updateLabelList()

        # Side effect 2: Switch to navigation mode if labels aren't
        #  allowed on this image.
        labelsAllowedSlot = self._labelingSlots.labelsAllowed[self.imageIndex]
        if labelsAllowedSlot.ready() and not labelsAllowedSlot.value:
            self.changeInteractionMode(Tool.Navigation)

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot[
                currentImageIndex].ready():
            layer = self.createStandardLayerFromSlot(
                self._rawInputSlot[currentImageIndex])
            layer.name = "Raw Input"
            layer.visible = True
            layer.opacity = 1.0

            layers.append(layer)

        return layers

    @traceLogged(traceLogger)
    def _createDefault16ColorColorTable(self):
        colors = []

        # Transparent for the zero label
        colors.append(QColor(0, 0, 0, 0))

        # ilastik v0.5 colors
        colors.append(QColor(Qt.red))
        colors.append(QColor(Qt.green))
        colors.append(QColor(Qt.yellow))
        colors.append(QColor(Qt.blue))
        colors.append(QColor(Qt.magenta))
        colors.append(QColor(Qt.darkYellow))
        colors.append(QColor(Qt.lightGray))

        # Additional colors
        colors.append(QColor(255, 105, 180))  #hot pink
        colors.append(QColor(102, 205, 170))  #dark aquamarine
        colors.append(QColor(165, 42, 42))  #brown
        colors.append(QColor(0, 0, 128))  #navy
        colors.append(QColor(255, 165, 0))  #orange
        colors.append(QColor(173, 255, 47))  #green-yellow
        colors.append(QColor(128, 0, 128))  #purple
        colors.append(QColor(240, 230, 140))  #khaki

        #        colors.append( QColor(192, 192, 192) ) #silver
        #        colors.append( QColor(69, 69, 69) )    # dark grey
        #        colors.append( QColor( Qt.cyan ) )

        assert len(colors) == 16

        return [c.rgba() for c in colors]
Beispiel #17
0
class SplitBodyCarvingGui(CarvingGui):
    def __init__(self, parentApplet, topLevelOperatorView):
        drawerUiPath = os.path.join(
            os.path.split(__file__)[0], 'splitBodyCarvingDrawer.ui')
        super(SplitBodyCarvingGui, self).__init__(parentApplet,
                                                  topLevelOperatorView,
                                                  drawerUiPath=drawerUiPath)
        self._splitInfoWidget = BodySplitInfoWidget(self,
                                                    self.topLevelOperatorView)
        self._splitInfoWidget.navigationRequested.connect(
            self._handleNavigationRequest)
        self._labelControlUi.annotationWindowButton.pressed.connect(
            self._splitInfoWidget.show)

        # Hide all controls related to uncertainty; they aren't used in this applet
        self._labelControlUi.uncertaintyLabel.hide()
        self._labelControlUi.uncertaintyCombo.hide()
        self._labelControlUi.pushButtonUncertaintyFG.hide()
        self._labelControlUi.pushButtonUncertaintyBG.hide()

        # Hide manual save buttons; user must use the annotation window to save/load objects
        self._labelControlUi.saveControlLabel.hide()
        self._labelControlUi.save.hide()
        self._labelControlUi.saveAs.hide()
        self._labelControlUi.namesButton.hide()

        self.thunkEventHandler = ThunkEventHandler(self)

        fragmentColors = [
            QColor(0, 0, 0, 0),  # transparent (background)
            QColor(0, 255, 255),  # cyan
            QColor(255, 0, 255),  # magenta
            QColor(0, 0, 128),  # navy
            QColor(165, 42, 42),  # brown        
            QColor(255, 105, 180),  # hot pink
            QColor(255, 165, 0),  # orange
            QColor(173, 255, 47),  # green-yellow
            QColor(102, 205, 170),  # dark aquamarine
            QColor(128, 0, 128),  # purple
            QColor(240, 230, 140),  # khaki
            QColor(192, 192, 192),  # silver
            QColor(69, 69, 69)
        ]  # dark grey

        self._fragmentColors = fragmentColors

        # In this workflow, you aren't allowed to make brushstrokes unless there is a "current fragment"
        def handleEditingFragmentChange(slot, *args):
            if slot.value == "":
                self._changeInteractionMode(Tool.Navigation)
            else:
                self._changeInteractionMode(Tool.Paint)
            self._labelControlUi.paintToolButton.setEnabled(slot.value != "")
            self._labelControlUi.eraserToolButton.setEnabled(slot.value != "")
            self._labelControlUi.labelListView.setEnabled(slot.value != "")

        topLevelOperatorView.CurrentEditingFragment.notifyDirty(
            handleEditingFragmentChange)
        handleEditingFragmentChange(
            topLevelOperatorView.CurrentEditingFragment)

    def _handleNavigationRequest(self, coord3d):
        self.editor.posModel.cursorPos = list(coord3d)
        self.editor.posModel.slicingPos = list(coord3d)
        self.editor.navCtrl.panSlicingViews(list(coord3d), [0, 1, 2])

        # Navigation change is passed to downstream applets via this special slot
        self.topLevelOperatorView.NavigationCoordinates.setValue(
            coord3d, check_changed=False)

    def labelingContextMenu(self, names, op, position5d):
        return None
#        pos = TinyVector(position5d)
#        sample_roi = (pos, pos+1)
#        ravelerLabelSample = self.topLevelOperatorView.RavelerLabels(*sample_roi).wait()
#        ravelerLabel = ravelerLabelSample[0,0,0,0,0]
#
#        menu = super( SplitBodyCarvingGui, self ).labelingContextMenu(names, op, position5d)
#        menu.addSeparator()
#        highlightAction = menu.addAction( "Highlight Raveler Object {}".format( ravelerLabel ) )
#        highlightAction.triggered.connect( partial(self.topLevelOperatorView.CurrentRavelerLabel.setValue, ravelerLabel ) )
#
#        # Auto-seed also auto-highlights
#        autoSeedAction = menu.addAction( "Auto-seed background for Raveler Object {}".format( ravelerLabel ) )
#        autoSeedAction.triggered.connect( partial(OpSplitBodyCarving.autoSeedBackground, self.topLevelOperatorView, ravelerLabel ) )
#        autoSeedAction.triggered.connect( partial(self.topLevelOperatorView.CurrentRavelerLabel.setValue, ravelerLabel ) )
#        return menu

    def _update_rendering(self):
        """
        Override from the base class.
        """
        # This update has to be performed in a different thread to avoid a deadlock
        # (Because this function is running in the context of a dirty notification!)
        req = Request(self.__update_rendering)

        def handle_rendering_failure(exc, exc_info):
            msg = "Exception raised during volume rendering update.  See traceack above.\n"
            log_exception(logger, msg, exc_info)

        req.notify_failed(handle_rendering_failure)
        req.submit()

    def __update_rendering(self):
        if not self.render:
            return

        if not self._labelControlUi.activate3DViewCheckbox.isChecked():
            return

        rendered_volume_shape = (250, 250, 250)

        logger.info("Starting to update 3D volume data")

        fragmentColors = self._fragmentColors
        op = self.topLevelOperatorView
        if not self._renderMgr.ready:
            self._renderMgr.setup(op.InputData.meta.shape[1:4])
        self._renderMgr.clear()

        # Create a 5D view of the render mgr's memory
        totalRenderVol5d = self._renderMgr.volume[numpy.newaxis, ...,
                                                  numpy.newaxis]

        # Block must not exceed total bounds.
        # Shift start up if necessary
        rendering_start_3d = TinyVector(
            self.editor.posModel.slicingPos
        ) - TinyVector(rendered_volume_shape) // 2
        rendering_start_3d = numpy.maximum((0, 0, 0), rendering_start_3d)

        # Compute stop and shift down if necessary
        rendering_stop_3d = rendering_start_3d + TinyVector(
            rendered_volume_shape)
        rendering_stop_3d = numpy.minimum(op.InputData.meta.shape[1:4],
                                          rendering_stop_3d)

        # Recompute start now that stop has been computed
        rendering_start_3d = rendering_stop_3d - TinyVector(
            rendered_volume_shape)

        rendering_roi_5d = ((0, ) + tuple(rendering_start_3d) + (0, ),
                            (1, ) + tuple(rendering_stop_3d) + (1, ))

        # View only the data we want to update.
        renderVol5d = totalRenderVol5d[roiToSlice(*rendering_roi_5d)]

        ravelerLabel = op.CurrentRavelerLabel.value
        if ravelerLabel != 0:
            logger.info(" Asking for fragment segmentation")
            op.CurrentFragmentSegmentation(
                *rendering_roi_5d).writeInto(renderVol5d).wait()
            logger.info(" Obtained Fragment Segmentation")

            fragmentNames = op.getFragmentNames(ravelerLabel)
            numFragments = len(fragmentNames)

            renderLabels = []
            for i, name in enumerate(fragmentNames):
                if name != op.CurrentEditingFragment.value:
                    assert i < len(
                        self._fragmentColors
                    ), "Too many fragments: colortable is too small"
                    color = (fragmentColors[i + 1].red() / 255.0,
                             fragmentColors[i + 1].green() / 255.0,
                             fragmentColors[i + 1].blue() / 255.0)
                    renderLabel = self._renderMgr.addObject(color=color)
                    renderLabels.append(renderLabel)

            if op.CurrentEditingFragment.value != "":
                logger.info(" Asking for masked editing segmentation")
                maskedSegmentation = op.MaskedSegmentation(
                    *rendering_roi_5d).wait()
                logger.info(" Obtained for masked editing segmentation")
                segLabel = numFragments

                logger.info(
                    " Start updating volume data with masked segmentation")
                renderVol5d[:] = numpy.where(maskedSegmentation != 0, segLabel,
                                             renderVol5d)
                logger.info(
                    " Finished updating volume data with masked segmentation")

                segmentationColor = (0.0, 1.0, 0.0)
                renderLabel = self._renderMgr.addObject(
                    color=segmentationColor)
                renderLabels.append(renderLabel)

            # Relabel with the labels we were given by the renderer.
            # (We can skip this step if the renderer is guaranteed to give labels 1,2,3...)
            if renderLabels != list(range(len(renderLabels))):
                renderVol5d[:] = numpy.array([0] + renderLabels)[renderVol5d]

        logger.info("Finished updating 3D volume data")
        self.thunkEventHandler.post(self._refreshRenderMgr)

    @threadRouted
    def _refreshRenderMgr(self):
        """
        The render mgr can segfault if this isn't called from the main thread.
        """
        logger.info("Begin render update")
        self._renderMgr.update()
        logger.info("End render update")

    def setupLayers(self):
        def findLayer(f, layerlist):
            for l in layerlist:
                if f(l):
                    return l
            return None

        layers = []
        baseCarvingLayers = super(SplitBodyCarvingGui, self).setupLayers()

        crosshairSlot = self.topLevelOperatorView.AnnotationCrosshairs
        if crosshairSlot.ready():
            # 0=Transparent, 1=pink
            colortable = [
                QColor(0, 0, 0, 0).rgba(),
                QColor(236, 184, 201).rgba()
            ]
            crosshairLayer = ColortableLayer(LazyflowSource(crosshairSlot),
                                             colortable,
                                             direct=True)
            crosshairLayer.name = "Annotation Points"
            crosshairLayer.visible = True
            crosshairLayer.opacity = 1.0
            layers.append(crosshairLayer)

        highlightedObjectSlot = self.topLevelOperatorView.CurrentRavelerObject
        if highlightedObjectSlot.ready():
            # 0=Transparent, 1=blue
            colortable = [QColor(0, 0, 0, 0).rgba(), QColor(0, 0, 255).rgba()]
            highlightedObjectLayer = ColortableLayer(
                LazyflowSource(highlightedObjectSlot), colortable, direct=True)
            highlightedObjectLayer.name = "Current Raveler Object"
            highlightedObjectLayer.visible = False
            highlightedObjectLayer.opacity = 0.25
            layers.append(highlightedObjectLayer)

        remainingRavelerObjectSlot = self.topLevelOperatorView.CurrentRavelerObjectRemainder
        if remainingRavelerObjectSlot.ready():
            # 0=Transparent, 1=blue
            colortable = [QColor(0, 0, 0, 0).rgba(), QColor(255, 0, 0).rgba()]
            remainingObjectLayer = ColortableLayer(
                LazyflowSource(remainingRavelerObjectSlot),
                colortable,
                direct=True)
            remainingObjectLayer.name = "Remaining Raveler Object"
            remainingObjectLayer.visible = True
            remainingObjectLayer.opacity = 0.25
            layers.append(remainingObjectLayer)

        fragmentSegSlot = self.topLevelOperatorView.CurrentFragmentSegmentation
        if fragmentSegSlot.ready():
            colortable = map(QColor.rgba, self._fragmentColors)
            fragSegLayer = ColortableLayer(LazyflowSource(fragmentSegSlot),
                                           colortable,
                                           direct=True)
            fragSegLayer.name = "Saved Fragments"
            fragSegLayer.visible = True
            fragSegLayer.opacity = 0.25
            layers.append(fragSegLayer)

        ravelerLabelsSlot = self.topLevelOperatorView.RavelerLabels
        if ravelerLabelsSlot.ready():
            colortable = []
            for i in range(256):
                r, g, b = numpy.random.randint(0, 255), numpy.random.randint(
                    0, 255), numpy.random.randint(0, 255)
                colortable.append(QColor(r, g, b).rgba())
            ravelerLabelLayer = ColortableLayer(
                LazyflowSource(ravelerLabelsSlot), colortable, direct=True)
            ravelerLabelLayer.name = "Raveler Labels"
            ravelerLabelLayer.visible = False
            ravelerLabelLayer.opacity = 0.4
            layers.append(ravelerLabelLayer)

        maskedSegSlot = self.topLevelOperatorView.MaskedSegmentation
        if maskedSegSlot.ready():
            colortable = [
                QColor(0, 0, 0, 0).rgba(),
                QColor(0, 0, 0, 0).rgba(),
                QColor(0, 255, 0).rgba()
            ]
            maskedSegLayer = ColortableLayer(LazyflowSource(maskedSegSlot),
                                             colortable,
                                             direct=True)
            maskedSegLayer.name = "Masked Segmentation"
            maskedSegLayer.visible = True
            maskedSegLayer.opacity = 0.3
            layers.append(maskedSegLayer)

            # Hide the original carving segmentation.
            # TODO: Remove it from the list altogether?
            carvingSeg = findLayer(lambda l: l.name == "segmentation",
                                   baseCarvingLayers)
            if carvingSeg is not None:
                carvingSeg.visible = False

        def removeBaseLayer(layerName):
            layer = findLayer(lambda l: l.name == layerName, baseCarvingLayers)
            if layer:
                baseCarvingLayers.remove(layer)

        # Don't show carving layers that aren't relevant to the split-body workflow
        removeBaseLayer("Uncertainty")
        removeBaseLayer("Segmentation")
        removeBaseLayer("Completed segments (unicolor)")
        #removeBaseLayer( "pmap" )
        #removeBaseLayer( "hints" )
        #removeBaseLayer( "done" )
        #removeBaseLayer( "done" )

        ActionInfo = ShortcutManager.ActionInfo

        # Attach a shortcut to the raw data layer
        if self.topLevelOperatorView.RawData.ready():
            rawLayer = findLayer(lambda l: l.name == "Raw Data",
                                 baseCarvingLayers)
            assert rawLayer is not None, "Couldn't find the raw data layer.  Did it's name change?"
            rawLayer.shortcutRegistration = (
                "f",
                ActionInfo("Carving", "Raw Data to Top", "Raw Data to Top",
                           partial(self._toggleRawDataPosition, rawLayer),
                           self.viewerControlWidget(), rawLayer))
        layers += baseCarvingLayers
        return layers

    def _toggleRawDataPosition(self, rawLayer):
        index = self.layerstack.layerIndex(rawLayer)
        self.layerstack.selectRow(index)
        if index <= 2:
            self.layerstack.moveSelectedToBottom()
        else:
            # Move it almost to the top (under the seeds and the annotation crosshairs)
            self.layerstack.moveSelectedToRow(2)
class IlastikShell( QMainWindow ):
    """
    The GUI's main window.  Simply a standard 'container' GUI for one or more applets.
    """


    def __init__( self, workflow = [], parent = None, flags = QtCore.Qt.WindowFlags(0), sideSplitterSizePolicy=SideSplitterSizePolicy.Manual ):
        QMainWindow.__init__(self, parent = parent, flags = flags )
        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._sideSplitterSizePolicy = sideSplitterSizePolicy

        self.projectManager = ProjectManager()
        
        import inspect, os
        ilastikShellFilePath = os.path.dirname(inspect.getfile(inspect.currentframe()))
        uic.loadUi( ilastikShellFilePath + "/ui/ilastikShell.ui", self )
        self._applets = []
        self.appletBarMapping = {}

        self.setAttribute(Qt.WA_AlwaysShowToolTips)
        
        if 'Ubuntu' in platform.platform():
            # Native menus are prettier, but aren't working on Ubuntu at this time (Qt 4.7, Ubuntu 11)
            self.menuBar().setNativeMenuBar(False)

        (self._projectMenu, self._shellActions) = self._createProjectMenu()
        self._settingsMenu = self._createSettingsMenu()
        self.menuBar().addMenu( self._projectMenu )
        self.menuBar().addMenu( self._settingsMenu )

        self.updateShellProjectDisplay()
        
        self.progressDisplayManager = ProgressDisplayManager(self.statusBar)

        for applet in workflow:
            self.addApplet(applet)

        self.appletBar.expanded.connect(self.handleAppleBarItemExpanded)
        self.appletBar.clicked.connect(self.handleAppletBarClick)
        self.appletBar.setVerticalScrollMode( QAbstractItemView.ScrollPerPixel )
        
        # By default, make the splitter control expose a reasonable width of the applet bar
        self.mainSplitter.setSizes([300,1])
        
        self.currentAppletIndex = 0

        self.currentImageIndex = -1
        self.populatingImageSelectionCombo = False
        self.imageSelectionCombo.currentIndexChanged.connect( self.changeCurrentInputImageIndex )
        
        self.enableWorkflow = False # Global mask applied to all applets
        self._controlCmds = []      # Track the control commands that have been issued by each applet so they can be popped.
        self._disableCounts = []    # Controls for each applet can be disabled by his peers.
                                    # No applet can be enabled unless his disableCount == 0

        
    def _createProjectMenu(self):
        # Create a menu for "General" (non-applet) actions
        menu = QMenu("&Project", self)

        shellActions = ShellActions()

        # Menu item: New Project
        shellActions.newProjectAction = menu.addAction("&New Project...")
        shellActions.newProjectAction.setShortcuts( QKeySequence.New )
        shellActions.newProjectAction.triggered.connect(self.onNewProjectActionTriggered)

        # Menu item: Open Project 
        shellActions.openProjectAction = menu.addAction("&Open Project...")
        shellActions.openProjectAction.setShortcuts( QKeySequence.Open )
        shellActions.openProjectAction.triggered.connect(self.onOpenProjectActionTriggered)

        # Menu item: Save Project
        shellActions.saveProjectAction = menu.addAction("&Save Project")
        shellActions.saveProjectAction.setShortcuts( QKeySequence.Save )
        shellActions.saveProjectAction.triggered.connect(self.onSaveProjectActionTriggered)

        # Menu item: Save Project As
        shellActions.saveProjectAsAction = menu.addAction("&Save Project As...")
        shellActions.saveProjectAsAction.setShortcuts( QKeySequence.SaveAs )
        shellActions.saveProjectAsAction.triggered.connect(self.onSaveProjectAsActionTriggered)

        # Menu item: Save Project Snapshot
        shellActions.saveProjectSnapshotAction = menu.addAction("&Take Snapshot...")
        shellActions.saveProjectSnapshotAction.triggered.connect(self.onSaveProjectSnapshotActionTriggered)

        # Menu item: Import Project
        shellActions.importProjectAction = menu.addAction("&Import Project...")
        shellActions.importProjectAction.triggered.connect(self.onImportProjectActionTriggered)

        # Menu item: Quit
        shellActions.quitAction = menu.addAction("&Quit")
        shellActions.quitAction.setShortcuts( QKeySequence.Quit )
        shellActions.quitAction.triggered.connect(self.onQuitActionTriggered)
        shellActions.quitAction.setShortcut( QKeySequence.Quit )
        
        return (menu, shellActions)
    
    def _createSettingsMenu(self):
        menu = QMenu("&Settings", self)
        # Menu item: Keyboard Shortcuts

        def editShortcuts():
            mgrDlg = ShortcutManagerDlg(self)
        shortcutsAction = menu.addAction("&Keyboard Shortcuts")
        shortcutsAction.triggered.connect(editShortcuts)
        
        return menu
    
    def show(self):
        """
        Show the window, and enable/disable controls depending on whether or not a project file present.
        """
        super(IlastikShell, self).show()
        self.enableWorkflow = (self.projectManager.currentProjectFile is not None)
        self.updateAppletControlStates()
        self.updateShellProjectDisplay()
        if self._sideSplitterSizePolicy == SideSplitterSizePolicy.Manual:
            self.autoSizeSideSplitter( SideSplitterSizePolicy.AutoLargestDrawer )
        else:
            self.autoSizeSideSplitter( SideSplitterSizePolicy.AutoCurrentDrawer )

    def updateShellProjectDisplay(self):
        """
        Update the title bar and allowable shell actions based on the state of the currently loaded project.
        """
        windowTitle = "ilastik - "
        projectPath = self.projectManager.currentProjectPath
        if projectPath is None:
            windowTitle += "No Project Loaded"
        else:
            windowTitle += projectPath

        readOnly = self.projectManager.currentProjectIsReadOnly
        if readOnly:
            windowTitle += " [Read Only]"

        self.setWindowTitle(windowTitle)        

        # Enable/Disable menu items
        projectIsOpen = self.projectManager.currentProjectFile is not None
        self._shellActions.saveProjectAction.setEnabled(projectIsOpen and not readOnly) # Can't save a read-only project
        self._shellActions.saveProjectAsAction.setEnabled(projectIsOpen)
        self._shellActions.saveProjectSnapshotAction.setEnabled(projectIsOpen)

    def setImageNameListSlot(self, multiSlot):
        assert multiSlot.level == 1
        self.imageNamesSlot = multiSlot
        
        def insertImageName( index, slot ):
            self.imageSelectionCombo.setItemText( index, slot.value )
            if self.currentImageIndex == -1:
                self.changeCurrentInputImageIndex(index)

        def handleImageNameSlotInsertion(multislot, index):
            assert multislot == self.imageNamesSlot
            self.populatingImageSelectionCombo = True
            self.imageSelectionCombo.insertItem(index, "uninitialized")
            self.populatingImageSelectionCombo = False
            multislot[index].notifyDirty( bind( insertImageName, index) )

        multiSlot.notifyInserted( bind(handleImageNameSlotInsertion) )

        def handleImageNameSlotRemoval(multislot, index):
            # Simply remove the combo entry, which causes the currentIndexChanged signal to fire if necessary.
            self.imageSelectionCombo.removeItem(index)
            if len(multislot) == 0:
                self.changeCurrentInputImageIndex(-1)
        multiSlot.notifyRemove( bind(handleImageNameSlotRemoval) )

    def changeCurrentInputImageIndex(self, newImageIndex):
        if newImageIndex != self.currentImageIndex \
        and self.populatingImageSelectionCombo == False:
            if newImageIndex != -1:
                try:
                    # Accessing the image name value will throw if it isn't properly initialized
                    self.imageNamesSlot[newImageIndex].value
                except:
                    # Revert to the original image index.
                    if self.currentImageIndex != -1:
                        self.imageSelectionCombo.setCurrentIndex(self.currentImageIndex)
                    return

            # Alert each central widget and viewer control widget that the image selection changed
            for i in range( len(self._applets) ):
                self._applets[i].gui.setImageIndex(newImageIndex)
                
            self.currentImageIndex = newImageIndex


    def handleAppleBarItemExpanded(self, modelIndex):
        """
        The user wants to view a different applet bar item.
        """
        drawerIndex = modelIndex.row()
        self.setSelectedAppletDrawer(drawerIndex)
    
    def setSelectedAppletDrawer(self, drawerIndex):
        """
        Show the correct applet central widget, viewer control widget, and applet drawer widget for this drawer index.
        """
        if self.currentAppletIndex != drawerIndex:
            self.currentAppletIndex = drawerIndex
            # Collapse all drawers in the applet bar...
            self.appletBar.collapseAll()
            # ...except for the newly selected item.
            self.appletBar.expand( self.getModelIndexFromDrawerIndex(drawerIndex) )
            
            if len(self.appletBarMapping) != 0:
                # Determine which applet this drawer belongs to
                assert drawerIndex in self.appletBarMapping
                applet_index = self.appletBarMapping[drawerIndex]

                # Select the appropriate central widget, menu widget, and viewer control widget for this applet
                self.appletStack.setCurrentIndex(applet_index)
                self.viewerControlStack.setCurrentIndex(applet_index)
                self.menuBar().clear()
                self.menuBar().addMenu(self._projectMenu)
                self.menuBar().addMenu(self._settingsMenu)
                for m in self._applets[applet_index].gui.menus():
                    self.menuBar().addMenu(m)
                
                self.autoSizeSideSplitter( self._sideSplitterSizePolicy )

    def getModelIndexFromDrawerIndex(self, drawerIndex):
        drawerTitleItem = self.appletBar.invisibleRootItem().child(drawerIndex)
        return self.appletBar.indexFromItem(drawerTitleItem)
                
    def autoSizeSideSplitter(self, sizePolicy):
        if sizePolicy == SideSplitterSizePolicy.Manual:
            # In manual mode, don't resize the splitter at all.
            return
        
        if sizePolicy == SideSplitterSizePolicy.AutoCurrentDrawer:
            # Get the height of the current applet drawer
            rootItem = self.appletBar.invisibleRootItem()
            appletDrawerItem = rootItem.child(self.currentAppletIndex).child(0)
            appletDrawerWidget = self.appletBar.itemWidget(appletDrawerItem, 0)
            appletDrawerHeight = appletDrawerWidget.frameSize().height()

        if sizePolicy == SideSplitterSizePolicy.AutoLargestDrawer:
            appletDrawerHeight = 0
            # Get the height of the largest drawer in the bar
            for drawerIndex in range( len(self.appletBarMapping) ):
                rootItem = self.appletBar.invisibleRootItem()
                appletDrawerItem = rootItem.child(drawerIndex).child(0)
                appletDrawerWidget = self.appletBar.itemWidget(appletDrawerItem, 0)
                appletDrawerHeight = max( appletDrawerHeight, appletDrawerWidget.frameSize().height() )
        
        # Get total height of the titles in the applet bar (not the widgets)
        firstItem = self.appletBar.invisibleRootItem().child(0)
        titleHeight = self.appletBar.visualItemRect(firstItem).size().height()
        numDrawers = len(self.appletBarMapping)
        totalTitleHeight = numDrawers * titleHeight    
    
        # Auto-size the splitter height based on the height of the applet bar.
        totalSplitterHeight = sum(self.sideSplitter.sizes())
        appletBarHeight = totalTitleHeight + appletDrawerHeight + 10 # Add a small margin so the scroll bar doesn't appear
        self.sideSplitter.setSizes([appletBarHeight, totalSplitterHeight-appletBarHeight])

    def handleAppletBarClick(self, modelIndex):
        # If the user clicks on a top-level item, automatically expand it.
        if modelIndex.parent() == self.appletBar.rootIndex():
            self.appletBar.expand(modelIndex)
        else:
            self.appletBar.setCurrentIndex( modelIndex.parent() )

    def addApplet( self, app ):
        assert isinstance( app, Applet ), "Applets must inherit from Applet base class."
        assert app.base_initialized, "Applets must call Applet.__init__ upon construction."

        assert issubclass( type(app.gui), AppletGuiInterface ), "Applet GUIs must conform to the Applet GUI interface."
        
        self._applets.append(app)
        applet_index = len(self._applets) - 1
        self.appletStack.addWidget( app.gui.centralWidget() )
        
        # Viewer controls are optional. If the applet didn't provide one, create an empty widget for him.
        if app.gui.viewerControlWidget() is None:
            self.viewerControlStack.addWidget( QWidget(parent=self) )
        else:
            self.viewerControlStack.addWidget( app.gui.viewerControlWidget() )

        # Add rows to the applet bar model
        rootItem = self.appletBar.invisibleRootItem()

        # Add all of the applet bar's items to the toolbox widget
        for controlName, controlGuiItem in app.gui.appletDrawers():
            appletNameItem = QTreeWidgetItem( self.appletBar, QtCore.QStringList( controlName ) )
            appletNameItem.setFont( 0, QFont("Ubuntu", 14) )
            drawerItem = QTreeWidgetItem(appletNameItem)
            drawerItem.setSizeHint( 0, controlGuiItem.frameSize() )
#            drawerItem.setBackground( 0, QBrush( QColor(224, 224, 224) ) )
#            drawerItem.setForeground( 0, QBrush( QColor(0,0,0) ) )
            self.appletBar.setItemWidget( drawerItem, 0, controlGuiItem )

            # Since each applet can contribute more than one applet bar item,
            #  we need to keep track of which applet this item is associated with
            self.appletBarMapping[rootItem.childCount()-1] = applet_index

        # Set up handling of GUI commands from this applet
        app.guiControlSignal.connect( bind(self.handleAppletGuiControlSignal, applet_index) )
        self._disableCounts.append(0)
        self._controlCmds.append( [] )

        # Set up handling of progress updates from this applet
        self.progressDisplayManager.addApplet(applet_index, app)
        
        # Set up handling of shell requests from this applet
        app.shellRequestSignal.connect( partial(self.handleShellRequest, applet_index) )

        self.projectManager.addApplet(app)
                
        return applet_index

    def handleAppletGuiControlSignal(self, applet_index, command=ControlCommand.DisableAll):
        """
        Applets fire a signal when they want other applet GUIs to be disabled.
        This function handles the signal.
        Each signal is treated as a command to disable other applets.
        A special command, Pop, undoes the applet's most recent command (i.e. re-enables the applets that were disabled).
        If an applet is disabled twice (e.g. by two different applets), then it won't become enabled again until both commands have been popped.
        """
        if command == ControlCommand.Pop:
            command = self._controlCmds[applet_index].pop()
            step = -1 # Since we're popping this command, we'll subtract from the disable counts
        else:
            step = 1
            self._controlCmds[applet_index].append( command ) # Push command onto the stack so we can pop it off when the applet isn't busy any more

        # Increase the disable count for each applet that is affected by this command.
        for index, count in enumerate(self._disableCounts):
            if (command == ControlCommand.DisableAll) \
            or (command == ControlCommand.DisableDownstream and index > applet_index) \
            or (command == ControlCommand.DisableUpstream and index < applet_index) \
            or (command == ControlCommand.DisableSelf and index == applet_index):
                self._disableCounts[index] += step

        # Update the control states in the GUI thread
        self.thunkEventHandler.post( self.updateAppletControlStates )

    def handleShellRequest(self, applet_index, requestAction):
        """
        An applet is asking us to do something.  Handle the request.
        """
        with Tracer(traceLogger):
            if requestAction == ShellRequest.RequestSave:
                # Call the handler directly to ensure this is a synchronous call (not queued to the GUI thread)
                self.projectManager.saveProject()

    def __len__( self ):
        return self.appletBar.count()

    def __getitem__( self, index ):
        return self._applets[index]
    
    def ensureNoCurrentProject(self, assertClean=False):
        """
        Close the current project.  If it's dirty, we ask the user for confirmation.
        
        The assertClean parameter is for tests.  Setting it to True will raise an assertion if the project was dirty.
        """
        closeProject = True
        if self.projectManager.isProjectDataDirty():
            # Testing assertion
            assert not assertClean, "Expected a clean project but found it to be dirty!"

            message = "Your current project is about to be closed, but it has unsaved changes which will be lost.\n"
            message += "Are you sure you want to proceed?"
            buttons = QMessageBox.Yes | QMessageBox.Cancel
            response = QMessageBox.warning(self, "Discard unsaved changes?", message, buttons, defaultButton=QMessageBox.Cancel)
            closeProject = (response == QMessageBox.Yes)
            

        if closeProject:
            self.closeCurrentProject()

        return closeProject

    def closeCurrentProject(self):
        for applet in self._applets:
            applet.gui.reset()
        self.projectManager.closeCurrentProject()
        self.enableWorkflow = False
        self.updateAppletControlStates()
        self.updateShellProjectDisplay()
    
    def onNewProjectActionTriggered(self):
        logger.debug("New Project action triggered")
        
        # Make sure the user is finished with the currently open project
        if not self.ensureNoCurrentProject():
            return
        
        newProjectFilePath = self.getProjectPathToCreate()

        if newProjectFilePath is not None:
            self.createAndLoadNewProject(newProjectFilePath)

    def createAndLoadNewProject(self, newProjectFilePath):
        newProjectFile = self.projectManager.createBlankProjectFile(newProjectFilePath)
        self.loadProject(newProjectFile, newProjectFilePath, False)
    
    def getProjectPathToCreate(self, defaultPath=None, caption="Create Ilastik Project"):
        """
        Ask the user where he would like to create a project file.
        """
        if defaultPath is None:
            defaultPath = os.path.expanduser("~/MyProject.ilp")
        
        fileSelected = False
        while not fileSelected:
            projectFilePath = QFileDialog.getSaveFileName(
               self, caption, defaultPath, "Ilastik project files (*.ilp)",
               options=QFileDialog.Options(QFileDialog.DontUseNativeDialog))
            
            # If the user cancelled, stop now
            if projectFilePath.isNull():
                return None
    
            projectFilePath = str(projectFilePath)
            fileSelected = True
            
            # Add extension if necessary
            fileExtension = os.path.splitext(projectFilePath)[1].lower()
            if fileExtension != '.ilp':
                projectFilePath += ".ilp"
                if os.path.exists(projectFilePath):
                    # Since we changed the file path, we need to re-check if we're overwriting an existing file.
                    message = "A file named '" + projectFilePath + "' already exists in this location.\n"
                    message += "Are you sure you want to overwrite it?"
                    buttons = QMessageBox.Yes | QMessageBox.Cancel
                    response = QMessageBox.warning(self, "Overwrite existing project?", message, buttons, defaultButton=QMessageBox.Cancel)
                    if response == QMessageBox.Cancel:
                        # Try again...
                        fileSelected = False

        return projectFilePath
    
    def onImportProjectActionTriggered(self):
        """
        Import an existing project into a new file.
        This involves opening the old file, saving it to a new file, and then opening the new file.
        """
        logger.debug("Import Project Action")

        if not self.ensureNoCurrentProject():
            return

        # Find the directory of the most recently *imported* project
        mostRecentImportPath = PreferencesManager().get( 'shell', 'recently imported' )
        if mostRecentImportPath is not None:
            defaultDirectory = os.path.split(mostRecentImportPath)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Select the paths to the ilp to import and the name of the new one we'll create
        importedFilePath = self.getProjectPathToOpen(defaultDirectory)
        if importedFilePath is not None:
            PreferencesManager().set('shell', 'recently imported', importedFilePath)
            defaultFile, ext = os.path.splitext(importedFilePath)
            defaultFile += "_imported"
            defaultFile += ext
            newProjectFilePath = self.getProjectPathToCreate(defaultFile)

        # If the user didn't cancel
        if importedFilePath is not None and newProjectFilePath is not None:
            self.importProject( importedFilePath, newProjectFilePath )

    def importProject(self, originalPath, newProjectFilePath):
        newProjectFile = self.projectManager.createBlankProjectFile(newProjectFilePath)
        self.projectManager.importProject(originalPath, newProjectFile, newProjectFilePath)

        self.updateShellProjectDisplay()

        # Enable all the applet controls
        self.enableWorkflow = True
        self.updateAppletControlStates()
        
    def getProjectPathToOpen(self, defaultDirectory):
        """
        Return the path of the project the user wants to open (or None if he cancels).
        """
        projectFilePath = QFileDialog.getOpenFileName(
           self, "Open Ilastik Project", defaultDirectory, "Ilastik project files (*.ilp)",
           options=QFileDialog.Options(QFileDialog.DontUseNativeDialog))

        # If the user canceled, stop now        
        if projectFilePath.isNull():
            return None

        return str(projectFilePath)

    def onOpenProjectActionTriggered(self):
        logger.debug("Open Project action triggered")
        
        # Make sure the user is finished with the currently open project
        if not self.ensureNoCurrentProject():
            return

        # Find the directory of the most recently opened project
        mostRecentProjectPath = PreferencesManager().get( 'shell', 'recently opened' )
        if mostRecentProjectPath is not None:
            defaultDirectory = os.path.split(mostRecentProjectPath)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        projectFilePath = self.getProjectPathToOpen(defaultDirectory)
        if projectFilePath is not None:
            PreferencesManager().set('shell', 'recently opened', projectFilePath)
            self.openProjectFile(projectFilePath)
    
    def openProjectFile(self, projectFilePath):
        try:
            hdf5File, readOnly = self.projectManager.openProjectFile(projectFilePath)
        except ProjectManager.ProjectVersionError,e:
            QMessageBox.warning(self, "Old Project", "Could not load old project file: " + projectFilePath + ".\nPlease try 'Import Project' instead.")
        except ProjectManager.FileMissingError:
            QMessageBox.warning(self, "Missing File", "Could not find project file: " + projectFilePath)
class LabelingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer 
    applet with the added functionality of labeling.
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawers(self):
        return [ ("Label Marking", self._labelControlUi) ]

    def reset(self):
        # Clear the label list GUI
        self.clearLabelListGui()
        
        # Start in navigation mode (not painting)
        self.changeInteractionMode(Tool.Navigation)

    def setImageIndex(self, index):
        super(LabelingGui, self).setImageIndex(index)
        
        # Reset the GUI for "labels allowed" status
        self.changeInteractionMode(self._toolId)

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

    @property
    def minLabelNumber(self):
        return self._minLabelNumber
    @minLabelNumber.setter
    def minLabelNumber(self, n):
        self._minLabelNumer = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self.addNewLabel()
    @property
    def maxLabelNumber(self):
        return self._maxLabelNumber
    @maxLabelNumber.setter
    def maxLabelNumber(self, n):
        self._maxLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self.removeLastLabel()

    @property
    def labelingDrawerUi(self):
        return self._labelControlUi
    
    @property
    def labelListData(self):
        return self._labelControlUi.labelListModel
    
    def selectLabel(self, labelIndex):
        """Programmatically select the given labelIndex, which start from 0.
           Equivalent to clicking on the (labelIndex+1)'th position in the label widget."""
        self._labelControlUi.labelListModel.select(labelIndex)
    
    class LabelingSlots(object):
        def __init__(self):
            # Label slots are multi (level=1) and accessed as shown.
            # Slot to insert labels onto
            self.labelInput = None # labelInput[image_index].setInSlot(xxx)
            # Slot to read labels from 
            self.labelOutput = None # labelOutput[image_index].get(roi)            
            # Slot that determines which label value corresponds to erased values
            self.labelEraserValue = None # labelEraserValue.setValue(xxx) 
            # Slot that is used to request wholesale label deletion
            self.labelDelete = None # labelDelete.setValue(xxx)
            # Slot that contains the maximum label value (for all images)
            self.maxLabelValue = None # maxLabelValue.value
            
            # Slot to specify which images the user is allowed to label.
            self.labelsAllowed = None # labelsAllowed[image_index].value == True

    @traceLogged(traceLogger)
    def __init__(self, labelingSlots, topLevelOperator, drawerUiPath=None, rawInputSlot=None ):
        """
        See LabelingSlots class (above) for expected type of labelingSlots parameter.
        
        observedSlots is the same as in the LayerViewer constructor.
        drawerUiPath can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        Data from the rawInputSlot parameter will be displayed directly underneatch the labels (if provided).
        """
        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert all( [v is not None for v in labelingSlots.__dict__.values()] )
       
        self._minLabelNumber = 0
        self._maxLabelNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot
        
        # Init base class
        super(LabelingGui, self).__init__( topLevelOperator )

        self._labelingSlots = labelingSlots
        self._labelingSlots.labelEraserValue.setValue(self.editor.brushingModel.erasingNumber)
        self._labelingSlots.maxLabelValue.notifyDirty( bind(self.updateLabelList) )

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingLabels = False
        
        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self.initLabelUic(drawerUiPath)
        
        self.changeInteractionMode(Tool.Navigation)
        
        self.__initShortcuts()

    @traceLogged(traceLogger)
    def initLabelUic(self, drawerUiPath):
        _labelControlUi = uic.loadUi(drawerUiPath)

        # We own the applet bar ui
        self._labelControlUi = _labelControlUi

        # Initialize the label list model
        model = LabelListModel()
        _labelControlUi.labelListView.setModel(model)
        _labelControlUi.labelListModel=model
        _labelControlUi.labelListModel.rowsRemoved.connect(self.onLabelRemoved)
        _labelControlUi.labelListModel.labelSelected.connect(self.onLabelSelected)
        
        @traceLogged(traceLogger)
        def onDataChanged(topLeft, bottomRight):
            """Handle changes to the label list selections."""
            firstRow = topLeft.row()
            lastRow  = bottomRight.row()
        
            firstCol = topLeft.column()
            lastCol  = bottomRight.column()
            
            if lastCol == firstCol == 0:
                assert(firstRow == lastRow) #only one data item changes at a time

                #in this case, the actual data (for example color) has changed
                color = _labelControlUi.labelListModel[firstRow].color
                self._colorTable16[firstRow+1] = color.rgba()
                self.editor.brushingModel.setBrushColor(color)
                
                # Update the label layer colortable to match the list entry
                labellayer = self.getLabelLayer()
                labellayer.colorTable = self._colorTable16                
            else:
                #this column is used for the 'delete' buttons, we don't care
                #about data changed here
                pass

        # Connect Applet GUI to our event handlers
        _labelControlUi.AddLabelButton.clicked.connect( bind(self.addNewLabel) )
        _labelControlUi.labelListModel.dataChanged.connect(onDataChanged)
        
        # Initialize the arrow tool button with an icon and handler
        iconPath = os.path.split(__file__)[0] + "/icons/arrow.jpg"
        arrowIcon = QIcon(iconPath)
        _labelControlUi.arrowToolButton.setIcon(arrowIcon)
        _labelControlUi.arrowToolButton.setCheckable(True)
        _labelControlUi.arrowToolButton.clicked.connect( lambda checked: self.handleToolButtonClicked(checked, Tool.Navigation) )

        # Initialize the paint tool button with an icon and handler
        paintBrushIconPath = os.path.split(__file__)[0] + "/icons/paintbrush.png"
        paintBrushIcon = QIcon(paintBrushIconPath)
        _labelControlUi.paintToolButton.setIcon(paintBrushIcon)
        _labelControlUi.paintToolButton.setCheckable(True)
        _labelControlUi.paintToolButton.clicked.connect( lambda checked: self.handleToolButtonClicked(checked, Tool.Paint) )

        # Initialize the erase tool button with an icon and handler
        eraserIconPath = os.path.split(__file__)[0] + "/icons/eraser.png"
        eraserIcon = QIcon(eraserIconPath)
        _labelControlUi.eraserToolButton.setIcon(eraserIcon)
        _labelControlUi.eraserToolButton.setCheckable(True)
        _labelControlUi.eraserToolButton.clicked.connect( lambda checked: self.handleToolButtonClicked(checked, Tool.Erase) )
        
        # This maps tool types to the buttons that enable them
        self.toolButtons = { Tool.Navigation : _labelControlUi.arrowToolButton,
                             Tool.Paint      : _labelControlUi.paintToolButton,
                             Tool.Erase      : _labelControlUi.eraserToolButton }
        
        self.brushSizes = [ (1,  ""),
                            (3,  "Tiny"),
                            (5,  "Small"),
                            (7,  "Medium"),
                            (11, "Large"),
                            (23, "Huge"),
                            (31, "Megahuge"),
                            (61, "Gigahuge") ]

        for size, name in self.brushSizes:
            _labelControlUi.brushSizeComboBox.addItem( str(size) + " " + name )
        
        _labelControlUi.brushSizeComboBox.currentIndexChanged.connect(self.onBrushSizeChange)

        self.paintBrushSizeIndex = PreferencesManager().get( 'labeling', 'paint brush size', default=0 )
        self.eraserSizeIndex = PreferencesManager().get( 'labeling', 'eraser brush size', default=4 )
        
    def __initShortcuts(self):
        mgr = ShortcutManager()
        shortcutGroupName = "Labeling"

        addLabel = QShortcut( QKeySequence("a"), self, member=self.labelingDrawerUi.AddLabelButton.click )
        mgr.register( shortcutGroupName,
                      "Add New Label Class",
                      addLabel,
                      self.labelingDrawerUi.AddLabelButton )

        navMode = QShortcut( QKeySequence("n"), self, member=self.labelingDrawerUi.arrowToolButton.click )
        mgr.register( shortcutGroupName,
                      "Navigation Cursor",
                      navMode,
                      self.labelingDrawerUi.arrowToolButton )

        brushMode = QShortcut( QKeySequence("b"), self, member=self.labelingDrawerUi.paintToolButton.click )
        mgr.register( shortcutGroupName,
                      "Brush Cursor",
                      brushMode,
                      self.labelingDrawerUi.paintToolButton )

        eraserMode = QShortcut( QKeySequence("e"), self, member=self.labelingDrawerUi.eraserToolButton.click )
        mgr.register( shortcutGroupName,
                      "Eraser Cursor",
                      eraserMode,
                      self.labelingDrawerUi.eraserToolButton )

        changeBrushSize = QShortcut( QKeySequence("c"), self, member=self.labelingDrawerUi.brushSizeComboBox.showPopup )
        mgr.register( shortcutGroupName,
                      "Change Brush Size",
                      changeBrushSize,
                      self.labelingDrawerUi.brushSizeComboBox )


        self._labelShortcuts = []

    def _updateLabelShortcuts(self):
        numShortcuts = len(self._labelShortcuts)
        numRows = len(self._labelControlUi.labelListModel)

        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts,numRows):
            shortcut = QShortcut( QKeySequence(str(i+1)),
                                  self,
                                  member=partial(self._labelControlUi.labelListView.selectRow, i) )
            self._labelShortcuts.append(shortcut)
            toolTipObject = LabelListModel.EntryToolTipAdapter(self._labelControlUi.labelListModel, i)
            ShortcutManager().register("Labeling", "", shortcut, toolTipObject)

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            shortcut = self._labelShortcuts[i]
            description = "Select " + self._labelControlUi.labelListModel[i].name
            ShortcutManager().setDescription(shortcut, description)

    def hideEvent(self, event):
        """
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        with PreferencesManager() as prefsMgr:
            prefsMgr.set('labeling', 'paint brush size', self.paintBrushSizeIndex)
            prefsMgr.set('labeling', 'eraser brush size', self.eraserSizeIndex)
        super(LabelingGui, self).hideEvent(event)

    @traceLogged(traceLogger)
    def handleToolButtonClicked(self, checked, toolId):
        """
        Called when the user clicks any of the "tool" buttons in the label applet bar GUI.
        """
        if not checked:
            # Users can only *switch between* tools, not turn them off.
            # If they try to turn a button off, re-select it automatically.
            self.toolButtons[toolId].setChecked(True)
        else:
            # If the user is checking a new button
            self.changeInteractionMode( toolId )

    @threadRouted
    @traceLogged(traceLogger)
    def changeInteractionMode( self, toolId ):
        """
        Implement the GUI's response to the user selecting a new tool.
        """
        # Uncheck all the other buttons
        for tool, button in self.toolButtons.items():
            if tool != toolId:
                button.setChecked(False)

        # If we have no editor, we can't do anything yet
        if self.editor is None:
            return
        
        # The volume editor expects one of two specific names
        modeNames = { Tool.Navigation   : "navigation",
                      Tool.Paint        : "brushing",
                      Tool.Erase        : "brushing" }

        # Hide everything by default
        self._labelControlUi.arrowToolButton.hide()
        self._labelControlUi.paintToolButton.hide()
        self._labelControlUi.eraserToolButton.hide()
        self._labelControlUi.brushSizeComboBox.hide()
        self._labelControlUi.brushSizeCaption.hide()

        # If the user can't label this image, disable the button and say why its disabled
        labelsAllowed = False
        if 0 <= self.imageIndex < len(self._labelingSlots.labelsAllowed) :
            labelsAllowedSlot = self._labelingSlots.labelsAllowed[self.imageIndex]
            if labelsAllowedSlot.ready():
                labelsAllowed = labelsAllowedSlot.value
    
                self._labelControlUi.AddLabelButton.setEnabled(labelsAllowed and self.maxLabelNumber > self._labelControlUi.labelListModel.rowCount())
                if labelsAllowed:
                    self._labelControlUi.AddLabelButton.setText("Add Label")
                else:
                    self._labelControlUi.AddLabelButton.setText("(Labeling Not Allowed)")

        if self.imageIndex != -1 and labelsAllowed:
            self._labelControlUi.arrowToolButton.show()
            self._labelControlUi.paintToolButton.show()
            self._labelControlUi.eraserToolButton.show()
            # Update the applet bar caption
            if toolId == Tool.Navigation:
                # Make sure the arrow button is pressed
                self._labelControlUi.arrowToolButton.setChecked(True)
                # Hide the brush size control
                self._labelControlUi.brushSizeCaption.hide()
                self._labelControlUi.brushSizeComboBox.hide()
            elif toolId == Tool.Paint:
                # Make sure the paint button is pressed
                self._labelControlUi.paintToolButton.setChecked(True)
                # Show the brush size control and set its caption
                self._labelControlUi.brushSizeCaption.show()
                self._labelControlUi.brushSizeComboBox.show()
                self._labelControlUi.brushSizeCaption.setText("Size:")
                
                # If necessary, tell the brushing model to stop erasing
                if self.editor.brushingModel.erasing:
                    self.editor.brushingModel.disableErasing()
                # Set the brushing size
                brushSize = self.brushSizes[self.paintBrushSizeIndex][0]
                self.editor.brushingModel.setBrushSize(brushSize)
    
                # Make sure the GUI reflects the correct size
                self._labelControlUi.brushSizeComboBox.setCurrentIndex(self.paintBrushSizeIndex)
                
            elif toolId == Tool.Erase:
                # Make sure the erase button is pressed
                self._labelControlUi.eraserToolButton.setChecked(True)
                # Show the brush size control and set its caption
                self._labelControlUi.brushSizeCaption.show()
                self._labelControlUi.brushSizeComboBox.show()
                self._labelControlUi.brushSizeCaption.setText("Size:")
                
                # If necessary, tell the brushing model to start erasing
                if not self.editor.brushingModel.erasing:
                    self.editor.brushingModel.setErasing()
                # Set the brushing size
                eraserSize = self.brushSizes[self.eraserSizeIndex][0]
                self.editor.brushingModel.setBrushSize(eraserSize)
                
                # Make sure the GUI reflects the correct size
                self._labelControlUi.brushSizeComboBox.setCurrentIndex(self.eraserSizeIndex)

        self.editor.setInteractionMode( modeNames[toolId] )
        self._toolId = toolId

    @traceLogged(traceLogger)
    def onBrushSizeChange(self, index):
        """
        Handle the user's new brush size selection.
        Note: The editor's brushing model currently maintains only a single 
              brush size, which is used for both painting and erasing. 
              However, we maintain two different sizes for the user and swap 
              them depending on which tool is selected.
        """
        newSize = self.brushSizes[index][0]
        if self.editor.brushingModel.erasing:
            self.eraserSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)
        else:
            self.paintBrushSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)

    @traceLogged(traceLogger)
    def onLabelSelected(self, row):
        logger.debug("switching to label=%r" % (self._labelControlUi.labelListModel[row]))

        # If the user is selecting a label, he probably wants to be in paint mode
        self.changeInteractionMode(Tool.Paint)

        #+1 because first is transparent
        #FIXME: shouldn't be just row+1 here
        self.editor.brushingModel.setDrawnNumber(row+1)
        self.editor.brushingModel.setBrushColor(self._labelControlUi.labelListModel[row].color)

    @traceLogged(traceLogger)
    def resetLabelSelection(self):
        logger.debug("Resetting label selection")
        if len(self._labelControlUi.labelListModel) > 0:
            self._labelControlUi.labelListView.selectRow(0)
        else:
            self.changeInteractionMode(Tool.Navigation)
        return True
    
    @traceLogged(traceLogger)
    def updateLabelList(self):
        """
        This function is called when the number of labels has changed without our knowledge.
        We need to add/remove labels until we have the right number
        """
        # Get the number of labels in the label data
        # (Or the number of the labels the user has added.)
        numLabels = max(self._labelingSlots.maxLabelValue.value, self._labelControlUi.labelListModel.rowCount())
        if numLabels == None:
            numLabels = 0

        # Add rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() < numLabels:
            self.addNewLabel()
       
        self._labelControlUi.AddLabelButton.setEnabled(numLabels < self.maxLabelNumber)
    
    @traceLogged(traceLogger)
    def addNewLabel(self):
        """
        Add a new label to the label list GUI control.
        Return the new number of labels in the control.
        """
        numLabels = len(self._labelControlUi.labelListModel)
        if numLabels >= len(self._colorTable16)-1:
            # If the color table isn't large enough to handle all our labels,
            #  append a random color
            randomColor = QColor(numpy.random.randint(0,255), numpy.random.randint(0,255), numpy.random.randint(0,255))
            self._colorTable16.append( randomColor.rgba() )

        color = QColor()
        color.setRgba(self._colorTable16[numLabels+1]) # First entry is transparent (for zero label)

        label = Label(self.getNextLabelName(), color)
        label.nameChanged.connect(self._updateLabelShortcuts)
        self._labelControlUi.labelListModel.insertRow( self._labelControlUi.labelListModel.rowCount(), label )
        nlabels = self._labelControlUi.labelListModel.rowCount()

        # Make the new label selected
        selectedRow = nlabels-1
        self._labelControlUi.labelListModel.select(selectedRow)
        
        self._updateLabelShortcuts()

    def getNextLabelName(self):
        maxNum = 0
        for index, label in enumerate(self._labelControlUi.labelListModel):
            nums = re.findall("\d+", label.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Label {}".format(maxNum+1)
    
    @traceLogged(traceLogger)
    def removeLastLabel(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the label list by one.
        """
        self._programmaticallyRemovingLabels = True
        numRows = self._labelControlUi.labelListModel.rowCount()
        # This will trigger the signal that calls onLabelRemoved()
        self._labelControlUi.labelListModel.removeRow(numRows-1)
        self._updateLabelShortcuts()
    
        self._programmaticallyRemovingLabels = False
    
    @traceLogged(traceLogger)
    def clearLabelListGui(self):
        # Remove rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() > 0:
            self.removeLastLabel()

    @traceLogged(traceLogger)
    def onLabelRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingLabels:
            return

        assert start == end
        row = start

        oldcount = self._labelControlUi.labelListModel.rowCount() + 1
        logger.debug("removing label {} out of {}".format( row, oldcount ))

        # Remove the deleted label's color from the color table so that renumbered labels keep their colors.                
        oldColor = self._colorTable16.pop(row+1)
        
        # Recycle the deleted color back into the table (for the next label to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the labellayer colortable with the new color mapping
        labellayer = self.getLabelLayer()
        labellayer.colorTable = self._colorTable16

        currentSelection = self._labelControlUi.labelListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post( self.resetLabelSelection )

        # Changing the deleteLabel input causes the operator (OpBlockedSparseArray)
        #  to search through the entire list of labels and delete the entries for the matching label.
        self._labelingSlots.labelDelete.setValue(row+1)
        
        # We need to "reset" the deleteLabel input to -1 when we're finished.
        #  Otherwise, you can never delete the same label twice in a row.
        #  (Only *changes* to the input are acted upon.)
        self._labelingSlots.labelDelete.setValue(-1)
        
    def getLabelLayer(self):
        # Find the labellayer in the viewer stack
        try:
            labellayer = itertools.ifilter(lambda l: l.name == "Labels", self.layerstack).next()
        except StopIteration:
            raise RuntimeError("Couldn't locate the label layer in the layer stack.  Does it have the expected name?")
        return labellayer

    @traceLogged(traceLogger)
    def createLabelLayer(self, currentImageIndex, direct=False):
        """
        Return a colortable layer that displays the label slot data, along with its associated label source.
        direct: whether this layer is drawn synchronously by volumina
        """
        labelOutput = self._labelingSlots.labelOutput[currentImageIndex]
        if not labelOutput.ready():
            return (None, None)
        else:
            traceLogger.debug("Setting up labels for image index={}".format(currentImageIndex) )
            # Add the layer to draw the labels, but don't add any labels
            labelsrc = LazyflowSinkSource( self._labelingSlots.labelOutput[currentImageIndex],
                                           self._labelingSlots.labelInput[currentImageIndex])
        
            labellayer = ColortableLayer(labelsrc, colorTable = self._colorTable16, direct=direct )
            labellayer.name = "Labels"
            labellayer.ref_object = None
            
            return labellayer, labelsrc

    @traceLogged(traceLogger)
    def setupLayers(self, currentImageIndex):
        """
        Sets up the label layer for display by our base class (layerviewer).
        If our subclass overrides this function to add his own layers,
        he must call this function explicitly.
        """
        layers = []

        # Labels
        labellayer, labelsrc = self.createLabelLayer(currentImageIndex)
        if labellayer is not None:
            layers.append(labellayer)
        
            # Tell the editor where to draw label data
            self.editor.setLabelSink(labelsrc)

        # Side effect 1: We want to guarantee that the label list 
        #  is up-to-date before our subclass adds his layers
        self.updateLabelList()
        
        # Side effect 2: Switch to navigation mode if labels aren't 
        #  allowed on this image.
        labelsAllowedSlot = self._labelingSlots.labelsAllowed[self.imageIndex]
        if labelsAllowedSlot.ready() and not labelsAllowedSlot.value:
            self.changeInteractionMode(Tool.Navigation)

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot[currentImageIndex].ready():
            layer = self.createStandardLayerFromSlot( self._rawInputSlot[currentImageIndex] )
            layer.name = "Raw Input"
            layer.visible = True
            layer.opacity = 1.0
            
            layers.append(layer)

        return layers
        
    @traceLogged(traceLogger)
    def _createDefault16ColorColorTable(self):
        colors = []

        # Transparent for the zero label
        colors.append(QColor(0,0,0,0))

        # ilastik v0.5 colors
        colors.append( QColor( Qt.red ) )
        colors.append( QColor( Qt.green ) )
        colors.append( QColor( Qt.yellow ) )
        colors.append( QColor( Qt.blue ) )
        colors.append( QColor( Qt.magenta ) )
        colors.append( QColor( Qt.darkYellow ) )
        colors.append( QColor( Qt.lightGray ) )

        # Additional colors
        colors.append( QColor(255, 105, 180) ) #hot pink
        colors.append( QColor(102, 205, 170) ) #dark aquamarine
        colors.append( QColor(165,  42,  42) ) #brown        
        colors.append( QColor(0, 0, 128) )     #navy
        colors.append( QColor(255, 165, 0) )   #orange
        colors.append( QColor(173, 255,  47) ) #green-yellow
        colors.append( QColor(128,0, 128) )    #purple
        colors.append( QColor(240, 230, 140) ) #khaki

#        colors.append( QColor(192, 192, 192) ) #silver
#        colors.append( QColor(69, 69, 69) )    # dark grey
#        colors.append( QColor( Qt.cyan ) )

        assert len(colors) == 16

        return [c.rgba() for c in colors]
class SplitBodyCarvingGui(CarvingGui):
    
    def __init__(self, topLevelOperatorView):
        drawerUiPath = os.path.join( os.path.split(__file__)[0], 'splitBodyCarvingDrawer.ui' )
        super( SplitBodyCarvingGui, self ).__init__(topLevelOperatorView, drawerUiPath=drawerUiPath)
        self._splitInfoWidget = BodySplitInfoWidget(self, self.topLevelOperatorView)
        self._splitInfoWidget.navigationRequested.connect( self._handleNavigationRequest )
        self._labelControlUi.annotationWindowButton.pressed.connect( self._splitInfoWidget.show )
        
        # Hide all controls related to uncertainty; they aren't used in this applet
        self._labelControlUi.uncertaintyLabel.hide()
        self._labelControlUi.uncertaintyCombo.hide()
        self._labelControlUi.pushButtonUncertaintyFG.hide()
        self._labelControlUi.pushButtonUncertaintyBG.hide()
        
        # Hide manual save buttons; user must use the annotation window to save/load objects
        self._labelControlUi.saveControlLabel.hide()
        self._labelControlUi.save.hide()
        self._labelControlUi.saveAs.hide()
        self._labelControlUi.namesButton.hide()

        self.thunkEventHandler = ThunkEventHandler(self)

        fragmentColors = [ QColor(0,0,0,0), # transparent (background)
                           QColor(0, 255, 255),   # cyan
                           QColor(255, 0, 255),   # magenta
                           QColor(0, 0, 128),     # navy
                           QColor(165,  42,  42), # brown        
                           QColor(255, 105, 180), # hot pink
                           QColor(255, 165, 0),   # orange
                           QColor(173, 255,  47), # green-yellow
                           QColor(102, 205, 170), # dark aquamarine
                           QColor(128,0, 128),    # purple
                           QColor(240, 230, 140), # khaki
                           QColor(192, 192, 192), # silver
                           QColor(69, 69, 69) ]   # dark grey

        self._fragmentColors = fragmentColors

        # In this workflow, you aren't allowed to make brushstrokes unless there is a "current fragment"
        def handleEditingFragmentChange(slot, *args):
            if slot.value == "":
                self._changeInteractionMode(Tool.Navigation)
            else:
                self._changeInteractionMode(Tool.Paint)
            self._labelControlUi.paintToolButton.setEnabled( slot.value != "" )
            self._labelControlUi.eraserToolButton.setEnabled( slot.value != "" )
            self._labelControlUi.labelListView.setEnabled( slot.value != "" )
        topLevelOperatorView.CurrentEditingFragment.notifyDirty( handleEditingFragmentChange )
        handleEditingFragmentChange(topLevelOperatorView.CurrentEditingFragment)

    def _handleNavigationRequest(self, coord3d):
        self.editor.posModel.cursorPos = list(coord3d)
        self.editor.posModel.slicingPos = list(coord3d)
        self.editor.navCtrl.panSlicingViews( list(coord3d), [0,1,2] )
        
        # Navigation change is passed to downstream applets via this special slot
        self.topLevelOperatorView.NavigationCoordinates.setValue( coord3d, check_changed=False )
        
    def labelingContextMenu(self, names, op, position5d):
        return None
#        pos = TinyVector(position5d)
#        sample_roi = (pos, pos+1)
#        ravelerLabelSample = self.topLevelOperatorView.RavelerLabels(*sample_roi).wait()
#        ravelerLabel = ravelerLabelSample[0,0,0,0,0]
#        
#        menu = super( SplitBodyCarvingGui, self ).labelingContextMenu(names, op, position5d)
#        menu.addSeparator()
#        highlightAction = menu.addAction( "Highlight Raveler Object {}".format( ravelerLabel ) )
#        highlightAction.triggered.connect( partial(self.topLevelOperatorView.CurrentRavelerLabel.setValue, ravelerLabel ) )
#
#        # Auto-seed also auto-highlights
#        autoSeedAction = menu.addAction( "Auto-seed background for Raveler Object {}".format( ravelerLabel ) )
#        autoSeedAction.triggered.connect( partial(OpSplitBodyCarving.autoSeedBackground, self.topLevelOperatorView, ravelerLabel ) )
#        autoSeedAction.triggered.connect( partial(self.topLevelOperatorView.CurrentRavelerLabel.setValue, ravelerLabel ) )
#        return menu

    def _update_rendering(self):
        """
        Override from the base class.
        """
        # This update has to be performed in a different thread to avoid a deadlock
        # (Because this function is running in the context of a dirty notification!)
        req = Request( self.__update_rendering )
        def handle_rendering_failure( exc, exc_info ):
            import traceback
            traceback.print_exception(*exc_info)
            sys.stderr.write("Exception raised during volume rendering update.  See traceack above.\n")
        req.notify_failed( handle_rendering_failure )
        req.submit()
    
    def __update_rendering(self):
        if not self.render:
            return

        if not self._labelControlUi.activate3DViewCheckbox.isChecked():
            return 

        rendered_volume_shape = (250, 250, 250)

        print "Starting to update 3D volume data"

        fragmentColors = self._fragmentColors
        op = self.topLevelOperatorView
        if not self._renderMgr.ready:
            self._renderMgr.setup(op.InputData.meta.shape[1:4])
        self._renderMgr.clear()
        
        # Create a 5D view of the render mgr's memory
        totalRenderVol5d = self._renderMgr.volume[numpy.newaxis, ..., numpy.newaxis]

        # Block must not exceed total bounds.
        # Shift start up if necessary
        rendering_start_3d = TinyVector(self.editor.posModel.slicingPos) - TinyVector(rendered_volume_shape)/2.0
        rendering_start_3d = numpy.maximum( (0,0,0), rendering_start_3d )

        # Compute stop and shift down if necessary
        rendering_stop_3d = rendering_start_3d + TinyVector(rendered_volume_shape)
        rendering_stop_3d = numpy.minimum( op.InputData.meta.shape[1:4], rendering_stop_3d )
        
        # Recompute start now that stop has been computed
        rendering_start_3d = rendering_stop_3d - TinyVector(rendered_volume_shape)

        rendering_roi_5d = ( (0,) + tuple(rendering_start_3d) + (0,),
                             (1,) + tuple(rendering_stop_3d) + (1,) )

        # View only the data we want to update.
        renderVol5d = totalRenderVol5d[roiToSlice( *rendering_roi_5d )]

        ravelerLabel = op.CurrentRavelerLabel.value
        if ravelerLabel != 0:
            print " Asking for fragment segmentation"
            op.CurrentFragmentSegmentation(*rendering_roi_5d).writeInto(renderVol5d).wait()
            print " Obtained Fragment Segmentation"

            fragmentNames = op.getFragmentNames(ravelerLabel)
            numFragments = len(fragmentNames)

            renderLabels = []
            for i, name in enumerate(fragmentNames):
                if name != op.CurrentEditingFragment.value:
                    assert i < len(self._fragmentColors), "Too many fragments: colortable is too small"
                    color = ( fragmentColors[i+1].red() / 255.0,
                              fragmentColors[i+1].green() / 255.0,
                              fragmentColors[i+1].blue() / 255.0 )
                    renderLabel = self._renderMgr.addObject( color=color )
                    renderLabels.append( renderLabel )

            if op.CurrentEditingFragment.value != "":
                print " Asking for masked editing segmentation"
                maskedSegmentation = op.MaskedSegmentation(*rendering_roi_5d).wait()
                print " Obtained for masked editing segmentation"
                segLabel = numFragments

                print " Start updating volume data with masked segmentation"
                renderVol5d[:] = numpy.where(maskedSegmentation != 0, segLabel, renderVol5d)
                print " Finished updating volume data with masked segmentation"

                segmentationColor = (0.0, 1.0, 0.0)
                renderLabel = self._renderMgr.addObject( color=segmentationColor )
                renderLabels.append( renderLabel )

            # Relabel with the labels we were given by the renderer.
            # (We can skip this step if the renderer is guaranteed to give labels 1,2,3...)
            if renderLabels != list(range(len(renderLabels))):
                renderVol5d[:] = numpy.array([0] + renderLabels)[renderVol5d]

        print "Finished updating 3D volume data"
        self.thunkEventHandler.post(self._refreshRenderMgr)

    @threadRouted
    def _refreshRenderMgr(self):
        """
        The render mgr can segfault if this isn't called from the main thread.
        """
        print "Begin render update"
        self._renderMgr.update()
        print "End render update"

    
    def setupLayers(self):
        def findLayer(f, layerlist):
            for l in layerlist:
                if f(l):
                    return l
            return None

        layers = []
        baseCarvingLayers = super(SplitBodyCarvingGui, self).setupLayers()        
        
        crosshairSlot = self.topLevelOperatorView.AnnotationCrosshairs
        if crosshairSlot.ready():
            # 0=Transparent, 1=pink
            colortable = [QColor(0, 0, 0, 0).rgba(), QColor(236, 184, 201).rgba()]
            crosshairLayer = ColortableLayer(LazyflowSource(crosshairSlot), colortable, direct=True)
            crosshairLayer.name = "Annotation Points"
            crosshairLayer.visible = True
            crosshairLayer.opacity = 1.0
            layers.append(crosshairLayer)
        
        
        highlightedObjectSlot = self.topLevelOperatorView.CurrentRavelerObject
        if highlightedObjectSlot.ready():
            # 0=Transparent, 1=blue
            colortable = [QColor(0, 0, 0, 0).rgba(), QColor(0, 0, 255).rgba()]
            highlightedObjectLayer = ColortableLayer(LazyflowSource(highlightedObjectSlot), colortable, direct=True)
            highlightedObjectLayer.name = "Current Raveler Object"
            highlightedObjectLayer.visible = False
            highlightedObjectLayer.opacity = 0.25
            layers.append(highlightedObjectLayer)

        remainingRavelerObjectSlot = self.topLevelOperatorView.CurrentRavelerObjectRemainder
        if remainingRavelerObjectSlot.ready():
            # 0=Transparent, 1=blue
            colortable = [QColor(0, 0, 0, 0).rgba(), QColor(255, 0, 0).rgba()]
            remainingObjectLayer = ColortableLayer(LazyflowSource(remainingRavelerObjectSlot), colortable, direct=True)
            remainingObjectLayer.name = "Remaining Raveler Object"
            remainingObjectLayer.visible = True
            remainingObjectLayer.opacity = 0.25
            layers.append(remainingObjectLayer)

        fragmentSegSlot = self.topLevelOperatorView.CurrentFragmentSegmentation
        if fragmentSegSlot.ready():
            colortable = map(QColor.rgba, self._fragmentColors)
            fragSegLayer = ColortableLayer(LazyflowSource(fragmentSegSlot), colortable, direct=True)
            fragSegLayer.name = "Saved Fragments"
            fragSegLayer.visible = True
            fragSegLayer.opacity = 0.25
            layers.append(fragSegLayer)

        ravelerLabelsSlot = self.topLevelOperatorView.RavelerLabels
        if ravelerLabelsSlot.ready():
            colortable = []
            for i in range(256):
                r,g,b = numpy.random.randint(0,255), numpy.random.randint(0,255), numpy.random.randint(0,255)
                colortable.append(QColor(r,g,b).rgba())
            ravelerLabelLayer = ColortableLayer(LazyflowSource(ravelerLabelsSlot), colortable, direct=True)
            ravelerLabelLayer.name = "Raveler Labels"
            ravelerLabelLayer.visible = False
            ravelerLabelLayer.opacity = 0.4
            layers.append(ravelerLabelLayer)

        maskedSegSlot = self.topLevelOperatorView.MaskedSegmentation
        if maskedSegSlot.ready():
            colortable = [QColor(0,0,0,0).rgba(), QColor(0,0,0,0).rgba(), QColor(0,255,0).rgba()]
            maskedSegLayer = ColortableLayer(LazyflowSource(maskedSegSlot), colortable, direct=True)
            maskedSegLayer.name = "Masked Segmentation"
            maskedSegLayer.visible = True
            maskedSegLayer.opacity = 0.3
            layers.append(maskedSegLayer)
            
            # Hide the original carving segmentation.
            # TODO: Remove it from the list altogether?
            carvingSeg = findLayer( lambda l: l.name == "segmentation", baseCarvingLayers )
            if carvingSeg is not None:
                carvingSeg.visible = False

        def removeBaseLayer(layerName):
            layer = findLayer(lambda l: l.name == layerName, baseCarvingLayers)
            if layer:
                baseCarvingLayers.remove(layer)

        # Don't show carving layers that aren't relevant to the split-body workflow
        removeBaseLayer( "uncertainty" )
        removeBaseLayer( "done seg" )
        removeBaseLayer( "pmap" )
        removeBaseLayer( "hints" )
        removeBaseLayer( "done" )
        removeBaseLayer( "done" )
        
        # Attach a shortcut to the raw data layer
        if self.topLevelOperatorView.RawData.ready():
            rawLayer = findLayer(lambda l: l.name == "raw", baseCarvingLayers)
            assert rawLayer is not None, "Couldn't find the raw data layer.  Did it's name change?"
            rawLayer.shortcutRegistration = ( "Carving",
                                              "Raw Data to Top",
                                              QShortcut( QKeySequence("f"),
                                                         self.viewerControlWidget(),
                                                         partial(self._toggleRawDataPosition, rawLayer) ),
                                             rawLayer )
        layers += baseCarvingLayers
        return layers

    def _toggleRawDataPosition(self, rawLayer):
        index = self.layerstack.layerIndex(rawLayer)
        self.layerstack.selectRow( index )
        if index <= 2:
            self.layerstack.moveSelectedToBottom()
        else:
            # Move it almost to the top (under the seeds and the annotation crosshairs)
            self.layerstack.moveSelectedToRow(2)
            
class VigraWatershedViewerGui(LayerViewerGui):
    """
    """

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

    def appletDrawer(self):
        return self.getAppletDrawerUi()

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

    # (Other methods already provided by our base class)

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

    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        super(VigraWatershedViewerGui, self).__init__(parentApplet,
                                                      topLevelOperatorView)
        self.topLevelOperatorView = topLevelOperatorView
        op = self.topLevelOperatorView

        op.FreezeCache.setValue(True)
        op.OverrideLabels.setValue({0: (0, 0, 0, 0)})

        # Default settings (will be overwritten by serializer)
        op.InputChannelIndexes.setValue([])
        op.SeedThresholdValue.setValue(0.0)
        op.MinSeedSize.setValue(0)

        # Init padding gui updates
        blockPadding = PreferencesManager().get('vigra watershed viewer',
                                                'block padding', 10)
        op.WatershedPadding.notifyDirty(self.updatePaddingGui)
        op.WatershedPadding.setValue(blockPadding)
        self.updatePaddingGui()

        # Init block shape gui updates
        cacheBlockShape = PreferencesManager().get('vigra watershed viewer',
                                                   'cache block shape',
                                                   (256, 10))
        op.CacheBlockShape.notifyDirty(self.updateCacheBlockGui)
        op.CacheBlockShape.setValue(tuple(cacheBlockShape))
        self.updateCacheBlockGui()

        # Init seeds gui updates
        op.SeedThresholdValue.notifyDirty(self.updateSeedGui)
        op.SeedThresholdValue.notifyReady(self.updateSeedGui)
        op.SeedThresholdValue.notifyUnready(self.updateSeedGui)
        op.MinSeedSize.notifyDirty(self.updateSeedGui)
        self.updateSeedGui()

        # Init input channel gui updates
        op.InputChannelIndexes.notifyDirty(self.updateInputChannelGui)
        op.InputChannelIndexes.setValue([0])
        op.InputImage.notifyMetaChanged(bind(self.updateInputChannelGui))
        self.updateInputChannelGui()

        self.thunkEventHandler = ThunkEventHandler(self)

        # Remember to unsubscribe during shutdown
        self.__cleanup_fns = []
        self.__cleanup_fns.append(
            partial(op.WatershedPadding.unregisterDirty,
                    self.updatePaddingGui))
        self.__cleanup_fns.append(
            partial(op.CacheBlockShape.unregisterDirty,
                    self.updateCacheBlockGui))
        self.__cleanup_fns.append(
            partial(op.SeedThresholdValue.unregisterDirty, self.updateSeedGui))
        self.__cleanup_fns.append(
            partial(op.SeedThresholdValue.unregisterReady, self.updateSeedGui))
        self.__cleanup_fns.append(
            partial(op.SeedThresholdValue.unregisterUnready,
                    self.updateSeedGui))
        self.__cleanup_fns.append(
            partial(op.MinSeedSize.unregisterDirty, self.updateSeedGui))
        self.__cleanup_fns.append(
            partial(op.InputChannelIndexes.unregisterDirty,
                    self.updateInputChannelGui))
        self.__cleanup_fns.append(
            partial(op.InputImage.unregisterDirty, self.updateInputChannelGui))

    def initAppletDrawerUi(self):
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir + "/drawer.ui")

        # Input channels
        self._inputChannelCheckboxes = []
        self._inputChannelCheckboxes.append(self._drawer.input_ch0)
        self._inputChannelCheckboxes.append(self._drawer.input_ch1)
        self._inputChannelCheckboxes.append(self._drawer.input_ch2)
        self._inputChannelCheckboxes.append(self._drawer.input_ch3)
        self._inputChannelCheckboxes.append(self._drawer.input_ch4)
        self._inputChannelCheckboxes.append(self._drawer.input_ch5)
        self._inputChannelCheckboxes.append(self._drawer.input_ch6)
        self._inputChannelCheckboxes.append(self._drawer.input_ch7)
        self._inputChannelCheckboxes.append(self._drawer.input_ch8)
        self._inputChannelCheckboxes.append(self._drawer.input_ch9)
        for checkbox in self._inputChannelCheckboxes:
            checkbox.toggled.connect(self.onInputSelectionsChanged)

        # Seed thresholds
        self._drawer.useSeedsCheckbox.toggled.connect(self.onUseSeedsToggled)
        self._drawer.seedThresholdSpinBox.valueChanged.connect(
            self.onSeedThresholdChanged)

        # Seed size
        self._drawer.seedSizeSpinBox.valueChanged.connect(
            self.onSeedSizeChanged)

        # Padding
        self._drawer.updateWatershedsButton.clicked.connect(
            self.onUpdateWatershedsButton)
        self._drawer.paddingSlider.valueChanged.connect(self.onPaddingChanged)
        self._drawer.paddingSpinBox.valueChanged.connect(self.onPaddingChanged)

        # Block shape
        self._drawer.blockWidthSpinBox.valueChanged.connect(
            self.onBlockShapeChanged)
        self._drawer.blockDepthSpinBox.valueChanged.connect(
            self.onBlockShapeChanged)

    def getAppletDrawerUi(self):
        return self._drawer

    def hideEvent(self, event):
        """
        This GUI is being hidden because the user selected another applet or the window is closing.
        Save all preferences.
        """
        if self.topLevelOperatorView.CacheBlockShape.ready(
        ) and self.topLevelOperatorView.WatershedPadding.ready():
            with PreferencesManager() as prefsMgr:
                prefsMgr.set(
                    'vigra watershed viewer', 'cache block shape',
                    tuple(self.topLevelOperatorView.CacheBlockShape.value))
                prefsMgr.set('vigra watershed viewer', 'block padding',
                             self.topLevelOperatorView.WatershedPadding.value)
        super(VigraWatershedViewerGui, self).hideEvent(event)

    def setupLayers(self):
        ActionInfo = ShortcutManager.ActionInfo
        layers = []

        self.updateInputChannelGui()

        # Show the watershed data
        outputImageSlot = self.topLevelOperatorView.ColoredPixels
        if outputImageSlot.ready():
            outputLayer = self.createStandardLayerFromSlot(
                outputImageSlot, lastChannelIsAlpha=True)
            outputLayer.name = "Watershed"
            outputLayer.visible = True
            outputLayer.opacity = 0.5
            outputLayer.shortcutRegistration = ("w",
                                                ActionInfo(
                                                    "Watershed Layers",
                                                    "Show/Hide Watershed",
                                                    "Show/Hide Watershed",
                                                    outputLayer.toggleVisible,
                                                    self, outputLayer))
            layers.append(outputLayer)

        # Show the watershed seeds
        seedSlot = self.topLevelOperatorView.ColoredSeeds
        if seedSlot.ready():
            seedLayer = self.createStandardLayerFromSlot(
                seedSlot, lastChannelIsAlpha=True)
            seedLayer.name = "Watershed Seeds"
            seedLayer.visible = True
            seedLayer.opacity = 0.5
            seedLayer.shortcutRegistration = ("s",
                                              ActionInfo(
                                                  "Watershed Layers",
                                                  "Show/Hide Watershed Seeds",
                                                  "Show/Hide Watershed Seeds",
                                                  seedLayer.toggleVisible,
                                                  self.viewerControlWidget(),
                                                  seedLayer))
            layers.append(seedLayer)

        selectedInputImageSlot = self.topLevelOperatorView.SelectedInputChannels
        if selectedInputImageSlot.ready():
            # Show the summed input if there's more than one input channel
            if len(selectedInputImageSlot) > 1:
                summedSlot = self.topLevelOperatorView.SummedInput
                if summedSlot.ready():
                    sumLayer = self.createStandardLayerFromSlot(summedSlot)
                    sumLayer.name = "Summed Input"
                    sumLayer.visible = True
                    sumLayer.opacity = 1.0
                    layers.append(sumLayer)

            # Show selected input channels
            inputChannelIndexes = self.topLevelOperatorView.InputChannelIndexes.value
            for channel, slot in enumerate(selectedInputImageSlot):
                inputLayer = self.createStandardLayerFromSlot(slot)
                inputLayer.name = "Input (Ch.{})".format(
                    inputChannelIndexes[channel])
                inputLayer.visible = True
                inputLayer.opacity = 1.0
                layers.append(inputLayer)

        # Show the raw input (if provided)
        rawImageSlot = self.topLevelOperatorView.RawImage
        if rawImageSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(rawImageSlot)
            rawLayer.name = "Raw Image"
            rawLayer.visible = True
            rawLayer.opacity = 1.0

            def toggleTopToBottom():
                index = self.layerstack.layerIndex(rawLayer)
                self.layerstack.selectRow(index)
                if index == 0:
                    self.layerstack.moveSelectedToBottom()
                else:
                    self.layerstack.moveSelectedToTop()

            rawLayer.shortcutRegistration = (
                "i",
                ActionInfo("Watershed Layers", "Bring Raw Data To Top/Bottom",
                           "Bring Raw Data To Top/Bottom", toggleTopToBottom,
                           self.viewerControlWidget(), rawLayer))
            layers.append(rawLayer)

        return layers

    @pyqtSlot()
    def onUpdateWatershedsButton(self):
        def updateThread():
            """
            Temporarily unfreeze the cache and freeze it again after the views are finished rendering.
            """
            self.topLevelOperatorView.FreezeCache.setValue(False)
            self.topLevelOperatorView.opWatershed.clearMaxLabels()

            # Force the cache to update.
            self.topLevelOperatorView.InputImage.setDirty(slice(None))

            # Wait for the image to be rendered into all three image views
            time.sleep(2)
            for imgView in self.editor.imageViews:
                imgView.scene().joinRenderingAllTiles()
            self.topLevelOperatorView.FreezeCache.setValue(True)

            self.updateSupervoxelStats()

        th = threading.Thread(target=updateThread)
        th.start()

    def updateSupervoxelStats(self):
        """
        Use the accumulated state in the watershed operator to display the stats for the most recent watershed computation.
        """
        totalVolume = 0
        totalCount = 0
        for (
                start, stop
        ), maxLabel in self.topLevelOperatorView.opWatershed.maxLabels.items():
            blockshape = numpy.subtract(stop, start)
            vol = numpy.prod(blockshape)
            totalVolume += vol

            totalCount += maxLabel

        vol_caption = "Refresh Volume: {} megavox".format(totalVolume /
                                                          float(1000 * 1000))
        count_caption = "Supervoxel Count: {}".format(totalCount)
        if totalVolume != 0:
            density_caption = "Density: {} supervox/megavox".format(
                totalCount * float(1000 * 1000) / totalVolume)
        else:
            density_caption = ""

        # Update the GUI text, but do it in the GUI thread (even if we were called from a worker thread)
        self.thunkEventHandler.post(self._drawer.refreshVolumeLabel.setText,
                                    vol_caption)
        self.thunkEventHandler.post(self._drawer.superVoxelCountLabel.setText,
                                    count_caption)
        self.thunkEventHandler.post(self._drawer.densityLabel.setText,
                                    density_caption)

    def getLabelAt(self, position5d):
        labelSlot = self.topLevelOperatorView.WatershedLabels
        if labelSlot.ready():
            labelData = labelSlot[index2slice(position5d)].wait()
            return labelData.squeeze()[()]
        else:
            return None

    def handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        """
        This is an override from the base class.  Called when the user clicks in the volume.
        
        For left clicks, we highlight the clicked label.
        """
        label = self.getLabelAt(position5d)
        if label != 0 and label is not None:
            overrideSlot = self.topLevelOperatorView.OverrideLabels
            overrides = copy.copy(overrideSlot.value)
            overrides[label] = (255, 255, 255, 255)
            overrideSlot.setValue(overrides)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        """
        This is an override from the base class.  Called when the user clicks in the volume.
        
        For right clicks, we un-highlight the clicked label.
        """
        label = self.getLabelAt(position5d)
        overrideSlot = self.topLevelOperatorView.OverrideLabels
        overrides = copy.copy(overrideSlot.value)
        if label != 0 and label in overrides:
            del overrides[label]
            overrideSlot.setValue(overrides)

    ##
    ## GUI -> Operator
    ##
    def onPaddingChanged(self, value):
        self.topLevelOperatorView.WatershedPadding.setValue(value)

    def onBlockShapeChanged(self, value):
        width = self._drawer.blockWidthSpinBox.value()
        depth = self._drawer.blockDepthSpinBox.value()
        self.topLevelOperatorView.CacheBlockShape.setValue((width, depth))

    def onInputSelectionsChanged(self):
        inputImageSlot = self.topLevelOperatorView.InputImage
        if inputImageSlot.ready():
            channelAxis = inputImageSlot.meta.axistags.channelIndex
            numInputChannels = inputImageSlot.meta.shape[channelAxis]
        else:
            numInputChannels = 0
        channels = []
        for i, checkbox in enumerate(
                self._inputChannelCheckboxes[0:numInputChannels]):
            if checkbox.isChecked():
                channels.append(i)

        self.topLevelOperatorView.InputChannelIndexes.setValue(channels)

    def onUseSeedsToggled(self):
        self.updateSeeds()

    def onSeedThresholdChanged(self):
        self.updateSeeds()

    def onSeedSizeChanged(self):
        self.updateSeeds()

    def updateSeeds(self):
        useSeeds = self._drawer.useSeedsCheckbox.isChecked()
        self._drawer.seedThresholdSpinBox.setEnabled(useSeeds)
        self._drawer.seedSizeSpinBox.setEnabled(useSeeds)
        if useSeeds:
            threshold = self._drawer.seedThresholdSpinBox.value()
            minSize = self._drawer.seedSizeSpinBox.value()
            self.topLevelOperatorView.SeedThresholdValue.setValue(threshold)
            self.topLevelOperatorView.MinSeedSize.setValue(minSize)
        else:
            self.topLevelOperatorView.SeedThresholdValue.disconnect()

    ##
    ## Operator -> GUI
    ##
    def updatePaddingGui(self, *args):
        padding = self.topLevelOperatorView.WatershedPadding.value
        self._drawer.paddingSlider.setValue(padding)
        self._drawer.paddingSpinBox.setValue(padding)

    def updateCacheBlockGui(self, *args):
        width, depth = self.topLevelOperatorView.CacheBlockShape.value
        self._drawer.blockWidthSpinBox.setValue(width)
        self._drawer.blockDepthSpinBox.setValue(depth)

    def updateSeedGui(self, *args):
        useSeeds = self.topLevelOperatorView.SeedThresholdValue.ready()
        self._drawer.seedThresholdSpinBox.setEnabled(useSeeds)
        self._drawer.seedSizeSpinBox.setEnabled(useSeeds)
        self._drawer.useSeedsCheckbox.setChecked(useSeeds)
        if useSeeds:
            threshold = self.topLevelOperatorView.SeedThresholdValue.value
            minSize = self.topLevelOperatorView.MinSeedSize.value
            self._drawer.seedThresholdSpinBox.setValue(threshold)
            self._drawer.seedSizeSpinBox.setValue(minSize)

    def updateInputChannelGui(self, *args):
        # Show only checkboxes that can be used (limited by number of input channels)
        numChannels = 0
        inputImageSlot = self.topLevelOperatorView.InputImage
        if inputImageSlot.ready():
            channelAxis = inputImageSlot.meta.axistags.channelIndex
            numChannels = inputImageSlot.meta.shape[channelAxis]
        for i, checkbox in enumerate(self._inputChannelCheckboxes):
            #            if i >= numChannels:
            #                checkbox.setChecked(False)
            if not sip.isdeleted(checkbox):
                checkbox.setVisible(i < numChannels)

        # Make sure the correct boxes are checked
        if self.topLevelOperatorView.InputChannelIndexes.ready():
            inputChannels = self.topLevelOperatorView.InputChannelIndexes.value
            for i, checkbox in enumerate(self._inputChannelCheckboxes):
                if not sip.isdeleted(checkbox):
                    checkbox.setChecked(i in inputChannels)
Beispiel #22
0
class IlastikShell(QMainWindow):
    """
    The GUI's main window.  Simply a standard 'container' GUI for one or more applets.
    """
    def __init__(self,
                 workflow=[],
                 parent=None,
                 flags=QtCore.Qt.WindowFlags(0),
                 sideSplitterSizePolicy=SideSplitterSizePolicy.Manual):
        QMainWindow.__init__(self, parent=parent, flags=flags)
        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

        self._sideSplitterSizePolicy = sideSplitterSizePolicy

        self.projectManager = ProjectManager()

        import inspect, os
        ilastikShellFilePath = os.path.dirname(
            inspect.getfile(inspect.currentframe()))
        uic.loadUi(ilastikShellFilePath + "/ui/ilastikShell.ui", self)
        self._applets = []
        self.appletBarMapping = {}

        self.setAttribute(Qt.WA_AlwaysShowToolTips)

        if 'Ubuntu' in platform.platform():
            # Native menus are prettier, but aren't working on Ubuntu at this time (Qt 4.7, Ubuntu 11)
            self.menuBar().setNativeMenuBar(False)

        (self._projectMenu, self._shellActions) = self._createProjectMenu()
        self._settingsMenu = self._createSettingsMenu()
        self.menuBar().addMenu(self._projectMenu)
        self.menuBar().addMenu(self._settingsMenu)

        self.updateShellProjectDisplay()

        self.progressDisplayManager = ProgressDisplayManager(self.statusBar)

        for applet in workflow:
            self.addApplet(applet)

        self.appletBar.expanded.connect(self.handleAppleBarItemExpanded)
        self.appletBar.clicked.connect(self.handleAppletBarClick)
        self.appletBar.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)

        # By default, make the splitter control expose a reasonable width of the applet bar
        self.mainSplitter.setSizes([300, 1])

        self.currentAppletIndex = 0

        self.currentImageIndex = -1
        self.populatingImageSelectionCombo = False
        self.imageSelectionCombo.currentIndexChanged.connect(
            self.changeCurrentInputImageIndex)

        self.enableWorkflow = False  # Global mask applied to all applets
        self._controlCmds = [
        ]  # Track the control commands that have been issued by each applet so they can be popped.
        self._disableCounts = [
        ]  # Controls for each applet can be disabled by his peers.
        # No applet can be enabled unless his disableCount == 0

    def _createProjectMenu(self):
        # Create a menu for "General" (non-applet) actions
        menu = QMenu("&Project", self)

        shellActions = ShellActions()

        # Menu item: New Project
        shellActions.newProjectAction = menu.addAction("&New Project...")
        shellActions.newProjectAction.setShortcuts(QKeySequence.New)
        shellActions.newProjectAction.triggered.connect(
            self.onNewProjectActionTriggered)

        # Menu item: Open Project
        shellActions.openProjectAction = menu.addAction("&Open Project...")
        shellActions.openProjectAction.setShortcuts(QKeySequence.Open)
        shellActions.openProjectAction.triggered.connect(
            self.onOpenProjectActionTriggered)

        # Menu item: Save Project
        shellActions.saveProjectAction = menu.addAction("&Save Project")
        shellActions.saveProjectAction.setShortcuts(QKeySequence.Save)
        shellActions.saveProjectAction.triggered.connect(
            self.onSaveProjectActionTriggered)

        # Menu item: Save Project As
        shellActions.saveProjectAsAction = menu.addAction(
            "&Save Project As...")
        shellActions.saveProjectAsAction.setShortcuts(QKeySequence.SaveAs)
        shellActions.saveProjectAsAction.triggered.connect(
            self.onSaveProjectAsActionTriggered)

        # Menu item: Save Project Snapshot
        shellActions.saveProjectSnapshotAction = menu.addAction(
            "&Take Snapshot...")
        shellActions.saveProjectSnapshotAction.triggered.connect(
            self.onSaveProjectSnapshotActionTriggered)

        # Menu item: Import Project
        shellActions.importProjectAction = menu.addAction("&Import Project...")
        shellActions.importProjectAction.triggered.connect(
            self.onImportProjectActionTriggered)

        # Menu item: Quit
        shellActions.quitAction = menu.addAction("&Quit")
        shellActions.quitAction.setShortcuts(QKeySequence.Quit)
        shellActions.quitAction.triggered.connect(self.onQuitActionTriggered)
        shellActions.quitAction.setShortcut(QKeySequence.Quit)

        return (menu, shellActions)

    def _createSettingsMenu(self):
        menu = QMenu("&Settings", self)

        # Menu item: Keyboard Shortcuts

        def editShortcuts():
            mgrDlg = ShortcutManagerDlg(self)

        shortcutsAction = menu.addAction("&Keyboard Shortcuts")
        shortcutsAction.triggered.connect(editShortcuts)

        return menu

    def show(self):
        """
        Show the window, and enable/disable controls depending on whether or not a project file present.
        """
        super(IlastikShell, self).show()
        self.enableWorkflow = (self.projectManager.currentProjectFile
                               is not None)
        self.updateAppletControlStates()
        self.updateShellProjectDisplay()
        if self._sideSplitterSizePolicy == SideSplitterSizePolicy.Manual:
            self.autoSizeSideSplitter(SideSplitterSizePolicy.AutoLargestDrawer)
        else:
            self.autoSizeSideSplitter(SideSplitterSizePolicy.AutoCurrentDrawer)

    def updateShellProjectDisplay(self):
        """
        Update the title bar and allowable shell actions based on the state of the currently loaded project.
        """
        windowTitle = "ilastik - "
        projectPath = self.projectManager.currentProjectPath
        if projectPath is None:
            windowTitle += "No Project Loaded"
        else:
            windowTitle += projectPath

        readOnly = self.projectManager.currentProjectIsReadOnly
        if readOnly:
            windowTitle += " [Read Only]"

        self.setWindowTitle(windowTitle)

        # Enable/Disable menu items
        projectIsOpen = self.projectManager.currentProjectFile is not None
        self._shellActions.saveProjectAction.setEnabled(
            projectIsOpen and not readOnly)  # Can't save a read-only project
        self._shellActions.saveProjectAsAction.setEnabled(projectIsOpen)
        self._shellActions.saveProjectSnapshotAction.setEnabled(projectIsOpen)

    def setImageNameListSlot(self, multiSlot):
        assert multiSlot.level == 1
        self.imageNamesSlot = multiSlot

        def insertImageName(index, slot):
            self.imageSelectionCombo.setItemText(index, slot.value)
            if self.currentImageIndex == -1:
                self.changeCurrentInputImageIndex(index)

        def handleImageNameSlotInsertion(multislot, index):
            assert multislot == self.imageNamesSlot
            self.populatingImageSelectionCombo = True
            self.imageSelectionCombo.insertItem(index, "uninitialized")
            self.populatingImageSelectionCombo = False
            multislot[index].notifyDirty(bind(insertImageName, index))

        multiSlot.notifyInserted(bind(handleImageNameSlotInsertion))

        def handleImageNameSlotRemoval(multislot, index):
            # Simply remove the combo entry, which causes the currentIndexChanged signal to fire if necessary.
            self.imageSelectionCombo.removeItem(index)
            if len(multislot) == 0:
                self.changeCurrentInputImageIndex(-1)

        multiSlot.notifyRemove(bind(handleImageNameSlotRemoval))

    def changeCurrentInputImageIndex(self, newImageIndex):
        if newImageIndex != self.currentImageIndex \
        and self.populatingImageSelectionCombo == False:
            if newImageIndex != -1:
                try:
                    # Accessing the image name value will throw if it isn't properly initialized
                    self.imageNamesSlot[newImageIndex].value
                except:
                    # Revert to the original image index.
                    if self.currentImageIndex != -1:
                        self.imageSelectionCombo.setCurrentIndex(
                            self.currentImageIndex)
                    return

            # Alert each central widget and viewer control widget that the image selection changed
            for i in range(len(self._applets)):
                self._applets[i].gui.setImageIndex(newImageIndex)

            self.currentImageIndex = newImageIndex

    def handleAppleBarItemExpanded(self, modelIndex):
        """
        The user wants to view a different applet bar item.
        """
        drawerIndex = modelIndex.row()
        self.setSelectedAppletDrawer(drawerIndex)

    def setSelectedAppletDrawer(self, drawerIndex):
        """
        Show the correct applet central widget, viewer control widget, and applet drawer widget for this drawer index.
        """
        if self.currentAppletIndex != drawerIndex:
            self.currentAppletIndex = drawerIndex
            # Collapse all drawers in the applet bar...
            self.appletBar.collapseAll()
            # ...except for the newly selected item.
            self.appletBar.expand(
                self.getModelIndexFromDrawerIndex(drawerIndex))

            if len(self.appletBarMapping) != 0:
                # Determine which applet this drawer belongs to
                assert drawerIndex in self.appletBarMapping
                applet_index = self.appletBarMapping[drawerIndex]

                # Select the appropriate central widget, menu widget, and viewer control widget for this applet
                self.appletStack.setCurrentIndex(applet_index)
                self.viewerControlStack.setCurrentIndex(applet_index)
                self.menuBar().clear()
                self.menuBar().addMenu(self._projectMenu)
                self.menuBar().addMenu(self._settingsMenu)
                for m in self._applets[applet_index].gui.menus():
                    self.menuBar().addMenu(m)

                self.autoSizeSideSplitter(self._sideSplitterSizePolicy)

    def getModelIndexFromDrawerIndex(self, drawerIndex):
        drawerTitleItem = self.appletBar.invisibleRootItem().child(drawerIndex)
        return self.appletBar.indexFromItem(drawerTitleItem)

    def autoSizeSideSplitter(self, sizePolicy):
        if sizePolicy == SideSplitterSizePolicy.Manual:
            # In manual mode, don't resize the splitter at all.
            return

        if sizePolicy == SideSplitterSizePolicy.AutoCurrentDrawer:
            # Get the height of the current applet drawer
            rootItem = self.appletBar.invisibleRootItem()
            appletDrawerItem = rootItem.child(self.currentAppletIndex).child(0)
            appletDrawerWidget = self.appletBar.itemWidget(appletDrawerItem, 0)
            appletDrawerHeight = appletDrawerWidget.frameSize().height()

        if sizePolicy == SideSplitterSizePolicy.AutoLargestDrawer:
            appletDrawerHeight = 0
            # Get the height of the largest drawer in the bar
            for drawerIndex in range(len(self.appletBarMapping)):
                rootItem = self.appletBar.invisibleRootItem()
                appletDrawerItem = rootItem.child(drawerIndex).child(0)
                appletDrawerWidget = self.appletBar.itemWidget(
                    appletDrawerItem, 0)
                appletDrawerHeight = max(
                    appletDrawerHeight,
                    appletDrawerWidget.frameSize().height())

        # Get total height of the titles in the applet bar (not the widgets)
        firstItem = self.appletBar.invisibleRootItem().child(0)
        titleHeight = self.appletBar.visualItemRect(firstItem).size().height()
        numDrawers = len(self.appletBarMapping)
        totalTitleHeight = numDrawers * titleHeight

        # Auto-size the splitter height based on the height of the applet bar.
        totalSplitterHeight = sum(self.sideSplitter.sizes())
        appletBarHeight = totalTitleHeight + appletDrawerHeight + 10  # Add a small margin so the scroll bar doesn't appear
        self.sideSplitter.setSizes(
            [appletBarHeight, totalSplitterHeight - appletBarHeight])

    def handleAppletBarClick(self, modelIndex):
        # If the user clicks on a top-level item, automatically expand it.
        if modelIndex.parent() == self.appletBar.rootIndex():
            self.appletBar.expand(modelIndex)
        else:
            self.appletBar.setCurrentIndex(modelIndex.parent())

    def addApplet(self, app):
        assert isinstance(
            app, Applet), "Applets must inherit from Applet base class."
        assert app.base_initialized, "Applets must call Applet.__init__ upon construction."

        assert issubclass(
            type(app.gui), AppletGuiInterface
        ), "Applet GUIs must conform to the Applet GUI interface."

        self._applets.append(app)
        applet_index = len(self._applets) - 1
        self.appletStack.addWidget(app.gui.centralWidget())

        # Viewer controls are optional. If the applet didn't provide one, create an empty widget for him.
        if app.gui.viewerControlWidget() is None:
            self.viewerControlStack.addWidget(QWidget(parent=self))
        else:
            self.viewerControlStack.addWidget(app.gui.viewerControlWidget())

        # Add rows to the applet bar model
        rootItem = self.appletBar.invisibleRootItem()

        # Add all of the applet bar's items to the toolbox widget
        for controlName, controlGuiItem in app.gui.appletDrawers():
            appletNameItem = QTreeWidgetItem(self.appletBar,
                                             QtCore.QStringList(controlName))
            appletNameItem.setFont(0, QFont("Ubuntu", 14))
            drawerItem = QTreeWidgetItem(appletNameItem)
            drawerItem.setSizeHint(0, controlGuiItem.frameSize())
            #            drawerItem.setBackground( 0, QBrush( QColor(224, 224, 224) ) )
            #            drawerItem.setForeground( 0, QBrush( QColor(0,0,0) ) )
            self.appletBar.setItemWidget(drawerItem, 0, controlGuiItem)

            # Since each applet can contribute more than one applet bar item,
            #  we need to keep track of which applet this item is associated with
            self.appletBarMapping[rootItem.childCount() - 1] = applet_index

        # Set up handling of GUI commands from this applet
        app.guiControlSignal.connect(
            bind(self.handleAppletGuiControlSignal, applet_index))
        self._disableCounts.append(0)
        self._controlCmds.append([])

        # Set up handling of progress updates from this applet
        self.progressDisplayManager.addApplet(applet_index, app)

        # Set up handling of shell requests from this applet
        app.shellRequestSignal.connect(
            partial(self.handleShellRequest, applet_index))

        self.projectManager.addApplet(app)

        return applet_index

    def handleAppletGuiControlSignal(self,
                                     applet_index,
                                     command=ControlCommand.DisableAll):
        """
        Applets fire a signal when they want other applet GUIs to be disabled.
        This function handles the signal.
        Each signal is treated as a command to disable other applets.
        A special command, Pop, undoes the applet's most recent command (i.e. re-enables the applets that were disabled).
        If an applet is disabled twice (e.g. by two different applets), then it won't become enabled again until both commands have been popped.
        """
        if command == ControlCommand.Pop:
            command = self._controlCmds[applet_index].pop()
            step = -1  # Since we're popping this command, we'll subtract from the disable counts
        else:
            step = 1
            self._controlCmds[applet_index].append(
                command
            )  # Push command onto the stack so we can pop it off when the applet isn't busy any more

        # Increase the disable count for each applet that is affected by this command.
        for index, count in enumerate(self._disableCounts):
            if (command == ControlCommand.DisableAll) \
            or (command == ControlCommand.DisableDownstream and index > applet_index) \
            or (command == ControlCommand.DisableUpstream and index < applet_index) \
            or (command == ControlCommand.DisableSelf and index == applet_index):
                self._disableCounts[index] += step

        # Update the control states in the GUI thread
        self.thunkEventHandler.post(self.updateAppletControlStates)

    def handleShellRequest(self, applet_index, requestAction):
        """
        An applet is asking us to do something.  Handle the request.
        """
        with Tracer(traceLogger):
            if requestAction == ShellRequest.RequestSave:
                # Call the handler directly to ensure this is a synchronous call (not queued to the GUI thread)
                self.projectManager.saveProject()

    def __len__(self):
        return self.appletBar.count()

    def __getitem__(self, index):
        return self._applets[index]

    def ensureNoCurrentProject(self, assertClean=False):
        """
        Close the current project.  If it's dirty, we ask the user for confirmation.
        
        The assertClean parameter is for tests.  Setting it to True will raise an assertion if the project was dirty.
        """
        closeProject = True
        if self.projectManager.isProjectDataDirty():
            # Testing assertion
            assert not assertClean, "Expected a clean project but found it to be dirty!"

            message = "Your current project is about to be closed, but it has unsaved changes which will be lost.\n"
            message += "Are you sure you want to proceed?"
            buttons = QMessageBox.Yes | QMessageBox.Cancel
            response = QMessageBox.warning(self,
                                           "Discard unsaved changes?",
                                           message,
                                           buttons,
                                           defaultButton=QMessageBox.Cancel)
            closeProject = (response == QMessageBox.Yes)

        if closeProject:
            self.closeCurrentProject()

        return closeProject

    def closeCurrentProject(self):
        for applet in self._applets:
            applet.gui.reset()
        self.projectManager.closeCurrentProject()
        self.enableWorkflow = False
        self.updateAppletControlStates()
        self.updateShellProjectDisplay()

    def onNewProjectActionTriggered(self):
        logger.debug("New Project action triggered")

        # Make sure the user is finished with the currently open project
        if not self.ensureNoCurrentProject():
            return

        newProjectFilePath = self.getProjectPathToCreate()

        if newProjectFilePath is not None:
            self.createAndLoadNewProject(newProjectFilePath)

    def createAndLoadNewProject(self, newProjectFilePath):
        newProjectFile = self.projectManager.createBlankProjectFile(
            newProjectFilePath)
        self.loadProject(newProjectFile, newProjectFilePath, False)

    def getProjectPathToCreate(self,
                               defaultPath=None,
                               caption="Create Ilastik Project"):
        """
        Ask the user where he would like to create a project file.
        """
        if defaultPath is None:
            defaultPath = os.path.expanduser("~/MyProject.ilp")

        fileSelected = False
        while not fileSelected:
            projectFilePath = QFileDialog.getSaveFileName(
                self,
                caption,
                defaultPath,
                "Ilastik project files (*.ilp)",
                options=QFileDialog.Options(QFileDialog.DontUseNativeDialog))

            # If the user cancelled, stop now
            if projectFilePath.isNull():
                return None

            projectFilePath = str(projectFilePath)
            fileSelected = True

            # Add extension if necessary
            fileExtension = os.path.splitext(projectFilePath)[1].lower()
            if fileExtension != '.ilp':
                projectFilePath += ".ilp"
                if os.path.exists(projectFilePath):
                    # Since we changed the file path, we need to re-check if we're overwriting an existing file.
                    message = "A file named '" + projectFilePath + "' already exists in this location.\n"
                    message += "Are you sure you want to overwrite it?"
                    buttons = QMessageBox.Yes | QMessageBox.Cancel
                    response = QMessageBox.warning(
                        self,
                        "Overwrite existing project?",
                        message,
                        buttons,
                        defaultButton=QMessageBox.Cancel)
                    if response == QMessageBox.Cancel:
                        # Try again...
                        fileSelected = False

        return projectFilePath

    def onImportProjectActionTriggered(self):
        """
        Import an existing project into a new file.
        This involves opening the old file, saving it to a new file, and then opening the new file.
        """
        logger.debug("Import Project Action")

        if not self.ensureNoCurrentProject():
            return

        # Find the directory of the most recently *imported* project
        mostRecentImportPath = PreferencesManager().get(
            'shell', 'recently imported')
        if mostRecentImportPath is not None:
            defaultDirectory = os.path.split(mostRecentImportPath)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        # Select the paths to the ilp to import and the name of the new one we'll create
        importedFilePath = self.getProjectPathToOpen(defaultDirectory)
        if importedFilePath is not None:
            PreferencesManager().set('shell', 'recently imported',
                                     importedFilePath)
            defaultFile, ext = os.path.splitext(importedFilePath)
            defaultFile += "_imported"
            defaultFile += ext
            newProjectFilePath = self.getProjectPathToCreate(defaultFile)

        # If the user didn't cancel
        if importedFilePath is not None and newProjectFilePath is not None:
            self.importProject(importedFilePath, newProjectFilePath)

    def importProject(self, originalPath, newProjectFilePath):
        newProjectFile = self.projectManager.createBlankProjectFile(
            newProjectFilePath)
        self.projectManager.importProject(originalPath, newProjectFile,
                                          newProjectFilePath)

        self.updateShellProjectDisplay()

        # Enable all the applet controls
        self.enableWorkflow = True
        self.updateAppletControlStates()

    def getProjectPathToOpen(self, defaultDirectory):
        """
        Return the path of the project the user wants to open (or None if he cancels).
        """
        projectFilePath = QFileDialog.getOpenFileName(
            self,
            "Open Ilastik Project",
            defaultDirectory,
            "Ilastik project files (*.ilp)",
            options=QFileDialog.Options(QFileDialog.DontUseNativeDialog))

        # If the user canceled, stop now
        if projectFilePath.isNull():
            return None

        return str(projectFilePath)

    def onOpenProjectActionTriggered(self):
        logger.debug("Open Project action triggered")

        # Make sure the user is finished with the currently open project
        if not self.ensureNoCurrentProject():
            return

        # Find the directory of the most recently opened project
        mostRecentProjectPath = PreferencesManager().get(
            'shell', 'recently opened')
        if mostRecentProjectPath is not None:
            defaultDirectory = os.path.split(mostRecentProjectPath)[0]
        else:
            defaultDirectory = os.path.expanduser('~')

        projectFilePath = self.getProjectPathToOpen(defaultDirectory)
        if projectFilePath is not None:
            PreferencesManager().set('shell', 'recently opened',
                                     projectFilePath)
            self.openProjectFile(projectFilePath)

    def openProjectFile(self, projectFilePath):
        try:
            hdf5File, readOnly = self.projectManager.openProjectFile(
                projectFilePath)
        except ProjectManager.ProjectVersionError, e:
            QMessageBox.warning(
                self, "Old Project", "Could not load old project file: " +
                projectFilePath + ".\nPlease try 'Import Project' instead.")
        except ProjectManager.FileMissingError:
            QMessageBox.warning(
                self, "Missing File",
                "Could not find project file: " + projectFilePath)
Beispiel #23
0
class CroppingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer
    applet with the added functionality of cropping.
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._cropControlUi

    def stopAndCleanUp(self):
        super(CroppingGui, self).stopAndCleanUp()

        for fn in self.__cleanup_fns:
            fn()

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

    @property
    def minCropNumber(self):
        return self._minCropNumber
    @minCropNumber.setter
    def minCropNumber(self, n):
        self._minCropNumer = n
        while self._cropControlUi.cropListModel.rowCount() < n:
            self._addNewCrop()
    @property
    def maxCropNumber(self):
        return self._maxCropNumber
    @maxCropNumber.setter
    def maxCropNumber(self, n):
        self._maxCropNumber = n
        while self._cropControlUi.cropListModel.rowCount() < n:
            self._removeLastCrop()

    @property
    def croppingDrawerUi(self):
        return self._cropControlUi

    @property
    def cropListData(self):
        return self._cropControlUi.cropListModel

    def selectCrop(self, cropIndex):
        """Programmatically select the given cropIndex, which start from 0.
           Equivalent to clicking on the (cropIndex+1)'th position in the crop widget."""
        self._cropControlUi.cropListModel.select(cropIndex)

    class CroppingSlots(object):
        """
        This class serves as the parameter for the CroppingGui constructor.
        It provides the slots that the cropping GUI uses to source crops to the display and sink crops from the
        user's mouse clicks.
        """
        def __init__(self):
            # Slot to insert elements onto
            self.cropInput = None # cropInput.setInSlot(xxx)

            # Slot to read elements from
            self.cropOutput = None # cropOutput.get(roi)

            # Slot that determines which crop value corresponds to erased values
            self.cropEraserValue = None # cropEraserValue.setValue(xxx)

            # Slot that is used to request wholesale crop deletion
            self.cropDelete = None # cropDelete.setValue(xxx)

            # Slot that gives a list of crop names
            self.cropNames = None # cropNames.value

            # Slot to specify which images the user is allowed to crop.
            self.cropsAllowed = None # cropsAllowed.value == True

    def __init__(self, parentApplet, croppingSlots, topLevelOperatorView, drawerUiPath=None, rawInputSlot=None, crosshair=True):
        """
        Constructor.

        :param croppingSlots: Provides the slots needed for sourcing/sinking crop data.  See CroppingGui.CroppingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        # Do we have all the slots we need?
        assert isinstance(croppingSlots, CroppingGui.CroppingSlots)
        assert croppingSlots.cropInput is not None, "Missing a required slot."
        assert croppingSlots.cropOutput is not None, "Missing a required slot."
        assert croppingSlots.cropEraserValue is not None, "Missing a required slot."
        assert croppingSlots.cropDelete is not None, "Missing a required slot."
        assert croppingSlots.cropNames is not None, "Missing a required slot."
        assert croppingSlots.cropsAllowed is not None, "Missing a required slot."

        self.__cleanup_fns = []
        self._croppingSlots = croppingSlots
        self._minCropNumber = 0
        self._maxCropNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self.topLevelOperatorView.Crops.notifyDirty( bind(self._updateCropList) )
        self.topLevelOperatorView.Crops.notifyDirty( bind(self._updateCropList) )
        self.__cleanup_fns.append( partial( self.topLevelOperatorView.Crops.unregisterDirty, bind(self._updateCropList) ) )
        
        self._colorTable16 = colortables.default16_new
        self._programmaticallyRemovingCrops = False

        self._initCropUic(drawerUiPath)

        self._maxCropNumUsed = 0

        self._allowDeleteLastCropOnly = False
        self.__initShortcuts()
        # Init base class
        super(CroppingGui, self).__init__(parentApplet,
                                          topLevelOperatorView,
                                          [croppingSlots.cropInput, croppingSlots.cropOutput],
                                          crosshair=crosshair)
        self._croppingSlots.cropEraserValue.setValue(self.editor.brushingModel.erasingNumber)

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

    def _initCropUic(self, drawerUiPath):

        self.cropSelectionWidget = CropSelectionWidget()

        self._cropControlUi = self.cropSelectionWidget

        # Initialize the crop list model
        model = CropListModel()
        self._cropControlUi.cropListView.setModel(model)
        self._cropControlUi.cropListModel=model
        self._cropControlUi.cropListModel.rowsRemoved.connect(self._onCropRemoved)
        self._cropControlUi.cropListModel.elementSelected.connect(self._onCropSelected)
        self._cropControlUi.cropListModel.dataChanged.connect(self.onCropListDataChanged)
        self.toolButtons = None

    def _initCropListView(self):
        if self.topLevelOperatorView.Crops.value != {}:
            self._cropControlUi.cropListModel=CropListModel()
            crops = self.topLevelOperatorView.Crops.value
            for key in sorted(crops):
                newRow = self._cropControlUi.cropListModel.rowCount()
                crop = Crop(
                        key,
                        [(crops[key]["time"][0],crops[key]["starts"][0],crops[key]["starts"][1],crops[key]["starts"][2]),(crops[key]["time"][1],crops[key]["stops"][0],crops[key]["stops"][1],crops[key]["stops"][2])],
                        QColor(crops[key]["cropColor"][0],crops[key]["cropColor"][1],crops[key]["cropColor"][2]),
                        pmapColor=QColor(crops[key]["pmapColor"][0],crops[key]["pmapColor"][1],crops[key]["pmapColor"][2])
                )
                self._cropControlUi.cropListModel.insertRow( newRow, crop )

            self._cropControlUi.cropListModel.elementSelected.connect(self._onCropSelected)
            self._cropControlUi.cropListView.setModel(self._cropControlUi.cropListModel)
            self._cropControlUi.cropListView.updateGeometry()
            self._cropControlUi.cropListView.update()
            self._cropControlUi.cropListView.selectRow(0)
            self._maxCropNumUsed = len(crops)
        else:
            self.editor.cropModel.set_volume_shape_3d(self.editor.dataShape[1:4])
            self.newCrop()
            self.setCrop()

    def onCropListDataChanged(self, topLeft, bottomRight):
        """Handle changes to the crop list selections."""
        firstRow = topLeft.row()
        lastRow  = bottomRight.row()

        firstCol = topLeft.column()
        lastCol  = bottomRight.column()

        # We only care about the color column
        if firstCol <= 0 <= lastCol:
            assert(firstRow == lastRow) # Only one data item changes at a time

            #in this case, the actual data (for example color) has changed
            color = self._cropControlUi.cropListModel[firstRow].brushColor()
            self._colorTable16[firstRow+1] = color.rgba()
            self.editor.brushingModel.setBrushColor(color)

            # Update the crop layer colortable to match the list entry
            croplayer = self._getCropLayer()
            if croplayer is not None:
                croplayer.colorTable = self._colorTable16

    def __initShortcuts(self):
        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        shortcutGroupName = "Cropping"

        if hasattr(self.croppingDrawerUi, "AddCropButton"):
            mgr.register("n", ActionInfo( shortcutGroupName,
                                          "New Crop",
                                          "Add a new crop.",
                                          self.croppingDrawerUi.AddCropButton.click,
                                          self.croppingDrawerUi.AddCropButton,
                                          self.croppingDrawerUi.AddCropButton ) )

        if hasattr(self.croppingDrawerUi, "SetCropButton"):
            mgr.register("s", ActionInfo( shortcutGroupName,
                                          "Save Crop",
                                          "Save the current crop.",
                                          self.croppingDrawerUi.SetCropButton.click,
                                          self.croppingDrawerUi.SetCropButton,
                                          self.croppingDrawerUi.SetCropButton ) )

        self._cropShortcuts = []

    def _updateCropShortcuts(self):
        numShortcuts = len(self._cropShortcuts)
        numRows = len(self._cropControlUi.cropListModel)

        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts,numRows):
            toolTipObject = CropListModel.EntryToolTipAdapter(self._cropControlUi.cropListModel, i)
            action_info = ActionInfo( "Cropping", 
                                      "Select Crop {}".format(i+1),
                                      "Select Crop {}".format(i+1),
                                      partial(self._cropControlUi.cropListView.selectRow, i),
                                      self._cropControlUi.cropListView,
                                      toolTipObject )
            mgr.register( str(i+1), action_info )
            self._cropShortcuts.append( action_info )

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            action_info = self._cropShortcuts[i]
            description = "Select " + self._cropControlUi.cropListModel[i].name
            new_action_info = mgr.update_description(action_info, description)
            self._cropShortcuts[i] = new_action_info

    def hideEvent(self, event):
        """
        QT event handler.
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        super(CroppingGui, self).hideEvent(event)

    @threadRouted
    def _changeInteractionMode( self, toolId ):
         """
         Implement the GUI's response to the user selecting a new tool.
         """
         # Uncheck all the other buttons
         if self.toolButtons != None:
             for tool, button in list(self.toolButtons.items()):
                 if tool != toolId:
                     button.setChecked(False)

         # If we have no editor, we can't do anything yet
         if self.editor is None:
             return

         # If the user can't crop this image, disable the button and say why its disabled
         cropsAllowed = False

         cropsAllowedSlot = self._croppingSlots.cropsAllowed
         if cropsAllowedSlot.ready():
             cropsAllowed = cropsAllowedSlot.value

             if hasattr(self._cropControlUi, "AddCropButton"):
                 if not cropsAllowed or self._cropControlUi.cropListModel.rowCount() == self.maxCropNumber:
                     self._cropControlUi.AddCropButton.setEnabled(False)
                 if cropsAllowed:
                     self._cropControlUi.AddCropButton.setText("Add Crop")
                 else:
                     self._cropControlUi.AddCropButton.setText("(Cropping Not Allowed)")

         e = cropsAllowed & (self._cropControlUi.cropListModel.rowCount() > 0)
         self._gui_enableCropping(e)

    def _resetCropSelection(self):
        logger.debug("Resetting crop selection")
        if len(self._cropControlUi.cropListModel) > 0:
            self._cropControlUi.cropListView.selectRow(0)
        else:
            self._changeInteractionMode(Tool.Navigation)
        return True

    def _updateCropList(self):
        """
        This function is called when the number of crops has changed without our knowledge.
        We need to add/remove crops until we have the right number
        """
        # Get the number of crops in the crop data
        # (Or the number of crops the user has added.)
        names = sorted(self.topLevelOperatorView.Crops.value.keys())
        numCrops = len(names)

        # Add rows until we have the right number
        while self._cropControlUi.cropListModel.rowCount() < numCrops:
            self._addNewCrop()

        # synchronize cropNames
        for i,n in enumerate(names):
            self._cropControlUi.cropListModel[i].name = n
                
        if hasattr(self._cropControlUi, "AddCropButton"):
            self._cropControlUi.AddCropButton.setEnabled(numCrops < self.maxCropNumber)

    def _addNewCrop(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        """
        Add a new crop to the crop list GUI control.
        Return the new number of crops in the control.
        """
        color = self.getNextCropColor()
        crop = Crop( self.getNextCropName(), self.get_roi_4d(), color,
                       pmapColor=self.getNextPmapColor(),
                   )
        crop.nameChanged.connect(self._updateCropShortcuts)
        crop.nameChanged.connect(self.onCropNameChanged)
        crop.colorChanged.connect(self.onCropColorChanged)
        crop.pmapColorChanged.connect(self.onPmapColorChanged)

        newRow = self._cropControlUi.cropListModel.rowCount()
        self._cropControlUi.cropListModel.insertRow( newRow, crop )

        if self._allowDeleteLastCropOnly:
            # make previous crop unremovable
            if newRow > 0:
                self._cropControlUi.cropListModel.makeRowPermanent(newRow - 1)

        newColorIndex = self._cropControlUi.cropListModel.index(newRow, 0)
        self.onCropListDataChanged(newColorIndex, newColorIndex) # Make sure crop layer colortable is in sync with the new color

        # Update operator with new name
        operator_names = self._croppingSlots.cropNames.value

        if len(operator_names) < self._cropControlUi.cropListModel.rowCount():
            operator_names.append( crop.name )

            try:
                self._croppingSlots.cropNames.setValue( operator_names, check_changed=False )
            except:
                # I have no idea why this is, but sometimes PyQt "loses" exceptions here.
                # Print it out before it's too late!
                log_exception( logger, "Logged the above exception just in case PyQt loses it." )
                raise


        # Call the 'changed' callbacks immediately to initialize any listeners
        self.onCropNameChanged()
        self.onCropColorChanged()
        self.onPmapColorChanged()


        self._maxCropNumUsed += 1
        self._updateCropShortcuts()

        e = self._cropControlUi.cropListModel.rowCount() > 0
        QApplication.restoreOverrideCursor()

    def getNextCropName(self):
        """
        Return a suitable name for the next crop added by the user.
        Subclasses may override this.
        """
        maxNum = 0
        for index, crop in enumerate(self._cropControlUi.cropListModel):
            nums = re.findall("\d+", crop.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Crop {}".format(maxNum+1)

    def getNextPmapColor(self):
        """
        Return a QColor to use for the next crop.
        """
        return None

    def onCropNameChanged(self):
        """
        Subclasses can override this to respond to changes in the crop names.
        """
        pass

    def onCropColorChanged(self):
        """
        Subclasses can override this to respond to changes in the crop colors.
        """
        pass
    
    def onPmapColorChanged(self):
        """
        Subclasses can override this to respond to changes in a crop associated probability color.
        """
        pass

    def _removeLastCrop(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the crop list by one.
        """
        self._programmaticallyRemovingCrops = True
        numRows = self._cropControlUi.cropListModel.rowCount()

        # This will trigger the signal that calls _onCropRemoved()
        self._cropControlUi.cropListModel.removeRow(numRows-1)
        self._updateCropShortcuts()

        self._programmaticallyRemovingCrops = False

    def _clearCropListGui(self):
        # Remove rows until we have the right number
        while self._cropControlUi.cropListModel.rowCount() > 0:
            self._removeLastCrop()

    def _onCropRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingCrops:
            return

        assert start == end
        row = start

        oldcount = self._cropControlUi.cropListModel.rowCount() + 1
        # we need at least one crop
        if oldcount <= 1:
            return

        logger.debug("removing crop {} out of {}".format( row, oldcount ))

        if self._allowDeleteLastCropOnly:
            # make previous crop removable again
            if oldcount >= 2:
                self._cropControlUi.cropListModel.makeRowRemovable(oldcount - 2)

        # Remove the deleted crop's color from the color table so that renumbered crops keep their colors.
        oldColor = self._colorTable16.pop(row+1)

        # Recycle the deleted color back into the table (for the next crop to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the croplayer colortable with the new color mapping
        croplayer = self._getCropLayer()
        if croplayer is not None:
            croplayer.colorTable = self._colorTable16

        currentSelection = self._cropControlUi.cropListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post( self._resetCropSelection )

        e = self._cropControlUi.cropListModel.rowCount() > 0
        #self._gui_enableCropping(e)

        # If the gui list model isn't in sync with the operator, update the operator.
        #if len(self._croppingSlots.cropNames.value) > self._cropControlUi.cropListModel.rowCount():
        if len(self.topLevelOperatorView.Crops.value) > self._cropControlUi.cropListModel.rowCount():
            # Changing the deleteCrop input causes the operator (OpBlockedSparseArray)
            #  to search through the entire list of crops and delete the entries for the matching crop.
            #self._croppingSlots.cropDelete.setValue(row+1)
            del self.topLevelOperatorView.Crops[self._cropControlUi.cropListModel[row].name]

            # We need to "reset" the deleteCrop input to -1 when we're finished.
            #  Otherwise, you can never delete the same crop twice in a row.
            #  (Only *changes* to the input are acted upon.)
            self._croppingSlots.cropDelete.setValue(-1)

    def getLayer(self, name):
        """find a layer by name"""
        try:
            croplayer = next(filter(lambda l: l.name == name, self.layerstack))
        except StopIteration:
            return None
        else:
            return croplayer

    def _getCropLayer(self):
        return self.getLayer('Crops')

    def createCropLayer(self, direct=False):
        """
        Return a colortable layer that displays the crop slot data, along with its associated crop source.
        direct: whether this layer is drawn synchronously by volumina
        """
        cropOutput = self._croppingSlots.cropOutput
        if not cropOutput.ready():
            return (None, None)
        else:
            # Add the layer to draw the crops, but don't add any crops
            cropsrc = LazyflowSinkSource( self._croppingSlots.cropOutput,
                                           self._croppingSlots.cropInput)

            croplayer = ColortableLayer(cropsrc, colorTable = self._colorTable16, direct=direct )
            croplayer.name = "Crops"
            croplayer.ref_object = None

            return croplayer, cropsrc

    def setupLayers(self):
        """
        Sets up the crop layer for display by our base class (LayerViewerGui).
        If our subclass overrides this function to add his own layers,
        he **must** call this function explicitly.
        """
        layers = []

        # Crops
        croplayer, cropsrc = self.createCropLayer()
        if croplayer is not None:
            layers.append(croplayer)

            # Tell the editor where to draw crop data
            self.editor.setCropSink(cropsrc)

        # Side effect 1: We want to guarantee that the crop list
        #  is up-to-date before our subclass adds his layers
        self._updateCropList()

        # Side effect 2: Switch to navigation mode if crops aren't
        #  allowed on this image.
        cropsAllowedSlot = self._croppingSlots.cropsAllowed
        if cropsAllowedSlot.ready() and not cropsAllowedSlot.value:
            self._changeInteractionMode(Tool.Navigation)

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot.ready():
            layer = self.createStandardLayerFromSlot( self._rawInputSlot )
            layer.name = "Raw Input"
            layer.visible = True
            layer.opacity = 1.0

            layers.append(layer)

        return layers


    def allowDeleteLastCropOnly(self, enabled):
        """
        In the TrackingWorkflow when cropping 0/1/2/.../N mergers we do not allow
        to remove another crop but the first, as the following processing steps
        assume that all previous cell counts are given.
        """
        self._allowDeleteLastCropOnly = enabled
Beispiel #24
0
class LabelingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer
    applet with the added functionality of labeling.
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._labelControlUi

    def stopAndCleanUp(self):
        super(LabelingGui, self).stopAndCleanUp()

        for fn in self.__cleanup_fns:
            fn()

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

    @property
    def minLabelNumber(self):
        return self._minLabelNumber
    @minLabelNumber.setter
    def minLabelNumber(self, n):
        self._minLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self._addNewLabel()
    @property
    def maxLabelNumber(self):
        return self._maxLabelNumber
    @maxLabelNumber.setter
    def maxLabelNumber(self, n):
        self._maxLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self._removeLastLabel()

    @property
    def labelingDrawerUi(self):
        return self._labelControlUi

    @property
    def labelListData(self):
        return self._labelControlUi.labelListModel

    def selectLabel(self, labelIndex):
        """Programmatically select the given labelIndex, which start from 0.
           Equivalent to clicking on the (labelIndex+1)'th position in the label widget."""
        self._labelControlUi.labelListModel.select(labelIndex)

    class LabelingSlots(object):
        """
        This class serves as the parameter for the LabelingGui constructor.
        It provides the slots that the labeling GUI uses to source labels to the display and sink labels from the
        user's mouse clicks.
        """
        def __init__(self):
            # Slot to insert elements onto
            self.labelInput = None # labelInput.setInSlot(xxx)
            # Slot to read elements from
            self.labelOutput = None # labelOutput.get(roi)
            # Slot that determines which label value corresponds to erased values
            self.labelEraserValue = None # labelEraserValue.setValue(xxx)
            # Slot that is used to request wholesale label deletion
            self.labelDelete = None # labelDelete.setValue(xxx)
            # Slot that gives a list of label names
            self.labelNames = None # labelNames.value

    def __init__(self, parentApplet, labelingSlots, topLevelOperatorView, drawerUiPath=None, rawInputSlot=None,
                 crosshair=True, is_3d_widget_visible=False):
        """
        Constructor.

        :param labelingSlots: Provides the slots needed for sourcing/sinking label data.  See LabelingGui.LabelingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        self._colorTable16 = list(colortables.default16_new)

        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert labelingSlots.labelInput is not None, "Missing a required slot."
        assert labelingSlots.labelOutput is not None, "Missing a required slot."
        assert labelingSlots.labelEraserValue is not None, "Missing a required slot."
        assert labelingSlots.labelDelete is not None, "Missing a required slot."
        assert labelingSlots.labelNames is not None, "Missing a required slot."

        self.__cleanup_fns = []

        self._labelingSlots = labelingSlots
        self._minLabelNumber = 0
        self._maxLabelNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self._labelingSlots.labelNames.notifyDirty(bind(self._updateLabelList))
        self.__cleanup_fns.append(partial(self._labelingSlots.labelNames.unregisterDirty, bind(self._updateLabelList)))
        self._colorTable16 = colortables.default16_new
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self._initLabelUic(drawerUiPath)

        # Init base class
        super(LabelingGui, self).__init__(parentApplet,
                                          topLevelOperatorView,
                                          [labelingSlots.labelInput, labelingSlots.labelOutput],
                                          crosshair=crosshair,
                                          is_3d_widget_visible=is_3d_widget_visible)

        self.__initShortcuts()
        self._labelingSlots.labelEraserValue.setValue(self.editor.brushingModel.erasingNumber)
        self._allowDeleteLastLabelOnly = False
        self._forceAtLeastTwoLabels = False

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
        self._changeInteractionMode(Tool.Navigation)

    def _initLabelUic(self, drawerUiPath):
        _labelControlUi = uic.loadUi(drawerUiPath)

        # We own the applet bar ui
        self._labelControlUi = _labelControlUi

        # Initialize the label list model
        model = LabelListModel()
        _labelControlUi.labelListView.setModel(model)
        _labelControlUi.labelListModel=model
        _labelControlUi.labelListModel.rowsRemoved.connect(self._onLabelRemoved)
        _labelControlUi.labelListModel.elementSelected.connect(self._onLabelSelected)

        def handleClearRequested(row, name):
            selection = QMessageBox.warning(self, "Clear labels?",
                          "All '{}' brush strokes will be erased.  Are you sure?"
                          .format(name),
                          QMessageBox.Ok | QMessageBox.Cancel)
            if selection != QMessageBox.Ok:
                return

            # This only works if the top-level operator has a 'clearLabel' function.
            self.topLevelOperatorView.clearLabel(row+1)

        _labelControlUi.labelListView.clearRequested.connect(handleClearRequested)

        def handleLabelMergeRequested(from_row, from_name, into_row, into_name):
            from_label = from_row+1
            into_label = into_row+1
            selection = QMessageBox.warning(self, "Merge labels?",
                          "All '{}' brush strokes will be converted to '{}'.  Are you sure?"
                          .format(from_name, into_name),
                          QMessageBox.Ok | QMessageBox.Cancel)
            if selection != QMessageBox.Ok:
                return

            # This only works if the top-level operator has a 'mergeLabels' function.
            self.topLevelOperatorView.mergeLabels(from_label, into_label)

            names = list(self._labelingSlots.labelNames.value)
            names.pop(from_label-1)
            self._labelingSlots.labelNames.setValue(names)

        _labelControlUi.labelListView.mergeRequested.connect(handleLabelMergeRequested)

        # Connect Applet GUI to our event handlers
        if hasattr(_labelControlUi, "AddLabelButton"):
            _labelControlUi.AddLabelButton.setIcon(QIcon(ilastikIcons.AddSel))
            _labelControlUi.AddLabelButton.clicked.connect(bind(self._addNewLabel))
        _labelControlUi.labelListModel.dataChanged.connect(self.onLabelListDataChanged)

        # Initialize the arrow tool button with an icon and handler
        iconPath = os.path.split(__file__)[0] + "/icons/arrow.png"
        arrowIcon = QIcon(iconPath)
        _labelControlUi.arrowToolButton.setIcon(arrowIcon)
        _labelControlUi.arrowToolButton.setCheckable(True)
        _labelControlUi.arrowToolButton.clicked.connect(lambda checked: self._handleToolButtonClicked(checked, Tool.Navigation))

        # Initialize the paint tool button with an icon and handler
        paintBrushIconPath = os.path.split(__file__)[0] + "/icons/paintbrush.png"
        paintBrushIcon = QIcon(paintBrushIconPath)
        _labelControlUi.paintToolButton.setIcon(paintBrushIcon)
        _labelControlUi.paintToolButton.setCheckable(True)
        _labelControlUi.paintToolButton.clicked.connect(lambda checked: self._handleToolButtonClicked(checked, Tool.Paint))

        # Initialize the erase tool button with an icon and handler
        eraserIconPath = os.path.split(__file__)[0] + "/icons/eraser.png"
        eraserIcon = QIcon(eraserIconPath)
        _labelControlUi.eraserToolButton.setIcon(eraserIcon)
        _labelControlUi.eraserToolButton.setCheckable(True)
        _labelControlUi.eraserToolButton.clicked.connect(lambda checked: self._handleToolButtonClicked(checked, Tool.Erase))

        # Initialize the thresholding tool
        if hasattr(_labelControlUi, "thresToolButton"):
            thresholdIconPath = os.path.split(__file__)[0] \
              + "/icons/threshold.png"
            thresholdIcon = QIcon(thresholdIconPath)
            _labelControlUi.thresToolButton.setIcon(thresholdIcon)
            _labelControlUi.thresToolButton.setCheckable(True)
            _labelControlUi.thresToolButton.clicked.connect(lambda checked: self._handleToolButtonClicked(checked, Tool.Threshold))


        # This maps tool types to the buttons that enable them
        if hasattr(_labelControlUi, "thresToolButton"):
            self.toolButtons = { Tool.Navigation : _labelControlUi.arrowToolButton,
                                 Tool.Paint      : _labelControlUi.paintToolButton,
                                 Tool.Erase      : _labelControlUi.eraserToolButton,
                                 Tool.Threshold  : _labelControlUi.thresToolButton}
        else:
            self.toolButtons = { Tool.Navigation : _labelControlUi.arrowToolButton,
                                 Tool.Paint      : _labelControlUi.paintToolButton,
                                 Tool.Erase      : _labelControlUi.eraserToolButton}

        self.brushSizes = [1, 3, 5, 7, 11, 23, 31, 61]

        for size in self.brushSizes:
            _labelControlUi.brushSizeComboBox.addItem(str(size))

        _labelControlUi.brushSizeComboBox.currentIndexChanged.connect(self._onBrushSizeChange)

        self.paintBrushSizeIndex = PreferencesManager().get( 'labeling', 'paint brush size', default=0 )
        self.eraserSizeIndex = PreferencesManager().get( 'labeling', 'eraser brush size', default=4 )

    def onLabelListDataChanged(self, topLeft, bottomRight):
        """Handle changes to the label list selections."""
        firstRow = topLeft.row()
        lastRow  = bottomRight.row()

        firstCol = topLeft.column()
        lastCol  = bottomRight.column()

        # We only care about the color column
        if firstCol <= 0 <= lastCol:
            assert(firstRow == lastRow) # Only one data item changes at a time

            #in this case, the actual data (for example color) has changed
            color = self._labelControlUi.labelListModel[firstRow].brushColor()
            color_value = color.rgba()
            color_index = firstRow + 1
            if color_index< len(self._colorTable16):

                self._colorTable16[color_index] = color_value

            else:
                self._colorTable16.append(color_value)
            self.editor.brushingModel.setBrushColor(color)

            # Update the label layer colortable to match the list entry
            labellayer = self._getLabelLayer()
            if labellayer is not None:
                labellayer.colorTable = self._colorTable16

    def __initShortcuts(self):
        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        shortcutGroupName = "Labeling"

        if hasattr(self.labelingDrawerUi, "AddLabelButton"):

            mgr.register("a", ActionInfo(shortcutGroupName,
                                         "New Label",
                                         "Add New Label Class",
                                         self.labelingDrawerUi.AddLabelButton.click,
                                         self.labelingDrawerUi.AddLabelButton,
                                         self.labelingDrawerUi.AddLabelButton))

        mgr.register("n", ActionInfo(shortcutGroupName,
                                     "Navigation Cursor",
                                     "Navigation Cursor",
                                     self.labelingDrawerUi.arrowToolButton.click,
                                     self.labelingDrawerUi.arrowToolButton,
                                     self.labelingDrawerUi.arrowToolButton))

        mgr.register("b", ActionInfo( shortcutGroupName,
                                      "Brush Cursor",
                                      "Brush Cursor",
                                      self.labelingDrawerUi.paintToolButton.click,
                                      self.labelingDrawerUi.paintToolButton,
                                      self.labelingDrawerUi.paintToolButton))

        mgr.register("e", ActionInfo(shortcutGroupName,
                                     "Eraser Cursor",
                                     "Eraser Cursor",
                                     self.labelingDrawerUi.eraserToolButton.click,
                                     self.labelingDrawerUi.eraserToolButton,
                                     self.labelingDrawerUi.eraserToolButton))

        mgr.register(",", ActionInfo( shortcutGroupName,
                                      "Decrease Brush Size",
                                      "Decrease Brush Size",
                                      partial(self._tweakBrushSize, False),
                                      self.labelingDrawerUi.brushSizeComboBox,
                                      self.labelingDrawerUi.brushSizeComboBox))

        mgr.register(".", ActionInfo(shortcutGroupName,
                                     "Increase Brush Size",
                                     "Increase Brush Size",
                                     partial(self._tweakBrushSize, True),
                                     self.labelingDrawerUi.brushSizeComboBox,
                                     self.labelingDrawerUi.brushSizeComboBox))

        if hasattr(self.labelingDrawerUi, "thresToolButton"):
            mgr.register("t", ActionInfo(shortcutGroupName,
                                           "Window Leveling",
                                           "<p>Window Leveling can be used to adjust the data range used for visualization. Pressing the left mouse button while moving the mouse back and forth changes the window width (data range). Moving the mouse in the left-right plane changes the window mean. Pressing the right mouse button resets the view back to the original data.",
                                           self.labelingDrawerUi.thresToolButton.click,
                                           self.labelingDrawerUi.thresToolButton,
                                           self.labelingDrawerUi.thresToolButton))


        self._labelShortcuts = []

    def _tweakBrushSize(self, increase):
        """
        Increment or decrement the paint brush size or eraser size (depending on which is currently selected).

        increase: Bool. If True, increment.  Otherwise, decrement.
        """
        if self._toolId == Tool.Erase:
            if increase:
                self.eraserSizeIndex += 1
                self.eraserSizeIndex = min(len(self.brushSizes)-1, self.eraserSizeIndex)
            else:
                self.eraserSizeIndex -=1
                self.eraserSizeIndex = max(0, self.eraserSizeIndex)
            self._changeInteractionMode(Tool.Erase)
        else:
            if increase:
                self.paintBrushSizeIndex += 1
                self.paintBrushSizeIndex = min(len(self.brushSizes)-1, self.paintBrushSizeIndex)
            else:
                self.paintBrushSizeIndex -=1
                self.paintBrushSizeIndex = max(0, self.paintBrushSizeIndex)
            self._changeInteractionMode(Tool.Paint)

    def _updateLabelShortcuts(self):
        numShortcuts = len(self._labelShortcuts)
        numRows = len(self._labelControlUi.labelListModel)

        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts,numRows):
            toolTipObject = LabelListModel.EntryToolTipAdapter(self._labelControlUi.labelListModel, i)
            action_info = ActionInfo("Labeling",
                                     "Select Label {}".format(i+1),
                                     "Select Label {}".format(i+1),
                                     partial(self._labelControlUi.labelListView.selectRow, i),
                                     self._labelControlUi.labelListView,
                                     toolTipObject)
            mgr.register(str(i+1), action_info)
            self._labelShortcuts.append(action_info)

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            action_info = self._labelShortcuts[i]
            description = "Select " + self._labelControlUi.labelListModel[i].name
            new_action_info = mgr.update_description(action_info, description)
            self._labelShortcuts[i] = new_action_info

    def hideEvent(self, event):
        """
        QT event handler.
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        with PreferencesManager() as prefsMgr:
            prefsMgr.set('labeling', 'paint brush size', self.paintBrushSizeIndex)
            prefsMgr.set('labeling', 'eraser brush size', self.eraserSizeIndex)
        super(LabelingGui, self).hideEvent(event)

    def _handleToolButtonClicked(self, checked, toolId):
        """
        Called when the user clicks any of the "tool" buttons in the label applet bar GUI.
        """
        if not checked:
            # Users can only *switch between* tools, not turn them off.
            # If they try to turn a button off, re-select it automatically.
            self.toolButtons[toolId].setChecked(True)
        else:
            # If the user is checking a new button
            self._changeInteractionMode(toolId)

    @threadRouted
    def _changeInteractionMode(self, toolId):
        """
        Implement the GUI's response to the user selecting a new tool.
        """
        # Uncheck all the other buttons
        for tool, button in list(self.toolButtons.items()):
            if tool != toolId:
                button.setChecked(False)

        # If we have no editor, we can't do anything yet
        if self.editor is None:
            return

        # The volume editor expects one of two specific names
        if hasattr(self.labelingDrawerUi, "thresToolButton"):
            modeNames = { Tool.Navigation   : "navigation",
                          Tool.Paint        : "brushing",
                          Tool.Erase        : "brushing" ,
                          Tool.Threshold    : "thresholding"}
        else:
            modeNames = { Tool.Navigation   : "navigation",
                          Tool.Paint        : "brushing",
                          Tool.Erase        : "brushing" }

        if hasattr(self._labelControlUi, "AddLabelButton"):
            if self._labelControlUi.labelListModel.rowCount() == self.maxLabelNumber:
                self._labelControlUi.AddLabelButton.setEnabled(False)
            self._labelControlUi.AddLabelButton.setText("Add Label")

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        # Update the applet bar caption
        if toolId == Tool.Navigation:
            # update GUI
            self._gui_setNavigation()

        elif toolId == Tool.Paint:
            # If necessary, tell the brushing model to stop erasing
            if self.editor.brushingModel.erasing:
                self.editor.brushingModel.disableErasing()
            # Set the brushing size
            brushSize = self.brushSizes[self.paintBrushSizeIndex]
            self.editor.brushingModel.setBrushSize(brushSize)
            # update GUI
            self._gui_setBrushing()

        elif toolId == Tool.Erase:
            # If necessary, tell the brushing model to start erasing
            if not self.editor.brushingModel.erasing:
                self.editor.brushingModel.setErasing()
            # Set the brushing size
            eraserSize = self.brushSizes[self.eraserSizeIndex]
            self.editor.brushingModel.setBrushSize(eraserSize)
            # update GUI
            self._gui_setErasing()
        elif toolId == Tool.Threshold:
            # If necessary, tell the brushing model to stop erasing
            if self.editor.brushingModel.erasing:
                self.editor.brushingModel.disableErasing()
            # display a curser that is static while moving arrow
            self.editor.brushingModel.setBrushSize(1)
            self._gui_setThresholding()
            self.setCursor(Qt.ArrowCursor)

        self.editor.setInteractionMode(modeNames[toolId])
        self._toolId = toolId

    def _gui_setThresholding(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(False)
        self._labelControlUi.brushSizeCaption.setEnabled(False)
        self._labelControlUi.thresToolButton.setChecked(True)

    def _gui_setErasing(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(True)
        self._labelControlUi.brushSizeCaption.setEnabled(True)
        self._labelControlUi.eraserToolButton.setChecked(True)
        self._labelControlUi.brushSizeCaption.setText("Size:")
        self._labelControlUi.brushSizeComboBox.setCurrentIndex(self.eraserSizeIndex)
    def _gui_setNavigation(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(False)
        self._labelControlUi.brushSizeCaption.setEnabled(False)
        self._labelControlUi.arrowToolButton.setChecked(True)
        # self._labelControlUi.arrowToolButton.setChecked(True) # why twice?
    def _gui_setBrushing(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(True)
        self._labelControlUi.brushSizeCaption.setEnabled(True)
        # Make sure the paint button is pressed
        self._labelControlUi.paintToolButton.setChecked(True)
        # Show the brush size control and set its caption
        self._labelControlUi.brushSizeCaption.setText("Size:")
        # Make sure the GUI reflects the correct size
        self._labelControlUi.brushSizeComboBox.setCurrentIndex(self.paintBrushSizeIndex)
    def _gui_enableLabeling(self, enable):
        self._labelControlUi.paintToolButton.setEnabled(enable)
        self._labelControlUi.eraserToolButton.setEnabled(enable)
        self._labelControlUi.brushSizeCaption.setEnabled(enable)
        self._labelControlUi.brushSizeComboBox.setEnabled(enable)


    def _onBrushSizeChange(self, index):
        """
        Handle the user's new brush size selection.
        Note: The editor's brushing model currently maintains only a single
              brush size, which is used for both painting and erasing.
              However, we maintain two different sizes for the user and swap
              them depending on which tool is selected.
        """
        newSize = self.brushSizes[index]
        if self.editor.brushingModel.erasing:
            self.eraserSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)
        else:
            self.paintBrushSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)

    def _onLabelSelected(self, row):
        logger.debug("switching to label=%r" % (self._labelControlUi.labelListModel[row]))

        # If the user is selecting a label, he probably wants to be in paint mode
        self._changeInteractionMode(Tool.Paint)

        #+1 because first is transparent
        #FIXME: shouldn't be just row+1 here
        self.editor.brushingModel.setDrawnNumber(row+1)
        brushColor = self._labelControlUi.labelListModel[row].brushColor()
        self.editor.brushingModel.setBrushColor(brushColor)

    def _resetLabelSelection(self):
        logger.debug("Resetting label selection")
        if len(self._labelControlUi.labelListModel) > 0:
            self._labelControlUi.labelListView.selectRow(0)
        else:
            self._changeInteractionMode(Tool.Navigation)
        return True

    def _updateLabelList(self):
        """
        This function is called when the number of labels has changed without our knowledge.
        We need to add/remove labels until we have the right number
        """
        # Get the number of labels in the label data
        # (Or the number of the labels the user has added.)
        names = self._labelingSlots.labelNames.value
        numLabels = len(self._labelingSlots.labelNames.value)

        # Add rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() < numLabels:
            self._addNewLabel()

        # If we have too many rows, remove the rows that aren't in the list of names.
        if self._labelControlUi.labelListModel.rowCount() > len(names):
            indices_to_remove = []
            for i in range(self._labelControlUi.labelListModel.rowCount()):
                if self._labelControlUi.labelListModel[i].name not in names:
                    indices_to_remove.append(i)

            for i in reversed(indices_to_remove):
                self._labelControlUi.labelListModel.removeRow(i)

        # synchronize labelNames
        for i,n in enumerate(names):
            self._labelControlUi.labelListModel[i].name = n

        if hasattr(self._labelControlUi, "AddLabelButton"):
            self._labelControlUi.AddLabelButton.setEnabled(numLabels < self.maxLabelNumber)

    def _addNewLabel(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)

        """
        Add a new label to the label list GUI control.
        Return the new number of labels in the control.
        """
        label = Label(self.getNextLabelName(), self.getNextLabelColor(),
                      pmapColor=self.getNextPmapColor())
        label.nameChanged.connect(self._updateLabelShortcuts)
        label.nameChanged.connect(self.onLabelNameChanged)
        label.colorChanged.connect(self.onLabelColorChanged)
        label.pmapColorChanged.connect(self.onPmapColorChanged)

        newRow = self._labelControlUi.labelListModel.rowCount()
        self._labelControlUi.labelListModel.insertRow(newRow, label)

        newColorIndex = self._labelControlUi.labelListModel.index(newRow, 0)
        self.onLabelListDataChanged(newColorIndex, newColorIndex)  # Make sure label layer colortable is in sync with the new color

        # Update operator with new name
        operator_names = self._labelingSlots.labelNames.value
        if len(operator_names) < self._labelControlUi.labelListModel.rowCount():
            operator_names.append(label.name)
            try:
                self._labelingSlots.labelNames.setValue(operator_names, check_changed=False)
            except:
                # I have no idea why this is, but sometimes PyQt "loses" exceptions here.
                # Print it out before it's too late!
                log_exception(logger, "Logged the above exception just in case PyQt loses it.")
                raise

        if self._allowDeleteLastLabelOnly and self._forceAtLeastTwoLabels:
            # make previous label permanent, when we have at least three labels since the first two are always permanent
            if newRow > 2:
                self._labelControlUi.labelListModel.makeRowPermanent(newRow - 1)
        elif self._allowDeleteLastLabelOnly:
            # make previous label permanent
            if newRow > 0:
                self._labelControlUi.labelListModel.makeRowPermanent(newRow - 1)
        elif self._forceAtLeastTwoLabels:
            # if a third label is added make all labels removable
            if self._labelControlUi.labelListModel.rowCount() == 3:
                self.labelingDrawerUi.labelListModel.makeRowRemovable(0)
                self.labelingDrawerUi.labelListModel.makeRowRemovable(1)

        # Call the 'changed' callbacks immediately to initialize any listeners
        self.onLabelNameChanged()
        self.onLabelColorChanged()
        self.onPmapColorChanged()

        # Make the new label selected
        nlabels = self._labelControlUi.labelListModel.rowCount()
        selectedRow = nlabels-1
        self._labelControlUi.labelListModel.select(selectedRow)

        self._updateLabelShortcuts()

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        QApplication.restoreOverrideCursor()

    def getNextLabelName(self):
        """
        Return a suitable name for the next label added by the user.
        Subclasses may override this.
        """
        maxNum = 0
        for index, label in enumerate(self._labelControlUi.labelListModel):
            nums = re.findall("\d+", label.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Label {}".format(maxNum+1)

    def getNextLabelColor(self):
        """
        Return a QColor to use for the next label.
        """
        numLabels = len(self._labelControlUi.labelListModel)
        if numLabels >= len(self._colorTable16)-1:
            # If the color table isn't large enough to handle all our labels,
            #  append a random color
            randomColor = QColor(numpy.random.randint(0,255), numpy.random.randint(0,255), numpy.random.randint(0,255))
            self._colorTable16.append(randomColor.rgba())

        color = QColor()
        color.setRgba(self._colorTable16[numLabels+1])  # First entry is transparent (for zero label)
        return color

    def getNextPmapColor(self):
        """
        Return a QColor to use for the next label.
        """
        return None

    def onLabelNameChanged(self):
        """
        Subclasses can override this to respond to changes in the label names.
        """
        pass

    def onLabelColorChanged(self):
        """
        Subclasses can override this to respond to changes in the label colors.
        This class gets updated before, in the _updateLabelList
        """
        pass

    def onPmapColorChanged(self):
        """
        Subclasses can override this to respond to changes in a label associated probability color.
        """
        pass

    def _removeLastLabel(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the label list by one.
        """
        self._programmaticallyRemovingLabels = True
        numRows = self._labelControlUi.labelListModel.rowCount()
        # This will trigger the signal that calls _onLabelRemoved()
        self._labelControlUi.labelListModel.removeRow(numRows-1)
        self._updateLabelShortcuts()

        self._programmaticallyRemovingLabels = False

    def _clearLabelListGui(self):
        # Remove rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() > 0:
            self._removeLastLabel()

    def _onLabelRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingLabels:
            return

        assert start == end
        row = start

        oldcount = self._labelControlUi.labelListModel.rowCount() + 1
        logger.debug("removing label {} out of {}".format(row, oldcount))

        # Remove the deleted label's color from the color table so that renumbered labels keep their colors.
        oldColor = self._colorTable16.pop(row+1)

        # Recycle the deleted color back into the table (for the next label to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the labellayer colortable with the new color mapping
        labellayer = self._getLabelLayer()
        if labellayer is not None:
            labellayer.colorTable = self._colorTable16

        currentSelection = self._labelControlUi.labelListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post( self._resetLabelSelection )

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        # If the gui list model isn't in sync with the operator, update the operator.
        if len(self._labelingSlots.labelNames.value) > self._labelControlUi.labelListModel.rowCount():
            # Changing the deleteLabel input causes the operator (OpBlockedSparseArray)
            #  to search through the entire list of labels and delete the entries for the matching label.
            self._labelingSlots.labelDelete.setValue(row+1)

            # We need to "reset" the deleteLabel input to -1 when we're finished.
            #  Otherwise, you can never delete the same label twice in a row.
            #  (Only *changes* to the input are acted upon.)
            self._labelingSlots.labelDelete.setValue(-1)

            labelNames = self._labelingSlots.labelNames.value
            labelNames.pop(start)
            self._labelingSlots.labelNames.setValue(labelNames, check_changed=False)

        if self._forceAtLeastTwoLabels and self._allowDeleteLastLabelOnly:
            # make previous label removable again and always leave at least two permanent labels
            if oldcount > 3:
                self._labelControlUi.labelListModel.makeRowRemovable(oldcount - 2)
        elif self._allowDeleteLastLabelOnly:
            # make previous label removable again
            if oldcount > 1:
                self._labelControlUi.labelListModel.makeRowRemovable(oldcount - 2)
        elif self._forceAtLeastTwoLabels:
            # if there are only two labels remaining make them permanent
            if self._labelControlUi.labelListModel.rowCount() == 2:
                self.labelingDrawerUi.labelListModel.makeRowPermanent(0)
                self.labelingDrawerUi.labelListModel.makeRowPermanent(1)

    def getLayer(self, name):
        """find a layer by name"""
        try:
            labellayer = next(filter(lambda l: l.name == name, self.layerstack))
        except StopIteration:
            return None
        else:
            return labellayer

    def _getLabelLayer(self):
        return self.getLayer('Labels')

    def createLabelLayer(self, direct=False):
        """
        Return a colortable layer that displays the label slot data, along with its associated label source.
        direct: whether this layer is drawn synchronously by volumina
        """
        labelOutput = self._labelingSlots.labelOutput
        if not labelOutput.ready():
            return (None, None)
        else:
            # Add the layer to draw the labels, but don't add any labels
            labelsrc = LazyflowSinkSource( self._labelingSlots.labelOutput,
                                           self._labelingSlots.labelInput)

            labellayer = ColortableLayer(labelsrc, colorTable = self._colorTable16, direct=direct)
            labellayer.name = "Labels"
            labellayer.ref_object = None

            labellayer.contexts.append(QAction("Import...", None,
                                        triggered=partial(import_labeling_layer, labellayer, self._labelingSlots, self)))

            labellayer.shortcutRegistration = ("0", ShortcutManager.ActionInfo(
                                                        "Labeling",
                                                        "LabelVisibility",
                                                        "Show/Hide Labels",
                                                        labellayer.toggleVisible,
                                                        self.viewerControlWidget(),
                                                        labellayer))

            return labellayer, labelsrc

    def setupLayers(self):
        """
        Sets up the label layer for display by our base class (LayerViewerGui).
        If our subclass overrides this function to add his own layers,
        he **must** call this function explicitly.
        """
        layers = []

        # Labels
        labellayer, labelsrc = self.createLabelLayer()
        if labellayer is not None:
            layers.append(labellayer)

            # Tell the editor where to draw label data
            self.editor.setLabelSink(labelsrc)

        # Side effect 1: We want to guarantee that the label list
        #  is up-to-date before our subclass adds his layers
        self._updateLabelList()

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot.ready():
            layer = self.createStandardLayerFromSlot(self._rawInputSlot, name="Raw Input")
            layers.append(layer)

            if isinstance(layer, GrayscaleLayer):
                self.labelingDrawerUi.thresToolButton.show()
            else:
                self.labelingDrawerUi.thresToolButton.hide()

            def toggleTopToBottom():
                index = self.layerstack.layerIndex(layer)
                self.layerstack.selectRow(index)
                if index == 0:
                    self.layerstack.moveSelectedToBottom()
                else:
                    self.layerstack.moveSelectedToTop()

            layer.shortcutRegistration = ("i", ShortcutManager.ActionInfo(
                                                "Prediction Layers",
                                                "Bring Input To Top/Bottom",
                                                "Bring Input To Top/Bottom",
                                                toggleTopToBottom,
                                                self.viewerControlWidget(),
                                                layer))

        return layers

    def allowDeleteLastLabelOnly(self, enabled):
        """
        In the TrackingWorkflow when labeling 0/1/2/.../N mergers we do not allow
        to remove another label but the first, as the following processing steps
        assume that all previous cell counts are given.
        """
        self._allowDeleteLastLabelOnly = enabled

    def forceAtLeastTwoLabels(self, enabled):
        """
        in some workflows it makes no sense to have less than two labels.
        This setting forces to have always at least two labels.
        If there are exaclty two, they will be made unremovable
        """
        self._addNewLabel()
        self._addNewLabel()
        self.labelingDrawerUi.labelListModel.makeRowPermanent(0)
        self.labelingDrawerUi.labelListModel.makeRowPermanent(1)

        self._forceAtLeastTwoLabels = enabled
Beispiel #25
0
class CroppingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer
    applet with the added functionality of cropping.
    """

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

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._cropControlUi

    def stopAndCleanUp(self):
        super(CroppingGui, self).stopAndCleanUp()

        for fn in self.__cleanup_fns:
            fn()

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

    @property
    def minCropNumber(self):
        return self._minCropNumber

    @minCropNumber.setter
    def minCropNumber(self, n):
        self._minCropNumer = n
        while self._cropControlUi.cropListModel.rowCount() < n:
            self._addNewCrop()

    @property
    def maxCropNumber(self):
        return self._maxCropNumber

    @maxCropNumber.setter
    def maxCropNumber(self, n):
        self._maxCropNumber = n
        while self._cropControlUi.cropListModel.rowCount() < n:
            self._removeLastCrop()

    @property
    def croppingDrawerUi(self):
        return self._cropControlUi

    @property
    def cropListData(self):
        return self._cropControlUi.cropListModel

    def selectCrop(self, cropIndex):
        """Programmatically select the given cropIndex, which start from 0.
           Equivalent to clicking on the (cropIndex+1)'th position in the crop widget."""
        self._cropControlUi.cropListModel.select(cropIndex)

    class CroppingSlots(object):
        """
        This class serves as the parameter for the CroppingGui constructor.
        It provides the slots that the cropping GUI uses to source crops to the display and sink crops from the
        user's mouse clicks.
        """
        def __init__(self):
            # Slot to insert elements onto
            self.cropInput = None  # cropInput.setInSlot(xxx)

            # Slot to read elements from
            self.cropOutput = None  # cropOutput.get(roi)

            # Slot that determines which crop value corresponds to erased values
            self.cropEraserValue = None  # cropEraserValue.setValue(xxx)

            # Slot that is used to request wholesale crop deletion
            self.cropDelete = None  # cropDelete.setValue(xxx)

            # Slot that gives a list of crop names
            self.cropNames = None  # cropNames.value

            # Slot to specify which images the user is allowed to crop.
            self.cropsAllowed = None  # cropsAllowed.value == True

    def __init__(self,
                 parentApplet,
                 croppingSlots,
                 topLevelOperatorView,
                 drawerUiPath=None,
                 rawInputSlot=None,
                 crosshair=True):
        """
        Constructor.

        :param croppingSlots: Provides the slots needed for sourcing/sinking crop data.  See CroppingGui.CroppingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        # Do we have all the slots we need?
        assert isinstance(croppingSlots, CroppingGui.CroppingSlots)
        assert croppingSlots.cropInput is not None, "Missing a required slot."
        assert croppingSlots.cropOutput is not None, "Missing a required slot."
        assert croppingSlots.cropEraserValue is not None, "Missing a required slot."
        assert croppingSlots.cropDelete is not None, "Missing a required slot."
        assert croppingSlots.cropNames is not None, "Missing a required slot."
        assert croppingSlots.cropsAllowed is not None, "Missing a required slot."

        self.__cleanup_fns = []
        self._croppingSlots = croppingSlots
        self._minCropNumber = 0
        self._maxCropNumber = 99  #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self.topLevelOperatorView.Crops.notifyDirty(bind(self._updateCropList))
        self.topLevelOperatorView.Crops.notifyDirty(bind(self._updateCropList))
        self.__cleanup_fns.append(
            partial(self.topLevelOperatorView.Crops.unregisterDirty,
                    bind(self._updateCropList)))

        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingCrops = False

        self._initCropUic(drawerUiPath)

        self._maxCropNumUsed = 0

        self._allowDeleteLastCropOnly = False
        self.__initShortcuts()
        # Init base class
        super(CroppingGui, self).__init__(
            parentApplet,
            topLevelOperatorView,
            [croppingSlots.cropInput, croppingSlots.cropOutput],
            crosshair=crosshair)
        self._croppingSlots.cropEraserValue.setValue(
            self.editor.brushingModel.erasingNumber)

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)

    def _initCropUic(self, drawerUiPath):

        self.cropSelectionWidget = CropSelectionWidget()

        self._cropControlUi = self.cropSelectionWidget

        # Initialize the crop list model
        model = CropListModel()
        self._cropControlUi.cropListView.setModel(model)
        self._cropControlUi.cropListModel = model
        self._cropControlUi.cropListModel.rowsRemoved.connect(
            self._onCropRemoved)
        self._cropControlUi.cropListModel.elementSelected.connect(
            self._onCropSelected)
        self._cropControlUi.cropListModel.dataChanged.connect(
            self.onCropListDataChanged)
        self.toolButtons = None

    def _initCropListView(self):
        if self.topLevelOperatorView.Crops.value != {}:
            self._cropControlUi.cropListModel = CropListModel()
            crops = self.topLevelOperatorView.Crops.value
            for key in sorted(crops):
                newRow = self._cropControlUi.cropListModel.rowCount()
                crop = Crop(
                    key, [(crops[key]["time"][0], crops[key]["starts"][0],
                           crops[key]["starts"][1], crops[key]["starts"][2]),
                          (crops[key]["time"][1], crops[key]["stops"][0],
                           crops[key]["stops"][1], crops[key]["stops"][2])],
                    QColor(crops[key]["cropColor"][0],
                           crops[key]["cropColor"][1],
                           crops[key]["cropColor"][2]),
                    pmapColor=QColor(crops[key]["pmapColor"][0],
                                     crops[key]["pmapColor"][1],
                                     crops[key]["pmapColor"][2]))
                self._cropControlUi.cropListModel.insertRow(newRow, crop)

            self._cropControlUi.cropListModel.elementSelected.connect(
                self._onCropSelected)
            self._cropControlUi.cropListView.setModel(
                self._cropControlUi.cropListModel)
            self._cropControlUi.cropListView.updateGeometry()
            self._cropControlUi.cropListView.update()
            self._cropControlUi.cropListView.selectRow(0)
            self._maxCropNumUsed = len(crops)
        else:
            self.editor.cropModel.set_volume_shape_3d(
                self.editor.dataShape[1:4])
            self.newCrop()
            self.setCrop()

    def onCropListDataChanged(self, topLeft, bottomRight):
        """Handle changes to the crop list selections."""
        firstRow = topLeft.row()
        lastRow = bottomRight.row()

        firstCol = topLeft.column()
        lastCol = bottomRight.column()

        # We only care about the color column
        if firstCol <= 0 <= lastCol:
            assert (firstRow == lastRow
                    )  # Only one data item changes at a time

            #in this case, the actual data (for example color) has changed
            color = self._cropControlUi.cropListModel[firstRow].brushColor()
            self._colorTable16[firstRow + 1] = color.rgba()
            self.editor.brushingModel.setBrushColor(color)

            # Update the crop layer colortable to match the list entry
            croplayer = self._getCropLayer()
            if croplayer is not None:
                croplayer.colorTable = self._colorTable16

    def __initShortcuts(self):
        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        shortcutGroupName = "Cropping"

        if hasattr(self.croppingDrawerUi, "AddCropButton"):
            mgr.register(
                "n",
                ActionInfo(shortcutGroupName, "New Crop", "Add a new crop.",
                           self.croppingDrawerUi.AddCropButton.click,
                           self.croppingDrawerUi.AddCropButton,
                           self.croppingDrawerUi.AddCropButton))

        if hasattr(self.croppingDrawerUi, "SetCropButton"):
            mgr.register(
                "s",
                ActionInfo(shortcutGroupName, "Save Crop",
                           "Save the current crop.",
                           self.croppingDrawerUi.SetCropButton.click,
                           self.croppingDrawerUi.SetCropButton,
                           self.croppingDrawerUi.SetCropButton))

        self._cropShortcuts = []

    def _updateCropShortcuts(self):
        numShortcuts = len(self._cropShortcuts)
        numRows = len(self._cropControlUi.cropListModel)

        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts, numRows):
            toolTipObject = CropListModel.EntryToolTipAdapter(
                self._cropControlUi.cropListModel, i)
            action_info = ActionInfo(
                "Cropping", "Select Crop {}".format(i + 1),
                "Select Crop {}".format(i + 1),
                partial(self._cropControlUi.cropListView.selectRow,
                        i), self._cropControlUi.cropListView, toolTipObject)
            mgr.register(str(i + 1), action_info)
            self._cropShortcuts.append(action_info)

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            action_info = self._cropShortcuts[i]
            description = "Select " + self._cropControlUi.cropListModel[i].name
            new_action_info = mgr.update_description(action_info, description)
            self._cropShortcuts[i] = new_action_info

    def hideEvent(self, event):
        """
        QT event handler.
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        super(CroppingGui, self).hideEvent(event)

    @threadRouted
    def _changeInteractionMode(self, toolId):
        """
         Implement the GUI's response to the user selecting a new tool.
         """
        # Uncheck all the other buttons
        if self.toolButtons != None:
            for tool, button in self.toolButtons.items():
                if tool != toolId:
                    button.setChecked(False)

        # If we have no editor, we can't do anything yet
        if self.editor is None:
            return

        # If the user can't crop this image, disable the button and say why its disabled
        cropsAllowed = False

        cropsAllowedSlot = self._croppingSlots.cropsAllowed
        if cropsAllowedSlot.ready():
            cropsAllowed = cropsAllowedSlot.value

            if hasattr(self._cropControlUi, "AddCropButton"):
                if not cropsAllowed or self._cropControlUi.cropListModel.rowCount(
                ) == self.maxCropNumber:
                    self._cropControlUi.AddCropButton.setEnabled(False)
                if cropsAllowed:
                    self._cropControlUi.AddCropButton.setText("Add Crop")
                else:
                    self._cropControlUi.AddCropButton.setText(
                        "(Cropping Not Allowed)")

        e = cropsAllowed & (self._cropControlUi.cropListModel.rowCount() > 0)
        self._gui_enableCropping(e)

    def _resetCropSelection(self):
        logger.debug("Resetting crop selection")
        if len(self._cropControlUi.cropListModel) > 0:
            self._cropControlUi.cropListView.selectRow(0)
        else:
            self._changeInteractionMode(Tool.Navigation)
        return True

    def _updateCropList(self):
        """
        This function is called when the number of crops has changed without our knowledge.
        We need to add/remove crops until we have the right number
        """
        # Get the number of crops in the crop data
        # (Or the number of crops the user has added.)
        names = sorted(self.topLevelOperatorView.Crops.value.keys())
        numCrops = len(names)

        # Add rows until we have the right number
        while self._cropControlUi.cropListModel.rowCount() < numCrops:
            self._addNewCrop()

        # If we have too many rows, remove the rows that aren't in the list of names.
        if self._cropControlUi.cropListModel.rowCount() > len(names):
            indices_to_remove = []
            for i in range(self._cropControlUi.cropListModel.rowCount()):
                if self._cropControlUi.cropListModel[i].name not in names:
                    indices_to_remove.append(i)

            for i in reversed(indices_to_remove):
                self._cropControlUi.cropListModel.removeRow(i)

        # synchronize cropNames
        for i, n in enumerate(names):
            self._cropControlUi.cropListModel[i].name = n

        if hasattr(self._cropControlUi, "AddCropButton"):
            self._cropControlUi.AddCropButton.setEnabled(
                numCrops < self.maxCropNumber)

    def _addNewCrop(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        """
        Add a new crop to the crop list GUI control.
        Return the new number of crops in the control.
        """
        color = self.getNextCropColor()
        crop = Crop(
            self.getNextCropName(),
            self.get_roi_4d(),
            color,
            pmapColor=self.getNextPmapColor(),
        )
        crop.nameChanged.connect(self._updateCropShortcuts)
        crop.nameChanged.connect(self.onCropNameChanged)
        crop.colorChanged.connect(self.onCropColorChanged)
        crop.pmapColorChanged.connect(self.onPmapColorChanged)

        newRow = self._cropControlUi.cropListModel.rowCount()
        self._cropControlUi.cropListModel.insertRow(newRow, crop)

        if self._allowDeleteLastCropOnly:
            # make previous crop unremovable
            if newRow > 0:
                self._cropControlUi.cropListModel.makeRowPermanent(newRow - 1)

        newColorIndex = self._cropControlUi.cropListModel.index(newRow, 0)
        self.onCropListDataChanged(
            newColorIndex, newColorIndex
        )  # Make sure crop layer colortable is in sync with the new color

        # Update operator with new name
        operator_names = self._croppingSlots.cropNames.value

        if len(operator_names) < self._cropControlUi.cropListModel.rowCount():
            operator_names.append(crop.name)

            try:
                self._croppingSlots.cropNames.setValue(operator_names,
                                                       check_changed=False)
            except:
                # I have no idea why this is, but sometimes PyQt "loses" exceptions here.
                # Print it out before it's too late!
                log_exception(
                    logger,
                    "Logged the above exception just in case PyQt loses it.")
                raise

        # Call the 'changed' callbacks immediately to initialize any listeners
        self.onCropNameChanged()
        self.onCropColorChanged()
        self.onPmapColorChanged()

        self._maxCropNumUsed += 1
        self._updateCropShortcuts()

        e = self._cropControlUi.cropListModel.rowCount() > 0
        QApplication.restoreOverrideCursor()

    def getNextCropName(self):
        """
        Return a suitable name for the next crop added by the user.
        Subclasses may override this.
        """
        maxNum = 0
        for index, crop in enumerate(self._cropControlUi.cropListModel):
            nums = re.findall("\d+", crop.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Crop {}".format(maxNum + 1)

    def getNextPmapColor(self):
        """
        Return a QColor to use for the next crop.
        """
        return None

    def onCropNameChanged(self):
        """
        Subclasses can override this to respond to changes in the crop names.
        """
        pass

    def onCropColorChanged(self):
        """
        Subclasses can override this to respond to changes in the crop colors.
        """
        pass

    def onPmapColorChanged(self):
        """
        Subclasses can override this to respond to changes in a crop associated probability color.
        """
        pass

    def _removeLastCrop(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the crop list by one.
        """
        self._programmaticallyRemovingCrops = True
        numRows = self._cropControlUi.cropListModel.rowCount()

        # This will trigger the signal that calls _onCropRemoved()
        self._cropControlUi.cropListModel.removeRow(numRows - 1)
        self._updateCropShortcuts()

        self._programmaticallyRemovingCrops = False

    def _clearCropListGui(self):
        # Remove rows until we have the right number
        while self._cropControlUi.cropListModel.rowCount() > 0:
            self._removeLastCrop()

    def _onCropRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingCrops:
            return

        assert start == end
        row = start

        oldcount = self._cropControlUi.cropListModel.rowCount() + 1
        # we need at least one crop
        if oldcount <= 1:
            return

        logger.debug("removing crop {} out of {}".format(row, oldcount))

        if self._allowDeleteLastCropOnly:
            # make previous crop removable again
            if oldcount >= 2:
                self._cropControlUi.cropListModel.makeRowRemovable(oldcount -
                                                                   2)

        # Remove the deleted crop's color from the color table so that renumbered crops keep their colors.
        oldColor = self._colorTable16.pop(row + 1)

        # Recycle the deleted color back into the table (for the next crop to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the croplayer colortable with the new color mapping
        croplayer = self._getCropLayer()
        if croplayer is not None:
            croplayer.colorTable = self._colorTable16

        currentSelection = self._cropControlUi.cropListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post(self._resetCropSelection)

        e = self._cropControlUi.cropListModel.rowCount() > 0
        #self._gui_enableCropping(e)

        # If the gui list model isn't in sync with the operator, update the operator.
        #if len(self._croppingSlots.cropNames.value) > self._cropControlUi.cropListModel.rowCount():
        if len(self.topLevelOperatorView.Crops.value
               ) > self._cropControlUi.cropListModel.rowCount():
            # Changing the deleteCrop input causes the operator (OpBlockedSparseArray)
            #  to search through the entire list of crops and delete the entries for the matching crop.
            #self._croppingSlots.cropDelete.setValue(row+1)
            del self.topLevelOperatorView.Crops[
                self._cropControlUi.cropListModel[row].name]

            # We need to "reset" the deleteCrop input to -1 when we're finished.
            #  Otherwise, you can never delete the same crop twice in a row.
            #  (Only *changes* to the input are acted upon.)
            self._croppingSlots.cropDelete.setValue(-1)

    def getLayer(self, name):
        """find a layer by name"""
        try:
            croplayer = itertools.ifilter(lambda l: l.name == name,
                                          self.layerstack).next()
        except StopIteration:
            return None
        else:
            return croplayer

    def _getCropLayer(self):
        return self.getLayer('Crops')

    def createCropLayer(self, direct=False):
        """
        Return a colortable layer that displays the crop slot data, along with its associated crop source.
        direct: whether this layer is drawn synchronously by volumina
        """
        cropOutput = self._croppingSlots.cropOutput
        if not cropOutput.ready():
            return (None, None)
        else:
            # Add the layer to draw the crops, but don't add any crops
            cropsrc = LazyflowSinkSource(self._croppingSlots.cropOutput,
                                         self._croppingSlots.cropInput)

            croplayer = ColortableLayer(cropsrc,
                                        colorTable=self._colorTable16,
                                        direct=direct)
            croplayer.name = "Crops"
            croplayer.ref_object = None

            return croplayer, cropsrc

    def setupLayers(self):
        """
        Sets up the crop layer for display by our base class (LayerViewerGui).
        If our subclass overrides this function to add his own layers,
        he **must** call this function explicitly.
        """
        layers = []

        # Crops
        croplayer, cropsrc = self.createCropLayer()
        if croplayer is not None:
            layers.append(croplayer)

            # Tell the editor where to draw crop data
            self.editor.setCropSink(cropsrc)

        # Side effect 1: We want to guarantee that the crop list
        #  is up-to-date before our subclass adds his layers
        self._updateCropList()

        # Side effect 2: Switch to navigation mode if crops aren't
        #  allowed on this image.
        cropsAllowedSlot = self._croppingSlots.cropsAllowed
        if cropsAllowedSlot.ready() and not cropsAllowedSlot.value:
            self._changeInteractionMode(Tool.Navigation)

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot.ready():
            layer = self.createStandardLayerFromSlot(self._rawInputSlot)
            layer.name = "Raw Input"
            layer.visible = True
            layer.opacity = 1.0

            layers.append(layer)

        return layers

    @staticmethod
    def _createDefault16ColorColorTable():
        colors = []
        # Transparent for the zero crop
        colors.append(QColor(0, 0, 0, 0))
        # ilastik v0.5 colors
        colors.append(QColor(Qt.red))
        colors.append(QColor(Qt.green))
        colors.append(QColor(Qt.yellow))
        colors.append(QColor(Qt.blue))
        colors.append(QColor(Qt.magenta))
        colors.append(QColor(Qt.darkYellow))
        colors.append(QColor(Qt.lightGray))
        # Additional colors
        colors.append(QColor(255, 105, 180))  #hot pink
        colors.append(QColor(102, 205, 170))  #dark aquamarine
        colors.append(QColor(165, 42, 42))  #brown
        colors.append(QColor(0, 0, 128))  #navy
        colors.append(QColor(255, 165, 0))  #orange
        colors.append(QColor(173, 255, 47))  #green-yellow
        colors.append(QColor(128, 0, 128))  #purple
        colors.append(QColor(240, 230, 140))  #khaki
        assert len(colors) == 16
        return [c.rgba() for c in colors]

    def allowDeleteLastCropOnly(self, enabled):
        """
        In the TrackingWorkflow when cropping 0/1/2/.../N mergers we do not allow
        to remove another crop but the first, as the following processing steps
        assume that all previous cell counts are given.
        """
        self._allowDeleteLastCropOnly = enabled
class VigraWatershedViewerGui(LayerViewerGui):
    """
    """

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

    def appletDrawer(self):
        return self.getAppletDrawerUi()

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

    # (Other methods already provided by our base class)

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

    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        super(VigraWatershedViewerGui, self).__init__(parentApplet, topLevelOperatorView)
        self.topLevelOperatorView = topLevelOperatorView
        op = self.topLevelOperatorView

        op.FreezeCache.setValue(True)
        op.OverrideLabels.setValue({0: (0, 0, 0, 0)})

        # Default settings (will be overwritten by serializer)
        op.InputChannelIndexes.setValue([])
        op.SeedThresholdValue.setValue(0.0)
        op.MinSeedSize.setValue(0)

        # Init padding gui updates
        blockPadding = PreferencesManager().get("vigra watershed viewer", "block padding", 10)
        op.WatershedPadding.notifyDirty(self.updatePaddingGui)
        op.WatershedPadding.setValue(blockPadding)
        self.updatePaddingGui()

        # Init block shape gui updates
        cacheBlockShape = PreferencesManager().get("vigra watershed viewer", "cache block shape", (256, 10))
        op.CacheBlockShape.notifyDirty(self.updateCacheBlockGui)
        op.CacheBlockShape.setValue(tuple(cacheBlockShape))
        self.updateCacheBlockGui()

        # Init seeds gui updates
        op.SeedThresholdValue.notifyDirty(self.updateSeedGui)
        op.SeedThresholdValue.notifyReady(self.updateSeedGui)
        op.SeedThresholdValue.notifyUnready(self.updateSeedGui)
        op.MinSeedSize.notifyDirty(self.updateSeedGui)
        self.updateSeedGui()

        # Init input channel gui updates
        op.InputChannelIndexes.notifyDirty(self.updateInputChannelGui)
        op.InputChannelIndexes.setValue([0])
        op.InputImage.notifyMetaChanged(bind(self.updateInputChannelGui))
        self.updateInputChannelGui()

        self.thunkEventHandler = ThunkEventHandler(self)

        # Remember to unsubscribe during shutdown
        self.__cleanup_fns = []
        self.__cleanup_fns.append(partial(op.WatershedPadding.unregisterDirty, self.updatePaddingGui))
        self.__cleanup_fns.append(partial(op.CacheBlockShape.unregisterDirty, self.updateCacheBlockGui))
        self.__cleanup_fns.append(partial(op.SeedThresholdValue.unregisterDirty, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.SeedThresholdValue.unregisterReady, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.SeedThresholdValue.unregisterUnready, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.MinSeedSize.unregisterDirty, self.updateSeedGui))
        self.__cleanup_fns.append(partial(op.InputChannelIndexes.unregisterDirty, self.updateInputChannelGui))
        self.__cleanup_fns.append(partial(op.InputImage.unregisterDirty, self.updateInputChannelGui))

    def initAppletDrawerUi(self):
        # Load the ui file (find it in our own directory)
        localDir = os.path.split(__file__)[0]
        self._drawer = uic.loadUi(localDir + "/drawer.ui")

        # Input channels
        self._inputChannelCheckboxes = []
        self._inputChannelCheckboxes.append(self._drawer.input_ch0)
        self._inputChannelCheckboxes.append(self._drawer.input_ch1)
        self._inputChannelCheckboxes.append(self._drawer.input_ch2)
        self._inputChannelCheckboxes.append(self._drawer.input_ch3)
        self._inputChannelCheckboxes.append(self._drawer.input_ch4)
        self._inputChannelCheckboxes.append(self._drawer.input_ch5)
        self._inputChannelCheckboxes.append(self._drawer.input_ch6)
        self._inputChannelCheckboxes.append(self._drawer.input_ch7)
        self._inputChannelCheckboxes.append(self._drawer.input_ch8)
        self._inputChannelCheckboxes.append(self._drawer.input_ch9)
        for checkbox in self._inputChannelCheckboxes:
            checkbox.toggled.connect(self.onInputSelectionsChanged)

        # Seed thresholds
        self._drawer.useSeedsCheckbox.toggled.connect(self.onUseSeedsToggled)
        self._drawer.seedThresholdSpinBox.valueChanged.connect(self.onSeedThresholdChanged)

        # Seed size
        self._drawer.seedSizeSpinBox.valueChanged.connect(self.onSeedSizeChanged)

        # Padding
        self._drawer.updateWatershedsButton.clicked.connect(self.onUpdateWatershedsButton)
        self._drawer.paddingSlider.valueChanged.connect(self.onPaddingChanged)
        self._drawer.paddingSpinBox.valueChanged.connect(self.onPaddingChanged)

        # Block shape
        self._drawer.blockWidthSpinBox.valueChanged.connect(self.onBlockShapeChanged)
        self._drawer.blockDepthSpinBox.valueChanged.connect(self.onBlockShapeChanged)

    def getAppletDrawerUi(self):
        return self._drawer

    def hideEvent(self, event):
        """
        This GUI is being hidden because the user selected another applet or the window is closing.
        Save all preferences.
        """
        if self.topLevelOperatorView.CacheBlockShape.ready() and self.topLevelOperatorView.WatershedPadding.ready():
            with PreferencesManager() as prefsMgr:
                prefsMgr.set(
                    "vigra watershed viewer",
                    "cache block shape",
                    tuple(self.topLevelOperatorView.CacheBlockShape.value),
                )
                prefsMgr.set(
                    "vigra watershed viewer", "block padding", self.topLevelOperatorView.WatershedPadding.value
                )
        super(VigraWatershedViewerGui, self).hideEvent(event)

    def setupLayers(self):
        ActionInfo = ShortcutManager.ActionInfo
        layers = []

        self.updateInputChannelGui()

        # Show the watershed data
        outputImageSlot = self.topLevelOperatorView.ColoredPixels
        if outputImageSlot.ready():
            outputLayer = self.createStandardLayerFromSlot(outputImageSlot, lastChannelIsAlpha=True)
            outputLayer.name = "Watershed"
            outputLayer.visible = True
            outputLayer.opacity = 0.5
            outputLayer.shortcutRegistration = (
                "w",
                ActionInfo(
                    "Watershed Layers",
                    "Show/Hide Watershed",
                    "Show/Hide Watershed",
                    outputLayer.toggleVisible,
                    self,
                    outputLayer,
                ),
            )
            layers.append(outputLayer)

        # Show the watershed seeds
        seedSlot = self.topLevelOperatorView.ColoredSeeds
        if seedSlot.ready():
            seedLayer = self.createStandardLayerFromSlot(seedSlot, lastChannelIsAlpha=True)
            seedLayer.name = "Watershed Seeds"
            seedLayer.visible = True
            seedLayer.opacity = 0.5
            seedLayer.shortcutRegistration = (
                "s",
                ActionInfo(
                    "Watershed Layers",
                    "Show/Hide Watershed Seeds",
                    "Show/Hide Watershed Seeds",
                    seedLayer.toggleVisible,
                    self.viewerControlWidget(),
                    seedLayer,
                ),
            )
            layers.append(seedLayer)

        selectedInputImageSlot = self.topLevelOperatorView.SelectedInputChannels
        if selectedInputImageSlot.ready():
            # Show the summed input if there's more than one input channel
            if len(selectedInputImageSlot) > 1:
                summedSlot = self.topLevelOperatorView.SummedInput
                if summedSlot.ready():
                    sumLayer = self.createStandardLayerFromSlot(summedSlot)
                    sumLayer.name = "Summed Input"
                    sumLayer.visible = True
                    sumLayer.opacity = 1.0
                    layers.append(sumLayer)

            # Show selected input channels
            inputChannelIndexes = self.topLevelOperatorView.InputChannelIndexes.value
            for channel, slot in enumerate(selectedInputImageSlot):
                inputLayer = self.createStandardLayerFromSlot(slot)
                inputLayer.name = "Input (Ch.{})".format(inputChannelIndexes[channel])
                inputLayer.visible = True
                inputLayer.opacity = 1.0
                layers.append(inputLayer)

        # Show the raw input (if provided)
        rawImageSlot = self.topLevelOperatorView.RawImage
        if rawImageSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(rawImageSlot)
            rawLayer.name = "Raw Image"
            rawLayer.visible = True
            rawLayer.opacity = 1.0

            def toggleTopToBottom():
                index = self.layerstack.layerIndex(rawLayer)
                self.layerstack.selectRow(index)
                if index == 0:
                    self.layerstack.moveSelectedToBottom()
                else:
                    self.layerstack.moveSelectedToTop()

            rawLayer.shortcutRegistration = (
                "i",
                ActionInfo(
                    "Watershed Layers",
                    "Bring Raw Data To Top/Bottom",
                    "Bring Raw Data To Top/Bottom",
                    toggleTopToBottom,
                    self.viewerControlWidget(),
                    rawLayer,
                ),
            )
            layers.append(rawLayer)

        return layers

    @pyqtSlot()
    def onUpdateWatershedsButton(self):
        def updateThread():
            """
            Temporarily unfreeze the cache and freeze it again after the views are finished rendering.
            """
            self.topLevelOperatorView.FreezeCache.setValue(False)
            self.topLevelOperatorView.opWatershed.clearMaxLabels()

            # Force the cache to update.
            self.topLevelOperatorView.InputImage.setDirty(slice(None))

            # Wait for the image to be rendered into all three image views
            time.sleep(2)
            for imgView in self.editor.imageViews:
                imgView.scene().joinRenderingAllTiles()
            self.topLevelOperatorView.FreezeCache.setValue(True)

            self.updateSupervoxelStats()

        th = threading.Thread(target=updateThread)
        th.start()

    def updateSupervoxelStats(self):
        """
        Use the accumulated state in the watershed operator to display the stats for the most recent watershed computation.
        """
        totalVolume = 0
        totalCount = 0
        for (start, stop), maxLabel in self.topLevelOperatorView.opWatershed.maxLabels.items():
            blockshape = numpy.subtract(stop, start)
            vol = numpy.prod(blockshape)
            totalVolume += vol

            totalCount += maxLabel

        vol_caption = "Refresh Volume: {} megavox".format(totalVolume / float(1000 * 1000))
        count_caption = "Supervoxel Count: {}".format(totalCount)
        if totalVolume != 0:
            density_caption = "Density: {} supervox/megavox".format(totalCount * float(1000 * 1000) / totalVolume)
        else:
            density_caption = ""

        # Update the GUI text, but do it in the GUI thread (even if we were called from a worker thread)
        self.thunkEventHandler.post(self._drawer.refreshVolumeLabel.setText, vol_caption)
        self.thunkEventHandler.post(self._drawer.superVoxelCountLabel.setText, count_caption)
        self.thunkEventHandler.post(self._drawer.densityLabel.setText, density_caption)

    def getLabelAt(self, position5d):
        labelSlot = self.topLevelOperatorView.WatershedLabels
        if labelSlot.ready():
            labelData = labelSlot[index2slice(position5d)].wait()
            return labelData.squeeze()[()]
        else:
            return None

    def handleEditorLeftClick(self, position5d, globalWindowCoordinate):
        """
        This is an override from the base class.  Called when the user clicks in the volume.
        
        For left clicks, we highlight the clicked label.
        """
        label = self.getLabelAt(position5d)
        if label != 0 and label is not None:
            overrideSlot = self.topLevelOperatorView.OverrideLabels
            overrides = copy.copy(overrideSlot.value)
            overrides[label] = (255, 255, 255, 255)
            overrideSlot.setValue(overrides)

    def handleEditorRightClick(self, position5d, globalWindowCoordinate):
        """
        This is an override from the base class.  Called when the user clicks in the volume.
        
        For right clicks, we un-highlight the clicked label.
        """
        label = self.getLabelAt(position5d)
        overrideSlot = self.topLevelOperatorView.OverrideLabels
        overrides = copy.copy(overrideSlot.value)
        if label != 0 and label in overrides:
            del overrides[label]
            overrideSlot.setValue(overrides)

    ##
    ## GUI -> Operator
    ##
    def onPaddingChanged(self, value):
        self.topLevelOperatorView.WatershedPadding.setValue(value)

    def onBlockShapeChanged(self, value):
        width = self._drawer.blockWidthSpinBox.value()
        depth = self._drawer.blockDepthSpinBox.value()
        self.topLevelOperatorView.CacheBlockShape.setValue((width, depth))

    def onInputSelectionsChanged(self):
        inputImageSlot = self.topLevelOperatorView.InputImage
        if inputImageSlot.ready():
            channelAxis = inputImageSlot.meta.axistags.channelIndex
            numInputChannels = inputImageSlot.meta.shape[channelAxis]
        else:
            numInputChannels = 0
        channels = []
        for i, checkbox in enumerate(self._inputChannelCheckboxes[0:numInputChannels]):
            if checkbox.isChecked():
                channels.append(i)

        self.topLevelOperatorView.InputChannelIndexes.setValue(channels)

    def onUseSeedsToggled(self):
        self.updateSeeds()

    def onSeedThresholdChanged(self):
        self.updateSeeds()

    def onSeedSizeChanged(self):
        self.updateSeeds()

    def updateSeeds(self):
        useSeeds = self._drawer.useSeedsCheckbox.isChecked()
        self._drawer.seedThresholdSpinBox.setEnabled(useSeeds)
        self._drawer.seedSizeSpinBox.setEnabled(useSeeds)
        if useSeeds:
            threshold = self._drawer.seedThresholdSpinBox.value()
            minSize = self._drawer.seedSizeSpinBox.value()
            self.topLevelOperatorView.SeedThresholdValue.setValue(threshold)
            self.topLevelOperatorView.MinSeedSize.setValue(minSize)
        else:
            self.topLevelOperatorView.SeedThresholdValue.disconnect()

    ##
    ## Operator -> GUI
    ##
    def updatePaddingGui(self, *args):
        padding = self.topLevelOperatorView.WatershedPadding.value
        self._drawer.paddingSlider.setValue(padding)
        self._drawer.paddingSpinBox.setValue(padding)

    def updateCacheBlockGui(self, *args):
        width, depth = self.topLevelOperatorView.CacheBlockShape.value
        self._drawer.blockWidthSpinBox.setValue(width)
        self._drawer.blockDepthSpinBox.setValue(depth)

    def updateSeedGui(self, *args):
        useSeeds = self.topLevelOperatorView.SeedThresholdValue.ready()
        self._drawer.seedThresholdSpinBox.setEnabled(useSeeds)
        self._drawer.seedSizeSpinBox.setEnabled(useSeeds)
        self._drawer.useSeedsCheckbox.setChecked(useSeeds)
        if useSeeds:
            threshold = self.topLevelOperatorView.SeedThresholdValue.value
            minSize = self.topLevelOperatorView.MinSeedSize.value
            self._drawer.seedThresholdSpinBox.setValue(threshold)
            self._drawer.seedSizeSpinBox.setValue(minSize)

    def updateInputChannelGui(self, *args):
        # Show only checkboxes that can be used (limited by number of input channels)
        numChannels = 0
        inputImageSlot = self.topLevelOperatorView.InputImage
        if inputImageSlot.ready():
            channelAxis = inputImageSlot.meta.axistags.channelIndex
            numChannels = inputImageSlot.meta.shape[channelAxis]
        for i, checkbox in enumerate(self._inputChannelCheckboxes):
            #            if i >= numChannels:
            #                checkbox.setChecked(False)
            if not sip.isdeleted(checkbox):
                checkbox.setVisible(i < numChannels)

        # Make sure the correct boxes are checked
        if self.topLevelOperatorView.InputChannelIndexes.ready():
            inputChannels = self.topLevelOperatorView.InputChannelIndexes.value
            for i, checkbox in enumerate(self._inputChannelCheckboxes):
                if not sip.isdeleted(checkbox):
                    checkbox.setChecked(i in inputChannels)
Beispiel #27
0
class LabelingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer
    applet with the added functionality of labeling.
    """

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

    def centralWidget(self):
        return self

    def appletDrawer(self):
        return self._labelControlUi

    def stopAndCleanUp(self):
        super(LabelingGui, self).stopAndCleanUp()

        for fn in self.__cleanup_fns:
            fn()

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

    @property
    def minLabelNumber(self):
        return self._minLabelNumber

    @minLabelNumber.setter
    def minLabelNumber(self, n):
        self._minLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self._addNewLabel()

    @property
    def maxLabelNumber(self):
        return self._maxLabelNumber

    @maxLabelNumber.setter
    def maxLabelNumber(self, n):
        self._maxLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self._removeLastLabel()

    @property
    def labelingDrawerUi(self):
        return self._labelControlUi

    @property
    def labelListData(self):
        return self._labelControlUi.labelListModel

    def selectLabel(self, labelIndex):
        """Programmatically select the given labelIndex, which start from 0.
           Equivalent to clicking on the (labelIndex+1)'th position in the label widget."""
        self._labelControlUi.labelListModel.select(labelIndex)

    class LabelingSlots(object):
        """
        This class serves as the parameter for the LabelingGui constructor.
        It provides the slots that the labeling GUI uses to source labels to the display and sink labels from the
        user's mouse clicks.
        """
        def __init__(self):
            # Slot to insert elements onto
            self.labelInput = None  # labelInput.setInSlot(xxx)
            # Slot to read elements from
            self.labelOutput = None  # labelOutput.get(roi)
            # Slot that determines which label value corresponds to erased values
            self.labelEraserValue = None  # labelEraserValue.setValue(xxx)
            # Slot that is used to request wholesale label deletion
            self.labelDelete = None  # labelDelete.setValue(xxx)
            # Slot that gives a list of label names
            self.labelNames = None  # labelNames.value

    def __init__(
        self,
        parentApplet,
        labelingSlots,
        topLevelOperatorView,
        drawerUiPath=None,
        rawInputSlot=None,
        crosshair=True,
        is_3d_widget_visible=False,
    ):
        """
        Constructor.

        :param labelingSlots: Provides the slots needed for sourcing/sinking label data.  See LabelingGui.LabelingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        self._colorTable16 = list(colortables.default16_new)

        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert labelingSlots.labelInput is not None, "Missing a required slot."
        assert labelingSlots.labelOutput is not None, "Missing a required slot."
        assert labelingSlots.labelEraserValue is not None, "Missing a required slot."
        assert labelingSlots.labelDelete is not None, "Missing a required slot."
        assert labelingSlots.labelNames is not None, "Missing a required slot."

        self.__cleanup_fns = []

        self._labelingSlots = labelingSlots
        self._minLabelNumber = 0
        self._maxLabelNumber = 99  # 100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self._labelingSlots.labelNames.notifyDirty(bind(self._updateLabelList))
        self.__cleanup_fns.append(
            partial(self._labelingSlots.labelNames.unregisterDirty,
                    bind(self._updateLabelList)))
        self._colorTable16 = colortables.default16_new
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + "/labelingDrawer.ui"
        self._initLabelUic(drawerUiPath)

        # Init base class
        super(LabelingGui, self).__init__(
            parentApplet,
            topLevelOperatorView,
            [labelingSlots.labelInput, labelingSlots.labelOutput],
            crosshair=crosshair,
            is_3d_widget_visible=is_3d_widget_visible,
        )

        self.__initShortcuts()
        self._labelingSlots.labelEraserValue.setValue(
            self.editor.brushingModel.erasingNumber)
        self._allowDeleteLastLabelOnly = False
        self._forceAtLeastTwoLabels = False

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
        self._changeInteractionMode(Tool.Navigation)

    def _initLabelUic(self, drawerUiPath):
        _labelControlUi = uic.loadUi(drawerUiPath)

        # We own the applet bar ui
        self._labelControlUi = _labelControlUi

        # Initialize the label list model
        model = LabelListModel()
        _labelControlUi.labelListView.setModel(model)
        _labelControlUi.labelListModel = model
        _labelControlUi.labelListModel.rowsRemoved.connect(
            self._onLabelRemoved)
        _labelControlUi.labelListModel.elementSelected.connect(
            self._onLabelSelected)

        def handleClearRequested(row, name):
            selection = QMessageBox.warning(
                self,
                "Clear labels?",
                "All '{}' brush strokes will be erased.  Are you sure?".format(
                    name),
                QMessageBox.Ok | QMessageBox.Cancel,
            )
            if selection != QMessageBox.Ok:
                return

            # This only works if the top-level operator has a 'clearLabel' function.
            self.topLevelOperatorView.clearLabel(row + 1)

        _labelControlUi.labelListView.clearRequested.connect(
            handleClearRequested)

        def handleLabelMergeRequested(from_row, from_name, into_row,
                                      into_name):
            from_label = from_row + 1
            into_label = into_row + 1
            selection = QMessageBox.warning(
                self,
                "Merge labels?",
                "All '{}' brush strokes will be converted to '{}'.  Are you sure?"
                .format(from_name, into_name),
                QMessageBox.Ok | QMessageBox.Cancel,
            )
            if selection != QMessageBox.Ok:
                return

            # This only works if the top-level operator has a 'mergeLabels' function.
            self.topLevelOperatorView.mergeLabels(from_label, into_label)

            names = list(self._labelingSlots.labelNames.value)
            names.pop(from_label - 1)
            self._labelingSlots.labelNames.setValue(names)

        _labelControlUi.labelListView.mergeRequested.connect(
            handleLabelMergeRequested)

        # Connect Applet GUI to our event handlers
        if hasattr(_labelControlUi, "AddLabelButton"):
            _labelControlUi.AddLabelButton.setIcon(QIcon(ilastikIcons.AddSel))
            _labelControlUi.AddLabelButton.clicked.connect(
                bind(self._addNewLabel))
        _labelControlUi.labelListModel.dataChanged.connect(
            self.onLabelListDataChanged)

        # Initialize the arrow tool button with an icon and handler
        iconPath = os.path.split(__file__)[0] + "/icons/arrow.png"
        arrowIcon = QIcon(iconPath)
        _labelControlUi.arrowToolButton.setIcon(arrowIcon)
        _labelControlUi.arrowToolButton.setCheckable(True)
        _labelControlUi.arrowToolButton.clicked.connect(
            lambda checked: self._handleToolButtonClicked(
                checked, Tool.Navigation))

        # Initialize the paint tool button with an icon and handler
        paintBrushIconPath = os.path.split(
            __file__)[0] + "/icons/paintbrush.png"
        paintBrushIcon = QIcon(paintBrushIconPath)
        _labelControlUi.paintToolButton.setIcon(paintBrushIcon)
        _labelControlUi.paintToolButton.setCheckable(True)
        _labelControlUi.paintToolButton.clicked.connect(
            lambda checked: self._handleToolButtonClicked(checked, Tool.Paint))

        # Initialize the erase tool button with an icon and handler
        eraserIconPath = os.path.split(__file__)[0] + "/icons/eraser.png"
        eraserIcon = QIcon(eraserIconPath)
        _labelControlUi.eraserToolButton.setIcon(eraserIcon)
        _labelControlUi.eraserToolButton.setCheckable(True)
        _labelControlUi.eraserToolButton.clicked.connect(
            lambda checked: self._handleToolButtonClicked(checked, Tool.Erase))

        # Initialize the thresholding tool
        if hasattr(_labelControlUi, "thresToolButton"):
            thresholdIconPath = os.path.split(
                __file__)[0] + "/icons/threshold.png"
            thresholdIcon = QIcon(thresholdIconPath)
            _labelControlUi.thresToolButton.setIcon(thresholdIcon)
            _labelControlUi.thresToolButton.setCheckable(True)
            _labelControlUi.thresToolButton.clicked.connect(
                lambda checked: self._handleToolButtonClicked(
                    checked, Tool.Threshold))

        # This maps tool types to the buttons that enable them
        if hasattr(_labelControlUi, "thresToolButton"):
            self.toolButtons = {
                Tool.Navigation: _labelControlUi.arrowToolButton,
                Tool.Paint: _labelControlUi.paintToolButton,
                Tool.Erase: _labelControlUi.eraserToolButton,
                Tool.Threshold: _labelControlUi.thresToolButton,
            }
        else:
            self.toolButtons = {
                Tool.Navigation: _labelControlUi.arrowToolButton,
                Tool.Paint: _labelControlUi.paintToolButton,
                Tool.Erase: _labelControlUi.eraserToolButton,
            }

        self.brushSizes = [1, 3, 5, 7, 11, 23, 31, 61]

        for size in self.brushSizes:
            _labelControlUi.brushSizeComboBox.addItem(str(size))

        _labelControlUi.brushSizeComboBox.currentIndexChanged.connect(
            self._onBrushSizeChange)

        self.paintBrushSizeIndex = preferences.get("labeling",
                                                   "paint brush size",
                                                   default=0)
        self.eraserSizeIndex = preferences.get("labeling",
                                               "eraser brush size",
                                               default=4)

    def onLabelListDataChanged(self, topLeft, bottomRight):
        """Handle changes to the label list selections."""
        firstRow = topLeft.row()
        lastRow = bottomRight.row()

        firstCol = topLeft.column()
        lastCol = bottomRight.column()

        # We only care about the color column
        if firstCol <= 0 <= lastCol:
            assert firstRow == lastRow  # Only one data item changes at a time

            # in this case, the actual data (for example color) has changed
            color = self._labelControlUi.labelListModel[firstRow].brushColor()
            color_value = color.rgba()
            color_index = firstRow + 1
            if color_index < len(self._colorTable16):

                self._colorTable16[color_index] = color_value

            else:
                self._colorTable16.append(color_value)
            self.editor.brushingModel.setBrushColor(color)

            # Update the label layer colortable to match the list entry
            labellayer = self._getLabelLayer()
            if labellayer is not None:
                labellayer.colorTable = self._colorTable16

    def __initShortcuts(self):
        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        shortcutGroupName = "Labeling"

        if hasattr(self.labelingDrawerUi, "AddLabelButton"):

            mgr.register(
                "a",
                ActionInfo(
                    shortcutGroupName,
                    "New Label",
                    "Add New Label Class",
                    self.labelingDrawerUi.AddLabelButton.click,
                    self.labelingDrawerUi.AddLabelButton,
                    self.labelingDrawerUi.AddLabelButton,
                ),
            )

        mgr.register(
            "n",
            ActionInfo(
                shortcutGroupName,
                "Navigation Cursor",
                "Navigation Cursor",
                self.labelingDrawerUi.arrowToolButton.click,
                self.labelingDrawerUi.arrowToolButton,
                self.labelingDrawerUi.arrowToolButton,
            ),
        )

        mgr.register(
            "b",
            ActionInfo(
                shortcutGroupName,
                "Brush Cursor",
                "Brush Cursor",
                self.labelingDrawerUi.paintToolButton.click,
                self.labelingDrawerUi.paintToolButton,
                self.labelingDrawerUi.paintToolButton,
            ),
        )

        mgr.register(
            "e",
            ActionInfo(
                shortcutGroupName,
                "Eraser Cursor",
                "Eraser Cursor",
                self.labelingDrawerUi.eraserToolButton.click,
                self.labelingDrawerUi.eraserToolButton,
                self.labelingDrawerUi.eraserToolButton,
            ),
        )

        mgr.register(
            ",",
            ActionInfo(
                shortcutGroupName,
                "Decrease Brush Size",
                "Decrease Brush Size",
                partial(self._tweakBrushSize, False),
                self.labelingDrawerUi.brushSizeComboBox,
                self.labelingDrawerUi.brushSizeComboBox,
            ),
        )

        mgr.register(
            ".",
            ActionInfo(
                shortcutGroupName,
                "Increase Brush Size",
                "Increase Brush Size",
                partial(self._tweakBrushSize, True),
                self.labelingDrawerUi.brushSizeComboBox,
                self.labelingDrawerUi.brushSizeComboBox,
            ),
        )

        if hasattr(self.labelingDrawerUi, "thresToolButton"):
            mgr.register(
                "t",
                ActionInfo(
                    shortcutGroupName,
                    "Window Leveling",
                    "<p>Window Leveling can be used to adjust the data range used for visualization. Pressing the left mouse button while moving the mouse back and forth changes the window width (data range). Moving the mouse in the left-right plane changes the window mean. Pressing the right mouse button resets the view back to the original data.",
                    self.labelingDrawerUi.thresToolButton.click,
                    self.labelingDrawerUi.thresToolButton,
                    self.labelingDrawerUi.thresToolButton,
                ),
            )

        self._labelShortcuts = []

    def _tweakBrushSize(self, increase):
        """
        Increment or decrement the paint brush size or eraser size (depending on which is currently selected).

        increase: Bool. If True, increment.  Otherwise, decrement.
        """
        if self._toolId == Tool.Erase:
            if increase:
                self.eraserSizeIndex += 1
                self.eraserSizeIndex = min(
                    len(self.brushSizes) - 1, self.eraserSizeIndex)
            else:
                self.eraserSizeIndex -= 1
                self.eraserSizeIndex = max(0, self.eraserSizeIndex)
            self._changeInteractionMode(Tool.Erase)
        else:
            if increase:
                self.paintBrushSizeIndex += 1
                self.paintBrushSizeIndex = min(
                    len(self.brushSizes) - 1, self.paintBrushSizeIndex)
            else:
                self.paintBrushSizeIndex -= 1
                self.paintBrushSizeIndex = max(0, self.paintBrushSizeIndex)
            self._changeInteractionMode(Tool.Paint)

    def _updateLabelShortcuts(self):
        numShortcuts = len(self._labelShortcuts)
        numRows = len(self._labelControlUi.labelListModel)

        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts, numRows):
            toolTipObject = LabelListModel.EntryToolTipAdapter(
                self._labelControlUi.labelListModel, i)
            action_info = ActionInfo(
                "Labeling",
                "Select Label {}".format(i + 1),
                "Select Label {}".format(i + 1),
                partial(self._labelControlUi.labelListView.selectRow, i),
                self._labelControlUi.labelListView,
                toolTipObject,
            )
            mgr.register(str(i + 1), action_info)
            self._labelShortcuts.append(action_info)

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            action_info = self._labelShortcuts[i]
            description = "Select " + self._labelControlUi.labelListModel[
                i].name
            new_action_info = mgr.update_description(action_info, description)
            self._labelShortcuts[i] = new_action_info

    def hideEvent(self, event):
        """
        QT event handler.
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        preferences.setmany(
            ("labeling", "paint brush size", self.paintBrushSizeIndex),
            ("labeling", "eraser brush size", self.eraserSizeIndex),
        )
        super(LabelingGui, self).hideEvent(event)

    def _handleToolButtonClicked(self, checked, toolId):
        """
        Called when the user clicks any of the "tool" buttons in the label applet bar GUI.
        """
        if not checked:
            # Users can only *switch between* tools, not turn them off.
            # If they try to turn a button off, re-select it automatically.
            self.toolButtons[toolId].setChecked(True)
        else:
            # If the user is checking a new button
            self._changeInteractionMode(toolId)

    @threadRouted
    def _changeInteractionMode(self, toolId):
        """
        Implement the GUI's response to the user selecting a new tool.
        """
        # Uncheck all the other buttons
        for tool, button in list(self.toolButtons.items()):
            if tool != toolId:
                button.setChecked(False)

        # If we have no editor, we can't do anything yet
        if self.editor is None:
            return

        # The volume editor expects one of two specific names
        if hasattr(self.labelingDrawerUi, "thresToolButton"):
            modeNames = {
                Tool.Navigation: "navigation",
                Tool.Paint: "brushing",
                Tool.Erase: "brushing",
                Tool.Threshold: "thresholding",
            }
        else:
            modeNames = {
                Tool.Navigation: "navigation",
                Tool.Paint: "brushing",
                Tool.Erase: "brushing"
            }

        if hasattr(self._labelControlUi, "AddLabelButton"):
            if self._labelControlUi.labelListModel.rowCount(
            ) == self.maxLabelNumber:
                self._labelControlUi.AddLabelButton.setEnabled(False)
            self._labelControlUi.AddLabelButton.setText("Add Label")

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        # Update the applet bar caption
        if toolId == Tool.Navigation:
            # update GUI
            self._gui_setNavigation()

        elif toolId == Tool.Paint:
            # If necessary, tell the brushing model to stop erasing
            if self.editor.brushingModel.erasing:
                self.editor.brushingModel.disableErasing()
            # Set the brushing size
            brushSize = self.brushSizes[self.paintBrushSizeIndex]
            self.editor.brushingModel.setBrushSize(brushSize)
            # update GUI
            self._gui_setBrushing()

        elif toolId == Tool.Erase:
            # If necessary, tell the brushing model to start erasing
            if not self.editor.brushingModel.erasing:
                self.editor.brushingModel.setErasing()
            # Set the brushing size
            eraserSize = self.brushSizes[self.eraserSizeIndex]
            self.editor.brushingModel.setBrushSize(eraserSize)
            # update GUI
            self._gui_setErasing()
        elif toolId == Tool.Threshold:
            # If necessary, tell the brushing model to stop erasing
            if self.editor.brushingModel.erasing:
                self.editor.brushingModel.disableErasing()
            # display a curser that is static while moving arrow
            self.editor.brushingModel.setBrushSize(1)
            self._gui_setThresholding()
            self.setCursor(Qt.ArrowCursor)

        self.editor.setInteractionMode(modeNames[toolId])
        self._toolId = toolId

    def _gui_setThresholding(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(False)
        self._labelControlUi.brushSizeCaption.setEnabled(False)
        self._labelControlUi.thresToolButton.setChecked(True)

    def _gui_setErasing(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(True)
        self._labelControlUi.brushSizeCaption.setEnabled(True)
        self._labelControlUi.eraserToolButton.setChecked(True)
        self._labelControlUi.brushSizeCaption.setText("Size:")
        self._labelControlUi.brushSizeComboBox.setCurrentIndex(
            self.eraserSizeIndex)

    def _gui_setNavigation(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(False)
        self._labelControlUi.brushSizeCaption.setEnabled(False)
        self._labelControlUi.arrowToolButton.setChecked(True)
        # self._labelControlUi.arrowToolButton.setChecked(True) # why twice?

    def _gui_setBrushing(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(True)
        self._labelControlUi.brushSizeCaption.setEnabled(True)
        # Make sure the paint button is pressed
        self._labelControlUi.paintToolButton.setChecked(True)
        # Show the brush size control and set its caption
        self._labelControlUi.brushSizeCaption.setText("Size:")
        # Make sure the GUI reflects the correct size
        self._labelControlUi.brushSizeComboBox.setCurrentIndex(
            self.paintBrushSizeIndex)

    def _gui_enableLabeling(self, enable):
        self._labelControlUi.paintToolButton.setEnabled(enable)
        self._labelControlUi.eraserToolButton.setEnabled(enable)
        self._labelControlUi.brushSizeCaption.setEnabled(enable)
        self._labelControlUi.brushSizeComboBox.setEnabled(enable)

    def _onBrushSizeChange(self, index):
        """
        Handle the user's new brush size selection.
        Note: The editor's brushing model currently maintains only a single
              brush size, which is used for both painting and erasing.
              However, we maintain two different sizes for the user and swap
              them depending on which tool is selected.
        """
        newSize = self.brushSizes[index]
        if self.editor.brushingModel.erasing:
            self.eraserSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)
        else:
            self.paintBrushSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)

    def _onLabelSelected(self, row):
        logger.debug("switching to label=%r" %
                     (self._labelControlUi.labelListModel[row]))

        # If the user is selecting a label, he probably wants to be in paint mode
        self._changeInteractionMode(Tool.Paint)

        # +1 because first is transparent
        # FIXME: shouldn't be just row+1 here
        self.editor.brushingModel.setDrawnNumber(row + 1)
        brushColor = self._labelControlUi.labelListModel[row].brushColor()
        self.editor.brushingModel.setBrushColor(brushColor)

    def _resetLabelSelection(self):
        logger.debug("Resetting label selection")
        if len(self._labelControlUi.labelListModel) > 0:
            self._labelControlUi.labelListView.selectRow(0)
        else:
            self._changeInteractionMode(Tool.Navigation)
        return True

    def _updateLabelList(self):
        """
        This function is called when the number of labels has changed without our knowledge.
        We need to add/remove labels until we have the right number
        """
        # Get the number of labels in the label data
        # (Or the number of the labels the user has added.)
        names = self._labelingSlots.labelNames.value
        numLabels = len(self._labelingSlots.labelNames.value)

        # Add rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() < numLabels:
            self._addNewLabel()

        # If we have too many rows, remove the rows that aren't in the list of names.
        if self._labelControlUi.labelListModel.rowCount() > len(names):
            indices_to_remove = []
            for i in range(self._labelControlUi.labelListModel.rowCount()):
                if self._labelControlUi.labelListModel[i].name not in names:
                    indices_to_remove.append(i)

            for i in reversed(indices_to_remove):
                self._labelControlUi.labelListModel.removeRow(i)

        # synchronize labelNames
        for i, n in enumerate(names):
            self._labelControlUi.labelListModel[i].name = n

        if hasattr(self._labelControlUi, "AddLabelButton"):
            self._labelControlUi.AddLabelButton.setEnabled(
                numLabels < self.maxLabelNumber)

    def _addNewLabel(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        """
        Add a new label to the label list GUI control.
        Return the new number of labels in the control.
        """
        label = Label(self.getNextLabelName(),
                      self.getNextLabelColor(),
                      pmapColor=self.getNextPmapColor())
        label.nameChanged.connect(self._updateLabelShortcuts)
        label.nameChanged.connect(self.onLabelNameChanged)
        label.colorChanged.connect(self.onLabelColorChanged)
        label.pmapColorChanged.connect(self.onPmapColorChanged)

        newRow = self._labelControlUi.labelListModel.rowCount()
        self._labelControlUi.labelListModel.insertRow(newRow, label)

        newColorIndex = self._labelControlUi.labelListModel.index(newRow, 0)
        self.onLabelListDataChanged(
            newColorIndex, newColorIndex
        )  # Make sure label layer colortable is in sync with the new color

        # Update operator with new name
        operator_names = self._labelingSlots.labelNames.value
        if len(operator_names) < self._labelControlUi.labelListModel.rowCount(
        ):
            operator_names.append(label.name)
            try:
                self._labelingSlots.labelNames.setValue(operator_names,
                                                        check_changed=False)
            except:
                # I have no idea why this is, but sometimes PyQt "loses" exceptions here.
                # Print it out before it's too late!
                log_exception(
                    logger,
                    "Logged the above exception just in case PyQt loses it.")
                raise

        if self._allowDeleteLastLabelOnly and self._forceAtLeastTwoLabels:
            # make previous label permanent, when we have at least three labels since the first two are always permanent
            if newRow > 2:
                self._labelControlUi.labelListModel.makeRowPermanent(newRow -
                                                                     1)
        elif self._allowDeleteLastLabelOnly:
            # make previous label permanent
            if newRow > 0:
                self._labelControlUi.labelListModel.makeRowPermanent(newRow -
                                                                     1)
        elif self._forceAtLeastTwoLabels:
            # if a third label is added make all labels removable
            if self._labelControlUi.labelListModel.rowCount() == 3:
                self.labelingDrawerUi.labelListModel.makeRowRemovable(0)
                self.labelingDrawerUi.labelListModel.makeRowRemovable(1)

        # Call the 'changed' callbacks immediately to initialize any listeners
        self.onLabelNameChanged()
        self.onLabelColorChanged()
        self.onPmapColorChanged()

        # Make the new label selected
        nlabels = self._labelControlUi.labelListModel.rowCount()
        selectedRow = nlabels - 1
        self._labelControlUi.labelListModel.select(selectedRow)

        self._updateLabelShortcuts()

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        QApplication.restoreOverrideCursor()

    def getNextLabelName(self):
        """
        Return a suitable name for the next label added by the user.
        Subclasses may override this.
        """
        maxNum = 0
        for index, label in enumerate(self._labelControlUi.labelListModel):
            nums = re.findall("\d+", label.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Label {}".format(maxNum + 1)

    def getNextLabelColor(self):
        """
        Return a QColor to use for the next label.
        """
        numLabels = len(self._labelControlUi.labelListModel)
        if numLabels >= len(self._colorTable16) - 1:
            # If the color table isn't large enough to handle all our labels,
            #  append a random color
            randomColor = QColor(numpy.random.randint(0, 255),
                                 numpy.random.randint(0, 255),
                                 numpy.random.randint(0, 255))
            self._colorTable16.append(randomColor.rgba())

        color = QColor()
        color.setRgba(self._colorTable16[
            numLabels + 1])  # First entry is transparent (for zero label)
        return color

    def getNextPmapColor(self):
        """
        Return a QColor to use for the next label.
        """
        return None

    def onLabelNameChanged(self):
        """
        Subclasses can override this to respond to changes in the label names.
        """
        pass

    def onLabelColorChanged(self):
        """
        Subclasses can override this to respond to changes in the label colors.
        This class gets updated before, in the _updateLabelList
        """
        pass

    def onPmapColorChanged(self):
        """
        Subclasses can override this to respond to changes in a label associated probability color.
        """
        pass

    def _removeLastLabel(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the label list by one.
        """
        self._programmaticallyRemovingLabels = True
        numRows = self._labelControlUi.labelListModel.rowCount()
        # This will trigger the signal that calls _onLabelRemoved()
        self._labelControlUi.labelListModel.removeRow(numRows - 1)
        self._updateLabelShortcuts()

        self._programmaticallyRemovingLabels = False

    def _clearLabelListGui(self):
        # Remove rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() > 0:
            self._removeLastLabel()

    def _onLabelRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingLabels:
            return

        assert start == end
        row = start

        oldcount = self._labelControlUi.labelListModel.rowCount() + 1
        logger.debug("removing label {} out of {}".format(row, oldcount))

        # Remove the deleted label's color from the color table so that renumbered labels keep their colors.
        oldColor = self._colorTable16.pop(row + 1)

        # Recycle the deleted color back into the table (for the next label to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the labellayer colortable with the new color mapping
        labellayer = self._getLabelLayer()
        if labellayer is not None:
            labellayer.colorTable = self._colorTable16

        currentSelection = self._labelControlUi.labelListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post(self._resetLabelSelection)

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        # If the gui list model isn't in sync with the operator, update the operator.
        if len(self._labelingSlots.labelNames.value
               ) > self._labelControlUi.labelListModel.rowCount():
            # Changing the deleteLabel input causes the operator (OpBlockedSparseArray)
            #  to search through the entire list of labels and delete the entries for the matching label.
            self._labelingSlots.labelDelete.setValue(row + 1)

            # We need to "reset" the deleteLabel input to -1 when we're finished.
            #  Otherwise, you can never delete the same label twice in a row.
            #  (Only *changes* to the input are acted upon.)
            self._labelingSlots.labelDelete.setValue(-1)

            labelNames = self._labelingSlots.labelNames.value
            labelNames.pop(start)
            self._labelingSlots.labelNames.setValue(labelNames,
                                                    check_changed=False)

        if self._forceAtLeastTwoLabels and self._allowDeleteLastLabelOnly:
            # make previous label removable again and always leave at least two permanent labels
            if oldcount > 3:
                self._labelControlUi.labelListModel.makeRowRemovable(oldcount -
                                                                     2)
        elif self._allowDeleteLastLabelOnly:
            # make previous label removable again
            if oldcount > 1:
                self._labelControlUi.labelListModel.makeRowRemovable(oldcount -
                                                                     2)
        elif self._forceAtLeastTwoLabels:
            # if there are only two labels remaining make them permanent
            if self._labelControlUi.labelListModel.rowCount() == 2:
                self.labelingDrawerUi.labelListModel.makeRowPermanent(0)
                self.labelingDrawerUi.labelListModel.makeRowPermanent(1)

    def getLayer(self, name):
        """find a layer by name"""
        try:
            labellayer = next(filter(lambda l: l.name == name,
                                     self.layerstack))
        except StopIteration:
            return None
        else:
            return labellayer

    def _getLabelLayer(self):
        return self.getLayer("Labels")

    def createLabelLayer(self, direct=False):
        """
        Return a colortable layer that displays the label slot data, along with its associated label source.
        direct: whether this layer is drawn synchronously by volumina
        """
        labelOutput = self._labelingSlots.labelOutput
        if not labelOutput.ready():
            return (None, None)
        else:
            # Add the layer to draw the labels, but don't add any labels
            labelsrc = LazyflowSinkSource(self._labelingSlots.labelOutput,
                                          self._labelingSlots.labelInput)

            labellayer = ColortableLayer(labelsrc,
                                         colorTable=self._colorTable16,
                                         direct=direct)
            labellayer.name = "Labels"
            labellayer.ref_object = None

            labellayer.contexts.append(
                QAction("Import...",
                        None,
                        triggered=partial(import_labeling_layer, labellayer,
                                          self._labelingSlots, self)))

            labellayer.shortcutRegistration = (
                "0",
                ShortcutManager.ActionInfo(
                    "Labeling",
                    "LabelVisibility",
                    "Show/Hide Labels",
                    labellayer.toggleVisible,
                    self.viewerControlWidget(),
                    labellayer,
                ),
            )

            return labellayer, labelsrc

    def setupLayers(self):
        """
        Sets up the label layer for display by our base class (LayerViewerGui).
        If our subclass overrides this function to add his own layers,
        he **must** call this function explicitly.
        """
        layers = []

        # Labels
        labellayer, labelsrc = self.createLabelLayer()
        if labellayer is not None:
            layers.append(labellayer)

            # Tell the editor where to draw label data
            self.editor.setLabelSink(labelsrc)

        # Side effect 1: We want to guarantee that the label list
        #  is up-to-date before our subclass adds his layers
        self._updateLabelList()

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot.ready():
            layer = self.createStandardLayerFromSlot(self._rawInputSlot,
                                                     name="Raw Input")
            layers.append(layer)

            if isinstance(layer, GrayscaleLayer):
                self.labelingDrawerUi.thresToolButton.show()
            else:
                self.labelingDrawerUi.thresToolButton.hide()

            def toggleTopToBottom():
                index = self.layerstack.layerIndex(layer)
                self.layerstack.selectRow(index)
                if index == 0:
                    self.layerstack.moveSelectedToBottom()
                else:
                    self.layerstack.moveSelectedToTop()

            layer.shortcutRegistration = (
                "i",
                ShortcutManager.ActionInfo(
                    "Prediction Layers",
                    "Bring Input To Top/Bottom",
                    "Bring Input To Top/Bottom",
                    toggleTopToBottom,
                    self.viewerControlWidget(),
                    layer,
                ),
            )

        return layers

    def allowDeleteLastLabelOnly(self, enabled):
        """
        In the TrackingWorkflow when labeling 0/1/2/.../N mergers we do not allow
        to remove another label but the first, as the following processing steps
        assume that all previous cell counts are given.
        """
        self._allowDeleteLastLabelOnly = enabled

    def forceAtLeastTwoLabels(self, enabled):
        """
        in some workflows it makes no sense to have less than two labels.
        This setting forces to have always at least two labels.
        If there are exaclty two, they will be made unremovable
        """
        self._addNewLabel()
        self._addNewLabel()
        self.labelingDrawerUi.labelListModel.makeRowPermanent(0)
        self.labelingDrawerUi.labelListModel.makeRowPermanent(1)

        self._forceAtLeastTwoLabels = enabled
Beispiel #28
0
class LabelingGui(LayerViewerGui):
    """
    Provides all the functionality of a simple layerviewer
    applet with the added functionality of labeling.
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################

    def centralWidget( self ):
        return self

    def appletDrawer(self):
        return self._labelControlUi

    def stopAndCleanUp(self):
        super(LabelingGui, self).stopAndCleanUp()

        for fn in self.__cleanup_fns:
            fn()

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

    @property
    def minLabelNumber(self):
        return self._minLabelNumber
    @minLabelNumber.setter
    def minLabelNumber(self, n):
        self._minLabelNumer = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self._addNewLabel()
    @property
    def maxLabelNumber(self):
        return self._maxLabelNumber
    @maxLabelNumber.setter
    def maxLabelNumber(self, n):
        self._maxLabelNumber = n
        while self._labelControlUi.labelListModel.rowCount() < n:
            self._removeLastLabel()

    @property
    def labelingDrawerUi(self):
        return self._labelControlUi

    @property
    def labelListData(self):
        return self._labelControlUi.labelListModel

    def selectLabel(self, labelIndex):
        """Programmatically select the given labelIndex, which start from 0.
           Equivalent to clicking on the (labelIndex+1)'th position in the label widget."""
        self._labelControlUi.labelListModel.select(labelIndex)

    class LabelingSlots(object):
        """
        This class serves as the parameter for the LabelingGui constructor.
        It provides the slots that the labeling GUI uses to source labels to the display and sink labels from the
        user's mouse clicks.
        """
        def __init__(self):
            # Slot to insert elements onto
            self.labelInput = None # labelInput.setInSlot(xxx)
            # Slot to read elements from
            self.labelOutput = None # labelOutput.get(roi)
            # Slot that determines which label value corresponds to erased values
            self.labelEraserValue = None # labelEraserValue.setValue(xxx)
            # Slot that is used to request wholesale label deletion
            self.labelDelete = None # labelDelete.setValue(xxx)
            # Slot that gives a list of label names
            self.labelNames = None # labelNames.value

            # Slot to specify which images the user is allowed to label.
            self.labelsAllowed = None # labelsAllowed.value == True

    def __init__(self, parentApplet, labelingSlots, topLevelOperatorView, drawerUiPath=None, rawInputSlot=None, crosshair=True):
        """
        Constructor.

        :param labelingSlots: Provides the slots needed for sourcing/sinking label data.  See LabelingGui.LabelingSlots
                              class source for details.
        :param topLevelOperatorView: is provided to the LayerViewerGui (the base class)
        :param drawerUiPath: can be given if you provide an extended drawer UI file.  Otherwise a default one is used.
        :param rawInputSlot: Data from the rawInputSlot parameter will be displayed directly underneath the elements
                             (if provided).
        """

        # Do have have all the slots we need?
        assert isinstance(labelingSlots, LabelingGui.LabelingSlots)
        assert labelingSlots.labelInput is not None, "Missing a required slot."
        assert labelingSlots.labelOutput is not None, "Missing a required slot."
        assert labelingSlots.labelEraserValue is not None, "Missing a required slot."
        assert labelingSlots.labelDelete is not None, "Missing a required slot."
        assert labelingSlots.labelNames is not None, "Missing a required slot."
        assert labelingSlots.labelsAllowed is not None, "Missing a required slot."

        self.__cleanup_fns = []

        self._labelingSlots = labelingSlots
        self._minLabelNumber = 0
        self._maxLabelNumber = 99 #100 or 255 is reserved for eraser

        self._rawInputSlot = rawInputSlot

        self._labelingSlots.labelNames.notifyDirty( bind(self._updateLabelList) )
        self.__cleanup_fns.append( partial( self._labelingSlots.labelNames.unregisterDirty, bind(self._updateLabelList) ) )
        
        self._colorTable16 = self._createDefault16ColorColorTable()
        self._programmaticallyRemovingLabels = False

        if drawerUiPath is None:
            # Default ui file
            drawerUiPath = os.path.split(__file__)[0] + '/labelingDrawer.ui'
        self._initLabelUic(drawerUiPath)

        # Init base class
        super(LabelingGui, self).__init__(parentApplet,
                                          topLevelOperatorView,
                                          [labelingSlots.labelInput, labelingSlots.labelOutput],
                                          crosshair=crosshair)

        self.__initShortcuts()
        self._labelingSlots.labelEraserValue.setValue(self.editor.brushingModel.erasingNumber)

        # Register for thunk events (easy UI calls from non-GUI threads)
        self.thunkEventHandler = ThunkEventHandler(self)
        self._changeInteractionMode(Tool.Navigation)

    def _initLabelUic(self, drawerUiPath):
        _labelControlUi = uic.loadUi(drawerUiPath)

        # We own the applet bar ui
        self._labelControlUi = _labelControlUi

        # Initialize the label list model
        model = LabelListModel()
        _labelControlUi.labelListView.setModel(model)
        _labelControlUi.labelListModel=model
        _labelControlUi.labelListModel.rowsRemoved.connect(self._onLabelRemoved)
        _labelControlUi.labelListModel.elementSelected.connect(self._onLabelSelected)

        # Connect Applet GUI to our event handlers
        if hasattr(_labelControlUi, "AddLabelButton"):
            _labelControlUi.AddLabelButton.setIcon( QIcon(ilastikIcons.AddSel) )
            _labelControlUi.AddLabelButton.clicked.connect( bind(self._addNewLabel) )
        _labelControlUi.labelListModel.dataChanged.connect(self.onLabelListDataChanged)

        # Initialize the arrow tool button with an icon and handler
        iconPath = os.path.split(__file__)[0] + "/icons/arrow.png"
        arrowIcon = QIcon(iconPath)
        _labelControlUi.arrowToolButton.setIcon(arrowIcon)
        _labelControlUi.arrowToolButton.setCheckable(True)
        _labelControlUi.arrowToolButton.clicked.connect( lambda checked: self._handleToolButtonClicked(checked, Tool.Navigation) )

        # Initialize the paint tool button with an icon and handler
        paintBrushIconPath = os.path.split(__file__)[0] + "/icons/paintbrush.png"
        paintBrushIcon = QIcon(paintBrushIconPath)
        _labelControlUi.paintToolButton.setIcon(paintBrushIcon)
        _labelControlUi.paintToolButton.setCheckable(True)
        _labelControlUi.paintToolButton.clicked.connect( lambda checked: self._handleToolButtonClicked(checked, Tool.Paint) )

        # Initialize the erase tool button with an icon and handler
        eraserIconPath = os.path.split(__file__)[0] + "/icons/eraser.png"
        eraserIcon = QIcon(eraserIconPath)
        _labelControlUi.eraserToolButton.setIcon(eraserIcon)
        _labelControlUi.eraserToolButton.setCheckable(True)
        _labelControlUi.eraserToolButton.clicked.connect( lambda checked: self._handleToolButtonClicked(checked, Tool.Erase) )

        # This maps tool types to the buttons that enable them
        self.toolButtons = { Tool.Navigation : _labelControlUi.arrowToolButton,
                             Tool.Paint      : _labelControlUi.paintToolButton,
                             Tool.Erase      : _labelControlUi.eraserToolButton }

        self.brushSizes = [ 1, 3, 5, 7, 11, 23, 31, 61 ]

        for size in self.brushSizes:
            _labelControlUi.brushSizeComboBox.addItem( str(size) )

        _labelControlUi.brushSizeComboBox.currentIndexChanged.connect(self._onBrushSizeChange)

        self.paintBrushSizeIndex = PreferencesManager().get( 'labeling', 'paint brush size', default=0 )
        self.eraserSizeIndex = PreferencesManager().get( 'labeling', 'eraser brush size', default=4 )

    def onLabelListDataChanged(self, topLeft, bottomRight):
        """Handle changes to the label list selections."""
        firstRow = topLeft.row()
        lastRow  = bottomRight.row()

        firstCol = topLeft.column()
        lastCol  = bottomRight.column()

        # We only care about the color column
        if firstCol <= 0 <= lastCol:
            assert(firstRow == lastRow) # Only one data item changes at a time

            #in this case, the actual data (for example color) has changed
            color = self._labelControlUi.labelListModel[firstRow].brushColor()
            self._colorTable16[firstRow+1] = color.rgba()
            self.editor.brushingModel.setBrushColor(color)

            # Update the label layer colortable to match the list entry
            labellayer = self._getLabelLayer()
            if labellayer is not None:
                labellayer.colorTable = self._colorTable16

    def __initShortcuts(self):
        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        shortcutGroupName = "Labeling"

        if hasattr(self.labelingDrawerUi, "AddLabelButton"):
            mgr.register("a", ActionInfo( shortcutGroupName,
                                          "New Label",
                                          "Add New Label Class",
                                          self.labelingDrawerUi.AddLabelButton.click,
                                          self.labelingDrawerUi.AddLabelButton,
                                          self.labelingDrawerUi.AddLabelButton ) )

        mgr.register( "n", ActionInfo( shortcutGroupName,
                                       "Navigation Cursor",
                                       "Navigation Cursor",
                                       self.labelingDrawerUi.arrowToolButton.click,
                                       self.labelingDrawerUi.arrowToolButton,
                                       self.labelingDrawerUi.arrowToolButton ) )

        mgr.register( "b", ActionInfo( shortcutGroupName,
                                       "Brush Cursor",
                                       "Brush Cursor",
                                       self.labelingDrawerUi.paintToolButton.click,
                                       self.labelingDrawerUi.paintToolButton,
                                       self.labelingDrawerUi.paintToolButton ) )

        mgr.register( "e", ActionInfo( shortcutGroupName,
                                       "Eraser Cursor",
                                       "Eraser Cursor",
                                       self.labelingDrawerUi.eraserToolButton.click,
                                       self.labelingDrawerUi.eraserToolButton,
                                       self.labelingDrawerUi.eraserToolButton ) )

        self._labelShortcuts = []

    def _updateLabelShortcuts(self):
        numShortcuts = len(self._labelShortcuts)
        numRows = len(self._labelControlUi.labelListModel)

        mgr = ShortcutManager()
        ActionInfo = ShortcutManager.ActionInfo
        # Add any shortcuts we don't have yet.
        for i in range(numShortcuts,numRows):
            toolTipObject = LabelListModel.EntryToolTipAdapter(self._labelControlUi.labelListModel, i)
            action_info = ActionInfo( "Labeling", 
                                      "Select Label {}".format(i+1),
                                      "Select Label {}".format(i+1),
                                      partial(self._labelControlUi.labelListView.selectRow, i),
                                      self._labelControlUi.labelListView,
                                      toolTipObject )
            mgr.register( str(i+1), action_info )
            self._labelShortcuts.append( action_info )

        # Make sure that all shortcuts have an appropriate description
        for i in range(numRows):
            action_info = self._labelShortcuts[i]
            description = "Select " + self._labelControlUi.labelListModel[i].name
            new_action_info = mgr.update_description(action_info, description)
            self._labelShortcuts[i] = new_action_info

    def hideEvent(self, event):
        """
        QT event handler.
        The user has selected another applet or is closing the whole app.
        Save all preferences.
        """
        with PreferencesManager() as prefsMgr:
            prefsMgr.set('labeling', 'paint brush size', self.paintBrushSizeIndex)
            prefsMgr.set('labeling', 'eraser brush size', self.eraserSizeIndex)
        super(LabelingGui, self).hideEvent(event)

    def _handleToolButtonClicked(self, checked, toolId):
        """
        Called when the user clicks any of the "tool" buttons in the label applet bar GUI.
        """
        if not checked:
            # Users can only *switch between* tools, not turn them off.
            # If they try to turn a button off, re-select it automatically.
            self.toolButtons[toolId].setChecked(True)
        else:
            # If the user is checking a new button
            self._changeInteractionMode( toolId )

    @threadRouted
    def _changeInteractionMode( self, toolId ):
        """
        Implement the GUI's response to the user selecting a new tool.
        """
        # Uncheck all the other buttons
        for tool, button in self.toolButtons.items():
            if tool != toolId:
                button.setChecked(False)

        # If we have no editor, we can't do anything yet
        if self.editor is None:
            return

        # The volume editor expects one of two specific names
        modeNames = { Tool.Navigation   : "navigation",
                      Tool.Paint        : "brushing",
                      Tool.Erase        : "brushing" }

        # If the user can't label this image, disable the button and say why its disabled
        labelsAllowed = False

        labelsAllowedSlot = self._labelingSlots.labelsAllowed
        if labelsAllowedSlot.ready():
            labelsAllowed = labelsAllowedSlot.value

            if hasattr(self._labelControlUi, "AddLabelButton"):
                if not labelsAllowed or self._labelControlUi.labelListModel.rowCount() == self.maxLabelNumber:
                    self._labelControlUi.AddLabelButton.setEnabled(False)
                if labelsAllowed:
                    self._labelControlUi.AddLabelButton.setText("Add Label")
                else:
                    self._labelControlUi.AddLabelButton.setText("(Labeling Not Allowed)")

        e = labelsAllowed & (self._labelControlUi.labelListModel.rowCount() > 0)
        self._gui_enableLabeling(e)
        
        if labelsAllowed:
            # Update the applet bar caption
            if toolId == Tool.Navigation:
                # update GUI 
                self._gui_setNavigation()
                
            elif toolId == Tool.Paint:
                # If necessary, tell the brushing model to stop erasing
                if self.editor.brushingModel.erasing:
                    self.editor.brushingModel.disableErasing()
                # Set the brushing size
                brushSize = self.brushSizes[self.paintBrushSizeIndex]
                self.editor.brushingModel.setBrushSize(brushSize)
                # update GUI 
                self._gui_setBrushing()

            elif toolId == Tool.Erase:
                # If necessary, tell the brushing model to start erasing
                if not self.editor.brushingModel.erasing:
                    self.editor.brushingModel.setErasing()
                # Set the brushing size
                eraserSize = self.brushSizes[self.eraserSizeIndex]
                self.editor.brushingModel.setBrushSize(eraserSize)
                # update GUI 
                self._gui_setErasing()

        self.editor.setInteractionMode( modeNames[toolId] )
        self._toolId = toolId
        
    def _gui_setErasing(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(True)
        self._labelControlUi.brushSizeCaption.setEnabled(True)
        self._labelControlUi.eraserToolButton.setChecked(True)
        self._labelControlUi.brushSizeCaption.setText("Size:")
        self._labelControlUi.brushSizeComboBox.setCurrentIndex(self.eraserSizeIndex)
    def _gui_setNavigation(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(False)
        self._labelControlUi.brushSizeCaption.setEnabled(False)
        self._labelControlUi.arrowToolButton.setChecked(True)
        self._labelControlUi.arrowToolButton.setChecked(True)
    def _gui_setBrushing(self):
        self._labelControlUi.brushSizeComboBox.setEnabled(True)
        self._labelControlUi.brushSizeCaption.setEnabled(True)
        # Make sure the paint button is pressed
        self._labelControlUi.paintToolButton.setChecked(True)
        # Show the brush size control and set its caption
        self._labelControlUi.brushSizeCaption.setText("Size:")
        # Make sure the GUI reflects the correct size
        self._labelControlUi.brushSizeComboBox.setCurrentIndex(self.paintBrushSizeIndex)
    def _gui_enableLabeling(self, enable):
        self._labelControlUi.paintToolButton.setEnabled(enable)
        self._labelControlUi.eraserToolButton.setEnabled(enable)
        self._labelControlUi.brushSizeCaption.setEnabled(enable)
        self._labelControlUi.brushSizeComboBox.setEnabled(enable)
    


    def _onBrushSizeChange(self, index):
        """
        Handle the user's new brush size selection.
        Note: The editor's brushing model currently maintains only a single
              brush size, which is used for both painting and erasing.
              However, we maintain two different sizes for the user and swap
              them depending on which tool is selected.
        """
        newSize = self.brushSizes[index]
        if self.editor.brushingModel.erasing:
            self.eraserSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)
        else:
            self.paintBrushSizeIndex = index
            self.editor.brushingModel.setBrushSize(newSize)

    def _onLabelSelected(self, row):
        logger.debug("switching to label=%r" % (self._labelControlUi.labelListModel[row]))

        # If the user is selecting a label, he probably wants to be in paint mode
        self._changeInteractionMode(Tool.Paint)

        #+1 because first is transparent
        #FIXME: shouldn't be just row+1 here
        self.editor.brushingModel.setDrawnNumber(row+1)
        brushColor = self._labelControlUi.labelListModel[row].brushColor()
        self.editor.brushingModel.setBrushColor( brushColor )

    def _resetLabelSelection(self):
        logger.debug("Resetting label selection")
        if len(self._labelControlUi.labelListModel) > 0:
            self._labelControlUi.labelListView.selectRow(0)
        else:
            self._changeInteractionMode(Tool.Navigation)
        return True

    def _updateLabelList(self):
        """
        This function is called when the number of labels has changed without our knowledge.
        We need to add/remove labels until we have the right number
        """
        # Get the number of labels in the label data
        # (Or the number of the labels the user has added.)
        names = self._labelingSlots.labelNames.value
        numLabels = len(self._labelingSlots.labelNames.value)

        # Add rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() < numLabels:
            self._addNewLabel()

        # If we have too many rows, remove the rows that aren't in the list of names.
        if self._labelControlUi.labelListModel.rowCount() > len(names):
            indices_to_remove = []
            for i in range(self._labelControlUi.labelListModel.rowCount()):
                if self._labelControlUi.labelListModel[i].name not in names:
                    indices_to_remove.append( i )
        
            for i in reversed(indices_to_remove):
                self._labelControlUi.labelListModel.removeRow(i)

        # synchronize labelNames
        for i,n in enumerate(names):
            self._labelControlUi.labelListModel[i].name = n
                
        if hasattr(self._labelControlUi, "AddLabelButton"):
            self._labelControlUi.AddLabelButton.setEnabled(numLabels < self.maxLabelNumber)

    def _addNewLabel(self):
        QApplication.setOverrideCursor(Qt.WaitCursor)
        
        """
        Add a new label to the label list GUI control.
        Return the new number of labels in the control.
        """
        label = Label( self.getNextLabelName(), self.getNextLabelColor(),
                       pmapColor=self.getNextPmapColor(),
                   )
        label.nameChanged.connect(self._updateLabelShortcuts)
        label.nameChanged.connect(self.onLabelNameChanged)
        label.colorChanged.connect(self.onLabelColorChanged)
        label.pmapColorChanged.connect(self.onPmapColorChanged)

        newRow = self._labelControlUi.labelListModel.rowCount()
        self._labelControlUi.labelListModel.insertRow( newRow, label )
        newColorIndex = self._labelControlUi.labelListModel.index(newRow, 0)
        self.onLabelListDataChanged(newColorIndex, newColorIndex) # Make sure label layer colortable is in sync with the new color

        # Update operator with new name
        operator_names = self._labelingSlots.labelNames.value
        if len(operator_names) < self._labelControlUi.labelListModel.rowCount():
            operator_names.append( label.name )
            self._labelingSlots.labelNames.setValue( operator_names, check_changed=False )

        # Call the 'changed' callbacks immediately to initialize any listeners
        self.onLabelNameChanged()
        self.onLabelColorChanged()
        self.onPmapColorChanged()

        # Make the new label selected
        nlabels = self._labelControlUi.labelListModel.rowCount()
        selectedRow = nlabels-1
        self._labelControlUi.labelListModel.select(selectedRow)

        self._updateLabelShortcuts()
       
        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)
        
        QApplication.restoreOverrideCursor()

    def getNextLabelName(self):
        """
        Return a suitable name for the next label added by the user.
        Subclasses may override this.
        """
        maxNum = 0
        for index, label in enumerate(self._labelControlUi.labelListModel):
            nums = re.findall("\d+", label.name)
            for n in nums:
                maxNum = max(maxNum, int(n))
        return "Label {}".format(maxNum+1)

    def getNextLabelColor(self):
        """
        Return a QColor to use for the next label.
        """
        numLabels = len(self._labelControlUi.labelListModel)
        if numLabels >= len(self._colorTable16)-1:
            # If the color table isn't large enough to handle all our labels,
            #  append a random color
            randomColor = QColor(numpy.random.randint(0,255), numpy.random.randint(0,255), numpy.random.randint(0,255))
            self._colorTable16.append( randomColor.rgba() )

        color = QColor()
        color.setRgba(self._colorTable16[numLabels+1]) # First entry is transparent (for zero label)
        return color

    def getNextPmapColor(self):
        """
        Return a QColor to use for the next label.
        """
        return None

    def onLabelNameChanged(self):
        """
        Subclasses can override this to respond to changes in the label names.
        """
        pass

    def onLabelColorChanged(self):
        """
        Subclasses can override this to respond to changes in the label colors.
        """
        pass
    
    def onPmapColorChanged(self):
        """
        Subclasses can override this to respond to changes in a label associated probability color.
        """
        pass

    def _removeLastLabel(self):
        """
        Programmatically (i.e. not from the GUI) reduce the size of the label list by one.
        """
        self._programmaticallyRemovingLabels = True
        numRows = self._labelControlUi.labelListModel.rowCount()
        # This will trigger the signal that calls _onLabelRemoved()
        self._labelControlUi.labelListModel.removeRow(numRows-1)
        self._updateLabelShortcuts()

        self._programmaticallyRemovingLabels = False

    def _clearLabelListGui(self):
        # Remove rows until we have the right number
        while self._labelControlUi.labelListModel.rowCount() > 0:
            self._removeLastLabel()

    def _onLabelRemoved(self, parent, start, end):
        # Don't respond unless this actually came from the GUI
        if self._programmaticallyRemovingLabels:
            return

        assert start == end
        row = start

        oldcount = self._labelControlUi.labelListModel.rowCount() + 1
        logger.debug("removing label {} out of {}".format( row, oldcount ))

        # Remove the deleted label's color from the color table so that renumbered labels keep their colors.
        oldColor = self._colorTable16.pop(row+1)

        # Recycle the deleted color back into the table (for the next label to be added)
        self._colorTable16.insert(oldcount, oldColor)

        # Update the labellayer colortable with the new color mapping
        labellayer = self._getLabelLayer()
        if labellayer is not None:
            labellayer.colorTable = self._colorTable16

        currentSelection = self._labelControlUi.labelListModel.selectedRow()
        if currentSelection == -1:
            # If we're deleting the currently selected row, then switch to a different row
            self.thunkEventHandler.post( self._resetLabelSelection )

        e = self._labelControlUi.labelListModel.rowCount() > 0
        self._gui_enableLabeling(e)

        # If the gui list model isn't in sync with the operator, update the operator.
        if len(self._labelingSlots.labelNames.value) > self._labelControlUi.labelListModel.rowCount():
            # Changing the deleteLabel input causes the operator (OpBlockedSparseArray)
            #  to search through the entire list of labels and delete the entries for the matching label.
            self._labelingSlots.labelDelete.setValue(row+1)
    
            # We need to "reset" the deleteLabel input to -1 when we're finished.
            #  Otherwise, you can never delete the same label twice in a row.
            #  (Only *changes* to the input are acted upon.)
            self._labelingSlots.labelDelete.setValue(-1)
            
            labelNames = self._labelingSlots.labelNames.value
            labelNames.pop(start)
            self._labelingSlots.labelNames.setValue(labelNames, check_changed=False)
       
    def getLayer(self, name):
        """find a layer by name"""
        try:
            labellayer = itertools.ifilter(lambda l: l.name == name, self.layerstack).next()
        except StopIteration:
            return None
        else:
            return labellayer

    def _getLabelLayer(self):
        return self.getLayer('Labels')

    def createLabelLayer(self, direct=False):
        """
        Return a colortable layer that displays the label slot data, along with its associated label source.
        direct: whether this layer is drawn synchronously by volumina
        """
        labelOutput = self._labelingSlots.labelOutput
        if not labelOutput.ready():
            return (None, None)
        else:
            # Add the layer to draw the labels, but don't add any labels
            labelsrc = LazyflowSinkSource( self._labelingSlots.labelOutput,
                                           self._labelingSlots.labelInput)

            labellayer = ColortableLayer(labelsrc, colorTable = self._colorTable16, direct=direct )
            labellayer.name = "Labels"
            labellayer.ref_object = None

            return labellayer, labelsrc

    def setupLayers(self):
        """
        Sets up the label layer for display by our base class (LayerViewerGui).
        If our subclass overrides this function to add his own layers,
        he **must** call this function explicitly.
        """
        layers = []

        # Labels
        labellayer, labelsrc = self.createLabelLayer()
        if labellayer is not None:
            layers.append(labellayer)

            # Tell the editor where to draw label data
            self.editor.setLabelSink(labelsrc)

        # Side effect 1: We want to guarantee that the label list
        #  is up-to-date before our subclass adds his layers
        self._updateLabelList()

        # Side effect 2: Switch to navigation mode if labels aren't
        #  allowed on this image.
        labelsAllowedSlot = self._labelingSlots.labelsAllowed
        if labelsAllowedSlot.ready() and not labelsAllowedSlot.value:
            self._changeInteractionMode(Tool.Navigation)

        # Raw Input Layer
        if self._rawInputSlot is not None and self._rawInputSlot.ready():
            layer = self.createStandardLayerFromSlot( self._rawInputSlot )
            layer.name = "Raw Input"
            layer.visible = True
            layer.opacity = 1.0

            layers.append(layer)

        return layers

    def _createDefault16ColorColorTable(self):
        colors = []
        # Transparent for the zero label
        colors.append(QColor(0,0,0,0))
        # ilastik v0.5 colors
        colors.append( QColor( Qt.red ) )
        colors.append( QColor( Qt.green ) )
        colors.append( QColor( Qt.yellow ) )
        colors.append( QColor( Qt.blue ) )
        colors.append( QColor( Qt.magenta ) )
        colors.append( QColor( Qt.darkYellow ) )
        colors.append( QColor( Qt.lightGray ) )
        # Additional colors
        colors.append( QColor(255, 105, 180) ) #hot pink
        colors.append( QColor(102, 205, 170) ) #dark aquamarine
        colors.append( QColor(165,  42,  42) ) #brown
        colors.append( QColor(0, 0, 128) )     #navy
        colors.append( QColor(255, 165, 0) )   #orange
        colors.append( QColor(173, 255,  47) ) #green-yellow
        colors.append( QColor(128,0, 128) )    #purple
        colors.append( QColor(240, 230, 140) ) #khaki
        assert len(colors) == 16
        return [c.rgba() for c in colors]