From 66781ad3329a16c42a6c5701b1cfab0836012f93 Mon Sep 17 00:00:00 2001 From: Tony Tung Date: Thu, 21 Nov 2019 17:30:22 -0800 Subject: [PATCH] Filters for mask collections This PR introduces filters to take a binary mask collection to produce another binary mask collection. A map filter is added that can take arbitrary methods in numpy/scipy/skimage and run them against the binary masks. Depends on #1655 Test plan: Added a simple test that does binary dilation on binary masks. --- starfish/core/morphology/Filter/__init__.py | 12 +++ starfish/core/morphology/Filter/_base.py | 17 ++++ starfish/core/morphology/Filter/map.py | 82 +++++++++++++++++++ .../core/morphology/Filter/test/__init__.py | 0 .../core/morphology/Filter/test/test_map.py | 33 ++++++++ .../morphology/binary_mask/binary_mask.py | 4 + starfish/core/types/_functionsource.py | 1 + starfish/morphology.py | 1 + 8 files changed, 150 insertions(+) create mode 100644 starfish/core/morphology/Filter/__init__.py create mode 100644 starfish/core/morphology/Filter/_base.py create mode 100644 starfish/core/morphology/Filter/map.py create mode 100644 starfish/core/morphology/Filter/test/__init__.py create mode 100644 starfish/core/morphology/Filter/test/test_map.py create mode 100644 starfish/morphology.py diff --git a/starfish/core/morphology/Filter/__init__.py b/starfish/core/morphology/Filter/__init__.py new file mode 100644 index 000000000..26d476d94 --- /dev/null +++ b/starfish/core/morphology/Filter/__init__.py @@ -0,0 +1,12 @@ +"""Algorithms in this module filter a BinaryMaskCollection, producing another +BinaryMaskCollection.""" +from ._base import FilterAlgorithm +from .map import Map + +# autodoc's automodule directive only captures the modules explicitly listed in __all__. +all_filters = { + filter_name: filter_cls + for filter_name, filter_cls in locals().items() + if isinstance(filter_cls, type) and issubclass(filter_cls, FilterAlgorithm) +} +__all__ = list(all_filters.keys()) diff --git a/starfish/core/morphology/Filter/_base.py b/starfish/core/morphology/Filter/_base.py new file mode 100644 index 000000000..056b5f0a5 --- /dev/null +++ b/starfish/core/morphology/Filter/_base.py @@ -0,0 +1,17 @@ +from abc import abstractmethod + +from starfish.core.morphology.binary_mask import BinaryMaskCollection +from starfish.core.pipeline.algorithmbase import AlgorithmBase + + +class FilterAlgorithm(metaclass=AlgorithmBase): + + @abstractmethod + def run( + self, + binary_mask_collection: BinaryMaskCollection, + *args, + **kwargs + ) -> BinaryMaskCollection: + """Performs a filter on the binary mask collection provided.""" + raise NotImplementedError() diff --git a/starfish/core/morphology/Filter/map.py b/starfish/core/morphology/Filter/map.py new file mode 100644 index 000000000..08d1ecefb --- /dev/null +++ b/starfish/core/morphology/Filter/map.py @@ -0,0 +1,82 @@ +from typing import Callable, Optional + +from starfish.core.morphology.binary_mask import BinaryMaskCollection +from starfish.core.types import FunctionSource +from ._base import FilterAlgorithm + + +class Map(FilterAlgorithm): + """ + Map from input to output by applying a specified function to the input. The output must have + the same shape as the input. + + Parameters + ---------- + func : str + Name of a function in the module specified by the ``module`` parameter to apply across the + dimension(s) specified by dims. The function is resolved by ``getattr(, func)``, + except in the cases of predefined aliases. See :py:class:`FunctionSource` for more + information about aliases. + module : FunctionSource + Python module that serves as the source of the function. It must be listed as one of the + members of :py:class:`FunctionSource`. + + Currently, the supported FunctionSources are: + - ``np``: the top-level package of numpy + - ``scipy``: the top-level package of scipy + + Examples + -------- + Applying a binary opening function. + >>> from starfish.core.morphology.object.binary_mask.test import factories + >>> from starfish.morphology import Filter + >>> from skimage.morphology import disk + >>> binary_mask_collection = factories.binary_mask_collection_2d() + >>> opener = Filter.Map("morphology.binary_opening", disk(4)) + >>> opened = opener.run(binary_mask_collection) + + See Also + -------- + starfish.core.types.Axes + + """ + + def __init__( + self, + func: str, + *func_args, + module: FunctionSource = FunctionSource.np, + **func_kwargs, + ) -> None: + self._func: Callable = module._resolve_method(func) + self._func_args = func_args + self._func_kwargs = func_kwargs + + def run( + self, + binary_mask_collection: BinaryMaskCollection, + n_processes: Optional[int] = None, + *args, + **kwargs + ) -> BinaryMaskCollection: + """Map from input to output by applying a specified function to the input. + + Parameters + ---------- + binary_mask_collection : BinaryMaskCollection + BinaryMaskCollection to be filtered. + n_processes : Optional[int] + The number of processes to use for apply. If None, uses the output of os.cpu_count() + (default = None). + + Returns + ------- + BinaryMaskCollection + Return the results of filter as a new BinaryMaskCollection. + """ + + # Apply the reducing function + return binary_mask_collection._apply( + self._func, + *self._func_args, + **self._func_kwargs) diff --git a/starfish/core/morphology/Filter/test/__init__.py b/starfish/core/morphology/Filter/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/starfish/core/morphology/Filter/test/test_map.py b/starfish/core/morphology/Filter/test/test_map.py new file mode 100644 index 000000000..a98fc9b5a --- /dev/null +++ b/starfish/core/morphology/Filter/test/test_map.py @@ -0,0 +1,33 @@ +import numpy as np + +from starfish.core.morphology.binary_mask.test.factories import binary_mask_collection_2d +from starfish.core.types import Axes, FunctionSource +from ..map import Map + + +def test_apply(): + input_mask_collection = binary_mask_collection_2d() + filt = Map("morphology.binary_dilation", module=FunctionSource.skimage) + output_mask_collection = filt.run(input_mask_collection) + + assert input_mask_collection._pixel_ticks == output_mask_collection._pixel_ticks + assert input_mask_collection._physical_ticks == output_mask_collection._physical_ticks + assert input_mask_collection._log == output_mask_collection._log + assert len(input_mask_collection) == len(output_mask_collection) + + region_0, region_1 = output_mask_collection.masks() + + assert region_0.name == '0' + assert region_1.name == '1' + + temp = np.ones((2, 6), dtype=np.bool) + assert np.array_equal(region_0, temp) + temp = np.ones((3, 4), dtype=np.bool) + temp[0, 0] = 0 + assert np.array_equal(region_1, temp) + + assert np.array_equal(region_0[Axes.Y.value], [0, 1]) + assert np.array_equal(region_0[Axes.X.value], [0, 1, 2, 3, 4, 5]) + + assert np.array_equal(region_1[Axes.Y.value], [2, 3, 4]) + assert np.array_equal(region_1[Axes.X.value], [2, 3, 4, 5]) diff --git a/starfish/core/morphology/binary_mask/binary_mask.py b/starfish/core/morphology/binary_mask/binary_mask.py index f6406787a..840e629cf 100644 --- a/starfish/core/morphology/binary_mask/binary_mask.py +++ b/starfish/core/morphology/binary_mask/binary_mask.py @@ -217,6 +217,10 @@ def max_shape(self) -> Mapping[Axes, int]: for ix, (axis, _) in enumerate(zip(*_get_axes_names(len(self._pixel_ticks)))) } + @property + def log(self) -> Log: + return self._log + @classmethod def from_label_image(cls, label_image: LabelImage) -> "BinaryMaskCollection": """Creates binary masks from a label image. Masks are cropped to the smallest size that diff --git a/starfish/core/types/_functionsource.py b/starfish/core/types/_functionsource.py index f2cddee33..7ccfacb41 100644 --- a/starfish/core/types/_functionsource.py +++ b/starfish/core/types/_functionsource.py @@ -69,3 +69,4 @@ def _resolve_method(self, method: str) -> Callable: np = ("numpy", {'max': 'amax'}) """Function source for the numpy libraries""" scipy = ("scipy",) + skimage = ("skimage",) diff --git a/starfish/morphology.py b/starfish/morphology.py new file mode 100644 index 000000000..66509e45a --- /dev/null +++ b/starfish/morphology.py @@ -0,0 +1 @@ +from starfish.core.morphology import Filter # noqa: F401