Skip to content

Commit

Permalink
Mechanism to reduce multiple masks into one (#1684)
Browse files Browse the repository at this point in the history
This is the counterpart to the apply provided in #1655.

Test plan: Added a test that reduced using xors. Because we have boring test data that don't overlap, this is one way of generating interesting output.
  • Loading branch information
Tony Tung authored Dec 13, 2019
1 parent 3b13184 commit 40c56c1
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 0 deletions.
1 change: 1 addition & 0 deletions starfish/core/morphology/Filter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
BinaryMaskCollection."""
from ._base import FilterAlgorithm
from .map import Map
from .reduce import Reduce

# autodoc's automodule directive only captures the modules explicitly listed in __all__.
all_filters = {
Expand Down
94 changes: 94 additions & 0 deletions starfish/core/morphology/Filter/reduce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from typing import Callable, Optional, Tuple, Union

import numpy as np

from starfish.core.morphology.binary_mask import BinaryMaskCollection
from starfish.core.types import FunctionSource, FunctionSourceBundle
from ._base import FilterAlgorithm


class Reduce(FilterAlgorithm):
"""
Reduce takes masks from one ``BinaryMaskCollection`` and reduces it down to a single mask by
applying a specified function. That mask is then returned as a new ``BinaryMaskCollection``.
An initial value is used to start the reduction process. The first call to the function will be
called with ``initial`` and ``M0`` and produce ``R0``. The second call to the function will be
called with ``R0`` and ``M1`` and produce ``R1``.
Parameters
----------
func : Union[str, FunctionSourceBundle]
Function to reduce the tiles in the input.
If this value is a string, then the python package is :py:attr:`FunctionSource.np`.
If this value is a ``FunctionSourceBundle``, then the python package and module name is
obtained from the bundle.
initial : Union[np.ndarray, Callable[[Tuple[int, ...]], np.ndarray]]
An initial array that is the same shape as an uncropped mask, or a callable that accepts the
shape of an uncropped mask as its parameter and produces an initial array.
Examples
--------
Applying a logical 'AND' across all the masks in a collection.
>>> from starfish.core.morphology.binary_mask.test import factories
>>> from starfish.morphology import Filter
>>> from starfish.types import FunctionSource
>>> import numpy as np
>>> from skimage.morphology import disk
>>> binary_mask_collection = factories.binary_mask_collection_2d()
>>> initial_mask_producer = lambda shape: np.ones(shape=shape)
>>> ander = Filter.Reduce(FunctionSource.np("logical_and"), initial_mask_producer)
>>> anded = anded.run(binary_mask_collection)
See Also
--------
starfish.core.types.Axes
"""

def __init__(
self,
func: Union[str, FunctionSourceBundle],
initial: Union[np.ndarray, Callable[[Tuple[int, ...]], np.ndarray]],
*func_args,
**func_kwargs,
) -> None:
if isinstance(func, str):
self._func = FunctionSource.np(func)
elif isinstance(func, FunctionSourceBundle):
self._func = func
self._initial = initial
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._reduce(
self._func.resolve(),
self._initial,
*self._func_args,
**self._func_kwargs)
29 changes: 29 additions & 0 deletions starfish/core/morphology/Filter/test/test_reduce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import numpy as np

from starfish.core.morphology.binary_mask.test.factories import binary_mask_collection_2d
from ..reduce import Reduce


def test_reduce():
def make_initial(shape):
constant_initial = np.zeros(shape=shape, dtype=np.bool)
constant_initial[0, 0] = 1
return constant_initial

input_mask_collection = binary_mask_collection_2d()
filt = Reduce("logical_xor", make_initial)
output_mask_collection = filt.run(input_mask_collection)

assert len(output_mask_collection) == 1
uncropped_output = output_mask_collection.uncropped_mask(0)
assert np.array_equal(
np.asarray(uncropped_output),
np.array(
[[False, True, True, True, True, True],
[False, False, False, False, False, False],
[False, False, False, False, False, False],
[False, False, False, True, True, True],
[False, False, False, True, True, False],
],
dtype=np.bool,
))
55 changes: 55 additions & 0 deletions starfish/core/morphology/binary_mask/binary_mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,61 @@ def _apply_single_mask(
None
)

def _reduce(
self,
function: Callable,
initial: Union[np.ndarray, Callable[[Tuple[int, ...]], np.ndarray]],
*args,
**kwargs
) -> "BinaryMaskCollection":
"""Given a function that takes two ndarray and outputs another, apply that function to all
the masks in this collection to form a new collection. Each time, the function is called
with the result of the previous call to the method and the next uncropped mask. The first
time the function is called, the first argument is provided by ``initial``, which is either
an array sized to match the uncropped mask, or a callable that takes a single parameter,
which is the shape of the array to be produced.
Parameters
----------
function : Callable[[np.ndarray, np.ndarray], np.ndarray]
A function that should produce an accumulated result when given an accumulated result
and a mask array. The shape of the inputs and the outputs should be identical.
initial : Union[np.ndarray, Callable[[Tuple[int, ...]], np.ndarray]]
An initial array that is the same shape as an uncropped mask, or a callable that accepts
the shape of an uncropped mask as its parameter and produces an initial array.
Examples
--------
Applying a logical 'AND' across all the masks.
>>> import numpy as np
>>> from starfish.core.morphology.binary_mask.test import factories
>>> binary_mask_collection = factories.binary_mask_collection_2d()
>>> anded_mask_collection = binary_mask_collection._reduce(
np.logical_and, np.ones(shape=(5, 6), dtype=np.bool))
Applying a logical 'AND' across all the masks, without hard-coding the size of the array.
>>> import numpy as np
>>> from starfish.core.morphology.binary_mask.test import factories
>>> binary_mask_collection = factories.binary_mask_collection_2d()
>>> anded_mask_collection = binary_mask_collection._reduce(
np.logical_and, lambda shape: np.ones(shape=shape, dtype=np.bool))
"""
if callable(initial):
shape = tuple(len(self._pixel_ticks[axis])
for axis, _ in zip(*_get_axes_names(len(self._pixel_ticks))))
result = initial(shape)
else:
result = initial
for ix in range(len(self)):
result = function(result, self.uncropped_mask(ix).values, *args, **kwargs)

return BinaryMaskCollection.from_binary_arrays_and_ticks(
[result],
self._pixel_ticks,
self._physical_ticks,
self._log,
)


# these need to be at the end to avoid recursive imports
from . import _io # noqa
42 changes: 42 additions & 0 deletions starfish/core/morphology/binary_mask/test/test_binary_mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,45 @@ def test_apply():

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])


def test_reduce():
def make_initial(shape):
constant_initial = np.zeros(shape=shape, dtype=np.bool)
constant_initial[0, 0] = 1
return constant_initial

input_mask_collection = binary_mask_collection_2d()

constant_initial = make_initial((5, 6))
xored_binary_mask_constant_initial = input_mask_collection._reduce(
np.logical_xor, constant_initial)
assert len(xored_binary_mask_constant_initial) == 1
uncropped_output = xored_binary_mask_constant_initial.uncropped_mask(0)
assert np.array_equal(
np.asarray(uncropped_output),
np.array(
[[False, True, True, True, True, True],
[False, False, False, False, False, False],
[False, False, False, False, False, False],
[False, False, False, True, True, True],
[False, False, False, True, True, False],
],
dtype=np.bool,
))

xored_binary_mask_programmatic_initial = input_mask_collection._reduce(
np.logical_xor, make_initial)
assert len(xored_binary_mask_programmatic_initial) == 1
uncropped_output = xored_binary_mask_programmatic_initial.uncropped_mask(0)
assert np.array_equal(
np.asarray(uncropped_output),
np.array(
[[False, True, True, True, True, True],
[False, False, False, False, False, False],
[False, False, False, False, False, False],
[False, False, False, True, True, True],
[False, False, False, True, True, False],
],
dtype=np.bool,
))

0 comments on commit 40c56c1

Please sign in to comment.