def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg()
        self.featureDlg.setWindowTitle("Features")
        
        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append( FeatureEntry(featureName) )
            groupedNames.append( (group, featureEntries) )
        self.featureDlg.createFeatureTable( groupedNames, self.ScalesList )
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.mainOperator.FeatureIds.value)
        cols = len(self.mainOperator.Scales.value)
        defaultFeatures = numpy.zeros((rows,cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)
示例#2
0
    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)

        self.featureDlg.accepted.connect(saveSize)

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append(FeatureEntry(featureName))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(groupedNames, self.ScalesList)
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.topLevelOperatorView.FeatureIds.value)
        cols = len(self.topLevelOperatorView.Scales.value)
        defaultFeatures = numpy.zeros((rows, cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

        # Disable the first column, except for the first item.
        # This is a slightly hacky way of fixing ilastik issue #610.
        # Besides color, the features at a sigma of 0.3 are not valid because the
        #  results are overwhelmed by the inherent sampling noise of the filter.
        # (This is a bit hacky because we ASSUME the first feature is Gaussian
        # Smoothing.  It works for now.)
        enabled_item_mask = numpy.ones(defaultFeatures.shape, dtype=bool)
        enabled_item_mask[1:, 0] = False  # hacky
        self.featureDlg.setEnableItemMask(enabled_item_mask)
示例#3
0
    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)
        self.featureDlg.accepted.connect(saveSize)
        self.featureDlg.setImageToPreView(None)
        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)
