Skip to content

Commit

Permalink
fix: Fix bug when loading already created project causing hide of ROI…
Browse files Browse the repository at this point in the history
… layer (#787)
  • Loading branch information
Czaki authored Nov 13, 2022
1 parent 426b918 commit 994088d
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
72 changes: 33 additions & 39 deletions package/PartSeg/common_gui/napari_image_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -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")
Expand All @@ -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_
13 changes: 8 additions & 5 deletions package/tests/test_PartSeg/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
39 changes: 35 additions & 4 deletions package/tests/test_PartSeg/test_napari_image_view.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# pylint: disable=R0201
import gc
import platform
import sys
from functools import partial
from unittest.mock import MagicMock

Expand All @@ -21,6 +20,7 @@
QMenu,
SearchComponentModal,
SearchType,
_calc_layer_filter,
_print_dict,
)
from PartSegCore.image_operations import NoiseFilterType
Expand Down Expand Up @@ -296,19 +296,36 @@ 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

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

Expand All @@ -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):
Expand All @@ -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
2 changes: 1 addition & 1 deletion package/tests/test_PartSegCore/test_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 994088d

Please sign in to comment.