Source code for phenotypic.enhance._contrast_gamma
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_gamma
from ..abc_ import ContrastAdjustment
from ..sdk_.mixin import InputLayerMixin, NormalizedOutputMixin
from ..sdk_.typing_ import TuneSpec
[docs]
class ContrastGamma(InputLayerMixin, NormalizedOutputMixin, ContrastAdjustment):
"""Apply a power-law (gamma) intensity curve to boost faint or washed-out colonies.
Raises each pixel to the power ``gamma`` after normalizing to [0, 1], then scales
by ``gain``. Values above 1 darken midtones and deepen the agar background, making
bright colonies stand out. Values below 1 brighten midtones, rescuing faint or
translucent colonies that a global threshold would otherwise miss.
Unlike :class:`ContrastStretching`, the mapping is non-linear, so it redistributes
tonal weight rather than merely rescaling the range.
Best For:
- Faint or translucent colonies lost against a bright agar background
(``gamma`` below 1).
- Over-exposed plates where colony interiors saturate (``gamma`` above 1).
- Pigmented colonies whose colour separation is stronger in a single channel:
set ``input_layer="rgb"`` so the curve applies per-channel before the
detection matrix is derived.
Consider Also:
- :class:`ContrastStretching` when the histogram is merely narrow and a
linear remap suffices.
- :class:`ContrastSigmoid` when you want to steepen contrast around a
specific intensity rather than across the whole range.
Args:
gamma: Power-law exponent. Below 1 brightens midtones; above 1 darkens them.
``1.0`` is the identity. Typical range: 0.5--2.5. Default: 1.0.
gain: Constant multiplier applied after the curve. Default: 1.0.
Has no effect when ``norm="rescale"``, which divides it back out.
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`` gamma-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:
Darken the background to sharpen bright yeast colonies:
>>> from phenotypic.data import load_synth_yeast_plate
>>> from phenotypic.enhance import ContrastGamma
>>> plate = load_synth_yeast_plate()
>>> enhanced = ContrastGamma(gamma=2.0).apply(plate)
>>> float(enhanced.detect_mat[:].max()) <= 1.0
True
Apply the curve in colour space before deriving the detection matrix.
Use a channel-*mixing* ``detect_mode`` such as ``'LabA'`` — under a
selection mode (``'MinRGB'``, ``'red'``, ...) the curve commutes with the
projection and ``input_layer='rgb'`` changes nothing:
>>> import numpy as np
>>> plate = load_synth_yeast_plate()
>>> plate.set_detect_mode('LabA')
>>> via_rgb = ContrastGamma(gamma=2.0, input_layer='rgb').apply(plate)
>>> via_dm = ContrastGamma(gamma=2.0, input_layer='detect_mat').apply(plate)
>>> bool(np.abs(via_rgb.detect_mat[:] - via_dm.detect_mat[:]).max() > 1e-3)
True
"""
gamma: Annotated[float, TuneSpec(0.1, 5.0, log=True)] = 1.0
gain: Annotated[float, TuneSpec(0.5, 2.0)] = 1.0
def _operate(self, image: Image) -> Image:
src = self._guard_input_range(self._read_input_layer(image))
adjusted = adjust_gamma(src, gamma=self.gamma, gain=self.gain)
collapsed = self._project_to_detect_mat(image, adjusted)
image.detect_mat[:] = self._apply_norm(collapsed).astype(np.float32)
return image