Skip to content

Commit 1734442

Browse files
authored
Merge pull request #49 from fractal-napari-plugins-collection/41_napari_050_support
Add napari 0.5.0 support & update prediction layer display
2 parents 5aa40dc + 3470d44 commit 1734442

File tree

8 files changed

+112
-56
lines changed

8 files changed

+112
-56
lines changed

.github/workflows/test_and_deploy.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
strategy:
2424
matrix:
2525
platform: [ubuntu-latest, windows-latest, macos-latest]
26-
python-version: ['3.9', '3.10']
26+
python-version: ['3.9', '3.10', '3.11']
2727

2828
steps:
2929
- uses: actions/checkout@v3

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ An interactive classifier plugin that allows the user to assign objects in a lab
1212
## Usage
1313
<p align="center"><img src="https://github.com/fractal-napari-plugins-collection/napari-feature-classifier/assets/18033446/1ebf0890-1a7b-4e4b-a21c-88ca8f1dd800" /></p>
1414

15-
To use the napari-feature-classifier, you need to have a label image and corresponding measurements: as a csv file, loaded to layer.features or in an [OME-Zarr Anndata table loaded with another plugin](https://github.com/jluethi/napari-ome-zarr-roi-loader). Your feature measurements need to contain a `label` column that matches the label objects in the label image.
15+
To use the napari-feature-classifier, you need to have a label image and corresponding measurements: as a csv file, loaded to layer.features or in an [OME-Zarr Anndata table loaded with another plugin](https://github.com/fractal-napari-plugins-collection/napari-ome-zarr-navigator). Your feature measurements need to contain a `label` column that matches the label objects in the label image.
1616
These interactive classification workflows are well suited to visually define cell types, find mitotic cells in images, do quality control by automatically detecting missegmented cells and other tasks where a user can easily assign objects to groups.
1717

1818
#### Prepare the label layer:

setup.cfg

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = napari-feature-classifier
3-
version = 0.1.2
3+
version = 0.2.0
44
author = Joel Luethi and Max Hess
55
author_email = [email protected]
66
url = https://github.com/fractal-napari-plugins-collection/napari-feature-classifier
@@ -37,7 +37,7 @@ package_dir =
3737
# add your package requirements here
3838
install_requires =
3939
numpy < 2.0
40-
napari < 0.4.19
40+
napari
4141
matplotlib
4242
magicgui
4343
pandas
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""Init"""
2-
__version__ = "0.1.2"
2+
__version__ = "0.2.0"

src/napari_feature_classifier/annotator_widget.py

+53-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import warnings
33
from enum import Enum
44
from functools import partial
5+
from packaging import version
56
from pathlib import Path
67
from typing import Optional, Sequence, cast
78

@@ -24,7 +25,8 @@
2425
# pylint: disable=R0801
2526
from napari_feature_classifier.utils import (
2627
get_colormap,
27-
reset_display_colormaps,
28+
reset_display_colormaps_legacy,
29+
reset_display_colormaps_modern,
2830
get_valid_label_layers,
2931
get_selected_or_valid_label_layer,
3032
napari_info,
@@ -245,7 +247,11 @@ def toggle_label(self, labels_layer, event):
245247
] = np.NaN
246248

247249
# Update only the single color value that changed
248-
self.update_single_color(labels_layer, label)
250+
napari_version = version.parse(napari.__version__)
251+
if napari_version >= version.parse("0.4.19"):
252+
self.update_single_color_slow(labels_layer, label)
253+
else:
254+
self.update_single_color_legacy(labels_layer, label)
249255

250256
def set_class_n(self, event, n: int): # pylint: disable=C0103
251257
self._class_selector.value = self.ClassSelection[
@@ -276,13 +282,23 @@ def _init_annotation(self, label_layer: napari.layers.Labels):
276282
self._annotations_layer.scale = label_layer.scale
277283
self._annotations_layer.translate = label_layer.translate
278284

279-
reset_display_colormaps(
280-
label_layer,
281-
feature_col="annotations",
282-
display_layer=self._annotations_layer,
283-
label_column=self._label_column,
284-
cmap=self.cmap,
285-
)
285+
napari_version = version.parse(napari.__version__)
286+
if napari_version >= version.parse("0.4.19"):
287+
reset_display_colormaps_modern(
288+
label_layer,
289+
feature_col="annotations",
290+
display_layer=self._annotations_layer,
291+
label_column=self._label_column,
292+
cmap=self.cmap,
293+
)
294+
else:
295+
reset_display_colormaps_legacy(
296+
label_layer,
297+
feature_col="annotations",
298+
display_layer=self._annotations_layer,
299+
label_column=self._label_column,
300+
cmap=self.cmap,
301+
)
286302
label_layer.mouse_drag_callbacks.append(self.toggle_label)
287303

288304
# keybindings for the available classes (0 = deselect)
@@ -300,7 +316,7 @@ def _update_save_destination(self, label_layer: napari.layers.Labels):
300316
base_path = Path(self._save_destination.value).parent
301317
self._save_destination.value = base_path / f"{label_layer.name}_annotation.csv"
302318

303-
def update_single_color(self, label_layer, label):
319+
def update_single_color_legacy(self, label_layer, label):
304320
"""
305321
Update the color of a single object in the annotations layer.
306322
"""
@@ -317,6 +333,33 @@ def update_single_color(self, label_layer, label):
317333
self._annotations_layer.opacity = 1.0
318334
self._annotations_layer.color_mode = "direct"
319335

336+
def update_single_color_slow(self, label_layer, label):
337+
"""
338+
Update the color of a single object in the annotations layer.
339+
340+
napari >= 0.4.19 does not have a direct API to only update a single
341+
color. It always validates & updates the whole colormap.
342+
Therefore, this update mode scales badly with the number of unique
343+
labels.
344+
See details in https://github.com/napari/napari/issues/6732
345+
346+
"""
347+
color = self.cmap(
348+
float(
349+
label_layer.features.loc[
350+
label_layer.features[self._label_column] == label,
351+
"annotations",
352+
].iloc[0]
353+
)
354+
/ len(self.cmap.colors)
355+
)
356+
from napari.utils.colormaps import DirectLabelColormap
357+
358+
colordict = self._annotations_layer.colormap.color_dict
359+
colordict[label] = color
360+
self._annotations_layer.colormap = DirectLabelColormap(color_dict=colordict)
361+
self._annotations_layer.opacity = 1.0
362+
320363
def _on_save_clicked(self):
321364
"""
322365
Save annotations to a csv file.

src/napari_feature_classifier/classifier_widget.py

+32-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import pickle
44

5+
from packaging import version
56
from pathlib import Path
67
from typing import Optional
78

@@ -27,7 +28,8 @@
2728
from napari_feature_classifier.classifier import Classifier
2829
from napari_feature_classifier.utils import (
2930
get_colormap,
30-
reset_display_colormaps,
31+
reset_display_colormaps_modern,
32+
reset_display_colormaps_legacy,
3133
get_valid_label_layers,
3234
get_selected_or_valid_label_layer,
3335
napari_info,
@@ -256,6 +258,7 @@ def __init__(
256258
name="Predictions",
257259
translate=self._last_selected_label_layer.translate,
258260
)
261+
self._prediction_layer.contour = 2
259262

260263
# Set the label selection to a valid label layer => Running into proxy bug
261264
self._viewer.layers.selection.active = self._last_selected_label_layer
@@ -294,9 +297,9 @@ def __init__(
294297
self._init_prediction_layer(self._last_selected_label_layer)
295298
# Whenever the label layer is clicked, hide the prediction layer
296299
# (e.g. new annotations are made)
297-
self._last_selected_label_layer.mouse_drag_callbacks.append(
298-
self.hide_prediction_layer
299-
)
300+
# self._last_selected_label_layer.mouse_drag_callbacks.append(
301+
# self.hide_prediction_layer
302+
# )
300303

301304
def run(self):
302305
"""
@@ -396,9 +399,9 @@ def selection_changed(self):
396399
):
397400
self._last_selected_label_layer = self._viewer.layers.selection.active
398401
self._init_prediction_layer(self._viewer.layers.selection.active)
399-
self._last_selected_label_layer.mouse_drag_callbacks.append(
400-
self.hide_prediction_layer
401-
)
402+
# self._last_selected_label_layer.mouse_drag_callbacks.append(
403+
# self.hide_prediction_layer
404+
# )
402405
self._update_export_destination(self._last_selected_label_layer)
403406

404407
def _init_prediction_layer(self, label_layer: napari.layers.Labels):
@@ -427,19 +430,29 @@ def _init_prediction_layer(self, label_layer: napari.layers.Labels):
427430
self._prediction_layer.translate = label_layer.translate
428431

429432
# Update the colormap of the prediction layer
430-
reset_display_colormaps(
431-
label_layer,
432-
feature_col="prediction",
433-
display_layer=self._prediction_layer,
434-
label_column=self._label_column,
435-
cmap=get_colormap(),
436-
)
433+
napari_version = version.parse(napari.__version__)
434+
if napari_version >= version.parse("0.4.19"):
435+
reset_display_colormaps_modern(
436+
label_layer,
437+
feature_col="prediction",
438+
display_layer=self._prediction_layer,
439+
label_column=self._label_column,
440+
cmap=get_colormap(),
441+
)
442+
else:
443+
reset_display_colormaps_legacy(
444+
label_layer,
445+
feature_col="prediction",
446+
display_layer=self._prediction_layer,
447+
label_column=self._label_column,
448+
cmap=get_colormap(),
449+
)
437450

438-
def hide_prediction_layer(self, labels_layer, event):
439-
"""
440-
Hide the prediction layer
441-
"""
442-
self._prediction_layer.visible = False
451+
# def hide_prediction_layer(self, labels_layer, event):
452+
# """
453+
# Hide the prediction layer
454+
# """
455+
# self._prediction_layer.visible = False
443456

444457
def get_relevant_label_layers(self):
445458
relevant_label_layers = []

src/napari_feature_classifier/utils.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def get_colormap(matplotlib_colormap="Set1"):
7070
return cmap
7171

7272

73-
def reset_display_colormaps(
73+
def reset_display_colormaps_legacy(
7474
label_layer, feature_col, display_layer, label_column, cmap
7575
):
7676
"""
@@ -85,25 +85,25 @@ def reset_display_colormaps(
8585
display_layer.color_mode = "direct"
8686

8787

88-
# # Check if it runs in napari
89-
# # This currently triggers an exception.
90-
# # Find a new way to ensure the warning is also shown in the napari
91-
# # interface # if _ipython_has_eventloop():
92-
# NapariQtNotification(message, 'INFO').show()
93-
94-
95-
# def napari_warn(message):
96-
# # Wrapper function to ensure a message o
97-
# warnings.warn(message)
98-
# show_info(message)
99-
# print('test')
100-
# # This currently triggers an exception.
101-
# # Find a new way to ensure the warning is also shown in the napari
102-
# # interface
103-
# if _ipython_has_eventloop():
104-
# pass
105-
# # NapariQtNotification(message, 'WARNING').show()
106-
#
88+
def reset_display_colormaps_modern(
89+
label_layer, feature_col, display_layer, label_column, cmap
90+
):
91+
"""
92+
Reset the colormap based on the annotations in
93+
label_layer.features['annotation'] and sends the updated colormap
94+
to the annotation label layer
95+
96+
Modern version to support napari >= 0.4.19
97+
"""
98+
from napari.utils.colormaps import DirectLabelColormap
99+
100+
colors = cmap(label_layer.features[feature_col].astype(float) / len(cmap.colors))
101+
colordict = dict(zip(label_layer.features[label_column], colors))
102+
colordict[None] = [0, 0, 0, 0]
103+
display_layer.colormap = DirectLabelColormap(color_dict=colordict)
104+
display_layer.opacity = 1.0
105+
106+
107107
def napari_info(message):
108108
"""
109109
Info message wrapper.

tox.ini

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# For more information about tox, see https://tox.readthedocs.io/en/latest/
22
[tox]
3-
envlist = py{38,39,310}-{linux,macos,windows}
3+
envlist = py{39,310,311}-{linux,macos,windows}
44
isolated_build=true
55

66
[gh-actions]
77
python =
8-
3.8: py38
98
3.9: py39
109
3.10: py310
10+
3.11: py311
1111

1212
[gh-actions:env]
1313
PLATFORM =

0 commit comments

Comments
 (0)