def _get_available_classifier_factories(self):
        # FIXME: Replace this logic with a proper plugin mechanism
        from lazyflow.classifiers import VigraRfLazyflowClassifierFactory, SklearnLazyflowClassifierFactory, \
                                         ParallelVigraRfLazyflowClassifierFactory, VigraRfPixelwiseClassifierFactory,\
                                         LazyflowVectorwiseClassifierFactoryABC, LazyflowPixelwiseClassifierFactoryABC
        classifiers = OrderedDict()
        classifiers["Parallel Random Forest (VIGRA)"] = ParallelVigraRfLazyflowClassifierFactory(100)
        
        try:
            from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier
            from sklearn.naive_bayes import GaussianNB
            from sklearn.tree import DecisionTreeClassifier
            from sklearn.neighbors import KNeighborsClassifier
            from sklearn.lda import LDA
            from sklearn.qda import QDA
            from sklearn.svm import SVC, NuSVC
            classifiers["Random Forest (scikit-learn)"] = SklearnLazyflowClassifierFactory( RandomForestClassifier, 100 )
            classifiers["Gaussian Naive Bayes (scikit-learn)"] = SklearnLazyflowClassifierFactory( GaussianNB )
            classifiers["AdaBoost (scikit-learn)"] = SklearnLazyflowClassifierFactory( AdaBoostClassifier, n_estimators=100 )
            classifiers["Single Decision Tree (scikit-learn)"] = SklearnLazyflowClassifierFactory( DecisionTreeClassifier, max_depth=5 )
            classifiers["K-Neighbors (scikit-learn)"] = SklearnLazyflowClassifierFactory( KNeighborsClassifier )
            classifiers["LDA (scikit-learn)"] = SklearnLazyflowClassifierFactory( LDA )
            classifiers["QDA (scikit-learn)"] = SklearnLazyflowClassifierFactory( QDA )
            classifiers["SVM C-Support (scikit-learn)"] = SklearnLazyflowClassifierFactory( SVC, probability=True )
            classifiers["SVM Nu-Support (scikit-learn)"] = SklearnLazyflowClassifierFactory( NuSVC, probability=True )
        except ImportError:
            import warnings
            warnings.warn("Couldn't import sklearn. Scikit-learn classifiers not available.")

        # Debug classifiers
        classifiers["Parallel Random Forest with Variable Importance (VIGRA)"] = ParallelVigraRfLazyflowClassifierFactory(100, variable_importance_enabled=True)        
        classifiers["(debug) Single-threaded Random Forest (VIGRA)"] = VigraRfLazyflowClassifierFactory(100)
        classifiers["(debug) Pixelwise Random Forest (VIGRA)"] = VigraRfPixelwiseClassifierFactory(100)
        
        return classifiers
Example #2
0
    def execute(self, slot, subindex, roi, result):
        all_features_and_labels_df = None

        for lane_index, (labels_dict_slot, features_slot) in \
                enumerate( zip(self.EdgeLabelsDict, self.EdgeFeaturesDataFrame) ):
            logger.info(
                "Retrieving features for lane {}...".format(lane_index))

            labels_dict = labels_dict_slot.value.copy(
            )  # Copy now to avoid threading issues.
            if not labels_dict:
                continue

            sp_columns = np.array(labels_dict.keys())
            edge_features_df = features_slot.value
            assert list(edge_features_df.columns[0:2]) == ['sp1', 'sp2']

            labels_df = pd.DataFrame(sp_columns, columns=['sp1', 'sp2'])
            labels_df['label'] = labels_dict.values()

            # Drop zero labels
            labels_df = labels_df[labels_df['label'] != 0]

            # Merge in features
            features_and_labels_df = pd.merge(edge_features_df,
                                              labels_df,
                                              how='right',
                                              on=['sp1', 'sp2'])
            if all_features_and_labels_df is not None:
                all_features_and_labels_df = all_features_and_labels_df.append(
                    features_and_labels_df)
            else:
                all_features_and_labels_df = features_and_labels_df

        if all_features_and_labels_df is None:
            # No labels yet.
            result[0] = None
            return

        assert list(all_features_and_labels_df.columns[0:2]) == ['sp1', 'sp2']
        assert all_features_and_labels_df.columns[-1] == 'label'

        feature_matrix = all_features_and_labels_df.iloc[:, 2:
                                                         -1].values  # Omit 'sp1', 'sp2', and 'label'
        labels = all_features_and_labels_df.iloc[:, -1].values

        logger.info("Training classifier with {} labels...".format(
            len(labels)))
        # TODO: Allow factory to be configured via an input slot
        classifier_factory = ParallelVigraRfLazyflowClassifierFactory()
        classifier = classifier_factory.create_and_train(
            feature_matrix,
            labels,
            feature_names=all_features_and_labels_df.columns[2:-1].values)
        assert set(classifier.known_classes).issubset(set([1, 2]))
        result[0] = classifier
    def test_basic(self):
        # Initialize factory
        factory = ParallelVigraRfLazyflowClassifierFactory(10)
        
        # Train
        classifier = factory.create_and_train(self.training_feature_matrix, self.training_labels)
        assert isinstance(classifier, ParallelVigraRfLazyflowClassifier)
        assert list(classifier.known_classes) == [1,2]

        # Predict        
        probabilities = classifier.predict_probabilities( self.prediction_data )
        assert probabilities.shape == (4,2)
        assert probabilities.dtype == numpy.float32
        assert (0 <= probabilities).all() and (probabilities <= 1.0).all()
        assert (numpy.argmax(probabilities, axis=-1)+1 == self.expected_classes).all()
