Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix bug when loading already created project causing hide of ROI layer #787

Merged
merged 28 commits into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -45,15 +45,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 @@ -153,7 +156,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 @@ -144,7 +144,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