Beispiel #1
0
    def test_custom_default_name(self, small_cont: ImageContainer):
        custom_features = small_cont.features_custom(np.mean, layer="image", channels=[0])
        summary_features = small_cont.features_summary("image", feature_name="summary", channels=[0])

        assert len(custom_features) == 1
        assert f"{np.mean.__name__}_0" in custom_features
        assert custom_features[f"{np.mean.__name__}_0"] == summary_features["summary_ch-0_mean"]
Beispiel #2
0
def small_cont_seg() -> ImageContainer:
    np.random.seed(42)
    img = ImageContainer(np.random.randint(low=0, high=255, size=(100, 100, 3), dtype=np.uint8), layer="image")
    mask = np.zeros((100, 100), dtype="uint8")
    mask[20:30, 10:20] = 1
    mask[50:60, 30:40] = 2
    img.add_img(mask, layer="segmented", channel_dim="mask")

    return img
Beispiel #3
0
    def test_textures_props(self, small_cont: ImageContainer, props: Sequence[str]):
        if not len(props):
            with pytest.raises(ValueError, match=r"No properties have been selected."):
                small_cont.features_texture("image", feature_name="foo", props=props)
        else:
            features = small_cont.features_texture("image", feature_name="foo", props=props)
            haystack = features.keys()

            for prop in props:
                assert any(f"{prop}_dist" in h for h in haystack), haystack
Beispiel #4
0
    def test_textures_distances(self, small_cont: ImageContainer, distances: Sequence[int]):
        if not len(distances):
            with pytest.raises(ValueError, match=r"No distances have been selected."):
                small_cont.features_texture("image", feature_name="foo", distances=distances)
        else:
            features = small_cont.features_texture("image", feature_name="foo", distances=distances)
            haystack = features.keys()

            for d in distances:
                assert any(f"dist-{d}" in h for h in haystack), haystack
Beispiel #5
0
    def test_textures_angles(self, small_cont: ImageContainer, angles: Sequence[float]):
        if not len(angles):
            with pytest.raises(ValueError, match=r"No angles have been selected."):
                small_cont.features_texture("image", feature_name="foo", angles=angles)
        else:
            features = small_cont.features_texture("image", feature_name="foo", angles=angles)
            haystack = features.keys()

            for a in angles:
                assert any(f"angle-{a:.2f}" in h for h in haystack), haystack
Beispiel #6
0
    def test_summary_quantiles(self, small_cont: ImageContainer, quantiles: Tuple[float, ...]):
        if not len(quantiles):
            with pytest.raises(ValueError, match=r"No quantiles have been selected."):
                small_cont.features_summary("image", quantiles=quantiles, feature_name="foo", channels=(0, 1))
        else:
            features = small_cont.features_summary("image", quantiles=quantiles, feature_name="foo", channels=(0, 1))
            haystack = features.keys()

            assert isinstance(features, dict)
            for c in (0, 1):
                for agg in ("mean", "std"):
                    assert f"foo_ch-{c}_{agg}" in haystack, haystack
                for q in quantiles:
                    assert f"foo_ch-{c}_quantile-{q}" in haystack, haystack
Beispiel #7
0
    def test_histogram_bins(self, small_cont: ImageContainer, bins: int):
        features = small_cont.features_histogram("image", bins=bins, feature_name="histogram", channels=(0,))

        assert isinstance(features, dict)
        haystack = features.keys()

        for c in (0,):
            for b in range(bins):
                assert f"histogram_ch-{c}_bin-{b}" in features, haystack
Beispiel #8
0
    def test_custom_returns_iterable(self, small_cont: ImageContainer):
        def dummy(_: np.ndarray) -> Tuple[int, int]:
            return 0, 1

        features = small_cont.features_custom(dummy, layer="image", feature_name="foo")

        assert len(features) == 2
        assert features["foo_0"] == 0
        assert features["foo_1"] == 1
Beispiel #9
0
    def test_segmentation_centroid(self, small_cont_seg: ImageContainer):
        features = small_cont_seg.features_segmentation(
            label_layer="segmented", intensity_layer=None, feature_name="foo", props=["centroid"]
        )

        assert isinstance(features, dict)
        assert "foo_centroid" in features
        assert isinstance(features["foo_centroid"], np.ndarray)
        assert features["foo_centroid"].ndim == 2
