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