Skip to content

Commit 570666a

Browse files
authored
Improve LayerSelecter + Tooltips update (#64)
* Improved layer selecter code - Better handling of removed/added layers independent of widget opening time - Still missing layer renaming events handling * Working layer rename * Tooltips update * Try to fix tests on Win * Disable tests on Win for now * Disable wandb for tests * Small WandB improvement * Change logger back to debug * Fix layer addition and WandB error * Fix bug with missing variable in some cases * Fix WandB project name for WNet * Fix make_csv
1 parent 4fa481b commit 570666a

File tree

7 files changed

+117
-13
lines changed

7 files changed

+117
-13
lines changed

napari_cellseg3d/_tests/test_plugin_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def test_utils_plugin(make_napari_viewer_proxy):
1313
view = make_napari_viewer_proxy()
1414
widget = Utilities(view)
1515

16-
image = rand_gen.random((10, 10, 10)).astype(np.uint8)
16+
image = rand_gen.random((10, 10, 10)) # .astype(np.uint8)
1717
image_layer = view.add_image(image, name="image")
1818
label_layer = view.add_labels(image.astype(np.uint8), name="labels")
1919

napari_cellseg3d/_tests/test_training.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
)
1818
from napari_cellseg3d.config import MODEL_LIST
1919

20+
WANDB_MODE = "disabled"
21+
2022
im_path = Path(__file__).resolve().parent / "res/test.tif"
2123
im_path_str = str(im_path)
2224
lab_path = Path(__file__).resolve().parent / "res/test_labels.tif"

napari_cellseg3d/code_plugins/plugin_convert.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,12 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None):
363363
self.container = self._build()
364364

365365
self.function = clear_small_objects
366+
self._set_tooltips()
367+
368+
def _set_tooltips(self):
369+
self.size_for_removal_counter.setToolTip(
370+
"Size of the objects to remove, in pixels."
371+
)
366372

367373
def _build(self):
368374
container = ui.ContainerWidget()
@@ -647,6 +653,15 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None):
647653
self.container = self._build()
648654
self.function = threshold
649655

656+
self._set_tooltips()
657+
658+
def _set_tooltips(self):
659+
self.binarize_counter.setToolTip(
660+
"Value to use as threshold for binarization."
661+
"For labels, use the highest ID you want to keep. All lower IDs will be removed."
662+
"For images, use the intensity value (pixel value) to threshold the image."
663+
)
664+
650665
def _build(self):
651666
container = ui.ContainerWidget()
652667

napari_cellseg3d/code_plugins/plugin_model_inference.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def _set_tooltips(self):
281281
)
282282

283283
thresh_desc = (
284+
"NOT RECOMMENDED ON FIRST RUN - check results without first!\n"
284285
"Thresholding : all values in the image below the chosen probability"
285286
" threshold will be set to 0, and all others to 1."
286287
)
@@ -301,6 +302,7 @@ def _set_tooltips(self):
301302
"If enabled, data will be kept on the RAM rather than the VRAM.\nCan avoid out of memory issues with CUDA"
302303
)
303304
self.use_instance_choice.setToolTip(
305+
"NOT RECOMMENDED ON FIRST RUN - check results without first!\n"
304306
"Instance segmentation will convert instance (0/1) labels to labels"
305307
" that attempt to assign an unique ID to each cell."
306308
)
@@ -653,6 +655,8 @@ def _display_results(self, result: InferenceResult):
653655
if result.semantic_segmentation[channel].sum() > 0:
654656
index_channel_least_labelled = channel
655657
break
658+
# if no channel has any label, use the first one
659+
index_channel_least_labelled = 0
656660
viewer.dims.set_point(
657661
0, index_channel_least_labelled
658662
) # TODO(cyril: check if this is always the right axis

napari_cellseg3d/code_plugins/plugin_model_training.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ def on_finish(self):
12471247
self.log.print_and_log("*" * 10)
12481248
try:
12491249
self._make_csv()
1250-
except ValueError as e:
1250+
except (ValueError, KeyError) as e:
12511251
logger.warning(f"Error while saving CSV report: {e}")
12521252

12531253
self.start_btn.setText("Start")
@@ -1375,11 +1375,11 @@ def _make_csv(self):
13751375
try:
13761376
self.loss_1_values["Loss"]
13771377
supervised = True
1378-
except KeyError("Loss"):
1378+
except KeyError:
13791379
try:
13801380
self.loss_1_values["SoftNCuts"]
13811381
supervised = False
1382-
except KeyError("SoftNCuts") as e:
1382+
except KeyError as e:
13831383
raise KeyError(
13841384
"Error when making csv. Check loss dict keys ?"
13851385
) from e
@@ -1398,8 +1398,8 @@ def _make_csv(self):
13981398
"validation": val,
13991399
}
14001400
)
1401-
if len(val) != len(self.loss_1_values):
1402-
err = f"Validation and loss values don't have the same length ! Got {len(val)} and {len(self.loss_1_values)}"
1401+
if len(val) != len(self.loss_1_values["Loss"]):
1402+
err = f"Validation and loss values don't have the same length ! Got {len(val)} and {len(self.loss_1_values['Loss'])}"
14031403
logger.error(err)
14041404
raise ValueError(err)
14051405
else:

napari_cellseg3d/interface.py

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""User interface functions and aliases."""
2+
import contextlib
23
import threading
34
from functools import partial
45
from typing import List, Optional
6+
from warnings import warn
57

68
import napari
79

@@ -804,9 +806,12 @@ def __init__(
804806
self.layer_description.setVisible(False)
805807
# self.layer_list.setSizeAdjustPolicy(QComboBox.AdjustToContents) # use tooltip instead ?
806808

809+
# connect to LayerList events
807810
self._viewer.layers.events.inserted.connect(partial(self._add_layer))
808811
self._viewer.layers.events.removed.connect(partial(self._remove_layer))
812+
self._viewer.layers.events.changed.connect(self._check_for_layers)
809813

814+
# update self.layer_list when layers are added or removed
810815
self.layer_list.currentIndexChanged.connect(self._update_tooltip)
811816
self.layer_list.currentTextChanged.connect(self._update_description)
812817

@@ -816,20 +821,81 @@ def __init__(
816821
)
817822
self._check_for_layers()
818823

824+
def _get_all_layers(self):
825+
return [
826+
self.layer_list.itemText(i) for i in range(self.layer_list.count())
827+
]
828+
819829
def _check_for_layers(self):
830+
"""Check for layers of the correct type and update the dropdown menu.
831+
832+
Also removes layers that have been removed from the viewer.
833+
"""
820834
for layer in self._viewer.layers:
821-
if isinstance(layer, self.layer_type):
835+
layer.events.name.connect(self._rename_layer)
836+
837+
if (
838+
isinstance(layer, self.layer_type)
839+
and layer.name not in self._get_all_layers()
840+
):
841+
logger.debug(
842+
f"Layer {layer.name} - List : {self._get_all_layers()}"
843+
)
844+
# add new layers of correct type
822845
self.layer_list.addItem(layer.name)
846+
logger.debug(f"Layer {layer.name} has been added to the menu")
847+
# break
848+
# once added, check again for previously renamed layers
849+
self._check_for_removed_layer(layer)
850+
851+
if layer.name in self._get_all_layers() and not isinstance(
852+
layer, self.layer_type
853+
):
854+
# remove layers of incorrect type
855+
index = self.layer_list.findText(layer.name)
856+
self.layer_list.removeItem(index)
857+
logger.debug(
858+
f"Layer {layer.name} has been removed from the menu"
859+
)
860+
861+
self._check_for_removed_layers()
862+
self._update_tooltip()
863+
self._update_description()
864+
865+
def _check_for_removed_layer(self, layer):
866+
"""Check if a specific layer has been removed from the viewer and must be removed from the menu."""
867+
if isinstance(layer, str):
868+
name = layer
869+
elif isinstance(layer, self.layer_type):
870+
name = layer.name
871+
else:
872+
logger.warning("Layer is not a string or a valid napari layer")
873+
return
874+
875+
if name in self._get_all_layers() and name not in [
876+
l.name for l in self._viewer.layers
877+
]:
878+
index = self.layer_list.findText(name)
879+
self.layer_list.removeItem(index)
880+
logger.debug(f"Layer {name} has been removed from the menu")
881+
882+
def _check_for_removed_layers(self):
883+
"""Check for layers that have been removed from the viewer and must be removed from the menu."""
884+
for layer in self._get_all_layers():
885+
self._check_for_removed_layer(layer)
823886

824887
def _update_tooltip(self):
825888
self.layer_list.setToolTip(self.layer_list.currentText())
826889

827890
def _update_description(self):
828891
try:
829892
if self.layer_list.currentText() != "":
830-
self.layer_description.setVisible(True)
831-
shape_desc = f"Shape : {self.layer_data().shape}"
832-
self.layer_description.setText(shape_desc)
893+
try:
894+
shape_desc = f"Shape : {self.layer_data().shape}"
895+
self.layer_description.setText(shape_desc)
896+
self.layer_description.setVisible(True)
897+
except AttributeError:
898+
self.layer_description.setVisible(False)
833899
else:
834900
self.layer_description.setVisible(False)
835901
except KeyError:
@@ -841,6 +907,13 @@ def _add_layer(self, event):
841907
if isinstance(inserted_layer, self.layer_type):
842908
self.layer_list.addItem(inserted_layer.name)
843909

910+
# check for renaming
911+
inserted_layer.events.name.connect(self._rename_layer)
912+
913+
def _rename_layer(self, _):
914+
# on layer rename, check for removed/new layers
915+
self._check_for_layers()
916+
844917
def _remove_layer(self, event):
845918
removed_layer = event.value
846919

@@ -867,15 +940,24 @@ def layer(self):
867940

868941
def layer_name(self):
869942
"""Returns the name of the layer selected in the dropdown menu."""
870-
return self.layer_list.currentText()
943+
try:
944+
return self.layer_list.currentText()
945+
except (KeyError, ValueError):
946+
logger.warning("Layer list is empty")
947+
return None
871948

872949
def layer_data(self):
873950
"""Returns the data of the layer selected in the dropdown menu."""
874951
if self.layer_list.count() < 1:
875952
logger.debug("Layer list is empty")
876953
return None
877-
878-
return self.layer().data
954+
try:
955+
return self.layer().data
956+
except (KeyError, ValueError):
957+
msg = f"Layer {self.layer_name()} has no data. Layer might have been renamed or removed."
958+
logger.warning(msg)
959+
warn(msg, stacklevel=1)
960+
return None
879961

880962

881963
class FilePathWidget(QWidget): # TODO include load as folder

napari_cellseg3d/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
LOGGER = logging.getLogger(__name__)
1616
###############
1717
# Global logging level setting
18+
# SET TO INFO FOR RELEASE
1819
# LOGGER.setLevel(logging.DEBUG)
1920
LOGGER.setLevel(logging.INFO)
2021
###############

0 commit comments

Comments
 (0)