Example #4
0
    def retrieve_segmentation_new(self, feat):
        '''
        Attempt to use the opSimplePixelClassification by Stuart. Could not get this to work so far...
        :param feat:
        :return:
        '''
        from . import opSimplePixelClassification
        from lazyflow import graph
        from lazyflow.classifiers import ParallelVigraRfLazyflowClassifierFactory

        self.opSimpleClassification = opSimplePixelClassification.OpSimplePixelClassification(parent = self.opPixelClassification.parent.pcApplet.topLevelOperator)
        self.opSimpleClassification.Labels.connect(self.opPixelClassification.opLabelPipeline.Output)
        self.opSimpleClassification.Features.connect(self.opPixelClassification.FeatureImages)
        self.opSimpleClassification.Labels.resize(1)
        self.opSimpleClassification.Features.resize(1)
        self.opSimpleClassification.ingest_labels()
        self.opSimpleClassification.ClassifierFactory.setValue(ParallelVigraRfLazyflowClassifierFactory(100))

        # resize of input slots required, otherwise "IndexError: list index out of range" after this line
        segmentation = self.opSimpleClassification.Predictions[0][0, :, :, 25, 0].wait()

        # now I get:
        '''RuntimeError:
        Precondition violation!
        Sampler(): Requested sample count must be at least as large as the number of strata.
        (/miniconda/conda-bld/work/include/vigra/sampling.hxx:371)'''


        '''
Example #5
0
    def testBasic(self):
        features = numpy.indices( (100,100) ).astype(numpy.float32) + 0.5
        features = numpy.rollaxis(features, 0, 3)
        features = vigra.taggedView(features, 'xyc')
        labels = numpy.zeros( (100,100,1), dtype=numpy.uint8 )
        labels = vigra.taggedView(labels, 'xyc')
        
        labels[10,10] = 1
        labels[10,11] = 1
        labels[20,20] = 2
        labels[20,21] = 2
        
        graph = Graph()
        opFeatureMatrixCache = OpFeatureMatrixCache(graph=graph)
        opFeatureMatrixCache.FeatureImage.setValue(features)
        opFeatureMatrixCache.LabelImage.setValue(labels)
        
        opFeatureMatrixCache.LabelImage.setDirty( numpy.s_[10:11, 10:12] )
        opFeatureMatrixCache.LabelImage.setDirty( numpy.s_[20:21, 20:22] )
        opFeatureMatrixCache.LabelImage.setDirty( numpy.s_[30:31, 30:32] )

        opTrain = OpTrainClassifierFromFeatureVectors( graph=graph )
        opTrain.ClassifierFactory.setValue( ParallelVigraRfLazyflowClassifierFactory(100) )
        opTrain.MaxLabel.setValue(2)
        opTrain.LabelAndFeatureMatrix.connect( opFeatureMatrixCache.LabelAndFeatureMatrix )
        
        assert opTrain.Classifier.ready()
        
        trained_classifier = opTrain.Classifier.value
        
        # This isn't much of a test at the moment...
        assert isinstance( trained_classifier, ParallelVigraRfLazyflowClassifier ), \
            "classifier is of the wrong type: {}".format(type(trained_classifier))
    def execute(self, slot, subindex, roi, result):
        all_features_and_labels_df = None

        for lane_index, (labels_dict_slot, features_slot) in \
                enumerate( zip(self.EdgeLabelsDict, self.EdgeFeaturesDataFrame) ):
            logger.info("Retrieving features for lane {}...".format(lane_index))

            labels_dict = labels_dict_slot.value.copy() # Copy now to avoid threading issues.
            if not labels_dict:
                continue

            sp_columns = np.array(labels_dict.keys())
            edge_features_df = features_slot.value
            assert list(edge_features_df.columns[0:2]) == ['sp1', 'sp2']

            labels_df = pd.DataFrame(sp_columns, columns=['sp1', 'sp2'])
            labels_df['label'] = labels_dict.values()

            # Drop zero labels
            labels_df = labels_df[labels_df['label'] != 0]
            
            # Merge in features
            features_and_labels_df = pd.merge(edge_features_df, labels_df, how='right', on=['sp1', 'sp2'])
            if all_features_and_labels_df is not None:
                all_features_and_labels_df = all_features_and_labels_df.append(features_and_labels_df)
            else:
                all_features_and_labels_df = features_and_labels_df

        if all_features_and_labels_df is None:
            # No labels yet.
            result[0] = None
            return

        assert list(all_features_and_labels_df.columns[0:2]) == ['sp1', 'sp2']
        assert all_features_and_labels_df.columns[-1] == 'label'

        feature_matrix = all_features_and_labels_df.iloc[:, 2:-1].values # Omit 'sp1', 'sp2', and 'label'
        labels = all_features_and_labels_df.iloc[:, -1].values

        logger.info("Training classifier with {} labels...".format( len(labels) ))
        # TODO: Allow factory to be configured via an input slot
        classifier_factory = ParallelVigraRfLazyflowClassifierFactory()
        classifier = classifier_factory.create_and_train( feature_matrix,
                                                          labels,
                                                          feature_names=all_features_and_labels_df.columns[2:-1].values )
        assert set(classifier.known_classes).issubset(set([1,2]))
        result[0] = classifier
    def test_basic(self):
        # Initialize factory
        factory = ParallelVigraRfLazyflowClassifierFactory(10)

        # Train
        classifier = factory.create_and_train(self.training_feature_matrix,
                                              self.training_labels)
        assert isinstance(classifier, ParallelVigraRfLazyflowClassifier)
        assert list(classifier.known_classes) == [1, 2]

        # Predict
        probabilities = classifier.predict_probabilities(self.prediction_data)
        assert probabilities.shape == (4, 2)
        assert probabilities.dtype == numpy.float32
        assert (0 <= probabilities).all() and (probabilities <= 1.0).all()
        assert (numpy.argmax(probabilities, axis=-1) +
                1 == self.expected_classes).all()
    def test():
        # Make up some garbage features for this test
        features = numpy.indices((100, 100)).astype(numpy.float32) + 0.5
        features = numpy.rollaxis(features, 0, 3)
        features = vigra.taggedView(features, 'yxc')
        assert features.shape == (100, 100, 2)

        # Define a couple arbitrary labels.
        labels = numpy.zeros((100, 100, 1), dtype=numpy.uint8)
        labels = vigra.taggedView(labels, 'yxc')

        labels[10, 10] = 1
        labels[10, 11] = 1
        labels[20, 20] = 2
        labels[20, 21] = 2

        graph = Graph()
        opPixelClassification = OpSimplePixelClassification(graph=Graph())

        # Specify the classifier type: A random forest with just 10 trees.
        opPixelClassification.ClassifierFactory.setValue(
            ParallelVigraRfLazyflowClassifierFactory(10))

        # In a typical use-case, the inputs to our operator would be connected to some upstream pipeline via Slot.connect().
        # But for this test, we will provide the data as raw VigraArrays via the special Slot.setValue() function.
        # Also, we have to manually resize() the level-1 slots.
        opPixelClassification.Features.resize(1)
        opPixelClassification.Features[0].setValue(features)

        opPixelClassification.Labels.resize(1)
        opPixelClassification.Labels.setValue(labels)

        # Load the label cache, which will pull from the Labels slot...
        print "Ingesting labels..."
        opPixelClassification.ingest_labels()

        print "Initiating prediction..."
        predictions = opPixelClassification.Predictions[0][:].wait()
        assert predictions.shape == (100, 100, 2)
        assert predictions.dtype == numpy.float32
        assert 0.0 <= predictions.min() <= predictions.max() <= 1.0
        print "Done predicting."
    def test_pickle_fields(self):
        """
        Classifier factories are meant to be pickled and restored, but that only
        works if the thing we're restoring has the EXACT SAME MEMBERS as the
        current version of the class.

        Any changes to the factory's member variables will change it's pickled representation.
        Therefore, we store a special member named VERSION as both a class member
        AND instance member (see LazyflowVectorwiseClassifierFactoryABC.__new__),
        so we can check for compatibility before attempting to unpickle a factory.

        In this test, we verify that the pickle interface hasn't changed.

        IF THIS TEST FAILS:
            - Think about whether that's what you intended (see below)
            - Update ParallelVigraRfLazyflowClassifierFactory.VERSION
            - and then change the version and members listed below.

        ... but think hard about whether or not the changes you made to
        ParallelVigraRfLazyflowClassifierFactory are important, because they will
        invalidate stored classifiers in existing ilastik project files.
        (The project file should still load, but a warning will be shown, explaining that
        the user will need to train a new classifer.)

        """
        factory = ParallelVigraRfLazyflowClassifierFactory(
            10, variable_importance_enabled=True)
        members = set(factory.__dict__.keys())

        # Quick way to get the updated set of members.
        # print members

        assert ParallelVigraRfLazyflowClassifierFactory.VERSION == 2
        assert members == set([
            "VERSION",
            "_variable_importance_path",
            "_kwargs",
            "_variable_importance_enabled",
            "_num_trees",
            "_label_proportion",
            "_num_forests",
        ])
class OpMockPixelClassifier(Operator):
    """
    This class is a simple stand-in for the real pixel classification operator.
    Uses hard-coded data shape and block shape.
    Provides hard-coded outputs.
    """
    name = "OpMockPixelClassifier"

    LabelInputs = InputSlot(optional = True, level=1) # Input for providing label data from an external source

    PredictionsFromDisk = InputSlot( optional = True, level=1 ) # TODO: Actually use this input for something

    ClassifierFactory = InputSlot( value=ParallelVigraRfLazyflowClassifierFactory(10,10) )

    NonzeroLabelBlocks = OutputSlot(level=1, stype='object') # A list if slices that contain non-zero label values
    LabelImages = OutputSlot(level=1) # Labels from the user
    
    Classifier = OutputSlot(stype='object')
    
    PredictionProbabilities = OutputSlot(level=1)
    
    FreezePredictions = InputSlot()
    
    LabelNames = OutputSlot()
    LabelColors = OutputSlot()
    PmapColors = OutputSlot()
    Bookmarks = OutputSlot(level=1)
    
    def __init__(self, *args, **kwargs):
        super(OpMockPixelClassifier, self).__init__(*args, **kwargs)

        self.LabelNames.setValue( ["Membrane", "Cytoplasm"] )
        self.LabelColors.setValue( [(255,0,0), (0,255,0)] ) # Red, Green
        self.PmapColors.setValue( [(255,0,0), (0,255,0)] ) # Red, Green
        
        self._data = []
        self.dataShape = (1,10,100,100,1)
        self.prediction_shape = self.dataShape[:-1] + (2,) # Hard-coded to provide 2 classes
        
        self.FreezePredictions.setValue(False)
        
        self.opClassifier = OpTrainClassifierBlocked(graph=self.graph, parent=self)
        self.opClassifier.ClassifierFactory.connect( self.ClassifierFactory )
        self.opClassifier.Labels.connect(self.LabelImages)
        self.opClassifier.nonzeroLabelBlocks.connect(self.NonzeroLabelBlocks)
        self.opClassifier.MaxLabel.setValue(2)
        
        self.classifier_cache = OpValueCache(graph=self.graph, parent=self)
        self.classifier_cache.Input.connect( self.opClassifier.Classifier )
        
        p1 = numpy.indices(self.dataShape).sum(0) / 207.0
        p2 = 1 - p1

        self.predictionData = numpy.concatenate((p1,p2), axis=4)
    
    def setupOutputs(self):
        numImages = len(self.LabelInputs)

        self.PredictionsFromDisk.resize( numImages )
        self.NonzeroLabelBlocks.resize( numImages )
        self.LabelImages.resize( numImages )
        self.PredictionProbabilities.resize( numImages )
        self.opClassifier.Images.resize( numImages )

        for i in range(numImages):
            self._data.append( numpy.zeros(self.dataShape) )
            self.NonzeroLabelBlocks[i].meta.shape = (1,)
            self.NonzeroLabelBlocks[i].meta.dtype = object

            # Hard-coded: Two prediction classes
            self.PredictionProbabilities[i].meta.shape = self.prediction_shape
            self.PredictionProbabilities[i].meta.dtype = numpy.float64
            self.PredictionProbabilities[i].meta.axistags = vigra.defaultAxistags('txyzc')
            
            # Classify with random data
            self.opClassifier.Images[i].setValue( vigra.taggedView( numpy.random.random(self.dataShape), 'txyzc' ) )
        
            self.LabelImages[i].meta.shape = self.dataShape
            self.LabelImages[i].meta.dtype = numpy.float64
            self.LabelImages[i].meta.axistags = self.opClassifier.Images[i].meta.axistags
            

        self.Classifier.connect( self.opClassifier.Classifier )
        
    def setInSlot(self, slot, subindex, roi, value):
        key = roi.toSlice()
        assert slot.name == "LabelInputs"
        self._data[subindex[0]][key] = value
        self.LabelImages[subindex[0]].setDirty(key)
    
    def execute(self, slot, subindex, roi, result):
        key = roiToSlice(roi.start, roi.stop)
        index = subindex[0]
        if slot.name == "NonzeroLabelBlocks":
            # Split into 10 chunks
            blocks = []
            slicing = [slice(0,maximum) for maximum in self.dataShape]
            for i in range(10):
                slicing[2] = slice(i*10, (i+1)*10)
                if not (self._data[index][slicing] == 0).all():
                    blocks.append( list(slicing) )

            result[0] = blocks
        if slot.name == "LabelImages":
            result[...] = self._data[index][key]
        if slot.name == "PredictionProbabilities":
            result[...] = self.predictionData[key]
    
    def propagateDirty(self, slot, subindex, roi):
        pass
Example #11
0
class OpPixelClassification(Operator):
    """
    Top-level operator for pixel classification
    """
    name = "OpPixelClassification"
    category = "Top-level"

    # Graph inputs

    InputImages = InputSlot(
        level=1)  # Original input data.  Used for display only.
    PredictionMasks = InputSlot(
        level=1, optional=True
    )  # Routed to OpClassifierPredict.PredictionMask.  See there for details.

    LabelInputs = InputSlot(
        optional=True,
        level=1)  # Input for providing label data from an external source

    FeatureImages = InputSlot(
        level=1
    )  # Computed feature images (each channel is a different feature)
    CachedFeatureImages = InputSlot(level=1)  # Cached feature data.

    FreezePredictions = InputSlot(stype='bool')
    ClassifierFactory = InputSlot(
        value=ParallelVigraRfLazyflowClassifierFactory(100))

    PredictionsFromDisk = InputSlot(optional=True, level=1)

    PredictionProbabilities = OutputSlot(
        level=1
    )  # Classification predictions (via feature cache for interactive speed)
    PredictionProbabilitiesUint8 = OutputSlot(
        level=1)  # Same thing, but converted to uint8 first

    PredictionProbabilityChannels = OutputSlot(
        level=2)  # Classification predictions, enumerated by channel
    SegmentationChannels = OutputSlot(
        level=2)  # Binary image of the final selections.

    LabelImages = OutputSlot(level=1)  # Labels from the user
    NonzeroLabelBlocks = OutputSlot(
        level=1)  # A list if slices that contain non-zero label values
    Classifier = OutputSlot(
    )  # We provide the classifier as an external output for other applets to use

    CachedPredictionProbabilities = OutputSlot(
        level=1
    )  # Classification predictions (via feature cache AND prediction cache)

    HeadlessPredictionProbabilities = OutputSlot(
        level=1
    )  # Classification predictions ( via no image caches (except for the classifier itself )
    HeadlessUint8PredictionProbabilities = OutputSlot(
        level=1)  # Same as above, but 0-255 uint8 instead of 0.0-1.0 float32
    HeadlessUncertaintyEstimate = OutputSlot(
        level=1
    )  # Same as uncertaintly estimate, but does not rely on cached data.

    UncertaintyEstimate = OutputSlot(level=1)

    SimpleSegmentation = OutputSlot(level=1)  # For debug, for now

    # GUI-only (not part of the pipeline, but saved to the project)
    LabelNames = OutputSlot()
    LabelColors = OutputSlot()
    PmapColors = OutputSlot()

    NumClasses = OutputSlot()

    def setupOutputs(self):
        self.LabelNames.meta.dtype = object
        self.LabelNames.meta.shape = (1, )
        self.LabelColors.meta.dtype = object
        self.LabelColors.meta.shape = (1, )
        self.PmapColors.meta.dtype = object
        self.PmapColors.meta.shape = (1, )

    def __init__(self, *args, **kwargs):
        """
        Instantiate all internal operators and connect them together.
        """
        super(OpPixelClassification, self).__init__(*args, **kwargs)

        # Default values for some input slots
        self.FreezePredictions.setValue(True)
        self.LabelNames.setValue([])
        self.LabelColors.setValue([])
        self.PmapColors.setValue([])

        # SPECIAL connection: The LabelInputs slot doesn't get it's data
        #  from the InputImages slot, but it's shape must match.
        self.LabelInputs.connect(self.InputImages)

        # Hook up Labeling Pipeline
        self.opLabelPipeline = OpMultiLaneWrapper(
            OpLabelPipeline,
            parent=self,
            broadcastingSlotNames=['DeleteLabel'])
        self.opLabelPipeline.RawImage.connect(self.InputImages)
        self.opLabelPipeline.LabelInput.connect(self.LabelInputs)
        self.opLabelPipeline.DeleteLabel.setValue(-1)
        self.LabelImages.connect(self.opLabelPipeline.Output)
        self.NonzeroLabelBlocks.connect(self.opLabelPipeline.nonzeroBlocks)

        # Hook up the Training operator
        self.opTrain = OpTrainClassifierBlocked(parent=self)
        self.opTrain.ClassifierFactory.connect(self.ClassifierFactory)
        self.opTrain.Labels.connect(self.opLabelPipeline.Output)
        self.opTrain.Images.connect(self.FeatureImages)
        self.opTrain.nonzeroLabelBlocks.connect(
            self.opLabelPipeline.nonzeroBlocks)

        # Hook up the Classifier Cache
        # The classifier is cached here to allow serializers to force in
        #   a pre-calculated classifier (loaded from disk)
        self.classifier_cache = OpValueCache(parent=self)
        self.classifier_cache.name = "OpPixelClassification.classifier_cache"
        self.classifier_cache.inputs["Input"].connect(
            self.opTrain.outputs['Classifier'])
        self.classifier_cache.inputs["fixAtCurrent"].connect(
            self.FreezePredictions)
        self.Classifier.connect(self.classifier_cache.Output)

        # Hook up the prediction pipeline inputs
        self.opPredictionPipeline = OpMultiLaneWrapper(OpPredictionPipeline,
                                                       parent=self)
        self.opPredictionPipeline.FeatureImages.connect(self.FeatureImages)
        self.opPredictionPipeline.CachedFeatureImages.connect(
            self.CachedFeatureImages)
        self.opPredictionPipeline.Classifier.connect(
            self.classifier_cache.Output)
        self.opPredictionPipeline.FreezePredictions.connect(
            self.FreezePredictions)
        self.opPredictionPipeline.PredictionsFromDisk.connect(
            self.PredictionsFromDisk)
        self.opPredictionPipeline.PredictionMask.connect(self.PredictionMasks)

        # Feature Selection Stuff
        self.opFeatureMatrixCaches = OpMultiLaneWrapper(OpFeatureMatrixCache,
                                                        parent=self)
        self.opFeatureMatrixCaches.LabelImage.connect(
            self.opLabelPipeline.Output)
        self.opFeatureMatrixCaches.FeatureImage.connect(self.FeatureImages)
        self.opFeatureMatrixCaches.LabelImage.setDirty(
        )  # do I still need this?

        def _updateNumClasses(*args):
            """
            When the number of labels changes, we MUST make sure that the prediction image changes its shape (the number of channels).
            Since setupOutputs is not called for mere dirty notifications, but is called in response to setValue(),
            we use this function to call setValue().
            """
            numClasses = len(self.LabelNames.value)
            self.opTrain.MaxLabel.setValue(numClasses)
            self.opPredictionPipeline.NumClasses.setValue(numClasses)
            self.NumClasses.setValue(numClasses)

        self.LabelNames.notifyDirty(_updateNumClasses)

        # Prediction pipeline outputs -> Top-level outputs
        self.PredictionProbabilities.connect(
            self.opPredictionPipeline.PredictionProbabilities)
        self.PredictionProbabilitiesUint8.connect(
            self.opPredictionPipeline.PredictionProbabilitiesUint8)
        self.CachedPredictionProbabilities.connect(
            self.opPredictionPipeline.CachedPredictionProbabilities)
        self.HeadlessPredictionProbabilities.connect(
            self.opPredictionPipeline.HeadlessPredictionProbabilities)
        self.HeadlessUint8PredictionProbabilities.connect(
            self.opPredictionPipeline.HeadlessUint8PredictionProbabilities)
        self.PredictionProbabilityChannels.connect(
            self.opPredictionPipeline.PredictionProbabilityChannels)
        self.SegmentationChannels.connect(
            self.opPredictionPipeline.SegmentationChannels)
        self.UncertaintyEstimate.connect(
            self.opPredictionPipeline.UncertaintyEstimate)
        self.SimpleSegmentation.connect(
            self.opPredictionPipeline.SimpleSegmentation)
        self.HeadlessUncertaintyEstimate.connect(
            self.opPredictionPipeline.HeadlessUncertaintyEstimate)

        def inputResizeHandler(slot, oldsize, newsize):
            if (newsize == 0):
                self.LabelImages.resize(0)
                self.NonzeroLabelBlocks.resize(0)
                self.PredictionProbabilities.resize(0)
                self.CachedPredictionProbabilities.resize(0)

        self.InputImages.notifyResized(inputResizeHandler)

        # Debug assertions: Check to make sure the non-wrapped operators stayed that way.
        assert self.opTrain.Images.operator == self.opTrain

        def handleNewInputImage(multislot, index, *args):
            def handleInputReady(slot):
                self._checkConstraints(index)
                self.setupCaches(multislot.index(slot))

            multislot[index].notifyReady(handleInputReady)

        self.InputImages.notifyInserted(handleNewInputImage)

        # If any feature image changes shape, we need to verify that the
        #  channels are consistent with the currently cached classifier
        # Otherwise, delete the currently cached classifier.
        def handleNewFeatureImage(multislot, index, *args):
            def handleFeatureImageReady(slot):
                def handleFeatureMetaChanged(slot):
                    if (self.classifier_cache.fixAtCurrent.value
                            and self.classifier_cache.Output.ready()
                            and slot.meta.shape is not None):
                        classifier = self.classifier_cache.Output.value
                        channel_names = slot.meta.channel_names
                        if classifier and classifier.feature_names != channel_names:
                            self.classifier_cache.resetValue()

                slot.notifyMetaChanged(handleFeatureMetaChanged)

            multislot[index].notifyReady(handleFeatureImageReady)

        self.FeatureImages.notifyInserted(handleNewFeatureImage)

        def handleNewMaskImage(multislot, index, *args):
            def handleInputReady(slot):
                self._checkConstraints(index)

            multislot[index].notifyReady(handleInputReady)

        self.PredictionMasks.notifyInserted(handleNewMaskImage)

        # All input multi-slots should be kept in sync
        # Output multi-slots will auto-sync via the graph
        multiInputs = filter(lambda s: s.level >= 1, self.inputs.values())
        for s1 in multiInputs:
            for s2 in multiInputs:
                if s1 != s2:

                    def insertSlot(a, b, position, finalsize):
                        a.insertSlot(position, finalsize)

                    s1.notifyInserted(partial(insertSlot, s2))

                    def removeSlot(a, b, position, finalsize):
                        a.removeSlot(position, finalsize)

                    s1.notifyRemoved(partial(removeSlot, s2))

    def setupCaches(self, imageIndex):
        numImages = len(self.InputImages)
        inputSlot = self.InputImages[imageIndex]
        #        # Can't setup if all inputs haven't been set yet.
        #        if numImages != len(self.FeatureImages) or \
        #           numImages != len(self.CachedFeatureImages):
        #            return
        #
        #        self.LabelImages.resize(numImages)
        self.LabelInputs.resize(numImages)

        # Special case: We have to set up the shape of our label *input* according to our image input shape
        shapeList = list(self.InputImages[imageIndex].meta.shape)
        try:
            channelIndex = self.InputImages[imageIndex].meta.axistags.index(
                'c')
            shapeList[channelIndex] = 1
        except:
            pass
        self.LabelInputs[imageIndex].meta.shape = tuple(shapeList)
        self.LabelInputs[imageIndex].meta.axistags = inputSlot.meta.axistags

    def _checkConstraints(self, laneIndex):
        """
        Ensure that all input images have the same number of channels.
        """
        if not self.InputImages[laneIndex].ready():
            return

        thisLaneTaggedShape = self.InputImages[laneIndex].meta.getTaggedShape()

        # Find a different lane and use it for comparison
        validShape = thisLaneTaggedShape
        for i, slot in enumerate(self.InputImages):
            if slot.ready() and i != laneIndex:
                validShape = slot.meta.getTaggedShape()
                break

        if 't' in thisLaneTaggedShape:
            del thisLaneTaggedShape['t']
        if 't' in validShape:
            del validShape['t']

        if validShape['c'] != thisLaneTaggedShape['c']:
            raise DatasetConstraintError(
                 "Pixel Classification",
                 "All input images must have the same number of channels.  "\
                 "Your new image has {} channel(s), but your other images have {} channel(s)."\
                 .format( thisLaneTaggedShape['c'], validShape['c'] ) )

        if len(validShape) != len(thisLaneTaggedShape):
            raise DatasetConstraintError(
                 "Pixel Classification",
                 "All input images must have the same dimensionality.  "\
                 "Your new image has {} dimensions (including channel), but your other images have {} dimensions."\
                 .format( len(thisLaneTaggedShape), len(validShape) ) )

        mask_slot = self.PredictionMasks[laneIndex]
        input_shape = self.InputImages[laneIndex].meta.shape
        if mask_slot.ready() and mask_slot.meta.shape[:-1] != input_shape[:-1]:
            raise DatasetConstraintError(
                 "Pixel Classification",
                 "If you supply a prediction mask, it must have the same shape as the input image."\
                 "Your input image has shape {}, but your mask has shape {}."\
                 .format( input_shape, mask_slot.meta.shape ) )

    def setInSlot(self, slot, subindex, roi, value):
        # Nothing to do here: All inputs that support __setitem__
        #   are directly connected to internal operators.
        pass

    def propagateDirty(self, slot, subindex, roi):
        # Nothing to do here: All outputs are directly connected to
        #  internal operators that handle their own dirty propagation.
        pass

    def addLane(self, laneIndex):
        numLanes = len(self.InputImages)
        assert numLanes == laneIndex, "Image lanes must be appended."
        self.InputImages.resize(numLanes + 1)

    def removeLane(self, laneIndex, finalLength):
        self.InputImages.removeSlot(laneIndex, finalLength)

    def getLane(self, laneIndex):
        return OperatorSubView(self, laneIndex)

    def importLabels(self, laneIndex, slot):
        # Load the data into the cache
        new_max = self.getLane(
            laneIndex).opLabelPipeline.opLabelArray.ingestData(slot)

        # Add to the list of label names if there's a new max label
        old_names = self.LabelNames.value
        old_max = len(old_names)
        if new_max > old_max:
            new_names = old_names + map(lambda x: "Label {}".format(x),
                                        range(old_max + 1, new_max + 1))
            self.LabelNames.setValue(new_names)

            # Make some default colors, too
            default_colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255),
                              (255, 255, 0), (255, 0, 255), (0, 255, 255),
                              (128, 128, 128), (255, 105, 180), (255, 165, 0),
                              (240, 230, 140)]
            label_colors = self.LabelColors.value
            pmap_colors = self.PmapColors.value

            self.LabelColors.setValue(label_colors +
                                      default_colors[old_max:new_max])
            self.PmapColors.setValue(pmap_colors +
                                     default_colors[old_max:new_max])

    def mergeLabels(self, from_label, into_label):
        for laneIndex in range(len(self.InputImages)):
            self.getLane(laneIndex).opLabelPipeline.opLabelArray.mergeLabels(
                from_label, into_label)

    def clearLabel(self, label_value):
        for laneIndex in range(len(self.InputImages)):
            self.getLane(laneIndex).opLabelPipeline.opLabelArray.clearLabel(
                label_value)