예제 #1
0
def segment(
    img: ImageContainer,
    layer: Optional[str] = None,
    method: Union[str, Callable[..., np.ndarray]] = "watershed",
    channel: int = 0,
    size: Optional[Union[int, Tuple[int, int]]] = None,
    layer_added: Optional[str] = None,
    copy: bool = False,
    show_progress_bar: bool = True,
    n_jobs: Optional[int] = None,
    backend: str = "loky",
    **kwargs: Any,
) -> Optional[ImageContainer]:
    """
    Segment an image.

    If ``size`` is defined, iterate over crops of that size and segment those. Recommended for large images.

    Parameters
    ----------
    %(img_container)s
    %(img_layer)s
    %(seg_blob.parameters)s
            - `{m.WATERSHED.s!r}` - :func:`skimage.segmentation.watershed`.

        %(custom_fn)s
    channel
        Channel index to use for segmentation.
    %(size)s
    %(layer_added)s
        If `None`, use ``'segmented_{{model}}'``.
    thresh
        Threshold for creation of masked image. The areas to segment should be contained in this mask.
        If `None`, it is determined by `Otsu's method <https://en.wikipedia.org/wiki/Otsu%27s_method>`_.
        Only used if ``method = {m.WATERSHED.s!r}``.
    geq
        Treat ``thresh`` as upper or lower bound for defining areas to segment. If ``geq = True``, mask is defined
        as ``mask = arr >= thresh``, meaning high values in ``arr`` denote areas to segment.
    invert
        Whether to segment an inverted array. Only used if ``method`` is one of :mod:`skimage` blob methods.
    %(copy_cont)s
    %(segment_kwargs)s
    %(parallelize)s
    kwargs
        Keyword arguments for ``method``.

    Returns
    -------
    If ``copy = True``, returns a new container with the segmented image in ``'{{layer_added}}'``.

    Otherwise, modifies the ``img`` with the following key:

        - :class:`squidpy.im.ImageContainer` ``['{{layer_added}}']`` - the segmented image.
    """
    layer = img._get_layer(layer)
    channel_dim = img[layer].dims[-1]

    kind = SegmentationBackend.CUSTOM if callable(method) else SegmentationBackend(method)
    layer_new = Key.img.segment(kind, layer_added=layer_added)

    if kind in (SegmentationBackend.LOG, SegmentationBackend.DOG, SegmentationBackend.DOH):
        segmentation_model: SegmentationModel = SegmentationBlob(model=kind)
    elif kind == SegmentationBackend.WATERSHED:
        segmentation_model = SegmentationWatershed()
    elif kind == SegmentationBackend.CUSTOM:
        if TYPE_CHECKING:
            assert callable(method)
        segmentation_model = SegmentationCustom(func=method)
    else:
        raise NotImplementedError(f"Model `{kind}` is not yet implemented.")

    n_jobs = _get_n_cores(n_jobs)
    crops: List[ImageContainer] = list(img.generate_equal_crops(size=size, as_array=False))
    start = logg.info(f"Segmenting `{len(crops)}` crops using `{segmentation_model}` and `{n_jobs}` core(s)")

    crops: List[ImageContainer] = parallelize(  # type: ignore[no-redef]
        _segment,
        collection=crops,
        unit="crop",
        extractor=lambda res: list(chain.from_iterable(res)),
        n_jobs=n_jobs,
        backend=backend,
        show_progress_bar=show_progress_bar and len(crops) > 1,
    )(model=segmentation_model, layer=layer, layer_new=layer_new, channel=channel, **kwargs)

    if isinstance(segmentation_model, SegmentationWatershed):
        # By convention, segments are numbered from 1..number of segments within each crop.
        # Next, we have to account for that before merging the crops so that segments are not confused.
        # TODO use overlapping crops to not create confusion at boundaries
        counter = 0
        for crop in crops:
            data = crop[layer_new].data
            data[data > 0] += counter
            counter += np.max(crop[layer_new].data)

    res: ImageContainer = ImageContainer.uncrop(crops, shape=img.shape)
    res._data = res.data.rename({channel_dim: f"{channel_dim}:{channel}"})

    logg.info("Finish", time=start)

    if copy:
        return res

    img.add_img(res, layer=layer_new, copy=False, channel_dim=res[layer_new].dims[-1])
예제 #2
0
def process(
    img: ImageContainer,
    layer: Optional[str] = None,
    method: Union[str, Callable[..., np.ndarray]] = "smooth",
    size: Optional[Tuple[int, int]] = None,
    layer_added: Optional[str] = None,
    channel_dim: Optional[str] = None,
    copy: bool = False,
    **kwargs: Any,
) -> Optional[ImageContainer]:
    """
    Process an image by applying a transformation.

    Note that crop-wise processing can save memory but may change behavior of cropping if global statistics are used.
    Leave ``size = None`` in order to process the full image in one go.

    Parameters
    ----------
    %(img_container)s
    %(img_layer)s
    method
        Processing method to use. Valid options are:

            - `{p.SMOOTH.s!r}` - :func:`skimage.filters.gaussian`.
            - `{p.GRAY.s!r}` - :func:`skimage.color.rgb2gray`.

        %(custom_fn)s
    %(size)s
    %(layer_added)s
        If `None`, use ``'{{layer}}_{{method}}'``.
    channel_dim
        Name of the channel dimension of the new image layer. Default is the same as the original, if the
        processing function does not change the number of channels, and ``'{{channel}}_{{processing}}'`` otherwise.
    %(copy_cont)s
    kwargs
        Keyword arguments for ``method``.

    Returns
    -------
    If ``copy = True``, returns a new container with the processed image in ``'{{layer_added}}'``.

    Otherwise, modifies the ``img`` with the following key:

        - :class:`squidpy.im.ImageContainer` ``['{{layer_added}}']`` - the processed image.

    Raises
    ------
    NotImplementedError
        If ``method`` has not been implemented.
    """
    layer = img._get_layer(layer)
    method = Processing(method) if isinstance(
        method, (str, Processing)) else method  # type: ignore[assignment]

    if channel_dim is None:
        channel_dim = img[layer].dims[-1]
    layer_new = Key.img.process(method, layer, layer_added=layer_added)

    if callable(method):
        callback = method
    elif method == Processing.SMOOTH:  # type: ignore[comparison-overlap]
        callback = partial(skimage.filters.gaussian, multichannel=True)
    elif method == Processing.GRAY:  # type: ignore[comparison-overlap]
        if img[layer].shape[-1] != 3:
            raise ValueError(
                f"Expected channel dimension to be `3`, found `{img[layer].shape[-1]}`."
            )
        callback = skimage.color.rgb2gray
    else:
        raise NotImplementedError(f"Method `{method}` is not yet implemented.")

    start = logg.info(f"Processing image using `{method}` method")

    crops = [
        crop.apply(callback, layer=layer, copy=True, **kwargs)
        for crop in img.generate_equal_crops(size=size)
    ]
    res: ImageContainer = ImageContainer.uncrop(crops=crops, shape=img.shape)

    # if the method changes the number of channels
    if res[layer].shape[-1] != img[layer].shape[-1]:
        modifier = "_".join(
            layer_new.split("_")[1:]) if layer_added is None else layer_added
        channel_dim = f"{channel_dim}_{modifier}"

    res._data = res.data.rename({
        res[layer].dims[-1]: channel_dim
    }).rename_vars({layer: layer_new})

    logg.info("Finish", time=start)

    if copy:
        return res

    img.add_img(img=res, layer=layer_new, channel_dim=channel_dim)