Source code for sigima.proc.image.measurement

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Measurement computation module
------------------------------

This module provides tools for extracting quantitative information from images,
such as object centroids, enclosing circles, and region-based statistics.

Main features include:

- Centroid and enclosing circle computation
- Region/property measurements
- Statistical analysis of image regions

These functions are useful for image quantification and morphometric analysis.
"""

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...

# Note:
# ----
# - All `guidata.dataset.DataSet` parameter classes must also be imported
#   in the `sigima.params` module.
# - All functions decorated by `computation_function` must be imported in the upper
#   level `sigima.proc.image` module.

from __future__ import annotations

import numpy as np
from numpy import ma

import sigima.tools.image
from sigima.config import _
from sigima.objects import (
    GeometryResult,
    ImageObj,
    KindShape,
    SignalObj,
    TableKind,
    TableResult,
    TableResultBuilder,
)
from sigima.proc.base import new_signal_result
from sigima.proc.decorator import computation_function
from sigima.proc.image.base import compute_geometry_from_obj

# NOTE: Only parameter classes DEFINED in this module should be included in __all__.
# Parameter classes imported from other modules (like sigima.proc.base) should NOT
# be re-exported to avoid Sphinx cross-reference conflicts. The sigima.params module
# serves as the central API point that imports and re-exports all parameter classes.
__all__ = [
    "centroid",
    "enclosing_circle",
    "horizontal_projection",
    "stats",
    "vertical_projection",
]


def get_centroid_coords(data: np.ndarray) -> np.ndarray:
    """Return centroid coordinates
    with :py:func:`sigima.tools.image.get_centroid_auto`

    Args:
        data: input data

    Returns:
        Centroid coordinates
    """
    y, x = sigima.tools.image.get_centroid_auto(data)
    return np.array([(x, y)])


[docs] @computation_function() def centroid(image: ImageObj) -> GeometryResult | None: """Compute centroid with :py:func:`sigima.tools.image.get_centroid_fourier` Args: image: input image Returns: Centroid coordinates """ return compute_geometry_from_obj( "centroid", KindShape.MARKER, image, get_centroid_coords )
def get_enclosing_circle_coords(data: np.ndarray) -> np.ndarray: """Return diameter coords for the circle contour enclosing image values above threshold (FWHM) Args: data: input data Returns: Diameter coords """ x, y, r = sigima.tools.image.get_enclosing_circle(data) return np.array([[x, y, r]])
[docs] @computation_function() def enclosing_circle(image: ImageObj) -> GeometryResult | None: """Compute minimum enclosing circle with :py:func:`sigima.tools.image.get_enclosing_circle` Args: image: input image Returns: Diameter coords """ return compute_geometry_from_obj( "enclosing_circle", KindShape.CIRCLE, image, get_enclosing_circle_coords )
def __calc_snr_without_warning(data: np.ndarray) -> float: """Calculate SNR based on <z>/σ(z), ignoring warnings Args: data: input data Returns: Signal-to-noise ratio """ with np.errstate(divide="ignore", invalid="ignore"): snr = ma.mean(data) / ma.std(data) return snr
[docs] @computation_function() def stats(obj: ImageObj) -> TableResult: """Compute statistics on an image Args: obj: input image object Returns: Result properties """ builder = TableResultBuilder(_("Image statistics"), kind=TableKind.STATISTICS) builder.add(ma.min, "min") builder.add(ma.max, "max") builder.add(ma.mean, "mean") builder.add(ma.median, "median") builder.add(ma.std, "std") builder.add(__calc_snr_without_warning, "snr") builder.add(ma.ptp, "ptp") builder.add(ma.sum, "sum") return builder.compute(obj)
[docs] @computation_function() def horizontal_projection(image: ImageObj) -> SignalObj: """Compute the sum of pixel intensities along each col. (projection on the x-axis). Args: image: Input image object. Returns: Signal object containing the profile. """ dst_signal = new_signal_result( image, "horizontal_projection", units=(image.xunit, image.zunit), labels=(image.xlabel, image.zlabel), ) x = np.linspace(image.x0, image.x0 + image.width - image.dx, image.data.shape[1]) y = image.data.sum(axis=0, dtype=float) dst_signal.set_xydata(x, y) return dst_signal
[docs] @computation_function() def vertical_projection(image: ImageObj) -> SignalObj: """Compute the sum of pixel intensities along each row (projection on the y-axis). Args: image: Input image object. Returns: Signal object containing the profile. """ dst_signal = new_signal_result( image, "vertical_projection", units=(image.yunit, image.zunit), labels=(image.ylabel, image.zlabel), ) x = np.linspace(image.y0, image.y0 + image.height - image.dy, image.data.shape[0]) y = image.data.sum(axis=1, dtype=float) dst_signal.set_xydata(x, y) return dst_signal