From 65ac81562ca72c31151287bc88bdeebdaf57c8fa Mon Sep 17 00:00:00 2001 From: Tony Tung Date: Mon, 16 Dec 2019 09:44:51 -0800 Subject: [PATCH] Pipeline component and implementation for merging BinaryMaskCollections This does not handle the case where the pixel/physical ticks do not line up. This is for cases where we derive the data from the exact same set of images, and inherit their pixel/physical ticks from the same source. Test plan: Added one positive test case, and tested the cases where the merge should fail. --- docs/source/api/morphology/index.rst | 18 +++- starfish/core/morphology/Merge/__init__.py | 10 +++ starfish/core/morphology/Merge/_base.py | 18 ++++ starfish/core/morphology/Merge/simple.py | 48 +++++++++++ .../core/morphology/Merge/test/__init__.py | 0 .../core/morphology/Merge/test/test_simple.py | 82 +++++++++++++++++++ starfish/morphology.py | 1 + 7 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 starfish/core/morphology/Merge/__init__.py create mode 100644 starfish/core/morphology/Merge/_base.py create mode 100644 starfish/core/morphology/Merge/simple.py create mode 100644 starfish/core/morphology/Merge/test/__init__.py create mode 100644 starfish/core/morphology/Merge/test/test_simple.py diff --git a/docs/source/api/morphology/index.rst b/docs/source/api/morphology/index.rst index 980f93617..00387a207 100644 --- a/docs/source/api/morphology/index.rst +++ b/docs/source/api/morphology/index.rst @@ -4,8 +4,8 @@ Morphology Transformations ========================== starfish provides a variety of methods to perform transformations on morphological data. These include :py:class:`~starfish.morphology.Binarize`, which transform image data into morphological data and -:py:class:`~starfish.morphology.Filter`, which performs filtering operations on morphological data. - +:py:class:`~starfish.morphology.Filter`, which performs filtering operations on morphological data, and +:py:class:`~starfish.morphology.Merge`, which combines different sets of morphological data. .. _binarize: @@ -34,3 +34,17 @@ Filtering operations can be imported using ``starfish.morphology.Filter``, which .. automodule:: starfish.morphology.Filter :members: + +.. _merge: + +Merge +----- + +Filtering operations can be imported using ``starfish.morphology.Merge``, which registers all classes that subclass :py:class:`~starfish.morphology.Merge.MergeAlgorithm`: + +.. code-block:: python + + from starfish.morphology import Merge + +.. automodule:: starfish.morphology.Merge + :members: diff --git a/starfish/core/morphology/Merge/__init__.py b/starfish/core/morphology/Merge/__init__.py new file mode 100644 index 000000000..e957fa73e --- /dev/null +++ b/starfish/core/morphology/Merge/__init__.py @@ -0,0 +1,10 @@ +"""Algorithms in this module merge multiple BinaryMaskCollections together.""" +from ._base import MergeAlgorithm +from .simple import SimpleMerge + +# autodoc's automodule directive only captures the modules explicitly listed in __all__. +__all__ = list(set( + implementation_name + for implementation_name, implementation_cls in locals().items() + if isinstance(implementation_cls, type) and issubclass(implementation_cls, MergeAlgorithm) +)) diff --git a/starfish/core/morphology/Merge/_base.py b/starfish/core/morphology/Merge/_base.py new file mode 100644 index 000000000..dc125c42e --- /dev/null +++ b/starfish/core/morphology/Merge/_base.py @@ -0,0 +1,18 @@ +from abc import abstractmethod +from typing import Sequence + +from starfish.core.morphology.binary_mask import BinaryMaskCollection +from starfish.core.pipeline.algorithmbase import AlgorithmBase + + +class MergeAlgorithm(metaclass=AlgorithmBase): + + @abstractmethod + def run( + self, + binary_mask_collections: Sequence[BinaryMaskCollection], + *args, + **kwargs + ) -> BinaryMaskCollection: + """Merge multiple binary mask collections together.""" + raise NotImplementedError() diff --git a/starfish/core/morphology/Merge/simple.py b/starfish/core/morphology/Merge/simple.py new file mode 100644 index 000000000..125a1ed4e --- /dev/null +++ b/starfish/core/morphology/Merge/simple.py @@ -0,0 +1,48 @@ +from typing import Mapping, Optional, Sequence + +import numpy as np + +from starfish.core.morphology.binary_mask import BinaryMaskCollection +from starfish.core.morphology.util import _ticks_equal +from starfish.core.types import ArrayLike, Axes, Coordinates, Number +from ._base import MergeAlgorithm + + +class SimpleMerge(MergeAlgorithm): + def run( + self, + binary_mask_collections: Sequence[BinaryMaskCollection], + *args, + **kwargs + ) -> BinaryMaskCollection: + """Merge multiple binary mask collections together. This implementation requires that all + the binary mask collections have the same pixel and physical ticks.""" + pixel_ticks: Optional[Mapping[Axes, ArrayLike[int]]] = None + physical_ticks: Optional[Mapping[Coordinates, ArrayLike[Number]]] = None + + # validate that they have the same pixel/physical ticks. + for binary_mask_collection in binary_mask_collections: + pixel_ticks = pixel_ticks or binary_mask_collection._pixel_ticks + physical_ticks = physical_ticks or binary_mask_collection._physical_ticks + + if not _ticks_equal(pixel_ticks, binary_mask_collection._pixel_ticks): + raise ValueError("not all masks have the same pixel ticks") + if not _ticks_equal(physical_ticks, binary_mask_collection._physical_ticks): + raise ValueError("not all masks have the same physical ticks") + + # gather up all the uncropped masks. + all_uncropped_masks = [ + np.asarray(binary_mask_collection.uncropped_mask(ix)) + for binary_mask_collection in binary_mask_collections + for ix in range(len(binary_mask_collection)) + ] + + assert pixel_ticks is not None + assert physical_ticks is not None + + return BinaryMaskCollection.from_binary_arrays_and_ticks( + all_uncropped_masks, + pixel_ticks, + physical_ticks, + None, + ) diff --git a/starfish/core/morphology/Merge/test/__init__.py b/starfish/core/morphology/Merge/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/starfish/core/morphology/Merge/test/test_simple.py b/starfish/core/morphology/Merge/test/test_simple.py new file mode 100644 index 000000000..f5e8ab562 --- /dev/null +++ b/starfish/core/morphology/Merge/test/test_simple.py @@ -0,0 +1,82 @@ +import numpy as np +import pytest + +from starfish.core.morphology.binary_mask import BinaryMaskCollection +from starfish.core.morphology.binary_mask.test.factories import ( + binary_arrays_2d, + binary_mask_collection_2d, + binary_mask_collection_3d, +) +from starfish.core.morphology.util import _ticks_equal +from starfish.core.types import Axes, Coordinates +from ..simple import SimpleMerge + + +def test_success(): + mask_collection_0 = binary_mask_collection_2d() + binary_arrays, physical_ticks = binary_arrays_2d() + binary_arrays_negated = [ + np.bitwise_not(binary_array) + for binary_array in binary_arrays + ] + mask_collection_1 = BinaryMaskCollection.from_binary_arrays_and_ticks( + binary_arrays_negated, None, physical_ticks, None) + + merged = SimpleMerge().run([mask_collection_0, mask_collection_1]) + + assert _ticks_equal(merged._pixel_ticks, mask_collection_0._pixel_ticks) + assert _ticks_equal(merged._physical_ticks, mask_collection_0._physical_ticks) + assert len(mask_collection_0) + len(mask_collection_1) == len(merged) + + # go through all the original uncroppped masks, and verify that they are somewhere in the merged + # set. + for mask_collection in (mask_collection_0, mask_collection_1): + for ix in range(len(mask_collection)): + uncropped_original_mask = mask_collection.uncropped_mask(ix) + for jx in range(len(merged)): + uncropped_copy_mask = merged.uncropped_mask(jx) + + if uncropped_original_mask.equals(uncropped_copy_mask): + # found the copy, break + break + else: + pytest.fail("could not find mask in merged set.") + +def test_pixel_tick_mismatch(): + mask_collection_0 = binary_mask_collection_2d() + mask_collection_0._pixel_ticks[Axes.X.value] = np.asarray( + mask_collection_0._pixel_ticks[Axes.X.value]) + 1 + binary_arrays, physical_ticks = binary_arrays_2d() + binary_arrays_negated = [ + np.bitwise_not(binary_array) + for binary_array in binary_arrays + ] + mask_collection_1 = BinaryMaskCollection.from_binary_arrays_and_ticks( + binary_arrays_negated, None, physical_ticks, None) + + with pytest.raises(ValueError): + SimpleMerge().run([mask_collection_0, mask_collection_1]) + + +def test_physical_tick_mismatch(): + mask_collection_0 = binary_mask_collection_2d() + mask_collection_0._physical_ticks[Coordinates.X] = np.asarray( + mask_collection_0._physical_ticks[Coordinates.X]) + 1 + binary_arrays, physical_ticks = binary_arrays_2d() + binary_arrays_negated = [ + np.bitwise_not(binary_array) + for binary_array in binary_arrays + ] + mask_collection_1 = BinaryMaskCollection.from_binary_arrays_and_ticks( + binary_arrays_negated, None, physical_ticks, None) + + with pytest.raises(ValueError): + SimpleMerge().run([mask_collection_0, mask_collection_1]) + + +def test_shape_mismatch(): + mask_collection_0 = binary_mask_collection_2d() + mask_collection_1 = binary_mask_collection_3d() + + with pytest.raises(ValueError): + SimpleMerge().run([mask_collection_0, mask_collection_1]) diff --git a/starfish/morphology.py b/starfish/morphology.py index bbfa6a0bb..3cf5b054d 100644 --- a/starfish/morphology.py +++ b/starfish/morphology.py @@ -1,6 +1,7 @@ from starfish.core.morphology import ( # noqa: F401 Binarize, Filter, + Merge, ) from starfish.core.morphology.binary_mask import BinaryMaskCollection # noqa: F401 from starfish.core.morphology.label_image import LabelImage # noqa: F401