示例#4
0
    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)

        self.featureDlg.accepted.connect(saveSize)

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append(FeatureEntry(featureName))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(groupedNames, self.ScalesList)
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.topLevelOperatorView.FeatureIds.value)
        cols = len(self.topLevelOperatorView.Scales.value)
        defaultFeatures = numpy.zeros((rows, cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)
示例#5
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """

    # Constants
    ScalesList = [0.3, 0.7, 1, 1.6, 3.5, 5.0, 10.0]

    # Map feature groups to lists of feature IDs
    FeatureGroups = [
        ("Color/Intensity", ["GaussianSmoothing"]),
        ("Edge", ["LaplacianOfGaussian", "GaussianGradientMagnitude", "DifferenceOfGaussians"]),
        ("Texture", ["StructureTensorEigenvalues", "HessianOfGaussianEigenvalues"]),
    ]

    # Map feature IDs to feature names
    FeatureNames = {
        "GaussianSmoothing": "Gaussian Smoothing",
        "LaplacianOfGaussian": "Laplacian of Gaussian",
        "GaussianGradientMagnitude": "Gaussian Gradient Magnitude",
        "DifferenceOfGaussians": "Difference of Gaussians",
        "StructureTensorEigenvalues": "Structure Tensor EigenValues",
        "HessianOfGaussianEigenvalues": "Hessian of Gaussian Eigenvalues",
    }

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

    def appletDrawer(self):
        return self.drawer

    def viewerControlWidget(self):
        return self._viewerControlWidget

    def stopAndCleanUp(self):
        super(FeatureSelectionGui, self).stopAndCleanUp()
        self.drawer.caption.setText("(No features selected)")

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

        # Why is this necessary?
        # Clearing the layerstack doesn't seem to call the rowsRemoved signal?
        self._viewerControlWidget.featureListWidget.clear()

    # (Other methods already provided by our base class)

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

    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        self.topLevelOperatorView = topLevelOperatorView
        super(FeatureSelectionGui, self).__init__(parentApplet, topLevelOperatorView, crosshair=False)
        self.parentApplet = parentApplet

        self.__cleanup_fns = []

        self.topLevelOperatorView.SelectionMatrix.notifyDirty(bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.FeatureListFilename.notifyDirty(bind(self.onFeaturesSelectionsChanged))
        self.__cleanup_fns.append(
            partial(self.topLevelOperatorView.SelectionMatrix.unregisterDirty, bind(self.onFeaturesSelectionsChanged))
        )
        self.__cleanup_fns.append(
            partial(
                self.topLevelOperatorView.FeatureListFilename.unregisterDirty, bind(self.onFeaturesSelectionsChanged)
            )
        )

        self.onFeaturesSelectionsChanged()

        # Init feature dialog
        self.initFeatureDlg()

    def getFeatureIdOrder(self):
        featureIrdOrder = []
        for group, featureIds in self.FeatureGroups:
            featureIrdOrder += featureIds
        return featureIrdOrder

    def initFeatureOrder(self):
        self.topLevelOperatorView.Scales.setValue(self.ScalesList)
        self.topLevelOperatorView.FeatureIds.setValue(self.getFeatureIdOrder())

    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir + "/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(self.onFeatureButtonClicked)
        self.drawer.UsePrecomputedFeaturesButton.clicked.connect(self.onUsePrecomputedFeaturesButtonClicked)
        dbg = ilastik_config.getboolean("ilastik", "debug")
        if not dbg:
            self.drawer.UsePrecomputedFeaturesButton.setHidden(True)

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        
        TODO: Right now we manage adding/removing entries to a plain listview 
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us, 
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(os.path.split(__file__)[0] + "/viewerControls.ui")

        layerListWidget = self._viewerControlWidget.featureListWidget
        layerListWidget.setSelectionMode(QAbstractItemView.SingleSelection)

        # Need to handle data changes because the layerstack model hasn't
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)

        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = i == row

        def handleInsertedLayers(parent, start, end):
            for i in range(start, end + 1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
                if layerListWidget.model().rowCount() == 1:
                    layerListWidget.item(0).setSelected(True)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(range(start, end + 1)):
                layerListWidget.takeItem(i)

        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        self.layerstack.rowsRemoved.connect(handleRemovedLayers)
        self.layerstack.rowsInserted.connect(handleInsertedLayers)
        layerListWidget.currentRowChanged.connect(handleSelectionChanged)

    def setupLayers(self):
        opFeatureSelection = self.topLevelOperatorView
        inputSlot = opFeatureSelection.InputImage

        layers = []

        if inputSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(inputSlot)
            rawLayer.visible = True
            rawLayer.opacity = 1.0
            rawLayer.name = "Raw Data (display only)"
            layers.append(rawLayer)

        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)

            layers[0].visible = True
        return layers

    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []

        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels / numInputChannels
        assert (
            0 < featureChannelsPerInputChannel <= 3
        ), "The feature selection Gui does not yet support features with more than three channels per input channel."

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ["R", "G", "B"]
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(parent=self.topLevelOperatorView.parent)
            opSubRegion.Input.connect(featureSlot)
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel + 1) * featureChannelsPerInputChannel
            opSubRegion.Start.setValue(tuple(start))
            opSubRegion.Stop.setValue(tuple(stop))

            featureLayer = self.createStandardLayerFromSlot(opSubRegion.Output)
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName

            layers.append(featureLayer)

        return layers

    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)

        self.featureDlg.accepted.connect(saveSize)

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append(FeatureEntry(featureName))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(groupedNames, self.ScalesList)
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.topLevelOperatorView.FeatureIds.value)
        cols = len(self.topLevelOperatorView.Scales.value)
        defaultFeatures = numpy.zeros((rows, cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

        # Disable the first column, except for the first item.
        # This is a slightly hacky way of fixing ilastik issue #610.
        # Besides color, the features at a sigma of 0.3 are not valid because the
        #  results are overwhelmed by the inherent sampling noise of the filter.
        # (This is a bit hacky because we ASSUME the first feature is Gaussian
        # Smoothing.  It works for now.)
        enabled_item_mask = numpy.ones(defaultFeatures.shape, dtype=bool)
        enabled_item_mask[1:, 0] = False  # hacky
        self.featureDlg.setEnableItemMask(enabled_item_mask)

    def onUsePrecomputedFeaturesButtonClicked(self):
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog

        filename = QFileDialog.getOpenFileName(self, "Open Feature List", ".", options=options)
        filename = encode_from_qstring(filename)

        # sanity checks on the given file
        if not filename:
            return
        if not os.path.exists(filename):
            QMessageBox.critical(self, "Open Feature List", "File '%s' does not exist" % filename)
            return
        f = open(filename, "r")
        with f:
            for line in f:
                line = line.strip()
                if len(line) == 0:
                    continue
                if not os.path.exists(line):
                    QMessageBox.critical(
                        self, "Open Feature List", "File '%s', referenced in '%s', does not exist" % (line, filename)
                    )
                    return
                try:
                    h = h5py.File(line, "r")
                    with h:
                        assert len(h["data"].shape) == 3
                except:
                    QMessageBox.critical(
                        self,
                        "Open Feature List",
                        "File '%s', referenced in '%s', could not be opened as an HDF5 file or does not contain a 3D dataset called 'data'"
                        % (line, filename),
                    )
                    return

        self.topLevelOperatorView.FeatureListFilename.setValue(filename)
        self.topLevelOperatorView._setupOutputs()
        self.onFeaturesSelectionsChanged()

        # Notify the workflow that some applets may have changed state now.
        # (For example, the downstream pixel classification applet can
        #  be used now that there are features selected)
        self.parentApplet.appletStateUpdateRequested.emit()

    def onFeatureButtonClicked(self):
        self.topLevelOperatorView.FeatureListFilename.setValue("")

        # Refresh the feature matrix in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        if self.topLevelOperatorView.SelectionMatrix.ready() and self.topLevelOperatorView.FeatureIds.ready():
            # Re-order the feature matrix using the loaded feature ids
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            featureOrdering = self.topLevelOperatorView.FeatureIds.value

            reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
            newrow = 0
            for group, featureIds in self.FeatureGroups:
                for featureId in featureIds:
                    oldrow = featureOrdering.index(featureId)
                    reorderedMatrix[newrow] = matrix[oldrow]
                    newrow += 1

            self.featureDlg.selectedFeatureBoolMatrix = reorderedMatrix

        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.topLevelOperatorView
        if opFeatureSelection is not None:
            # Re-initialize the scales and features
            self.initFeatureOrder()

            # Give the new features to the pipeline (if there are any)
            featureMatrix = numpy.asarray(self.featureDlg.selectedFeatureBoolMatrix)
            if featureMatrix.any():
                # Disable gui
                self.parentApplet.busy = True
                self.parentApplet.appletStateUpdateRequested.emit()
                QApplication.instance().setOverrideCursor(QCursor(Qt.WaitCursor))
                QApplication.instance().processEvents()

                try:
                    opFeatureSelection.SelectionMatrix.setValue(featureMatrix)
                except DatasetConstraintError as ex:
                    # The user selected some scales that were too big.
                    QMessageBox.critical(self, "Invalid selections", ex.message)
                    opFeatureSelection.SelectionMatrix.disconnect()

                # Re-enable gui
                QApplication.instance().restoreOverrideCursor()
                self.parentApplet.busy = False
                self.parentApplet.appletStateUpdateRequested.emit()
            else:
                # Not valid to give a matrix with no features selected.
                # Disconnect.
                opFeatureSelection.SelectionMatrix.disconnect()

                # Notify the workflow that some applets may have changed state now.
                # (For example, the downstream pixel classification applet can
                #  be used now that there are features selected)
                self.parentApplet.appletStateUpdateRequested.emit()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's matrix of feature selections.
        """
        # Update the drawer caption

        fff = (
            self.topLevelOperatorView.FeatureListFilename.ready()
            and len(self.topLevelOperatorView.FeatureListFilename.value) != 0
        )

        if not self.topLevelOperatorView.SelectionMatrix.ready() and not fff:
            self.drawer.caption.setText("(No features selected)")
            self.layerstack.clear()
        elif fff:
            self.drawer.caption.setText("(features from files)")
        else:
            self.initFeatureOrder()
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            self.drawer.caption.setText("(Selected %d features)" % numpy.sum(matrix))
示例#6
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """

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

    def appletDrawer(self):
        return self.drawer

    def viewerControlWidget(self):
        return self._viewerControlWidget

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

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

    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        self.topLevelOperatorView = topLevelOperatorView
        super(FeatureSelectionGui, self).__init__(parentApplet,
                                                  topLevelOperatorView,
                                                  crosshair=False)
        self.parentApplet = parentApplet

        self.__cleanup_fns = []

        self.topLevelOperatorView.InputImage.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.SelectionMatrix.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.FeatureListFilename.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.__cleanup_fns.append(
            partial(self.topLevelOperatorView.SelectionMatrix.unregisterDirty,
                    bind(self.onFeaturesSelectionsChanged)))
        self.__cleanup_fns.append(
            partial(
                self.topLevelOperatorView.FeatureListFilename.unregisterDirty,
                bind(self.onFeaturesSelectionsChanged)))

        # Init feature dialog
        self.initFeatureDlg()

        self.onFeaturesSelectionsChanged()

    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir + "/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(
            self.onFeatureButtonClicked)
        self.drawer.UsePrecomputedFeaturesButton.clicked.connect(
            self.onUsePrecomputedFeaturesButtonClicked)
        dbg = ilastik_config.getboolean("ilastik", "debug")
        if not dbg:
            self.drawer.UsePrecomputedFeaturesButton.setHidden(True)

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.

        TODO: Right now we manage adding/removing entries to a plain listview
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us,
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(
            os.path.split(__file__)[0] + "/viewerControls.ui")

        layerListWidget = self._viewerControlWidget.featureListWidget
        layerListWidget.setSelectionMode(QAbstractItemView.SingleSelection)

        # Need to handle data changes because the layerstack model hasn't
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)

        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = i == row

        def handleInsertedLayers(parent, start, end):
            for i in range(start, end + 1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
                if layerListWidget.model().rowCount() == 1:
                    layerListWidget.item(0).setSelected(True)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(list(range(start, end + 1))):
                layerListWidget.takeItem(i)

        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        self.layerstack.rowsRemoved.connect(handleRemovedLayers)
        self.layerstack.rowsInserted.connect(handleInsertedLayers)
        layerListWidget.currentRowChanged.connect(handleSelectionChanged)

        # Support the same right-click menu as 'normal' layer list widgets
        def showLayerContextMenu(pos):
            idx = layerListWidget.indexAt(pos)
            layer = self.layerstack[idx.row()]
            layercontextmenu(layer, layerListWidget.mapToGlobal(pos),
                             layerListWidget)

        layerListWidget.customContextMenuRequested.connect(
            showLayerContextMenu)
        layerListWidget.setContextMenuPolicy(Qt.CustomContextMenu)

    def setupLayers(self):
        if hasattr(self.drawer, "feature2dBox"
                   ):  # drawer has to be initialized (initAppletDrawerUi)
            # set hidden status of feature2dBox again (presence of z axis may have changed)
            if "z" in self.topLevelOperatorView.InputImage.meta.original_axistags:
                self.drawer.feature2dBox.setHidden(False)
            else:
                self.drawer.feature2dBox.setHidden(True)

        opFeatureSelection = self.topLevelOperatorView
        inputSlot = opFeatureSelection.InputImage

        layers = []

        if inputSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(inputSlot)
            rawLayer.visible = True
            rawLayer.opacity = 1.0
            rawLayer.name = "Raw Data (display only)"
            layers.append(rawLayer)

        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)

            layers[0].visible = True
        return layers

    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []

        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels // numInputChannels
        if not 0 < featureChannelsPerInputChannel <= 3:
            logger.warning(
                "The feature selection Gui does not yet support features with more than three channels per "
                "input channel. Some features will not be displayed entirely.")

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ["R", "G", "B"]
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(parent=self.topLevelOperatorView.parent)
            opSubRegion.Input.connect(featureSlot)
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel +
                                 1) * featureChannelsPerInputChannel

            opSubRegion.Roi.setValue((tuple(start), tuple(stop)))

            featureLayer = self.createStandardLayerFromSlot(opSubRegion.Output)
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName

            layers.append(featureLayer)

        return layers

    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = preferences.get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            preferences.set("featureSelection", "dialog size", s)

        self.featureDlg.accepted.connect(saveSize)
        self.featureDlg.setImageToPreView(None)
        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

    def onUsePrecomputedFeaturesButtonClicked(self):
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog

        filenames, _filter = QFileDialog.getOpenFileNames(self,
                                                          "Open Feature Files",
                                                          ".",
                                                          options=options)

        # Check if file exists
        if not filenames:
            return

        for filename in filenames:
            if not os.path.exists(filename):
                QMessageBox.critical(self, "Open Feature List",
                                     "File '%s' does not exist" % filename)
                return

        num_lanes = len(self.parentApplet.topLevelOperator.FeatureListFilename)
        if num_lanes != len(filenames):
            QMessageBox.critical(
                self,
                "Wrong number of feature files",
                "You must select all pre-computed feature files at once (shift-click).\n"
                "You selected {} file(s), but there are {} image(s) loaded".
                format(len(filenames), num_lanes),
            )
            return

        for filename, slot in zip(
                filenames,
                self.parentApplet.topLevelOperator.FeatureListFilename):
            slot.setValue(filename)

        # Create a dummy SelectionMatrix, just so the operator knows it is configured
        # This is a little hacky.  We should really make SelectionMatrix optional,
        # and then handle the choice correctly in setupOutputs, probably involving
        # the Output.meta.NOTREADY flag
        dummy_matrix = numpy.zeros((6, 7), dtype=bool)
        dummy_matrix[0, 0] = True
        self.parentApplet.topLevelOperator.SelectionMatrix.setValue(True)

        # Notify the workflow that some applets may have changed state now.
        # (For example, the downstream pixel classification applet can
        #  be used now that there are features selected)
        self.parentApplet.appletStateUpdateRequested()

    def onFeatureButtonClicked(self):
        # Remove all pre-computed feature files
        for slot in self.parentApplet.topLevelOperator.FeatureListFilename:
            slot.disconnect()

        # The first time we open feature selection, the minimal set of features should be set. Afterwards, if the
        # data input is changed, the feature selection dialog should appear, if adjustments are necessary
        if not self.topLevelOperatorView.SelectionMatrix.ready():
            self.topLevelOperatorView.SelectionMatrix.setValue(
                self.topLevelOperatorView.MinimalFeatures)

        # Other slots need to be ready (they also should, as they have default values)
        assert self.topLevelOperatorView.FeatureIds.ready()
        assert self.topLevelOperatorView.Scales.ready()
        assert self.topLevelOperatorView.ComputeIn2d.ready(
        ), self.topLevelOperatorView.ComputeIn2d.value

        # Refresh the dialog data in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        # This also ensures to restore the selection after previously canceling the feature dialog
        opFeatureSelection = self.topLevelOperatorView

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in opFeatureSelection.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = opFeatureSelection.FeatureNames[featureId]
                availableFilterOps = {
                    key[2:]: value
                    for key, value in filterOps.__dict__.items()
                    if key.startswith("Op")
                }
                minimum_scale = availableFilterOps[featureId].minimum_scale
                featureEntries.append(FeatureEntry(featureName, minimum_scale))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(
            groupedNames,
            opFeatureSelection.Scales.value,
            opFeatureSelection.ComputeIn2d.value,
            opFeatureSelection.WINDOW_SIZE,
        )
        # update feature dialog to show/hide z dimension specific 'compute in 2d' flags
        if self.topLevelOperatorView.InputImage.ready(
        ) and self.topLevelOperatorView.ComputeIn2d.value:
            ts = self.topLevelOperatorView.InputImage.meta.getTaggedShape()
            hide = ("z" not in ts or ts["z"] == 1) and all(
                self.topLevelOperatorView.ComputeIn2d.value)
            self.featureDlg.setComputeIn2dHidden(hide)

        matrix = opFeatureSelection.SelectionMatrix.value
        featureOrdering = opFeatureSelection.FeatureIds.value

        # Re-order the feature matrix using the loaded feature ids
        reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
        newrow = 0
        for group, featureIds in OpFeatureSelection.FeatureGroups:
            for featureId in featureIds:
                oldrow = featureOrdering.index(featureId)
                reorderedMatrix[newrow] = matrix[oldrow]
                newrow += 1

        self.featureDlg.selectionMatrix = reorderedMatrix

        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.topLevelOperatorView
        if opFeatureSelection is not None:
            # Save previous settings
            old_scales = opFeatureSelection.Scales.value
            old_computeIn2d = opFeatureSelection.ComputeIn2d.value
            old_features = opFeatureSelection.SelectionMatrix.value

            # Disable gui
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested()
            QApplication.instance().setOverrideCursor(QCursor(Qt.WaitCursor))

            try:
                # Apply new settings
                # Disconnect an input (used like a transaction slot)
                opFeatureSelection.SelectionMatrix.disconnect()

                opFeatureSelection.Scales.setValue(self.featureDlg.scales)
                opFeatureSelection.ComputeIn2d.setValue(
                    self.featureDlg.computeIn2d)
                # set disconnected slot at last (used like a transaction slot)
                opFeatureSelection.SelectionMatrix.setValue(
                    self.featureDlg.selectionMatrix)
            except (DatasetConstraintError, RuntimeError) as ex:
                # The user selected some scales that were too big.
                if isinstance(ex, DatasetConstraintError):
                    QMessageBox.critical(self, "Invalid selection", ex.message)
                else:
                    QMessageBox.critical(
                        self, "Invalid selection",
                        "You selected the exact same feature twice.")

                # Restore previous settings
                opFeatureSelection.SelectionMatrix.disconnect()
                opFeatureSelection.Scales.setValue(old_scales)
                opFeatureSelection.ComputeIn2d.setValue(old_computeIn2d)
                opFeatureSelection.SelectionMatrix.setValue(old_features)

            # Re-enable gui
            QApplication.instance().restoreOverrideCursor()
            self.parentApplet.busy = False

            # Notify the workflow that some applets may have changed state now.
            # (For example, the downstream pixel classification applet can
            #  be used now that there are features selected)
            self.parentApplet.appletStateUpdateRequested()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's ImageInput and matrix of feature selections.
        """
        # Update the drawer caption
        fff = (self.topLevelOperatorView.FeatureListFilename.ready() and
               len(self.topLevelOperatorView.FeatureListFilename.value) != 0)

        if not self.topLevelOperatorView.SelectionMatrix.ready() and not fff:
            self.drawer.caption.setText("(No features selected)")
            self.layerstack.clear()
        elif fff:
            self.drawer.caption.setText("(features from files)")
        else:
            nr_feat = self.topLevelOperatorView.SelectionMatrix.value.sum()
            self.drawer.caption.setText(f"(Selected {nr_feat} features)")
示例#7
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """
    # ##########################################
    # ## AppletGuiInterface Concrete Methods ###
    # ##########################################

    def appletDrawer(self):
        return self.drawer

    def viewerControlWidget(self):
        return self._viewerControlWidget

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

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

    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        self.topLevelOperatorView = topLevelOperatorView
        super(FeatureSelectionGui, self).__init__(parentApplet, topLevelOperatorView, crosshair=False)
        self.parentApplet = parentApplet

        self.__cleanup_fns = []

        self.topLevelOperatorView.InputImage.notifyDirty(bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.SelectionMatrix.notifyDirty(bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.FeatureListFilename.notifyDirty(bind(self.onFeaturesSelectionsChanged))
        self.__cleanup_fns.append(partial(self.topLevelOperatorView.SelectionMatrix.unregisterDirty,
                                          bind(self.onFeaturesSelectionsChanged)))
        self.__cleanup_fns.append(partial(self.topLevelOperatorView.FeatureListFilename.unregisterDirty,
                                          bind(self.onFeaturesSelectionsChanged)))

        # Init feature dialog
        self.initFeatureDlg()

        self.onFeaturesSelectionsChanged()

    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir + "/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(self.onFeatureButtonClicked)
        self.drawer.UsePrecomputedFeaturesButton.clicked.connect(self.onUsePrecomputedFeaturesButtonClicked)
        dbg = ilastik_config.getboolean("ilastik", "debug")
        if not dbg:
            self.drawer.UsePrecomputedFeaturesButton.setHidden(True)

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.

        TODO: Right now we manage adding/removing entries to a plain listview
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us,
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(os.path.split(__file__)[0] + "/viewerControls.ui")

        layerListWidget = self._viewerControlWidget.featureListWidget
        layerListWidget.setSelectionMode(QAbstractItemView.SingleSelection)

        # Need to handle data changes because the layerstack model hasn't
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)

        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = (i == row)

        def handleInsertedLayers(parent, start, end):
            for i in range(start, end + 1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
                if layerListWidget.model().rowCount() == 1:
                    layerListWidget.item(0).setSelected(True)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(list(range(start, end + 1))):
                layerListWidget.takeItem(i)

        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        self.layerstack.rowsRemoved.connect(handleRemovedLayers)
        self.layerstack.rowsInserted.connect(handleInsertedLayers)
        layerListWidget.currentRowChanged.connect(handleSelectionChanged)

        # Support the same right-click menu as 'normal' layer list widgets
        def showLayerContextMenu(pos):
            idx = layerListWidget.indexAt(pos)
            layer = self.layerstack[idx.row()]
            layercontextmenu(layer, layerListWidget.mapToGlobal(pos), layerListWidget)
        layerListWidget.customContextMenuRequested.connect(showLayerContextMenu)
        layerListWidget.setContextMenuPolicy(Qt.CustomContextMenu)

    def setupLayers(self):
        if hasattr(self.drawer, 'feature2dBox'):  # drawer has to be initialized (initAppletDrawerUi)
            # set hidden status of feature2dBox again (presence of z axis may have changed)
            if 'z' in self.topLevelOperatorView.InputImage.meta.original_axistags:
                self.drawer.feature2dBox.setHidden(False)
            else:
                self.drawer.feature2dBox.setHidden(True)

        opFeatureSelection = self.topLevelOperatorView
        inputSlot = opFeatureSelection.InputImage

        layers = []

        if inputSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(inputSlot)
            rawLayer.visible = True
            rawLayer.opacity = 1.0
            rawLayer.name = "Raw Data (display only)"
            layers.append(rawLayer)

        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)

            layers[0].visible = True
        return layers

    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []

        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels // numInputChannels
        if not 0 < featureChannelsPerInputChannel <= 3:
            logger.warning('The feature selection Gui does not yet support features with more than three channels per '
                           'input channel. Some features will not be displayed entirely.')

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ['R', 'G', 'B']
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(parent=self.topLevelOperatorView.parent)
            opSubRegion.Input.connect(featureSlot)
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel + 1) * featureChannelsPerInputChannel

            opSubRegion.Roi.setValue((tuple(start), tuple(stop)))

            featureLayer = self.createStandardLayerFromSlot(opSubRegion.Output)
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName

            layers.append(featureLayer)

        return layers

    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)
        self.featureDlg.accepted.connect(saveSize)
        self.featureDlg.setImageToPreView(None)
        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

    def onUsePrecomputedFeaturesButtonClicked(self):
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog

        filenames, _filter = QFileDialog.getOpenFileNames(self, 'Open Feature Files', '.', options=options)

        # Check if file exists
        if not filenames:
            return

        for filename in filenames:
            if not os.path.exists(filename):
                QMessageBox.critical(self, "Open Feature List", "File '%s' does not exist" % filename)
                return

        num_lanes = len(self.parentApplet.topLevelOperator.FeatureListFilename)
        if num_lanes != len(filenames):
            QMessageBox.critical(self, "Wrong number of feature files",
                                 "You must select all pre-computed feature files at once (shift-click).\n"
                                 "You selected {} file(s), but there are {} image(s) loaded"
                                 .format(len(filenames), num_lanes))
            return

        for filename, slot in zip(filenames, self.parentApplet.topLevelOperator.FeatureListFilename):
            slot.setValue(filename)

        # Create a dummy SelectionMatrix, just so the operator knows it is configured
        # This is a little hacky.  We should really make SelectionMatrix optional,
        # and then handle the choice correctly in setupOutputs, probably involving
        # the Output.meta.NOTREADY flag
        dummy_matrix = numpy.zeros((6, 7), dtype=bool)
        dummy_matrix[0, 0] = True
        self.parentApplet.topLevelOperator.SelectionMatrix.setValue(True)

        # Notify the workflow that some applets may have changed state now.
        # (For example, the downstream pixel classification applet can
        #  be used now that there are features selected)
        self.parentApplet.appletStateUpdateRequested()

    def onFeatureButtonClicked(self):
        # Remove all pre-computed feature files
        for slot in self.parentApplet.topLevelOperator.FeatureListFilename:
            slot.disconnect()

        # The first time we open feature selection, the minimal set of features should be set. Afterwards, if the
        # data input is changed, the feature selection dialog should appear, if adjustments are necessary
        if not self.topLevelOperatorView.SelectionMatrix.ready():
            self.topLevelOperatorView.SelectionMatrix.setValue(self.topLevelOperatorView.MinimalFeatures)

        # Other slots need to be ready (they also should, as they have default values)
        assert self.topLevelOperatorView.FeatureIds.ready()
        assert self.topLevelOperatorView.Scales.ready()
        assert self.topLevelOperatorView.ComputeIn2d.ready(), self.topLevelOperatorView.ComputeIn2d.value

        # Refresh the dialog data in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        # This also ensures to restore the selection after previously canceling the feature dialog
        opFeatureSelection = self.topLevelOperatorView

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in opFeatureSelection.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = opFeatureSelection.FeatureNames[featureId]
                availableFilterOps = {key[2:]: value for key, value in filterOps.__dict__.items()
                                      if key.startswith('Op')}
                minimum_scale = availableFilterOps[featureId].minimum_scale
                featureEntries.append(FeatureEntry(featureName, minimum_scale))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(groupedNames, opFeatureSelection.Scales.value,
                                           opFeatureSelection.ComputeIn2d.value, opFeatureSelection.WINDOW_SIZE)
        # update feature dialog to show/hide z dimension specific 'compute in 2d' flags
        if self.topLevelOperatorView.InputImage.ready() and self.topLevelOperatorView.ComputeIn2d.value:
            ts = self.topLevelOperatorView.InputImage.meta.getTaggedShape()
            hide = ('z' not in ts or ts['z'] == 1) and all(self.topLevelOperatorView.ComputeIn2d.value)
            self.featureDlg.setComputeIn2dHidden(hide)

        matrix = opFeatureSelection.SelectionMatrix.value
        featureOrdering = opFeatureSelection.FeatureIds.value

        # Re-order the feature matrix using the loaded feature ids
        reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
        newrow = 0
        for group, featureIds in OpFeatureSelection.FeatureGroups:
            for featureId in featureIds:
                oldrow = featureOrdering.index(featureId)
                reorderedMatrix[newrow] = matrix[oldrow]
                newrow += 1

        self.featureDlg.selectionMatrix = reorderedMatrix

        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.topLevelOperatorView
        if opFeatureSelection is not None:
            # Save previous settings
            old_scales = opFeatureSelection.Scales.value
            old_computeIn2d = opFeatureSelection.ComputeIn2d.value
            old_features = opFeatureSelection.SelectionMatrix.value

            # Disable gui
            self.parentApplet.busy = True
            self.parentApplet.appletStateUpdateRequested()
            QApplication.instance().setOverrideCursor(QCursor(Qt.WaitCursor))

            try:
                # Apply new settings
                # Disconnect an input (used like a transaction slot)
                opFeatureSelection.SelectionMatrix.disconnect()

                opFeatureSelection.Scales.setValue(self.featureDlg.scales)
                opFeatureSelection.ComputeIn2d.setValue(self.featureDlg.computeIn2d)
                # set disconnected slot at last (used like a transaction slot)
                opFeatureSelection.SelectionMatrix.setValue(self.featureDlg.selectionMatrix)
            except (DatasetConstraintError, RuntimeError) as ex:
                # The user selected some scales that were too big.
                if isinstance(ex, DatasetConstraintError):
                    QMessageBox.critical(self, 'Invalid selection', ex.message)
                else:
                    QMessageBox.critical(self, 'Invalid selection', 'You selected the exact same feature twice.')

                # Restore previous settings
                opFeatureSelection.SelectionMatrix.disconnect()
                opFeatureSelection.Scales.setValue(old_scales)
                opFeatureSelection.ComputeIn2d.setValue(old_computeIn2d)
                opFeatureSelection.SelectionMatrix.setValue(old_features)

            # Re-enable gui
            QApplication.instance().restoreOverrideCursor()
            self.parentApplet.busy = False

            # Notify the workflow that some applets may have changed state now.
            # (For example, the downstream pixel classification applet can
            #  be used now that there are features selected)
            self.parentApplet.appletStateUpdateRequested()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's ImageInput and matrix of feature selections.
        """
        # Update the drawer caption
        fff = (self.topLevelOperatorView.FeatureListFilename.ready() and
               len(self.topLevelOperatorView.FeatureListFilename.value) != 0)

        if not self.topLevelOperatorView.SelectionMatrix.ready() and not fff:
            self.drawer.caption.setText("(No features selected)")
            self.layerstack.clear()
        elif fff:
            self.drawer.caption.setText("(features from files)")
        else:
            nr_feat = self.topLevelOperatorView.SelectionMatrix.value.sum()
            self.drawer.caption.setText(f'(Selected {nr_feat} features)')
class FeatureSelectionGui(LayerViewerGui):
    """
    """
    
    # Constants    
    ScalesList = [0.3, 0.7, 1, 1.6, 3.5, 5.0, 10.0]
    DefaultColorTable = None

#    # Default order
#    FeatureIds = [ 'GaussianSmoothing',
#                   'LaplacianOfGaussian',
#                   'GaussianGradientMagnitude',
#                   'DifferenceOfGaussians',
#                   'StructureTensorEigenvalues',
#                   'HessianOfGaussianEigenvalues' ]

    # Map feature groups to lists of feature IDs
    FeatureGroups = [ ( "Color/Intensity",   [ "GaussianSmoothing" ] ),
                      ( "Edge",    [ "LaplacianOfGaussian", "GaussianGradientMagnitude", "DifferenceOfGaussians" ] ),
                      ( "Texture", [ "StructureTensorEigenvalues", "HessianOfGaussianEigenvalues" ] ) ]

    # Map feature IDs to feature names
    FeatureNames = { 'GaussianSmoothing' : 'Gaussian Smoothing',
                     'LaplacianOfGaussian' : "Laplacian of Gaussian",
                     'GaussianGradientMagnitude' : "Gaussian Gradient Magnitude",
                     'DifferenceOfGaussians' : "Difference of Gaussians",
                     'StructureTensorEigenvalues' : "Structure Tensor EigenValues",
                     'HessianOfGaussianEigenvalues' : "Hessian of Gaussian Eigenvalues" }

    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################
    
    def appletDrawers(self):
        return [ ("Feature Selection", self.drawer ) ]

    def viewerControlWidget(self):
        return self._viewerControlWidget

    def reset(self):
        super(FeatureSelectionGui, self).reset()
        self.drawer.caption.setText( "(No features selected)" )

        # Why is this necessary?
        # Clearing the layerstack doesn't seem to call the rowsRemoved signal?
        self._viewerControlWidget.listWidget.clear()

    # (Other methods already provided by our base class)

    ###########################################
    ###########################################
    
    @traceLogged(traceLogger)
    def __init__(self, mainOperator):
        """
        """
        super(FeatureSelectionGui, self).__init__(mainOperator)
        self.mainOperator = mainOperator

        self.mainOperator.SelectionMatrix.notifyDirty( bind(self.onFeaturesSelectionsChanged) )

        # Init feature dialog
        self.initFeatureDlg()

    def getFeatureIdOrder(self):
        featureIrdOrder = []
        for group, featureIds in self.FeatureGroups:
            featureIrdOrder += featureIds
        return featureIrdOrder

    def initFeatureOrder(self):
        self.mainOperator.Scales.setValue( self.ScalesList )
        self.mainOperator.FeatureIds.setValue( self.getFeatureIdOrder() )
            
    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir+"/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(self.onFeatureButtonClicked)
    

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        
        TODO: Right now we manage adding/removing entries to a plain listview 
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us, 
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(os.path.split(__file__)[0] + "/viewerControls.ui")
        
        layerListWidget = self._viewerControlWidget.listWidget

        # Need to handle data changes because the layerstack model hasn't 
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)
        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        
        def handleInsertedLayers(parent, start, end):
            for i in range(start, end+1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
        self.layerstack.rowsInserted.connect( handleInsertedLayers )

        def handleRemovedLayers(parent, start, end):
            for i in reversed(range(start, end+1)):
                layerListWidget.takeItem(i)
        self.layerstack.rowsRemoved.connect( handleRemovedLayers )
        
        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = (i == row)
        layerListWidget.currentRowChanged.connect( handleSelectionChanged )
    
    @traceLogged(traceLogger)
    def setupLayers(self, currentImageIndex):
        layers = []
        
        opFeatureSelection = self.operatorForCurrentImage()

        inputSlot = opFeatureSelection.InputImage
        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)
            
            layers[0].visible = True
        return layers

    @traceLogged(traceLogger)
    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []
        
        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels / numInputChannels
        assert 0 < featureChannelsPerInputChannel <= 3, "The feature selection Gui does not yet support features with more than three channels per input channel." 

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ['R', 'G', 'B']
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(graph=self.mainOperator.graph)
            opSubRegion.Input.connect( featureSlot )
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel+1) * featureChannelsPerInputChannel
            opSubRegion.Start.setValue( tuple(start) )
            opSubRegion.Stop.setValue( tuple(stop) )
            
            featureLayer = self.createStandardLayerFromSlot( opSubRegion.Output )
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName
            
            layers.append(featureLayer)

        return layers

    @traceLogged(traceLogger)
    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg()
        self.featureDlg.setWindowTitle("Features")
        
        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append( FeatureEntry(featureName) )
            groupedNames.append( (group, featureEntries) )
        self.featureDlg.createFeatureTable( groupedNames, self.ScalesList )
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.mainOperator.FeatureIds.value)
        cols = len(self.mainOperator.Scales.value)
        defaultFeatures = numpy.zeros((rows,cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

    def onFeatureButtonClicked(self):
        # Refresh the feature matrix in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        if self.mainOperator.SelectionMatrix.ready() and self.mainOperator.FeatureIds.ready():
            # Re-order the feature matrix using the loaded feature ids
            matrix = self.mainOperator.SelectionMatrix.value
            featureOrdering = self.mainOperator.FeatureIds.value
            
            reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
            newrow = 0
            for group, featureIds in self.FeatureGroups:
                for featureId in featureIds:
                    oldrow = featureOrdering.index(featureId)
                    reorderedMatrix[newrow] = matrix[oldrow]
                    newrow += 1
                
            self.featureDlg.selectedFeatureBoolMatrix = reorderedMatrix
        
        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.operatorForCurrentImage()
        if opFeatureSelection is not None:
            # Re-initialize the scales and features
            self.initFeatureOrder()

            # Give the new features to the pipeline (if there are any)
            featureMatrix = numpy.asarray(self.featureDlg.selectedFeatureBoolMatrix)
            if featureMatrix.any():
                opFeatureSelection.SelectionMatrix.setValue( featureMatrix )
            else:
                # Not valid to give a matrix with no features selected.
                # Disconnect.
                opFeatureSelection.SelectionMatrix.disconnect()
    
    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's matrix of feature selections.
        """
        # Update the drawer caption
        if not self.mainOperator.SelectionMatrix.ready():
            self.drawer.caption.setText( "(No features selected)" )
            self.layerstack.clear()
        else:
            self.initFeatureOrder()
            matrix = self.mainOperator.SelectionMatrix.value
            self.drawer.caption.setText( "(Selected %d features)" % numpy.sum(matrix) )
示例#9
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """

    # Constants
    ScalesList = [0.3, 0.7, 1, 1.6, 3.5, 5.0, 10.0]

    # Map feature groups to lists of feature IDs
    FeatureGroups = [
        ("Color/Intensity", ["GaussianSmoothing"]),
        ("Edge", [
            "LaplacianOfGaussian", "GaussianGradientMagnitude",
            "DifferenceOfGaussians"
        ]),
        ("Texture",
         ["StructureTensorEigenvalues", "HessianOfGaussianEigenvalues"])
    ]

    # Map feature IDs to feature names
    FeatureNames = {
        'GaussianSmoothing': 'Gaussian Smoothing',
        'LaplacianOfGaussian': "Laplacian of Gaussian",
        'GaussianGradientMagnitude': "Gaussian Gradient Magnitude",
        'DifferenceOfGaussians': "Difference of Gaussians",
        'StructureTensorEigenvalues': "Structure Tensor EigenValues",
        'HessianOfGaussianEigenvalues': "Hessian of Gaussian Eigenvalues"
    }

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

    def appletDrawer(self):
        return self.drawer

    def viewerControlWidget(self):
        return self._viewerControlWidget

    def stopAndCleanUp(self):
        super(FeatureSelectionGui, self).stopAndCleanUp()
        self.drawer.caption.setText("(No features selected)")

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

        # Why is this necessary?
        # Clearing the layerstack doesn't seem to call the rowsRemoved signal?
        self._viewerControlWidget.featureListWidget.clear()

    # (Other methods already provided by our base class)

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

    def __init__(self, topLevelOperatorView, applet):
        """
        """
        self.topLevelOperatorView = topLevelOperatorView
        super(FeatureSelectionGui, self).__init__(topLevelOperatorView,
                                                  crosshair=False)
        self.applet = applet

        self.__cleanup_fns = []

        self.topLevelOperatorView.SelectionMatrix.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.FeatureListFilename.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.__cleanup_fns.append(
            partial(self.topLevelOperatorView.SelectionMatrix.unregisterDirty,
                    bind(self.onFeaturesSelectionsChanged)))
        self.__cleanup_fns.append(
            partial(
                self.topLevelOperatorView.FeatureListFilename.unregisterDirty,
                bind(self.onFeaturesSelectionsChanged)))

        self.onFeaturesSelectionsChanged()

        # Init feature dialog
        self.initFeatureDlg()

    def getFeatureIdOrder(self):
        featureIrdOrder = []
        for group, featureIds in self.FeatureGroups:
            featureIrdOrder += featureIds
        return featureIrdOrder

    def initFeatureOrder(self):
        self.topLevelOperatorView.Scales.setValue(self.ScalesList)
        self.topLevelOperatorView.FeatureIds.setValue(self.getFeatureIdOrder())

    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir + "/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(
            self.onFeatureButtonClicked)
        self.drawer.UsePrecomputedFeaturesButton.clicked.connect(
            self.onUsePrecomputedFeaturesButtonClicked)
        dbg = ilastik_config.getboolean("ilastik", "debug")
        if not dbg:
            self.drawer.UsePrecomputedFeaturesButton.setHidden(True)

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        
        TODO: Right now we manage adding/removing entries to a plain listview 
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us, 
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(
            os.path.split(__file__)[0] + "/viewerControls.ui")

        layerListWidget = self._viewerControlWidget.featureListWidget
        layerListWidget.setSelectionMode(QAbstractItemView.SingleSelection)

        # Need to handle data changes because the layerstack model hasn't
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)

        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = (i == row)

        def handleInsertedLayers(parent, start, end):
            for i in range(start, end + 1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
                if layerListWidget.model().rowCount() == 1:
                    layerListWidget.item(0).setSelected(True)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(range(start, end + 1)):
                layerListWidget.takeItem(i)

        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        self.layerstack.rowsRemoved.connect(handleRemovedLayers)
        self.layerstack.rowsInserted.connect(handleInsertedLayers)
        layerListWidget.currentRowChanged.connect(handleSelectionChanged)

    def setupLayers(self):
        opFeatureSelection = self.topLevelOperatorView
        inputSlot = opFeatureSelection.InputImage

        layers = []

        if inputSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(inputSlot)
            rawLayer.visible = True
            rawLayer.opacity = 1.0
            rawLayer.name = "raw data"
            layers.append(rawLayer)

        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)

            layers[0].visible = True
        return layers

    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []

        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels / numInputChannels
        assert 0 < featureChannelsPerInputChannel <= 3, "The feature selection Gui does not yet support features with more than three channels per input channel."

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ['R', 'G', 'B']
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(parent=self.topLevelOperatorView.parent)
            opSubRegion.Input.connect(featureSlot)
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel +
                                 1) * featureChannelsPerInputChannel
            opSubRegion.Start.setValue(tuple(start))
            opSubRegion.Stop.setValue(tuple(stop))

            featureLayer = self.createStandardLayerFromSlot(opSubRegion.Output)
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName

            layers.append(featureLayer)

        return layers

    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)

        self.featureDlg.accepted.connect(saveSize)

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append(FeatureEntry(featureName))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(groupedNames, self.ScalesList)
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.topLevelOperatorView.FeatureIds.value)
        cols = len(self.topLevelOperatorView.Scales.value)
        defaultFeatures = numpy.zeros((rows, cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

    def onUsePrecomputedFeaturesButtonClicked(self):
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog

        filename = QFileDialog.getOpenFileName(self,
                                               'Open Feature List',
                                               '.',
                                               options=options)

        #sanity checks on the given file
        if not filename:
            return
        if not os.path.exists(filename):
            QMessageBox.critical(self, "Open Feature List",
                                 "File '%s' does not exist" % filename)
            return
        f = open(filename, 'r')
        with f:
            for line in f:
                line = line.strip()
                if len(line) == 0:
                    continue
                if not os.path.exists(line):
                    QMessageBox.critical(
                        self, "Open Feature List",
                        "File '%s', referenced in '%s', does not exist" %
                        (line, filename))
                    return
                try:
                    h = h5py.File(line, 'r')
                    with h:
                        assert len(h["data"].shape) == 3
                except:
                    QMessageBox.critical(
                        self, "Open Feature List",
                        "File '%s', referenced in '%s', could not be opened as an HDF5 file or does not contain a 3D dataset called 'data'"
                        % (line, filename))
                    return

        self.topLevelOperatorView.FeatureListFilename.setValue(filename)
        self.topLevelOperatorView._setupOutputs()
        self.onFeaturesSelectionsChanged()

    def onFeatureButtonClicked(self):
        self.topLevelOperatorView.FeatureListFilename.setValue("")

        # Refresh the feature matrix in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        if self.topLevelOperatorView.SelectionMatrix.ready(
        ) and self.topLevelOperatorView.FeatureIds.ready():
            # Re-order the feature matrix using the loaded feature ids
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            featureOrdering = self.topLevelOperatorView.FeatureIds.value

            reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
            newrow = 0
            for group, featureIds in self.FeatureGroups:
                for featureId in featureIds:
                    oldrow = featureOrdering.index(featureId)
                    reorderedMatrix[newrow] = matrix[oldrow]
                    newrow += 1

            self.featureDlg.selectedFeatureBoolMatrix = reorderedMatrix

        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.topLevelOperatorView
        if opFeatureSelection is not None:
            # Re-initialize the scales and features
            self.initFeatureOrder()

            # Give the new features to the pipeline (if there are any)
            featureMatrix = numpy.asarray(
                self.featureDlg.selectedFeatureBoolMatrix)
            if featureMatrix.any():
                self.applet.guiControlSignal.emit(ControlCommand.DisableAll)
                QApplication.instance().setOverrideCursor(
                    QCursor(Qt.WaitCursor))
                QApplication.instance().processEvents()
                opFeatureSelection.SelectionMatrix.setValue(featureMatrix)
                self.applet.guiControlSignal.emit(ControlCommand.Pop)
                QApplication.instance().restoreOverrideCursor()
                self.topLevelOperatorView._setupOutputs()

            else:
                # Not valid to give a matrix with no features selected.
                # Disconnect.
                opFeatureSelection.SelectionMatrix.disconnect()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's matrix of feature selections.
        """
        # Update the drawer caption

        fff = ( self.topLevelOperatorView.FeatureListFilename.ready() and \
                len(self.topLevelOperatorView.FeatureListFilename.value) != 0)

        if not self.topLevelOperatorView.SelectionMatrix.ready() and not fff:
            self.drawer.caption.setText("(No features selected)")
            self.layerstack.clear()
        elif fff:
            self.drawer.caption.setText("(features from files)")
        else:
            self.initFeatureOrder()
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            self.drawer.caption.setText("(Selected %d features)" %
                                        numpy.sum(matrix))
示例#10
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """

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

    def appletDrawer(self):
        return self.drawer

    def viewerControlWidget(self):
        return self._viewerControlWidget

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

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

    # (Other methods already provided by our base class)

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

    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        self.topLevelOperatorView = topLevelOperatorView
        super(FeatureSelectionGui, self).__init__(parentApplet,
                                                  topLevelOperatorView,
                                                  crosshair=False)
        self.parentApplet = parentApplet

        self.__cleanup_fns = []

        self.topLevelOperatorView.SelectionMatrix.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.topLevelOperatorView.FeatureListFilename.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))
        self.__cleanup_fns.append(
            partial(self.topLevelOperatorView.SelectionMatrix.unregisterDirty,
                    bind(self.onFeaturesSelectionsChanged)))
        self.__cleanup_fns.append(
            partial(
                self.topLevelOperatorView.FeatureListFilename.unregisterDirty,
                bind(self.onFeaturesSelectionsChanged)))

        self.onFeaturesSelectionsChanged()

        # Init feature dialog
        self.initFeatureDlg()

    def getFeatureIdOrder(self):
        featureIrdOrder = []
        for group, featureIds in OpFeatureSelection.FeatureGroups:
            featureIrdOrder += featureIds
        return featureIrdOrder

    def initFeatureOrder(self):
        self.topLevelOperatorView.Scales.setValue(
            OpFeatureSelection.ScalesList)
        self.topLevelOperatorView.FeatureIds.setValue(self.getFeatureIdOrder())

    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir + "/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(
            self.onFeatureButtonClicked)
        self.drawer.UsePrecomputedFeaturesButton.clicked.connect(
            self.onUsePrecomputedFeaturesButtonClicked)
        dbg = ilastik_config.getboolean("ilastik", "debug")
        if not dbg:
            self.drawer.UsePrecomputedFeaturesButton.setHidden(True)

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        
        TODO: Right now we manage adding/removing entries to a plain listview 
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us, 
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(
            os.path.split(__file__)[0] + "/viewerControls.ui")

        layerListWidget = self._viewerControlWidget.featureListWidget
        layerListWidget.setSelectionMode(QAbstractItemView.SingleSelection)

        # Need to handle data changes because the layerstack model hasn't
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)

        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = (i == row)

        def handleInsertedLayers(parent, start, end):
            for i in range(start, end + 1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
                if layerListWidget.model().rowCount() == 1:
                    layerListWidget.item(0).setSelected(True)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(range(start, end + 1)):
                layerListWidget.takeItem(i)

        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        self.layerstack.rowsRemoved.connect(handleRemovedLayers)
        self.layerstack.rowsInserted.connect(handleInsertedLayers)
        layerListWidget.currentRowChanged.connect(handleSelectionChanged)

        # Support the same right-click menu as 'normal' layer list widgets
        def showLayerContextMenu(pos):
            idx = layerListWidget.indexAt(pos)
            layer = self.layerstack[idx.row()]
            layercontextmenu(layer, layerListWidget.mapToGlobal(pos),
                             layerListWidget)

        layerListWidget.customContextMenuRequested.connect(
            showLayerContextMenu)
        layerListWidget.setContextMenuPolicy(Qt.CustomContextMenu)

    def setupLayers(self):
        opFeatureSelection = self.topLevelOperatorView
        inputSlot = opFeatureSelection.InputImage

        layers = []

        if inputSlot.ready():
            rawLayer = self.createStandardLayerFromSlot(inputSlot)
            rawLayer.visible = True
            rawLayer.opacity = 1.0
            rawLayer.name = "Raw Data (display only)"
            layers.append(rawLayer)

        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)

            layers[0].visible = True
        return layers

    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []

        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels // numInputChannels
        if not 0 < featureChannelsPerInputChannel <= 3:
            logger.warn(
                "The feature selection Gui does not yet support features with more than three channels per input channel. Some features will not be displayed entirely."
            )

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ['R', 'G', 'B']
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(parent=self.topLevelOperatorView.parent)
            opSubRegion.Input.connect(featureSlot)
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel +
                                 1) * featureChannelsPerInputChannel

            opSubRegion.Roi.setValue((tuple(start), tuple(stop)))

            featureLayer = self.createStandardLayerFromSlot(opSubRegion.Output)
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName

            layers.append(featureLayer)

        return layers

    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg(parent=self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection", "dialog size")
            self.featureDlg.resize(*size)
        except TypeError:
            pass

        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(), size.height())
            PreferencesManager().set("featureSelection", "dialog size", s)

        self.featureDlg.accepted.connect(saveSize)

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in OpFeatureSelection.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = OpFeatureSelection.FeatureNames[featureId]
                featureEntries.append(FeatureEntry(featureName))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(
            groupedNames, OpFeatureSelection.ScalesList,
            self.topLevelOperatorView.WINDOW_SIZE)
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.topLevelOperatorView.FeatureIds.value)
        cols = len(self.topLevelOperatorView.Scales.value)
        defaultFeatures = numpy.zeros((rows, cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

        # Disable the first column, except for the first item.
        # This is a slightly hacky way of fixing ilastik issue #610.
        # Besides color, the features at a sigma of 0.3 are not valid because the
        #  results are overwhelmed by the inherent sampling noise of the filter.
        # (This is a bit hacky because we ASSUME the first feature is Gaussian
        # Smoothing.  It works for now.)
        enabled_item_mask = numpy.ones(defaultFeatures.shape, dtype=bool)
        enabled_item_mask[1:, 0] = False  # hacky
        self.featureDlg.setEnableItemMask(enabled_item_mask)

    def onUsePrecomputedFeaturesButtonClicked(self):
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog

        filenames = QFileDialog.getOpenFileNames(self,
                                                 'Open Feature Files',
                                                 '.',
                                                 options=options)
        filenames = map(encode_from_qstring, filenames)

        # Check if file exists
        if not filenames:
            return

        for filename in filenames:
            if not os.path.exists(filename):
                QMessageBox.critical(self, "Open Feature List",
                                     "File '%s' does not exist" % filename)
                return

        num_lanes = len(self.parentApplet.topLevelOperator.FeatureListFilename)
        if num_lanes != len(filenames):
            QMessageBox.critical(
                self, "Wrong number of feature files",
                "You must select all pre-computed feature files at once (shift-click).\n"
                "You selected {} file(s), but there are {} image(s) loaded".
                format(len(filenames), num_lanes))
            return

        for filename, slot in zip(
                filenames,
                self.parentApplet.topLevelOperator.FeatureListFilename):
            slot.setValue(filename)

        # Create a dummy SelectionMatrix, just so the operator knows it is configured
        # This is a little hacky.  We should really make SelectionMatrix optional,
        # and then handle the choice correctly in setupOutputs, probably involving
        # the Output.meta.NOTREADY flag
        dummy_matrix = numpy.zeros((6, 7), dtype=bool)
        dummy_matrix[0, 0] = True
        self.parentApplet.topLevelOperator.SelectionMatrix.setValue(True)

        # Notify the workflow that some applets may have changed state now.
        # (For example, the downstream pixel classification applet can
        #  be used now that there are features selected)
        self.parentApplet.appletStateUpdateRequested.emit()

    def onFeatureButtonClicked(self):
        # Remove all pre-computed feature files
        for slot in self.parentApplet.topLevelOperator.FeatureListFilename:
            slot.disconnect()

        # Refresh the feature matrix in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        if self.topLevelOperatorView.SelectionMatrix.ready(
        ) and self.topLevelOperatorView.FeatureIds.ready():
            # Re-order the feature matrix using the loaded feature ids
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            featureOrdering = self.topLevelOperatorView.FeatureIds.value

            reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
            newrow = 0
            for group, featureIds in OpFeatureSelection.FeatureGroups:
                for featureId in featureIds:
                    oldrow = featureOrdering.index(featureId)
                    reorderedMatrix[newrow] = matrix[oldrow]
                    newrow += 1

            self.featureDlg.selectedFeatureBoolMatrix = reorderedMatrix
        else:
            assert self.topLevelOperatorView.FeatureIds.ready()
            assert self.topLevelOperatorView.Scales.ready()

            num_rows = len(self.topLevelOperatorView.FeatureIds.value)
            num_cols = len(self.topLevelOperatorView.Scales.value)
            blank_matrix = numpy.zeros((num_rows, num_cols), dtype=bool)
            self.featureDlg.selectedFeatureBoolMatrix = blank_matrix

        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.topLevelOperatorView
        old_features = None
        if opFeatureSelection.SelectionMatrix.ready():
            old_features = opFeatureSelection.SelectionMatrix.value

        if opFeatureSelection is not None:
            # Re-initialize the scales and features
            self.initFeatureOrder()

            # Give the new features to the pipeline (if there are any)
            featureMatrix = numpy.asarray(
                self.featureDlg.selectedFeatureBoolMatrix)
            if featureMatrix.any():
                # Disable gui
                self.parentApplet.busy = True
                self.parentApplet.appletStateUpdateRequested.emit()
                QApplication.instance().setOverrideCursor(
                    QCursor(Qt.WaitCursor))
                QApplication.instance().processEvents()

                try:
                    opFeatureSelection.SelectionMatrix.setValue(featureMatrix)
                except DatasetConstraintError as ex:
                    # The user selected some scales that were too big.
                    QMessageBox.critical(self, "Invalid selections",
                                         ex.message)
                    if old_features is not None:
                        opFeatureSelection.SelectionMatrix.setValue(
                            old_features)
                    else:
                        opFeatureSelection.SelectionMatrix.disconnect()

                # Re-enable gui
                QApplication.instance().restoreOverrideCursor()
                self.parentApplet.busy = False
                self.parentApplet.appletStateUpdateRequested.emit()
            else:
                # Not valid to give a matrix with no features selected.
                # Disconnect.
                opFeatureSelection.SelectionMatrix.disconnect()

                # Notify the workflow that some applets may have changed state now.
                # (For example, the downstream pixel classification applet can
                #  be used now that there are features selected)
                self.parentApplet.appletStateUpdateRequested.emit()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's matrix of feature selections.
        """
        # Update the drawer caption

        fff = ( self.topLevelOperatorView.FeatureListFilename.ready() and \
                len(self.topLevelOperatorView.FeatureListFilename.value) != 0)

        if not self.topLevelOperatorView.SelectionMatrix.ready() and not fff:
            self.drawer.caption.setText("(No features selected)")
            self.layerstack.clear()
        elif fff:
            self.drawer.caption.setText("(features from files)")
        else:
            self.initFeatureOrder()
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            self.drawer.caption.setText("(Selected %d features)" %
                                        numpy.sum(matrix))
示例#11
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """

    # Constants
    ScalesList = [0.3, 0.7, 1, 1.6, 3.5, 5.0, 10.0]
    DefaultColorTable = None

    #    # Default order
    #    FeatureIds = [ 'GaussianSmoothing',
    #                   'LaplacianOfGaussian',
    #                   'GaussianGradientMagnitude',
    #                   'DifferenceOfGaussians',
    #                   'StructureTensorEigenvalues',
    #                   'HessianOfGaussianEigenvalues' ]

    # Map feature groups to lists of feature IDs
    FeatureGroups = [
        ("Color/Intensity", ["GaussianSmoothing"]),
        ("Edge", [
            "LaplacianOfGaussian", "GaussianGradientMagnitude",
            "DifferenceOfGaussians"
        ]),
        ("Texture",
         ["StructureTensorEigenvalues", "HessianOfGaussianEigenvalues"])
    ]

    # Map feature IDs to feature names
    FeatureNames = {
        'GaussianSmoothing': 'Gaussian Smoothing',
        'LaplacianOfGaussian': "Laplacian of Gaussian",
        'GaussianGradientMagnitude': "Gaussian Gradient Magnitude",
        'DifferenceOfGaussians': "Difference of Gaussians",
        'StructureTensorEigenvalues': "Structure Tensor EigenValues",
        'HessianOfGaussianEigenvalues': "Hessian of Gaussian Eigenvalues"
    }

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

    def appletDrawers(self):
        return [("Feature Selection", self.drawer)]

    def viewerControlWidget(self):
        return self._viewerControlWidget

    def reset(self):
        super(FeatureSelectionGui, self).reset()
        self.drawer.caption.setText("(No features selected)")

        # Why is this necessary?
        # Clearing the layerstack doesn't seem to call the rowsRemoved signal?
        self._viewerControlWidget.listWidget.clear()

    # (Other methods already provided by our base class)

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

    @traceLogged(traceLogger)
    def __init__(self, mainOperator):
        """
        """
        super(FeatureSelectionGui, self).__init__(mainOperator)
        self.mainOperator = mainOperator

        self.mainOperator.SelectionMatrix.notifyDirty(
            bind(self.onFeaturesSelectionsChanged))

        # Init feature dialog
        self.initFeatureDlg()

    def getFeatureIdOrder(self):
        featureIrdOrder = []
        for group, featureIds in self.FeatureGroups:
            featureIrdOrder += featureIds
        return featureIrdOrder

    def initFeatureOrder(self):
        self.mainOperator.Scales.setValue(self.ScalesList)
        self.mainOperator.FeatureIds.setValue(self.getFeatureIdOrder())

    @traceLogged(traceLogger)
    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir + "/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(
            self.onFeatureButtonClicked)

    @traceLogged(traceLogger)
    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        
        TODO: Right now we manage adding/removing entries to a plain listview 
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us, 
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(
            os.path.split(__file__)[0] + "/viewerControls.ui")

        layerListWidget = self._viewerControlWidget.listWidget

        # Need to handle data changes because the layerstack model hasn't
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)

        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)

        def handleInsertedLayers(parent, start, end):
            for i in range(start, end + 1):
                layerListWidget.insertItem(i, self.layerstack[i].name)

        self.layerstack.rowsInserted.connect(handleInsertedLayers)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(range(start, end + 1)):
                layerListWidget.takeItem(i)

        self.layerstack.rowsRemoved.connect(handleRemovedLayers)

        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = (i == row)

        layerListWidget.currentRowChanged.connect(handleSelectionChanged)

    @traceLogged(traceLogger)
    def setupLayers(self, currentImageIndex):
        layers = []

        opFeatureSelection = self.operatorForCurrentImage()

        inputSlot = opFeatureSelection.InputImage
        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)

            layers[0].visible = True
        return layers

    @traceLogged(traceLogger)
    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []

        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels / numInputChannels
        assert 0 < featureChannelsPerInputChannel <= 3, "The feature selection Gui does not yet support features with more than three channels per input channel."

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ['R', 'G', 'B']
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(graph=self.mainOperator.graph)
            opSubRegion.Input.connect(featureSlot)
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel +
                                 1) * featureChannelsPerInputChannel
            opSubRegion.Start.setValue(tuple(start))
            opSubRegion.Stop.setValue(tuple(stop))

            featureLayer = self.createStandardLayerFromSlot(opSubRegion.Output)
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName

            layers.append(featureLayer)

        return layers

    @traceLogged(traceLogger)
    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg()
        self.featureDlg.setWindowTitle("Features")

        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in self.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = self.FeatureNames[featureId]
                featureEntries.append(FeatureEntry(featureName))
            groupedNames.append((group, featureEntries))
        self.featureDlg.createFeatureTable(groupedNames, self.ScalesList)
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.mainOperator.FeatureIds.value)
        cols = len(self.mainOperator.Scales.value)
        defaultFeatures = numpy.zeros((rows, cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

    def onFeatureButtonClicked(self):
        # Refresh the feature matrix in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        if self.mainOperator.SelectionMatrix.ready(
        ) and self.mainOperator.FeatureIds.ready():
            # Re-order the feature matrix using the loaded feature ids
            matrix = self.mainOperator.SelectionMatrix.value
            featureOrdering = self.mainOperator.FeatureIds.value

            reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
            newrow = 0
            for group, featureIds in self.FeatureGroups:
                for featureId in featureIds:
                    oldrow = featureOrdering.index(featureId)
                    reorderedMatrix[newrow] = matrix[oldrow]
                    newrow += 1

            self.featureDlg.selectedFeatureBoolMatrix = reorderedMatrix

        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.operatorForCurrentImage()
        if opFeatureSelection is not None:
            # Re-initialize the scales and features
            self.initFeatureOrder()

            # Give the new features to the pipeline (if there are any)
            featureMatrix = numpy.asarray(
                self.featureDlg.selectedFeatureBoolMatrix)
            if featureMatrix.any():
                opFeatureSelection.SelectionMatrix.setValue(featureMatrix)
            else:
                # Not valid to give a matrix with no features selected.
                # Disconnect.
                opFeatureSelection.SelectionMatrix.disconnect()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's matrix of feature selections.
        """
        # Update the drawer caption
        if not self.mainOperator.SelectionMatrix.ready():
            self.drawer.caption.setText("(No features selected)")
            self.layerstack.clear()
        else:
            self.initFeatureOrder()
            matrix = self.mainOperator.SelectionMatrix.value
            self.drawer.caption.setText("(Selected %d features)" %
                                        numpy.sum(matrix))
示例#12
0
class FeatureSelectionGui(LayerViewerGui):
    """
    """
    ###########################################
    ### AppletGuiInterface Concrete Methods ###
    ###########################################
    
    def appletDrawer(self):
        return self.drawer

    def viewerControlWidget(self):
        return self._viewerControlWidget

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

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

    # (Other methods already provided by our base class)

    ###########################################
    ###########################################
    
    def __init__(self, parentApplet, topLevelOperatorView):
        """
        """
        self.topLevelOperatorView = topLevelOperatorView
        super(FeatureSelectionGui, self).__init__(parentApplet, topLevelOperatorView, crosshair=False)
        self.parentApplet = parentApplet
        
        self.__cleanup_fns = []

        self.topLevelOperatorView.SelectionMatrix.notifyDirty( bind(self.onFeaturesSelectionsChanged) )
        self.topLevelOperatorView.FeatureListFilename.notifyDirty( bind(self.onFeaturesSelectionsChanged) )
        self.__cleanup_fns.append( partial( self.topLevelOperatorView.SelectionMatrix.unregisterDirty, bind(self.onFeaturesSelectionsChanged) ) )
        self.__cleanup_fns.append( partial( self.topLevelOperatorView.FeatureListFilename.unregisterDirty, bind(self.onFeaturesSelectionsChanged) ) )

        self.onFeaturesSelectionsChanged()


        # Init feature dialog
        self.initFeatureDlg()

    def getFeatureIdOrder(self):
        featureIrdOrder = []
        for group, featureIds in OpFeatureSelection.FeatureGroups:
            featureIrdOrder += featureIds
        return featureIrdOrder

    def initFeatureOrder(self):
        self.topLevelOperatorView.Scales.setValue( OpFeatureSelection.ScalesList )
        self.topLevelOperatorView.FeatureIds.setValue( self.getFeatureIdOrder() )
            
    def initAppletDrawerUi(self):
        """
        Load the ui file for the applet drawer, which we own.
        """
        localDir = os.path.split(__file__)[0]
        # (We don't pass self here because we keep the drawer ui in a separate object.)
        self.drawer = uic.loadUi(localDir+"/featureSelectionDrawer.ui")
        self.drawer.SelectFeaturesButton.clicked.connect(self.onFeatureButtonClicked)
        self.drawer.UsePrecomputedFeaturesButton.clicked.connect(self.onUsePrecomputedFeaturesButtonClicked)
        dbg = ilastik_config.getboolean("ilastik", "debug") 
        if not dbg:
            self.drawer.UsePrecomputedFeaturesButton.setHidden(True)

    def initViewerControlUi(self):
        """
        Load the viewer controls GUI, which appears below the applet bar.
        In our case, the viewer control GUI consists mainly of a layer list.
        
        TODO: Right now we manage adding/removing entries to a plain listview 
              widget by monitoring the layerstack for changes.
              Ideally, we should implement a custom widget that does this for us, 
              which would be initialized with the layer list model (like volumina.layerwidget)
        """
        self._viewerControlWidget = uic.loadUi(os.path.split(__file__)[0] + "/viewerControls.ui")
        
        layerListWidget = self._viewerControlWidget.featureListWidget
        layerListWidget.setSelectionMode(QAbstractItemView.SingleSelection)

        # Need to handle data changes because the layerstack model hasn't 
        # updated his data yet by the time he calls the rowsInserted signal
        def handleLayerStackDataChanged(startIndex, stopIndex):
            row = startIndex.row()
            layerListWidget.item(row).setText(self.layerstack[row].name)
        
        def handleSelectionChanged(row):
            # Only one layer is visible at a time
            for i, layer in enumerate(self.layerstack):
                layer.visible = (i == row)
                
        def handleInsertedLayers(parent, start, end):
            for i in range(start, end+1):
                layerListWidget.insertItem(i, self.layerstack[i].name)
                if layerListWidget.model().rowCount() == 1:
                    layerListWidget.item(0).setSelected(True)

        def handleRemovedLayers(parent, start, end):
            for i in reversed(list(range(start, end+1))):
                layerListWidget.takeItem(i)
        
        self.layerstack.dataChanged.connect(handleLayerStackDataChanged)
        self.layerstack.rowsRemoved.connect( handleRemovedLayers )
        self.layerstack.rowsInserted.connect( handleInsertedLayers )
        layerListWidget.currentRowChanged.connect( handleSelectionChanged )
        
        # Support the same right-click menu as 'normal' layer list widgets
        def showLayerContextMenu( pos ):
            idx = layerListWidget.indexAt(pos)
            layer = self.layerstack[idx.row()]
            layercontextmenu( layer, layerListWidget.mapToGlobal(pos), layerListWidget )
        layerListWidget.customContextMenuRequested.connect( showLayerContextMenu )
        layerListWidget.setContextMenuPolicy( Qt.CustomContextMenu )
    
    def setupLayers(self):
        opFeatureSelection = self.topLevelOperatorView
        inputSlot = opFeatureSelection.InputImage
        
        layers = []
       
        if inputSlot.ready(): 
            rawLayer = self.createStandardLayerFromSlot(inputSlot)
            rawLayer.visible = True
            rawLayer.opacity = 1.0
            rawLayer.name = "Raw Data (display only)" 
            layers.append(rawLayer)

        featureMultiSlot = opFeatureSelection.FeatureLayers
        if inputSlot.ready() and featureMultiSlot.ready():
            for featureIndex, featureSlot in enumerate(featureMultiSlot):
                assert featureSlot.ready()
                layers += self.getFeatureLayers(inputSlot, featureSlot)
            
            layers[0].visible = True
        return layers

    def getFeatureLayers(self, inputSlot, featureSlot):
        """
        Generate a list of layers for the feature image produced by the given slot.
        """
        layers = []
        
        channelAxis = inputSlot.meta.axistags.channelIndex
        assert channelAxis == featureSlot.meta.axistags.channelIndex
        numInputChannels = inputSlot.meta.shape[channelAxis]
        numFeatureChannels = featureSlot.meta.shape[channelAxis]

        # Determine how many channels this feature has (up to 3)
        featureChannelsPerInputChannel = numFeatureChannels // numInputChannels
        if not 0 < featureChannelsPerInputChannel <= 3:
            logger.warning( "The feature selection Gui does not yet support features with more than three channels per input channel. Some features will not be displayed entirely." ) 

        for inputChannel in range(numInputChannels):
            # Determine the name for this feature
            featureName = featureSlot.meta.description
            assert featureName is not None
            if 2 <= numInputChannels <= 3:
                channelNames = ['R', 'G', 'B']
                featureName += " (" + channelNames[inputChannel] + ")"
            if numInputChannels > 3:
                featureName += " (Ch. {})".format(inputChannel)

            opSubRegion = OpSubRegion(parent=self.topLevelOperatorView.parent)
            opSubRegion.Input.connect( featureSlot )
            start = [0] * len(featureSlot.meta.shape)
            start[channelAxis] = inputChannel * featureChannelsPerInputChannel
            stop = list(featureSlot.meta.shape)
            stop[channelAxis] = (inputChannel+1) * featureChannelsPerInputChannel
            
            opSubRegion.Roi.setValue( (tuple(start), tuple(stop)) )
            
            featureLayer = self.createStandardLayerFromSlot( opSubRegion.Output )
            featureLayer.visible = False
            featureLayer.opacity = 1.0
            featureLayer.name = featureName
            
            layers.append(featureLayer)

        return layers

    def initFeatureDlg(self):
        """
        Initialize the feature selection widget.
        """
        self.initFeatureOrder()

        self.featureDlg = FeatureDlg(parent = self)
        self.featureDlg.setWindowTitle("Features")
        try:
            size = PreferencesManager().get("featureSelection","dialog size")
            self.featureDlg.resize(*size)
        except TypeError:pass
        
        def saveSize():
            size = self.featureDlg.size()
            s = (size.width(),size.height())
            PreferencesManager().set("featureSelection","dialog size",s)
        self.featureDlg.accepted.connect(saveSize)
        
        # Map from groups of feature IDs to groups of feature NAMEs
        groupedNames = []
        for group, featureIds in OpFeatureSelection.FeatureGroups:
            featureEntries = []
            for featureId in featureIds:
                featureName = OpFeatureSelection.FeatureNames[featureId]
                featureEntries.append( FeatureEntry(featureName) )
            groupedNames.append( (group, featureEntries) )
        self.featureDlg.createFeatureTable( groupedNames, OpFeatureSelection.ScalesList, self.topLevelOperatorView.WINDOW_SIZE )
        self.featureDlg.setImageToPreView(None)

        # Init with no features
        rows = len(self.topLevelOperatorView.FeatureIds.value)
        cols = len(self.topLevelOperatorView.Scales.value)
        defaultFeatures = numpy.zeros((rows,cols), dtype=bool)
        self.featureDlg.selectedFeatureBoolMatrix = defaultFeatures

        self.featureDlg.accepted.connect(self.onNewFeaturesFromFeatureDlg)

        # Disable the first column, except for the first item.
        # This is a slightly hacky way of fixing ilastik issue #610.
        # Besides color, the features at a sigma of 0.3 are not valid because the 
        #  results are overwhelmed by the inherent sampling noise of the filter.
        # (This is a bit hacky because we ASSUME the first feature is Gaussian 
        # Smoothing.  It works for now.)
        enabled_item_mask = numpy.ones( defaultFeatures.shape, dtype=bool )
        enabled_item_mask[1:,0] = False # hacky
        self.featureDlg.setEnableItemMask( enabled_item_mask )

    def onUsePrecomputedFeaturesButtonClicked(self):
        options = QFileDialog.Options()
        if ilastik_config.getboolean("ilastik", "debug"):
            options |= QFileDialog.DontUseNativeDialog

        filenames, _filter = QFileDialog.getOpenFileNames(self, 'Open Feature Files', '.', options=options)
        
        # Check if file exists
        if not filenames:
            return
        
        for filename in filenames:
            if not os.path.exists(filename):
                QMessageBox.critical(self, "Open Feature List", "File '%s' does not exist" % filename)
                return

        num_lanes = len(self.parentApplet.topLevelOperator.FeatureListFilename)
        if num_lanes != len(filenames):
            QMessageBox.critical(self, "Wrong number of feature files",
                                 "You must select all pre-computed feature files at once (shift-click).\n"
                                 "You selected {} file(s), but there are {} image(s) loaded"
                                 .format(len(filenames), num_lanes))
            return
        
        
        for filename, slot in zip(filenames, self.parentApplet.topLevelOperator.FeatureListFilename):
            slot.setValue(filename)

        # Create a dummy SelectionMatrix, just so the operator knows it is configured
        # This is a little hacky.  We should really make SelectionMatrix optional, 
        # and then handle the choice correctly in setupOutputs, probably involving 
        # the Output.meta.NOTREADY flag
        dummy_matrix = numpy.zeros((6,7), dtype=bool)
        dummy_matrix[0,0] = True
        self.parentApplet.topLevelOperator.SelectionMatrix.setValue(True)

        # Notify the workflow that some applets may have changed state now.
        # (For example, the downstream pixel classification applet can 
        #  be used now that there are features selected)
        self.parentApplet.appletStateUpdateRequested()

    def onFeatureButtonClicked(self):
        # Remove all pre-computed feature files
        for slot in self.parentApplet.topLevelOperator.FeatureListFilename:
            slot.disconnect()
        
        # Refresh the feature matrix in case it has changed since the last time we were opened
        # (e.g. if the user loaded a project from disk)
        if self.topLevelOperatorView.SelectionMatrix.ready() and self.topLevelOperatorView.FeatureIds.ready():
            # Re-order the feature matrix using the loaded feature ids
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            featureOrdering = self.topLevelOperatorView.FeatureIds.value
            
            reorderedMatrix = numpy.zeros(matrix.shape, dtype=bool)
            newrow = 0
            for group, featureIds in OpFeatureSelection.FeatureGroups:
                for featureId in featureIds:
                    oldrow = featureOrdering.index(featureId)
                    reorderedMatrix[newrow] = matrix[oldrow]
                    newrow += 1
                
            self.featureDlg.selectedFeatureBoolMatrix = reorderedMatrix
        else:
            assert self.topLevelOperatorView.FeatureIds.ready()
            assert self.topLevelOperatorView.Scales.ready()

            num_rows = len(self.topLevelOperatorView.FeatureIds.value)
            num_cols = len(self.topLevelOperatorView.Scales.value)
            blank_matrix = numpy.zeros( (num_rows, num_cols), dtype=bool )
            self.featureDlg.selectedFeatureBoolMatrix = blank_matrix
        
        # Now open the feature selection dialog
        self.featureDlg.exec_()

    def onNewFeaturesFromFeatureDlg(self):
        opFeatureSelection = self.topLevelOperatorView
        old_features = None
        if opFeatureSelection.SelectionMatrix.ready():
            old_features = opFeatureSelection.SelectionMatrix.value
            
        if opFeatureSelection is not None:
            # Re-initialize the scales and features
            self.initFeatureOrder()

            # Give the new features to the pipeline (if there are any)
            featureMatrix = numpy.asarray(self.featureDlg.selectedFeatureBoolMatrix)
            if featureMatrix.any():
                # Disable gui
                self.parentApplet.busy = True
                self.parentApplet.appletStateUpdateRequested()
                QApplication.instance().setOverrideCursor( QCursor(Qt.WaitCursor) )
                
                try:
                    opFeatureSelection.SelectionMatrix.setValue( featureMatrix )
                except DatasetConstraintError as ex:
                    # The user selected some scales that were too big.
                    QMessageBox.critical(self, "Invalid selections", ex.message)
                    if old_features is not None:
                        opFeatureSelection.SelectionMatrix.setValue( old_features )
                    else:
                        opFeatureSelection.SelectionMatrix.disconnect()
                
                # Re-enable gui
                QApplication.instance().restoreOverrideCursor()
                self.parentApplet.busy = False
                self.parentApplet.appletStateUpdateRequested()
            else:
                # Not valid to give a matrix with no features selected.
                # Disconnect.
                opFeatureSelection.SelectionMatrix.disconnect()

                # Notify the workflow that some applets may have changed state now.
                # (For example, the downstream pixel classification applet can 
                #  be used now that there are features selected)
                self.parentApplet.appletStateUpdateRequested()

    def onFeaturesSelectionsChanged(self):
        """
        Handles changes to our top-level operator's matrix of feature selections.
        """
        # Update the drawer caption
        
        fff = ( self.topLevelOperatorView.FeatureListFilename.ready() and \
                len(self.topLevelOperatorView.FeatureListFilename.value) != 0)
        
        if not self.topLevelOperatorView.SelectionMatrix.ready() and not fff:
            self.drawer.caption.setText( "(No features selected)" )
            self.layerstack.clear()
        elif fff:
            self.drawer.caption.setText( "(features from files)" )
        else:
            self.initFeatureOrder()
            matrix = self.topLevelOperatorView.SelectionMatrix.value
            self.drawer.caption.setText( "(Selected %d features)" % numpy.sum(matrix) )