示例#1
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.set_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.get_scales())
                opFeatureSelection.ComputeIn2d.setValue(
                    self.featureDlg.get_computeIn2d())
                # set disconnected slot at last (used like a transaction slot)
                opFeatureSelection.SelectionMatrix.setValue(
                    self.featureDlg.get_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)")