Skip to content

Commit

Permalink
Thresholded binarize conversion algorithm
Browse files Browse the repository at this point in the history
BinarizeImage takes an ImageStack and binarizes it into a BinaryMaskCollection.

Depends on #1637
Test plan: add tests for simple binarizing, and to test the input requirements.
  • Loading branch information
Tony Tung committed Dec 3, 2019
1 parent 841d799 commit a4a3c27
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 0 deletions.
11 changes: 11 additions & 0 deletions starfish/core/morphology/Binarize/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Algorithms in this module binarize an ImageStack into a BinaryMaskCollection."""
from ._base import BinarizeAlgorithm
from .threshold import ThresholdBinarize

# 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, BinarizeAlgorithm)
}
__all__ = list(all_filters.keys())
13 changes: 13 additions & 0 deletions starfish/core/morphology/Binarize/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from abc import abstractmethod

from starfish.core.imagestack.imagestack import ImageStack
from starfish.core.morphology.binary_mask import BinaryMaskCollection
from starfish.core.pipeline.algorithmbase import AlgorithmBase


class BinarizeAlgorithm(metaclass=AlgorithmBase):

@abstractmethod
def run(self, image: ImageStack, *args, **kwargs) -> BinaryMaskCollection:
"""Performs binarization on the stack provided."""
raise NotImplementedError()
Empty file.
41 changes: 41 additions & 0 deletions starfish/core/morphology/Binarize/test/test_threshold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import numpy as np
import pytest

from starfish import ImageStack
from starfish.types import Number
from ..threshold import ThresholdBinarize


@pytest.mark.parametrize(["threshold"], [[threshold] for threshold in np.linspace(0, 1, 3)])
def test_binarize(threshold: Number, num_rounds=1, num_chs=1, num_zplanes=4, ysize=5, xsize=6):
data = np.linspace(0, 1, num_rounds * num_chs * num_zplanes * ysize * xsize, dtype=np.float32)
data = data.reshape((num_rounds, num_chs, num_zplanes, ysize, xsize))

imagestack = ImageStack.from_numpy(data)
binarizer = ThresholdBinarize(threshold)
binary_mask_collection = binarizer.run(imagestack)

assert len(binary_mask_collection) == 1
mask = binary_mask_collection.uncropped_mask(0)

expected_value = data[0, 0] >= threshold

assert np.array_equal(mask, expected_value)


@pytest.mark.parametrize(
["num_rounds", "num_chs"],
[
[1, 2],
[2, 1],
[2, 2],
])
def test_binarize_non_3d(num_rounds, num_chs, num_zplanes=4, ysize=5, xsize=6):
data = np.linspace(0, 1, num_rounds * num_chs * num_zplanes * ysize * xsize, dtype=np.float32)
data = data.reshape((num_rounds, num_chs, num_zplanes, ysize, xsize))

imagestack = ImageStack.from_numpy(data)
binarizer = ThresholdBinarize(0.0)

with pytest.raises(ValueError):
binarizer.run(imagestack)
68 changes: 68 additions & 0 deletions starfish/core/morphology/Binarize/threshold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Mapping, Union

import numpy as np
import xarray as xr

from starfish.core.imagestack.imagestack import ImageStack
from starfish.core.morphology.binary_mask import BinaryMaskCollection
from starfish.core.morphology.util import _get_axes_names
from starfish.core.types import ArrayLike, Axes, Coordinates, Number
from ._base import BinarizeAlgorithm


class ThresholdBinarize(BinarizeAlgorithm):
"""Binarizes an image using a threshold. Pixels that exceed the threshold are considered True
and all remaining pixels are considered False.
The image being binarized must be an ImageStack with num_rounds == 1 and num_chs == 1.
"""
def __init__(self, threshold: Number):
self.threshold = threshold

def _binarize(self, result: np.ndarray, tile_data: Union[np.ndarray, xr.DataArray]) -> None:
result[:] = np.asarray(tile_data) >= self.threshold

def run(self, image: ImageStack, *args, **kwargs) -> BinaryMaskCollection:
if image.num_rounds != 1:
raise ValueError(
f"{ThresholdBinarize.__name__} given an image with more than one round "
f"{image.num_rounds}")
if image.num_chs != 1:
raise ValueError(
f"{ThresholdBinarize.__name__} given an image with more than one channel "
f"{image.num_chs}")

result_array = np.empty(
shape=[
image.shape[axis]
for axis, _ in zip(*_get_axes_names(3))
],
dtype=np.bool)

# TODO: (ttung) This could theoretically be done with ImageStack.transform, but
# ImageStack.transform doesn't provide the selectors to the worker method. In this case,
# we need the selectors to select the correct region of the output array. The alternative
# is for each worker thread to create a new array, and then merge them at the end, but that
# effectively doubles our memory consumption.
#
# For now, we will just do it in-process, because it's not a particularly compute-intensive
# task.
self._binarize(result_array, image.xarray[0, 0])

pixel_ticks: Mapping[Axes, ArrayLike[int]] = {
Axes(axis): axis_data
for axis, axis_data in image.xarray.coords.items()
if axis in _get_axes_names(3)[0]
}
physical_ticks: Mapping[Coordinates, ArrayLike[Number]] = {
Coordinates(coord): coord_data
for coord, coord_data in image.xarray.coords.items()
if coord in _get_axes_names(3)[1]
}

return BinaryMaskCollection.from_binary_arrays_and_ticks(
(result_array,),
pixel_ticks,
physical_ticks,
image.log,
)

0 comments on commit a4a3c27

Please sign in to comment.