Source code for phenotypic.enhance._contrast_streching
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 rescale_intensity
from ..abc_ import ContrastAdjustment
from ..sdk_.typing_ import TuneSpec
[docs]
class ContrastStretching(ContrastAdjustment):
"""Stretch the intensity range of ``detect_mat`` to fill the full dynamic range.
Rescales pixel values by clipping at lower and upper percentiles, then
linearly remapping the retained range to [0, 1]. Outliers such as specular
highlights and deep shadows are clamped, expanding the range where colony
intensities reside. Simpler and faster than :class:`EnhanceLocalContrast`,
with no local tile artefacts.
For how contrast adjustment fits into the pipeline, see
:doc:`/explanation/what_enhancement_does`.
Best For:
- Plates with narrow intensity histograms from under-exposure or low
scanner gain.
- Normalizing exposure variation across imaging sessions or plate batches.
- Quick preprocessing before global thresholding (Otsu, Triangle).
- Images with bright specular highlights or very dark border regions
that compress the useful intensity range.
Consider Also:
- :class:`EnhanceLocalContrast` when illumination varies spatially
across the plate and per-tile equalization is needed.
- :class:`FlattenIllumination` when the primary issue is a large-scale
brightness gradient rather than a narrow dynamic range.
Args:
lower_percentile: Dark clipping point. Pixels below this percentile
are mapped to 0. Typical range: 1--5. Default: 2.
upper_percentile: Bright clipping point. Pixels above this percentile
are mapped to 1. Typical range: 95--99. Default: 98.
Returns:
Image: Input image with ``detect_mat`` rescaled to the full dynamic
range. ``rgb`` and ``gray`` are unchanged.
See Also:
:doc:`/how_to/notebooks/enhance_low_contrast` for a comparison of
contrast enhancement methods on real plate images.
:doc:`/explanation/what_enhancement_does` for how enhancement fits
into the pipeline model.
"""
lower_percentile: Annotated[int, TuneSpec(1, 5)] = 2
upper_percentile: Annotated[int, TuneSpec(95, 99)] = 98
def _operate(self, image: Image) -> Image:
p_lower, p_upper = np.percentile(
image.detect_mat[:], (self.lower_percentile, self.upper_percentile)
)
image.detect_mat[:] = rescale_intensity(
image=image.detect_mat[:],
in_range=(p_lower, p_upper),
out_range=(0, 1),
)
return image