Source code for ilastik.applets.labeling.labelingGui

###############################################################################
#   ilastik: interactive learning and segmentation toolkit
#
#       Copyright (C) 2011-2014, the ilastik developers
#                                <team@ilastik.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# In addition, as a special exception, the copyright holders of
# ilastik give you permission to combine ilastik with applets,
# workflows and plugins which are not covered under the GNU
# General Public License.
#
# See the LICENSE file for details. License information is also available
# on the ilastik web site at:
#		   http://ilastik.org/license.html
###############################################################################
# Built-in
import os
import re
import logging
import itertools
from functools import partial

# Third-party
import numpy
from PyQt4 import uic
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QIcon, QColor, QApplication, QMessageBox, QAction

# HCI
from volumina.api import LazyflowSinkSource, ColortableLayer
from volumina.utility import ShortcutManager, PreferencesManager
from ilastik.shell.gui.iconMgr import ilastikIcons
from ilastik.widgets.labelListView import Label
from ilastik.widgets.labelListModel import LabelListModel

# ilastik
from ilastik.utility import bind, log_exception
from ilastik.utility.gui import ThunkEventHandler, threadRouted
from ilastik.applets.layerViewer.layerViewerGui import LayerViewerGui

from ilastik.applets.labeling.labelingImport import import_labeling_layer


# Loggers
logger = logging.getLogger(__name__)

#===----------------------------------------------------------------------------------------------------------------===

class Tool():
    """Enumerate the types of toolbar buttons."""
    Navigation = 0 # Arrow
    Paint      = 1
    Erase      = 2
    Threshold  = 3

[docs]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
[docs] 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)
[docs] 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
[docs] 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." 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) self._allowDeleteLastLabelOnly = 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 )
[docs] 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 ) ) 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
[docs] 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 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: self._gui_setThresholding() 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 ) if self._allowDeleteLastLabelOnly: # make previous label unremovable if newRow > 0: self._labelControlUi.labelListModel.makeRowPermanent(newRow - 1) 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 # 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()
[docs] 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)
[docs] 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
[docs] def getNextPmapColor(self): """ Return a QColor to use for the next label. """ return None
[docs] def onLabelNameChanged(self): """ Subclasses can override this to respond to changes in the label names. """ pass
[docs] def onLabelColorChanged(self): """ Subclasses can override this to respond to changes in the label colors. """ pass
[docs] 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 )) if self._allowDeleteLastLabelOnly: # make previous label removable again if oldcount >= 2: self._labelControlUi.labelListModel.makeRowRemovable(oldcount - 2) # 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)
[docs] 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')
[docs] 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
[docs] 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 ) layer.name = "Raw Input" layer.visible = True layer.opacity = 1.0 layers.append(layer) return layers
@staticmethod def _createDefault16ColorColorTable(): 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]
[docs] 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