Source code for phenotypic.enhance._focus_edge_sato

from __future__ import annotations
from typing import Annotated, Iterable, TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._image import Image

import numpy as np
from pydantic import field_validator
from skimage.feature import hessian_matrix, hessian_matrix_eigvals

from phenotypic.abc_ import FocusEdge
from phenotypic.sdk_.typing_ import TuneSpec


[docs] class FocusEdgeSato(FocusEdge): """Enhance hyphal ridges and tubular colony structures in ``detect_mat`` using the Sato tubeness filter. Computes a tubeness response from Hessian matrix eigenvalues at each specified scale, then takes the per-pixel maximum across all scales to produce a response map where bright ridges correspond to continuous filamentous structures. Intermediates are deleted between scales to reduce peak memory usage. For algorithm details, see :doc:`/explanation/what_enhancement_does`. Best For: - Thin filamentous colonies and mycelial networks (Aspergillus, Neurospora, streptomycetes). - Continuous ridge-like morphologies that global thresholding misses. - Branching or root-like colony forms requiring multi-scale detection. - Plates where hyphal width varies across the image and a single sigma would miss structures at one end of the size range. Consider Also: - :class:`FocusEdgeFrangi` when blob-versus-tube discrimination is needed via explicit alpha/beta sensitivity controls. - :class:`FocusEdgeHessian` for combined edge and ridge detection with adjustable background suppression. - :class:`StructureSmoothing` when anisotropic preprocessing is needed to reinforce coherent hyphal orientation before ridge detection. Args: sigmas: Scales (standard deviations in pixels) at which the Hessian is evaluated. Each sigma responds maximally to ridges whose cross-sectional half-width matches that value. The output is the per-pixel maximum across all scales, so extra sigmas can only raise the response. Typical tuple span: ``(1, 2, 3)`` for standard 300--600 dpi scans where hyphae are 2--8 px wide; extend to ``range(1, 10, 2)`` for thick mature filaments or lower magnification. A reasonable starting point for standard agar plate scans is to span the expected minimum and maximum hyphal width in pixels, e.g. ``(1, 3, 5)`` for 600 dpi images of Neurospora. Default: ``(1, 2, 3)``. black_ridges: Detect dark ridges on a bright background when ``True``. ``False`` (default) detects bright ridges on a dark background, matching the ``detect_mat`` convention where colonies appear bright. mode: Boundary handling for Gaussian derivative convolution. Accepted values: ``'constant'``, ``'reflect'``, ``'wrap'``, ``'nearest'``, ``'mirror'``. ``'reflect'`` (default) mirrors image data at the border, minimising spurious ridge responses at plate edges. Use ``'constant'`` (with ``cval`` set to the background level) only when stitching multi-tile acquisitions. cval: Fill value used when ``mode='constant'``. Has no effect for other border modes. Default: 0. Returns: Image: Input image with ``detect_mat`` replaced by the Sato tubeness response map. Brighter pixels indicate stronger ridge-like structures at the sampled scales. ``rgb`` and ``gray`` are unchanged. References: [1] Y. Sato et al., "Three-dimensional multi-scale line filter for segmentation and visualization of curvilinear structures in medical images," *Med. Image Anal.*, vol. 2, no. 2, pp. 143--168, Jun. 1998. See Also: :doc:`/tutorials/notebooks/03_enhancing_before_detection` for a visual walkthrough of ridge enhancement on plate images. :doc:`/explanation/what_enhancement_does` for background on Hessian-based ridge detection methods. :doc:`/tutorials/notebooks/10_detecting_filamentous_fungi` for filamentous fungi detection pipelines that use this enhancer. """ sigmas: tuple[float, ...] = (1, 2, 3) black_ridges: bool = False mode: str = "reflect" cval: Annotated[float, TuneSpec(tunable=False)] = 0 @field_validator("sigmas", mode="before") @classmethod def _coerce_sigmas(cls, sigmas: Iterable[float]) -> tuple[float, ...]: """Coerce any iterable of sigmas to a tuple. Reproduces the pre-migration ``__setattr__`` override, which normalized ``sigmas`` (passed as a list or other iterable) to a tuple before storing it. """ return tuple(sigmas) def _operate(self, image: Image) -> Image: # Manual Sato tubeness loop (replaces skimage.filters.sato) for # explicit deletion of Hessian intermediates, reducing peak memory # by ~50% for multi-sigma runs. Algorithm: Sato et al. 1998, eqs. 9/22. img = np.asarray(image.detect_mat[:], dtype=np.float32) if not self.black_ridges: img = -img filtered_max = np.zeros(img.shape, dtype=np.float32) for sigma in self.sigmas: hessian_elems = hessian_matrix( img, sigma=sigma, mode=self.mode, cval=self.cval, use_gaussian_derivatives=True, ) eigvals = hessian_matrix_eigvals(hessian_elems) del hessian_elems eigvals = eigvals[:-1] filtered = ( sigma ** 2 * np.prod(np.maximum(eigvals, 0), axis=0) ** (1.0 / len(eigvals)) ) del eigvals np.maximum(filtered_max, filtered, out=filtered_max) del filtered image.detect_mat[:] = filtered_max return image