Beispiel #10
0
def _calculate_image_features_helper(
    obs_ids: Sequence[str],
    adata: AnnData,
    img: ImageContainer,
    layer: str,
    features: List[ImageFeature],
    features_kwargs: Mapping[str, Any],
    queue: Optional[SigQueue] = None,
    **kwargs: Any,
) -> pd.DataFrame:
    features_list = []
    for crop in img.generate_spot_crops(adata,
                                        obs_names=obs_ids,
                                        return_obs=False,
                                        as_array=False,
                                        **kwargs):
        if TYPE_CHECKING:
            assert isinstance(crop, ImageContainer)
        # load crop in memory to enable faster processing
        crop._data = crop.data.load()

        features_dict = {}
        for feature in features:
            feature = ImageFeature(feature)
            feature_kwargs = features_kwargs.get(feature.s, {})

            if feature == ImageFeature.TEXTURE:
                res = crop.features_texture(layer=layer, **feature_kwargs)
            elif feature == ImageFeature.COLOR_HIST:
                res = crop.features_histogram(layer=layer, **feature_kwargs)
            elif feature == ImageFeature.SUMMARY:
                res = crop.features_summary(layer=layer, **feature_kwargs)
            elif feature == ImageFeature.SEGMENTATION:
                res = crop.features_segmentation(intensity_layer=layer,
                                                 **feature_kwargs)
            elif feature == ImageFeature.CUSTOM:
                res = crop.features_custom(layer=layer, **feature_kwargs)
            else:
                # should never get here
                raise NotImplementedError(
                    f"Feature `{feature}` is not yet implemented.")

            features_dict.update(res)
        features_list.append(features_dict)

        if queue is not None:
            queue.put(Signal.UPDATE)

    if queue is not None:
        queue.put(Signal.FINISH)

    return pd.DataFrame(features_list, index=list(obs_ids))
Beispiel #11
0
    def test_segmentation_props(self, small_cont_seg: ImageContainer, props: Sequence[str]):
        if not len(props):
            with pytest.raises(ValueError, match=r"No properties have been selected."):
                small_cont_seg.features_segmentation(
                    label_layer="segmented", intensity_layer="image", feature_name="foo", props=props
                )
        else:
            features = small_cont_seg.features_segmentation(
                label_layer="segmented", intensity_layer="image", feature_name="foo", props=props, channels=[0]
            )
            haystack = features.keys()

            int_props = [p for p in props if "intensity" in props]
            no_int_props = [p for p in props if "intensity" not in props]

            for p in no_int_props:
                assert any(f"{p}_mean" in h for h in haystack), haystack
                assert any(f"{p}_std" in h for h in haystack), haystack

            for p in int_props:
                assert any(f"ch-0_{p}_mean" in h for h in haystack), haystack
                assert any(f"ch-0_{p}_std" in h for h in haystack), haystack
Beispiel #12
0
def napari_cont() -> ImageContainer:
    return ImageContainer("tests/_data/test_img.jpg", layer="V1_Adult_Mouse_Brain")
Beispiel #13
0
def cont_dot() -> ImageContainer:
    ys, xs = 100, 200
    img_orig = np.zeros((ys, xs, 10), dtype=np.uint8)
    img_orig[20, 50, :] = range(10, 20)  # put a dot at y 20, x 50
    return ImageContainer(img_orig, layer="image_0")
Beispiel #14
0
def small_cont_1c() -> ImageContainer:
    np.random.seed(42)
    return ImageContainer(np.random.normal(size=(100, 100, 1)) + 1, layer="image")
Beispiel #15
0
def cont() -> ImageContainer:
    return ImageContainer("tests/_data/test_img.jpg")
Beispiel #16
0
def small_cont() -> ImageContainer:
    np.random.seed(42)
    return ImageContainer(np.random.uniform(size=(100, 100, 3), low=0, high=1), layer="image")
Beispiel #17
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])
Beispiel #18
0
 def _(self, img: ImageContainer, layer: str, channel: int = 0, **kwargs: Any) -> ImageContainer:
     # simple inversion of control, we rename the channel dim later
     return img.apply(self.segment, layer=layer, channel=channel, **kwargs)
