From 23b01de7e37133d86e03fbb3448ec78823b50b79 Mon Sep 17 00:00:00 2001 From: jluethi Date: Mon, 16 Sep 2024 17:23:26 +0200 Subject: [PATCH 1/4] Ensure that prediction layer exists (Closes #51) --- .../classifier_widget.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/napari_feature_classifier/classifier_widget.py b/src/napari_feature_classifier/classifier_widget.py index 0052318..31761cf 100644 --- a/src/napari_feature_classifier/classifier_widget.py +++ b/src/napari_feature_classifier/classifier_widget.py @@ -248,7 +248,6 @@ def __init__( self._viewer, get_class_selection(class_names=self.class_names) ) - # Handle existing predictions layer for layer in self._viewer.layers: if type(layer) == napari.layers.Labels and layer.name == "Predictions": self._viewer.layers.remove(layer) @@ -295,11 +294,6 @@ def __init__( self._export_button.clicked.connect(self.export_results) self._viewer.layers.selection.events.changed.connect(self.selection_changed) self._init_prediction_layer(self._last_selected_label_layer) - # Whenever the label layer is clicked, hide the prediction layer - # (e.g. new annotations are made) - # self._last_selected_label_layer.mouse_drag_callbacks.append( - # self.hide_prediction_layer - # ) def run(self): """ @@ -398,17 +392,30 @@ def selection_changed(self): viewer=self._viewer ): self._last_selected_label_layer = self._viewer.layers.selection.active - self._init_prediction_layer(self._viewer.layers.selection.active) - # self._last_selected_label_layer.mouse_drag_callbacks.append( - # self.hide_prediction_layer - # ) + self._init_prediction_layer( + self._viewer.layers.selection.active, ensure_layer_presence=False + ) self._update_export_destination(self._last_selected_label_layer) - def _init_prediction_layer(self, label_layer: napari.layers.Labels): + def _init_prediction_layer( + self, label_layer: napari.layers.Labels, ensure_layer_presence: bool = True + ): """ Initialize the prediction layer and reset its data (to fit the input label_layer) and its colormap """ + # Ensure that prediction layer exists + if ( + "Predictions" not in [x.name for x in self._viewer.layers] + and ensure_layer_presence + ): + self._prediction_layer = self._viewer.add_labels( + self._last_selected_label_layer.data, + scale=self._last_selected_label_layer.scale, + name="Predictions", + translate=self._last_selected_label_layer.translate, + ) + # Check if the predict column already exists in the layer.features if "prediction" not in label_layer.features: unique_labels = np.unique(label_layer.data)[1:] @@ -448,12 +455,6 @@ def _init_prediction_layer(self, label_layer: napari.layers.Labels): cmap=get_colormap(), ) - # def hide_prediction_layer(self, labels_layer, event): - # """ - # Hide the prediction layer - # """ - # self._prediction_layer.visible = False - def get_relevant_label_layers(self): relevant_label_layers = [] required_columns = [self._label_column, self._roi_id_colum] From e778a75cab95189840c04cc015cb7dad8dfb2d2a Mon Sep 17 00:00:00 2001 From: jluethi Date: Mon, 16 Sep 2024 18:23:33 +0200 Subject: [PATCH 2/4] Reorder layers when predicting to ensure Prediction layer is on top --- .../classifier_widget.py | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/napari_feature_classifier/classifier_widget.py b/src/napari_feature_classifier/classifier_widget.py index 31761cf..8559103 100644 --- a/src/napari_feature_classifier/classifier_widget.py +++ b/src/napari_feature_classifier/classifier_widget.py @@ -397,12 +397,49 @@ def selection_changed(self): ) self._update_export_destination(self._last_selected_label_layer) + def reorder_layers(self): + """Reorders layers if needed to ensure Annotation & Prediction layers + are above the currently selected label layer. + """ + # Get the current order of layers + all_layers = list(self._viewer.layers) + + # Determine the indices of the layers if they exist + indices_to_move = [] + + # Find the index of "Prediction" layer if it exists + if "Predictions" in self._viewer.layers: + indices_to_move.append(self._viewer.layers.index("Predictions")) + + # Find the index of "Annotation" layer if it exists + if "Annotations" in self._viewer.layers: + indices_to_move.append(self._viewer.layers.index("Annotations")) + + # Find the index of the reference_label_layer + if self._last_selected_label_layer.name in self._viewer.layers: + indices_to_move.append( + self._viewer.layers.index(self._last_selected_label_layer.name) + ) + + # Calculate the new order of layer indices + remaining_indices = [ + i for i in range(len(all_layers)) if i not in indices_to_move + ] + remaining_indices.reverse() + new_order = indices_to_move + remaining_indices + new_order.reverse() + + # Reorder the layers using the move_multiple function + self._viewer.layers.move_multiple(new_order) + def _init_prediction_layer( self, label_layer: napari.layers.Labels, ensure_layer_presence: bool = True ): """ Initialize the prediction layer and reset its data (to fit the input - label_layer) and its colormap + label_layer) and its colormap. + ensure_layer_presence creates the Predictions layer if it doesn't exist + yet and triggers layer reordering. """ # Ensure that prediction layer exists if ( @@ -415,6 +452,16 @@ def _init_prediction_layer( name="Predictions", translate=self._last_selected_label_layer.translate, ) + if ensure_layer_presence: + # Ensure correct layer order: This sometimes fails with weird + # EmitLoopError & IndexError that should be ignored + try: + self.reorder_layers() + except: # noqa + pass + + # Ensure that prediction layer is above the current label layer + self._last_selected_label_layer # Check if the predict column already exists in the layer.features if "prediction" not in label_layer.features: From cd7e0176525832f998647fa63be313840c8afaaf Mon Sep 17 00:00:00 2001 From: jluethi Date: Mon, 16 Sep 2024 18:35:19 +0200 Subject: [PATCH 3/4] Ensure Annotations layer exists & bring prediction layer creation into helper function --- .../annotator_widget.py | 26 ++++++++++++------- .../classifier_widget.py | 24 ++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/napari_feature_classifier/annotator_widget.py b/src/napari_feature_classifier/annotator_widget.py index 9776475..839d613 100644 --- a/src/napari_feature_classifier/annotator_widget.py +++ b/src/napari_feature_classifier/annotator_widget.py @@ -136,16 +136,7 @@ def __init__( for layer in self._viewer.layers: if type(layer) == napari.layers.Labels and layer.name == "Annotations": self._viewer.layers.remove(layer) - self._annotations_layer = self._viewer.add_labels( - self._last_selected_label_layer.data, - scale=self._last_selected_label_layer.scale, - name="Annotations", - translate=self._last_selected_label_layer.translate, - ) - self._annotations_layer.editable = False - - # Set the label selection to a valid label layer => Running into proxy bug - self._viewer.layers.selection.active = self._last_selected_label_layer + self.add_annotations_layer() # Class selection self.ClassSelection = ClassSelection # pylint: disable=C0103 @@ -212,11 +203,26 @@ def selection_changed(self, event): self._save_destination.enabled = False self._class_selector.enabled = False + def add_annotations_layer(self): + self._annotations_layer = self._viewer.add_labels( + self._last_selected_label_layer.data, + scale=self._last_selected_label_layer.scale, + name="Annotations", + translate=self._last_selected_label_layer.translate, + ) + self._annotations_layer.editable = False + # Set the label selection to a valid label layer + self._viewer.layers.selection.active = self._last_selected_label_layer + def toggle_label(self, labels_layer, event): """ Callback for when a label is clicked. It then updates the color of that label in the annotation layer. """ + # If the annotations layer is missing, add it back + if "Annotations" not in [x.name for x in self._viewer.layers]: + self.add_annotations_layer() + # Need to translate & scale position that event.position returns by the # label_layer scale. # If scale is (1, 1, 1), nothing changes diff --git a/src/napari_feature_classifier/classifier_widget.py b/src/napari_feature_classifier/classifier_widget.py index 8559103..9a8b466 100644 --- a/src/napari_feature_classifier/classifier_widget.py +++ b/src/napari_feature_classifier/classifier_widget.py @@ -251,13 +251,7 @@ def __init__( for layer in self._viewer.layers: if type(layer) == napari.layers.Labels and layer.name == "Predictions": self._viewer.layers.remove(layer) - self._prediction_layer = self._viewer.add_labels( - self._last_selected_label_layer.data, - scale=self._last_selected_label_layer.scale, - name="Predictions", - translate=self._last_selected_label_layer.translate, - ) - self._prediction_layer.contour = 2 + self.add_prediction_layer() # Set the label selection to a valid label layer => Running into proxy bug self._viewer.layers.selection.active = self._last_selected_label_layer @@ -343,6 +337,15 @@ def add_features_to_classifier(self): dict_of_features[layer.name] = layer.features self._classifier.add_dict_of_features(dict_of_features) + def add_prediction_layer(self): + self._prediction_layer = self._viewer.add_labels( + self._last_selected_label_layer.data, + scale=self._last_selected_label_layer.scale, + name="Predictions", + translate=self._last_selected_label_layer.translate, + ) + self._prediction_layer.contour = 2 + def make_predictions(self): """ Make predictions for all relevant label layers and add them to the @@ -446,12 +449,7 @@ def _init_prediction_layer( "Predictions" not in [x.name for x in self._viewer.layers] and ensure_layer_presence ): - self._prediction_layer = self._viewer.add_labels( - self._last_selected_label_layer.data, - scale=self._last_selected_label_layer.scale, - name="Predictions", - translate=self._last_selected_label_layer.translate, - ) + self.add_prediction_layer() if ensure_layer_presence: # Ensure correct layer order: This sometimes fails with weird # EmitLoopError & IndexError that should be ignored From 6981f2b75a5729a3a14436dcf2a263d9bcb8a707 Mon Sep 17 00:00:00 2001 From: jluethi Date: Mon, 16 Sep 2024 18:40:30 +0200 Subject: [PATCH 4/4] Add catch for no label image present (Closes #52) --- .../classifier_widget.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/napari_feature_classifier/classifier_widget.py b/src/napari_feature_classifier/classifier_widget.py index 9a8b466..0131c27 100644 --- a/src/napari_feature_classifier/classifier_widget.py +++ b/src/napari_feature_classifier/classifier_widget.py @@ -223,6 +223,7 @@ def __init__( self._last_selected_label_layer = get_selected_or_valid_label_layer( viewer=self._viewer ) + # Initialize the classifier if classifier: self._classifier = classifier @@ -659,12 +660,19 @@ def load(self): with open(clf_path, "rb") as f: # pylint: disable=C0103 clf = pickle.load(f) - self._run_container = ClassifierRunContainer( - self._viewer, - clf, - classifier_save_path=clf_path, - auto_save=True, - ) + try: + self._run_container = ClassifierRunContainer( + self._viewer, + clf, + classifier_save_path=clf_path, + auto_save=True, + ) + except NotImplementedError: + napari_info( + "Create a label layer with a feature dataframe before loading " + "the classifier" + ) + return self.clear() self.append(self._run_container)