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 __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 __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.
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 __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))
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()
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))
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
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)
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()
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, 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)
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 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
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
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)
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
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]