Beispiel #19
0
    def test_segmentation_label(self, small_cont_seg: ImageContainer):
        features = small_cont_seg.features_segmentation("image", feature_name="foo", props=["label"])

        assert isinstance(features, dict)
        assert "foo_label" in features
        assert features["foo_label"] == 254
Beispiel #20
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)
Beispiel #21
0
 def test_segmentation_invalid_props(self, small_cont: ImageContainer):
     with pytest.raises(ValueError, match=r"Invalid property `foobar`. Valid properties are"):
         small_cont.features_segmentation("image", feature_name="foo", props=["foobar"])
Beispiel #22
0
 def test_container_empty(self):
     cont = ImageContainer()
     with pytest.raises(ValueError, match=r"The container is empty."):
         cont.features_summary("image")
Beispiel #23
0
def calculate_image_features(
    adata: AnnData,
    img: ImageContainer,
    layer: Optional[str] = None,
    features: Union[str, Sequence[str]] = ImageFeature.SUMMARY.s,
    features_kwargs: Mapping[str, Mapping[str, Any]] = MappingProxyType({}),
    key_added: str = "img_features",
    copy: bool = False,
    n_jobs: Optional[int] = None,
    backend: str = "loky",
    show_progress_bar: bool = True,
    **kwargs: Any,
) -> Optional[pd.DataFrame]:
    """
    Calculate image features for all observations in ``adata``.

    Parameters
    ----------
    %(adata)s
    %(img_container)s
    %(img_layer)s
    features
        Features to be calculated. Valid options are:

        - `{f.TEXTURE.s!r}` - summary stats based on repeating patterns
          :meth:`squidpy.im.ImageContainer.features_texture`.
        - `{f.SUMMARY.s!r}` - summary stats of each image channel
          :meth:`squidpy.im.ImageContainer.features_summary`.
        - `{f.COLOR_HIST.s!r}` - counts in bins of image channel's histogram
          :meth:`squidpy.im.ImageContainer.features_histogram`.
        - `{f.SEGMENTATION.s!r}` - stats of a cell segmentation mask
          :meth:`squidpy.im.ImageContainer.features_segmentation`.
        - `{f.CUSTOM.s!r}` - extract features using a custom function
          :meth:`squidpy.im.ImageContainer.features_custom`.

    features_kwargs
        Keyword arguments for the different features that should be generated, such as
        ``{{ {f.TEXTURE.s!r}: {{ ... }}, ... }}``.
    key_added
        Key in :attr:`anndata.AnnData.obsm` where to store the calculated features.
    %(copy)s
    %(parallelize)s
    kwargs
        Keyword arguments for :meth:`squidpy.im.ImageContainer.generate_spot_crops`.

    Returns
    -------
    If ``copy = True``, returns a :class:`panda.DataFrame` where columns correspond to the calculated features.

    Otherwise, modifies the ``adata`` object with the following key:

        - :attr:`anndata.AnnData.uns` ``['{{key_added}}']`` - the above mentioned dataframe.

    Raises
    ------
    ValueError
        If a feature is not known.
    """
    layer = img._get_layer(layer)
    if isinstance(features, (str, ImageFeature)):
        features = [features]
    features = sorted({ImageFeature(f).s for f in features})

    n_jobs = _get_n_cores(n_jobs)
    start = logg.info(
        f"Calculating features `{list(features)}` using `{n_jobs}` core(s)")

    res = parallelize(
        _calculate_image_features_helper,
        collection=adata.obs_names,
        extractor=pd.concat,
        n_jobs=n_jobs,
        backend=backend,
        show_progress_bar=show_progress_bar,
    )(adata,
      img,
      layer=layer,
      features=features,
      features_kwargs=features_kwargs,
      **kwargs)

    if copy:
        logg.info("Finish", time=start)
        return res

    _save_data(adata, attr="obsm", key=key_added, data=res, time=start)
Beispiel #24
0
 def test_invalid_layer(self, small_cont: ImageContainer):
     with pytest.raises(KeyError, match=r"Image layer `foobar` not found in"):
         small_cont.features_summary("foobar")
Beispiel #25
0
 def test_invalid_channels(self, small_cont: ImageContainer):
     with pytest.raises(ValueError, match=r"Channel `-1` is not in"):
         small_cont.features_summary("image", channels=-1)