diff --git a/pyproject.toml b/pyproject.toml index 2d27d5b..0a9b6b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ write_to = "src/arcospx/_version.py" line-length = 79 target-version = ['py38', 'py39', 'py310'] - [tool.ruff] line-length = 79 select = [ diff --git a/setup.cfg b/setup.cfg index 3ec74da..c38b25f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = arcosPx-napari - +version = 0.0.1 description = A plugin to track spatio-temporal correlations in images long_description = file: README.md long_description_content_type = text/markdown @@ -34,6 +34,7 @@ install_requires = numpy magicgui qtpy + arcos4py python_requires = >=3.8 include_package_data = True diff --git a/src/arcospx/__init__.py b/src/arcospx/__init__.py index e692002..b2512f1 100644 --- a/src/arcospx/__init__.py +++ b/src/arcospx/__init__.py @@ -3,10 +3,10 @@ except ImportError: __version__ = "unknown" from ._sample_data import make_sample_data -from ._widget import ExampleQWidget, example_magic_widget +from ._widget import remove_background, track_events __all__ = ( "make_sample_data", - "ExampleQWidget", - "example_magic_widget", + "remove_background", + "track_events" ) diff --git a/src/arcospx/_sample_data.py b/src/arcospx/_sample_data.py index 453db51..ac3800f 100644 --- a/src/arcospx/_sample_data.py +++ b/src/arcospx/_sample_data.py @@ -6,6 +6,7 @@ Replace code below according to your needs. """ + from __future__ import annotations import numpy @@ -16,6 +17,16 @@ def make_sample_data(): # Return list of tuples # [(data1, add_image_kwargs1), (data2, add_image_kwargs2)] # Check the documentation for more information about the - # add_image_kwargs + # add_image_kwargs: # https://napari.org/stable/api/napari.Viewer.html#napari.Viewer.add_image - return [(numpy.random.rand(512, 512), {})] + image_kwargs = { + "rgb": False, + "colormap": "gray", + "contrast_limits": [0, 255], + "visible": True, + "gamma": 1, + "interpolation2d":"nearest", + "name": "sample_data", + } + return [(numpy.random.rand(512, 512), image_kwargs)] + diff --git a/src/arcospx/_tests/test_data/1_growing.tif b/src/arcospx/_tests/test_data/1_growing.tif new file mode 100644 index 0000000..d4c48e4 Binary files /dev/null and b/src/arcospx/_tests/test_data/1_growing.tif differ diff --git a/src/arcospx/_tests/test_data/1_growing_true.tif b/src/arcospx/_tests/test_data/1_growing_true.tif new file mode 100644 index 0000000..5b8a11f Binary files /dev/null and b/src/arcospx/_tests/test_data/1_growing_true.tif differ diff --git a/src/arcospx/_tests/test_data/4_colliding.tif b/src/arcospx/_tests/test_data/4_colliding.tif new file mode 100644 index 0000000..1ad828d Binary files /dev/null and b/src/arcospx/_tests/test_data/4_colliding.tif differ diff --git a/src/arcospx/_tests/test_data/test_data_track_events.tif b/src/arcospx/_tests/test_data/test_data_track_events.tif new file mode 100644 index 0000000..0f6a125 Binary files /dev/null and b/src/arcospx/_tests/test_data/test_data_track_events.tif differ diff --git a/src/arcospx/_tests/test_data/test_data_track_events_true.tif b/src/arcospx/_tests/test_data/test_data_track_events_true.tif new file mode 100644 index 0000000..6fee558 Binary files /dev/null and b/src/arcospx/_tests/test_data/test_data_track_events_true.tif differ diff --git a/src/arcospx/_tests/test_sample_data.py b/src/arcospx/_tests/test_sample_data.py index 3982ea2..535e2c3 100644 --- a/src/arcospx/_tests/test_sample_data.py +++ b/src/arcospx/_tests/test_sample_data.py @@ -1,7 +1,19 @@ -# from arcospx import make_sample_data +import pytest +from arcospx import make_sample_data +import numpy as np +from qtpy import QtCore -# add your tests here... +def test_make_sample_data(): + """Test make_sample_data.""" + data = make_sample_data() + + # Check the shape of the numpy array + assert data[0][0].shape == (512, 512) + + # Check if the first element of the tuple is a numpy array + assert isinstance(data[0][0], np.ndarray) + + # Check if the second element of the tuple is a dictionary + assert isinstance(data[0][1], dict) -def test_something(): - pass diff --git a/src/arcospx/_tests/test_widget.py b/src/arcospx/_tests/test_widget.py index 4333b9f..282bf23 100644 --- a/src/arcospx/_tests/test_widget.py +++ b/src/arcospx/_tests/test_widget.py @@ -1,36 +1,53 @@ import numpy as np - -from arcospx import ExampleQWidget, example_magic_widget - - -# make_napari_viewer is a pytest fixture that returns a napari viewer object -# capsys is a pytest fixture that captures stdout and stderr output streams -def test_example_q_widget(make_napari_viewer, capsys): - # make viewer and add an image layer using our fixture +import pytest +from qtpy import QtCore +from arcospx import make_sample_data, remove_background, track_events +import napari +from skimage.io import imread +from numpy.testing import assert_array_equal +from qtpy.QtCore import QTimer +from napari.layers.image import Image +from arcos4py.tools import remove_image_background, track_events_image +from pytestqt import qtbot + +def test_remove_background(make_napari_viewer, qtbot): + """ + Test background removal on a simple image. + """ viewer = make_napari_viewer() - viewer.add_image(np.random.random((100, 100))) - - # create our widget, passing in the viewer - my_widget = ExampleQWidget(viewer) - - # call our widget method - my_widget._on_click() - - # read captured output and check that it's as we expected - captured = capsys.readouterr() - assert captured.out == "napari has 1 layers\n" - - -def test_example_magic_widget(make_napari_viewer, capsys): + test_img = imread('test_data/1_growing.tif') + viewer.add_image(test_img, name='test_img') + true_img = imread('test_data/1_growing_true.tif') + _,widget = viewer.window.add_plugin_dock_widget("arcosPx-napari", "Remove Background") + widget.image.value = viewer.layers['test_img'] + widget.filter_type.value = "gaussian" + widget.size_0.value = 1 + widget.size_1.value = 1 + widget.size_2.value = 1 + worker = widget() + with qtbot.waitSignal(worker.finished, timeout=10000): + pass + assert_array_equal(viewer.layers[1].data, true_img) + + +def test_track_events(make_napari_viewer, qtbot): + """ + Test tracking on a simple image. + """ viewer = make_napari_viewer() - layer = viewer.add_image(np.random.random((100, 100))) - - # this time, our widget will be a MagicFactory or FunctionGui instance - my_widget = example_magic_widget() - - # if we "call" this object, it'll execute our function - my_widget(viewer.layers[0]) + test_img = imread('test_data/test_data_track_events.tif') + viewer.add_image(test_img, name='test_img') + true_img = imread('test_data/test_track_events_true.tif') + _, widget = viewer.window.add_plugin_dock_widget("arcosPx-napari", "Track Events") + widget.image_selector.value = viewer.layers['test_img'] + widget.threshold.value = 300 + widget.eps.value = 10 + widget.epsPrev.value = 50 + widget.minClSz.value = 50 + widget.minSamples.value = 2 + widget.nPrev.value = 2 + worker = widget() + with qtbot.waitSignal(worker.finished, timeout=10000): + pass + assert_array_equal(viewer.layers[1].data, true_img) - # read captured output and check that it's as we expected - captured = capsys.readouterr() - assert captured.out == f"you have selected {layer}\n" diff --git a/src/arcospx/_widget.py b/src/arcospx/_widget.py index fe7e1e7..c1a51bb 100644 --- a/src/arcospx/_widget.py +++ b/src/arcospx/_widget.py @@ -8,39 +8,131 @@ """ from typing import TYPE_CHECKING -from magicgui import magic_factory +from arcos4py.tools import remove_image_background, track_events_image +from magicgui import magic_factory, widgets +from napari import viewer from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget +from napari.types import LayerDataTuple +from napari.layers import Image +from napari.qt.threading import thread_worker, FunctionWorker +from napari.utils import progress +from qtpy.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel + +from magicgui import magicgui + if TYPE_CHECKING: import napari +# A global flag for aborting the process +abort_flag = False + +# Optional: Define a custom exception for aborting the process +class AbortException(Exception): + pass + +@magic_factory() +def remove_background( + image: Image, + filter_type: str = "gaussian", + size_0: int = 20, + size_1: int = 5, + size_2: int = 5, + dims: str = "TXY", + crop_time_axis: bool = False + ) -> FunctionWorker[LayerDataTuple]: + size = (size_0, size_1, size_2) + pbar = progress(total=0) + @thread_worker(connect={'returned': pbar.close}) + def remove_image_background_2() -> LayerDataTuple: + global abort_flag + + # Reset the abort flag at the start of each execution + abort_flag = False + + removed_background = remove_image_background(image.data, filter_type, size, dims, crop_time_axis) + + if abort_flag: + pbar.close() + raise AbortException("Operation aborted by user.") + + layer_properties = { + "name": f"{image.name} background removed", + "metadata": { + "filter_type": filter_type, + "size_0": size_0, + "size_1": size_1, + "size_2": size_2, + "dims": dims, + "crop_time_axis": crop_time_axis, + "filename": image.name,}} + + + return (removed_background, layer_properties, "image") + + return remove_image_background_2() + + +@magic_factory() +def track_events( + image_selector: Image, + threshold: int = 300, + eps: int = 10, + epsPrev: int = 50, + minClSz: int = 50, + minSamples: int = 2, + nPrev: int = 2, + dims: str = "TXY", +) -> FunctionWorker[LayerDataTuple]: + t_filter_size = 20 + pbar = progress(total=0) + + @thread_worker(connect={'returned': pbar.close}) + def track_events_2() -> LayerDataTuple: + global abort_flag + + # Reset the abort flag at the start of each execution + abort_flag = False + + if abort_flag: + # Return an error message + # return "Interrupt error: Operation aborted by user." + # Or raise a custom exception + pbar.close() + raise AbortException("Operation aborted by user.") -class ExampleQWidget(QWidget): - # your QWidget.__init__ can optionally request the napari viewer instance - # in one of two ways: - # 1. use a parameter called `napari_viewer`, as done here - # 2. use a type annotation of 'napari.viewer.Viewer' for any parameter - def __init__(self, napari_viewer): - super().__init__() - self.viewer = napari_viewer + selected_image = image_selector.data + img_tracked = track_events_image(selected_image >= threshold, eps = eps, epsPrev = epsPrev, minClSz = minClSz, minSamples = minSamples, nPrev = nPrev, dims = dims) - btn = QPushButton("Click me!") - btn.clicked.connect(self._on_click) + if abort_flag: + # Return an error message + # return "Interrupt error: Operation aborted by user." + # Or raise a custom exception + pbar.close() + raise AbortException("Operation aborted by user.") - self.setLayout(QHBoxLayout()) - self.layout().addWidget(btn) + # Like this we create the layer as a layer-data-tuple object which will automatically be parsed by napari and added to the viewer + # This is more flexible and does not require the function to know about the viewer directly + # Additionally like this you can now set the metadata of the layer + layer_properties = { + "name": f"{image_selector.name} tracked", + "metadata": { + "threshold": threshold, + "eps": eps, + "epsPrev": epsPrev, + "minClSz": minClSz, + "nPrev": nPrev, + "filename": image_selector.name, ## eg. setting medatada to the name of the image layer + },} + return (img_tracked, layer_properties, "labels") - def _on_click(self): - print("napari has", len(self.viewer.layers), "layers") + # return the layer data tuple + return track_events_2() -@magic_factory -def example_magic_widget(img_layer: "napari.layers.Image"): - print(f"you have selected {img_layer}") +@magic_factory() +def abort_process(): + global abort_flag + abort_flag = True -# Uses the `autogenerate: true` flag in the plugin manifest -# to indicate it should be wrapped as a magicgui to autogenerate -# a widget. -def example_function_widget(img_layer: "napari.layers.Image"): - print(f"you have selected {img_layer}") diff --git a/src/arcospx/napari.yaml b/src/arcospx/napari.yaml index b5097f2..622eb2e 100644 --- a/src/arcospx/napari.yaml +++ b/src/arcospx/napari.yaml @@ -6,27 +6,23 @@ visibility: public categories: ["Annotation", "Segmentation", "Acquisition"] contributions: commands: - - id: arcosPx-napari.make_sample_data - python_name: arcospx._sample_data:make_sample_data - title: Load sample data from arcosPx - - id: arcosPx-napari.make_qwidget - python_name: arcospx._widget:ExampleQWidget - title: Make example QWidget - - id: arcosPx-napari.make_magic_widget - python_name: arcospx._widget:example_magic_widget - title: Make example magic widget - - id: arcosPx-napari.make_func_widget - python_name: arcospx._widget:example_function_widget - title: Make example function widget + - id: arcosPx-napari.remove_background + python_name: arcospx._widget:remove_background + title: Remove Background + - id: arcosPx-napari.track_events + python_name: arcospx._widget:track_events + title: Track Events + - id: arcosPx-napari.abort_process + python_name: arcospx._widget:abort_process + title: Abort Process sample_data: - command: arcosPx-napari.make_sample_data display_name: arcosPx key: unique_id.1 widgets: - - command: arcosPx-napari.make_qwidget - display_name: Example QWidget - - command: arcosPx-napari.make_magic_widget - display_name: Example Magic Widget - - command: arcosPx-napari.make_func_widget - autogenerate: true - display_name: Example Function Widget + - command: arcosPx-napari.remove_background + display_name: Remove Background + - command: arcosPx-napari.track_events + display_name: Track Events + - command: arcosPx-napari.abort_process + display_name: Abort Current Process