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

Mechanism to reduce multiple masks into one #1684

Merged
merged 1 commit into from
Dec 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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,
))