diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b69f5ab36..c0f061c80 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -67,7 +67,7 @@ jobs: python_version: >- ["3.9"] os: >- - ["windows-2019", "ubuntu-20.04", "macos-11"] + ["windows-2019", "ubuntu-20.04", "macos-12"] test_coverage: needs: download_data diff --git a/changelog.md b/changelog.md index a5f2f682e..82196f336 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 0.14.6 - 2022-11-13 + +### Bug Fixes + +- Fix bug when loading already created project causing hide of ROI layer (#787) + +### Features + +- Improve error message if segmentation do not fit in ROI Mask (#788) + ## 0.14.5 - 2022-11-09 ### Bug Fixes diff --git a/package/PartSeg/common_gui/napari_image_view.py b/package/PartSeg/common_gui/napari_image_view.py index c3f4d18d5..6db93a501 100644 --- a/package/PartSeg/common_gui/napari_image_view.py +++ b/package/PartSeg/common_gui/napari_image_view.py @@ -17,7 +17,7 @@ from napari.utils.colormaps.colormap import ColormapInterpolationMode from nme import register_class from packaging.version import parse as parse_version -from qtpy.QtCore import QEvent, QObject, QPoint, Qt, QTimer, Signal +from qtpy.QtCore import QEvent, QPoint, Qt, QTimer, Signal, Slot from qtpy.QtWidgets import QApplication, QCheckBox, QHBoxLayout, QLabel, QMenu, QSpinBox, QToolTip, QVBoxLayout, QWidget from scipy.ndimage import binary_dilation from superqt import QEnumComboBox, ensure_main_thread @@ -582,36 +582,44 @@ def calculate_filter(array: np.ndarray, parameters: Tuple[NoiseFilterType, float return bilateral(array, parameters[1]) return median(array, int(parameters[1])) - def _remove_worker(self, sender): + def _remove_worker(self, sender=None): + if sender is None: + sender = self.sender() for worker in self.worker_list: if hasattr(worker, "signals") and sender is worker.signals: self.worker_list.remove(worker) break else: - logging.debug(f"[_remove_worker] {sender}") + logging.debug("[_remove_worker] %s", sender) def _add_layer_util(self, index, layer, filters): if layer not in self.viewer.layers: self.viewer.add_layer(layer) - set_data_obj = _SetData(self) - - @thread_worker(connect={"returned": set_data_obj.set_data}) - def calc_filter(j, layer_): - if filters[j][0] == NoiseFilterType.No or filters[j][1] == 0: - return None, layer_ - return self.calculate_filter(layer_.data, parameters=filters[j]), layer_ + if filters[index][0] == NoiseFilterType.No or filters[index][1] == 0: + return - set_data_obj.worker = calc_filter(index, layer) + worker = calc_layer_filter(layer, filters[index][0], filters[index][1]) + worker.returned.connect(self._add_layer_util_end) + worker.finished.connect(self._remove_worker) + self.worker_list.append(worker) + worker.start() - self.worker_list.append(set_data_obj) + @Slot(object) + def _add_layer_util_end(self, val): + data_, layer_ = val + if data_ is not None: + layer_.data = data_ def _add_image(self, image_data: Tuple[ImageInfo, bool]): - self._remove_worker(self.sender()) - - image_info, _replace = image_data + image_info, replace = image_data image = image_info.image + if replace: + for layer in list(reversed(self.viewer.layers)): + self.viewer.layers.remove(layer) + QApplication.instance().processEvents() + filters = self.channel_control.get_filter() for i, layer in enumerate(image_info.layers): try: @@ -658,11 +666,6 @@ def add_image(self, image: Optional[Image], replace=False): if image.file_path in self.image_info: raise ValueError("Image already added") - if replace: - for layer in list(reversed(self.viewer.layers)): - self.viewer.layers.remove(layer) - QApplication.instance().processEvents() - self.image_info[image.file_path] = ImageInfo(image, []) channels = image.channels @@ -687,6 +690,7 @@ def add_image(self, image: Optional[Image], replace=False): def _prepare_layers(self, image, parameters, replace): worker = prepare_layers(image, parameters, replace) worker.returned.connect(self._add_image) + worker.finished.connect(self._remove_worker) self.worker_list.append(worker) worker.start() @@ -1012,6 +1016,15 @@ def _prepare_layers(image: Image, param: ImageParameters, replace: bool) -> Tupl prepare_layers = thread_worker(_prepare_layers) +def _calc_layer_filter(layer: NapariImage, filter_type: NoiseFilterType, radius: float): + if filter_type == NoiseFilterType.No or radius == 0: + return None, layer + return ImageView.calculate_filter(layer.data, parameters=(filter_type, radius)), layer + + +calc_layer_filter = thread_worker(_calc_layer_filter) + + def _print_dict(dkt: MutableMapping, indent="") -> str: if not isinstance(dkt, MutableMapping): logging.error(f"{type(dkt)} instead of dict passed to _print_dict") @@ -1023,22 +1036,3 @@ def _print_dict(dkt: MutableMapping, indent="") -> str: else: res.append(f"{indent}{k}: {v}") return "\n".join(res) - - -class _SetData(QObject): - def __init__(self, viewer: ImageView): - super().__init__() - self.viewer = viewer - self.worker = None - - def set_data(self, val): - self._set_data(val) - self.viewer.worker_list.remove(self) - - def _set_data(self, val): - data_, layer_ = val - if data_ is None: - return - if layer_ not in self.viewer.viewer.layers: - return - layer_.data = data_ diff --git a/package/tests/test_PartSeg/conftest.py b/package/tests/test_PartSeg/conftest.py index ca8e0bedd..3cf9ed438 100644 --- a/package/tests/test_PartSeg/conftest.py +++ b/package/tests/test_PartSeg/conftest.py @@ -46,15 +46,18 @@ def part_settings_with_project(image, analysis_segmentation2, tmp_path): @pytest.fixture(autouse=True) -def disable_threads_viewer(monkeypatch, request): - if "no_viewer_patch" in request.keywords: - return - +def disable_threads_viewer_patch_prepare_leyers(monkeypatch): def _prepare_layers(self, image, parameters, replace): self._add_image(napari_image_view._prepare_layers(image, parameters, replace)) monkeypatch.setattr(napari_image_view.ImageView, "_prepare_layers", _prepare_layers) + +@pytest.fixture(autouse=True) +def disable_threads_viewer_patch_add_layer(monkeypatch, request): + if "no_patch_add_layer" in request.keywords: + return + def _add_layer_util(self, index, layer, filters): if layer not in self.viewer.layers: self.viewer.add_layer(layer) @@ -154,7 +157,7 @@ def __init__(self, *args, **kwargs): self._call_list = [] def setTimeout(self, *args, **kwargs): - pass + pass # as it is dummy throttler then timeout is obsolete. def throttle(self, *args, **kwargs): for cl in self._call_list: diff --git a/package/tests/test_PartSeg/test_napari_image_view.py b/package/tests/test_PartSeg/test_napari_image_view.py index dae1d7bfe..863e36fca 100644 --- a/package/tests/test_PartSeg/test_napari_image_view.py +++ b/package/tests/test_PartSeg/test_napari_image_view.py @@ -1,7 +1,6 @@ # pylint: disable=R0201 import gc import platform -import sys from functools import partial from unittest.mock import MagicMock @@ -21,6 +20,7 @@ QMenu, SearchComponentModal, SearchType, + _calc_layer_filter, _print_dict, ) from PartSegCore.image_operations import NoiseFilterType @@ -296,8 +296,23 @@ def test_roi_removed_add_image_restore(self, base_settings, image_view): assert "ROI" in image_view.viewer.layers assert "Mask" in image_view.viewer.layers - @pytest.mark.no_viewer_patch - @pytest.mark.skipif(sys.platform == "darwin", reason="fails on macos because of SIGILL") + @pytest.mark.parametrize("filter_type", NoiseFilterType.__members__.values()) + @pytest.mark.parametrize("radius", [1, 2, 3.5]) + def test_calculate_filter(self, filter_type, radius, image): + ch = image.get_channel(0) + filtered = ImageView.calculate_filter(ch, (filter_type, radius)) + assert filtered.shape == ch.shape + assert (filter_type == NoiseFilterType.No) != (filtered is not ch) + + @pytest.mark.no_patch_add_layer + @pytest.mark.enablethread + def test_add_layer_util_check_init(self, base_settings, image_view, qtbot): + def has_layers(): + return len(image_view.viewer.layers) > 0 + + qtbot.waitUntil(has_layers) + + @pytest.mark.no_patch_add_layer def test_add_layer_util(self, base_settings, image_view, qtbot): def has_layers(): return len(image_view.viewer.layers) > 0 @@ -305,10 +320,12 @@ def has_layers(): qtbot.waitUntil(has_layers) layer = image_view.viewer.layers[0] + del image_view.viewer.layers[layer.name] assert isinstance(layer, NapariImage) + layer.visible = False def no_worker(): - return not image_view.worker_list + return len(image_view.worker_list) == 0 prev_data = layer.data @@ -317,10 +334,13 @@ def no_worker(): qtbot.waitUntil(no_worker) assert layer.data is prev_data + del image_view.viewer.layers[layer.name] + image_view._add_layer_util(0, layer, [(NoiseFilterType.Median, 1)]) qtbot.waitUntil(no_worker) assert layer.data is not prev_data + qtbot.wait(50) def test_search_component_modal(qtbot, image_view, monkeypatch): @@ -340,3 +360,14 @@ def test_search_component_modal(qtbot, image_view, monkeypatch): image_view.component_zoom.assert_called_with(1) modal.close() image_view.component_unmark.assert_called_once() + + +def test_calc_layer_filter(): + layer = NapariImage(np.zeros((10, 10)), name="test") + + assert _calc_layer_filter(layer, NoiseFilterType.No, 0)[0] is None + assert _calc_layer_filter(layer, NoiseFilterType.No, 1)[0] is None + assert _calc_layer_filter(layer, NoiseFilterType.Gauss, 0)[0] is None + + assert _calc_layer_filter(layer, NoiseFilterType.Gauss, 1)[0] is not None + assert _calc_layer_filter(layer, NoiseFilterType.Gauss, 1)[0] is not layer.data diff --git a/package/tests/test_PartSegCore/test_measurements.py b/package/tests/test_PartSegCore/test_measurements.py index b7e3b1249..faa4430f9 100644 --- a/package/tests/test_PartSegCore/test_measurements.py +++ b/package/tests/test_PartSegCore/test_measurements.py @@ -2334,7 +2334,7 @@ def test_per_component(method, area): ) assert len(result["Measurement per component"][0]) == 1 assert isinstance(result["Measurement"][0], (float, int)) - assert result["Measurement per component"][0][0] == result["Measurement"][0] + assert isclose(result["Measurement per component"][0][0], result["Measurement"][0]) @pytest.mark.parametrize("method", CorrelationEnum.__members__.values()) diff --git a/setup.cfg b/setup.cfg index 9859a0d20..7f2771562 100644 --- a/setup.cfg +++ b/setup.cfg @@ -145,7 +145,7 @@ filterwarnings = markers = enablethread: Allow to use thread in test enabledialog: Allow to use dialog in test - no_viewer_patch: Do not patch napari viewer + no_patch_add_layer: Do not patch napari viewer [coverage:paths] source =