Source code for phenotypic.enhance._contrast_log

from __future__ import annotations

from typing import TYPE_CHECKING, Annotated

if TYPE_CHECKING:
    from phenotypic._core._image import Image

import numpy as np
from skimage.exposure import adjust_log

from ..abc_ import ContrastAdjustment
from ..sdk_.mixin import InputLayerMixin, NormalizedOutputMixin
from ..sdk_.typing_ import TuneSpec


[docs] class ContrastLog(InputLayerMixin, NormalizedOutputMixin, ContrastAdjustment): """Apply a logarithmic intensity curve to lift faint colonies out of dark agar. Computes ``gain * log2(1 + I)``, which expands the dark end of the histogram while compressing highlights. Faint colonies sitting just above the agar background gain contrast; already-bright colonies compress toward saturation. Setting ``inv=True`` applies the inverse exponential curve, which expands the bright end instead. Best For: - Dark-field or transmitted-light plates where colonies are dim and the background is near black. - Recovering small colonies whose intensity sits within a few percent of the agar background. Consider Also: - :class:`ContrastGamma` for a tunable power-law curve rather than a fixed logarithmic shape. - :class:`ContrastSigmoid` when contrast should steepen around one intensity rather than across the shadows. Args: gain: Constant multiplier applied after the curve. Default: 1.0. Has no effect when ``norm="rescale"``, which divides it back out. inv: When ``True``, apply the inverse (exponential) curve, expanding the bright end rather than the dark end. Default: ``False``. norm: Output range policy. ``"clip"`` (default) saturates values outside [0, 1]; ``"rescale"`` remaps the full observed range onto [0, 1]; ``None`` passes values through untouched. input_layer: Source layer. ``"detect_mat"`` (default) applies the curve to the 2-D detection matrix. ``"rgb"`` applies it to all three colour channels, then collapses the result to 2-D through the image's own ``detect_mode``. Because the curve is non-linear, the two routes generally differ -- except under a ``detect_mode`` that is a per-pixel selection (``"red"``/``"green"``/``"blue"``/``"MinRGB"``/``"HsvV"``), which commutes with any monotonically increasing curve and so yields an identical result. Default: ``"detect_mat"``. Returns: Image: Input image with ``detect_mat`` log-corrected. ``rgb`` and ``gray`` are unchanged. With ``input_layer="rgb"``, any enhancement a prior operation wrote to ``detect_mat`` is discarded, as with :class:`SetDetectMode`. Examples: Lift dim colonies on a dark plate: >>> from phenotypic.data import load_synth_yeast_plate >>> from phenotypic.enhance import ContrastLog >>> plate = load_synth_yeast_plate() >>> enhanced = ContrastLog().apply(plate) >>> float(enhanced.detect_mat[:].mean()) > float(plate.detect_mat[:].mean()) True """ gain: Annotated[float, TuneSpec(0.5, 2.0)] = 1.0 inv: bool = False def _operate(self, image: Image) -> Image: src = self._guard_input_range(self._read_input_layer(image)) adjusted = adjust_log(src, gain=self.gain, inv=self.inv) collapsed = self._project_to_detect_mat(image, adjusted) image.detect_mat[:] = self._apply_norm(collapsed).astype(np.float32) return image