Source code for ultrasound_metrics.metrics.sharpness

"""Image sharpness metrics, such as Tenengrad."""

from types import ModuleType
from typing import Optional

from array_api_compat import array_namespace
from beartype import beartype as typechecker
from jaxtyping import Bool, Num, jaxtyped

from .._utils.array_api import ArrayAPIObj, convolve2d


@jaxtyped(typechecker=typechecker)
[docs] def tenengrad( image: Num[ArrayAPIObj, "height width"], roi: Optional[Bool[ArrayAPIObj, "height width"]] = None, sobel_kernel: Optional[Num[ArrayAPIObj, "kernel_height kernel_width"]] = None, reduce_sum: bool = True, normalize: bool = False, ) -> ArrayAPIObj: """ Compute the Tenengrad sharpness metric of an image. Tenengrad sharpness [1][2] is a robust and accurate measure for focusing quality that performs well in digital photography. The metric applies a lateral Sobel filter to detect edges primarily in the horizontal direction, then sums the magnitude of the filtered response within a region of interest. This implementation is particularly suitable for ultrasound images where the lateral focusing quality is of primary interest. Parameters ---------- image Input image for sharpness calculation. Should be a 2D array. roi Binary mask defining the region of interest. If provided, only pixels where roi is True (or non-zero) will contribute to the sharpness metric. If None, the entire image is used. Can be boolean or numeric array. sobel_kernel Custom convolution kernel to use for edge detection. If None, uses the default lateral Sobel filter optimized for ultrasound applications [3]: ``` [ 1 0 -1] [ 2 0 -2] [ 1 0 -1] ``` Custom kernels should be 2D arrays with odd dimensions. reduce_sum If True (default), returns the summed Tenengrad value as a scalar. If False, returns the magnitude image after Sobel filtering for visualization. normalize If True and reduce_sum=True, returns the mean instead of sum to normalize by ROI size. This makes the metric independent of ROI area. Default is False. Returns ------- ndarray If reduce_sum=True: The Tenengrad sharpness value (scalar). Higher values indicate sharper images. If normalize=True, returns mean instead of sum. If reduce_sum=False: The magnitude image after Sobel filtering (same shape as input). Notes ----- The Tenengrad metric is computed as: .. math:: F = \\sum |G(r)| where :math:`G(r)` is the result of convolving the image with the lateral Sobel filter, and the sum is taken over all pixels r in the region of interest. For ultrasound applications, it's recommended to exclude near-field artifacts by using an ROI that begins at approximately 10mm depth. The Tenengrad metric was originally developed for autofocus [1]_ and has been extensively studied for digital photography [2]_. The lateral Sobel kernel used here follows the ultrasound-specific implementation in [3]_. References ---------- .. [1] Groen, F. C., Young, I. T., & Ligthart, G. (1985). A comparison of different focus functions for use in autofocus algorithms. Cytometry, 6(2), 81-91. .. [2] Mir, R. N., et al. (2014). An extensive study of focus measures for digital photography. In Proc. Int. Conf. Comput. Sci. Inf. Technol. .. [3] Vr˚alstad, A.E. et al. (2024). Coherence Based Sound Speed Aberration Correction — with clinical validation in fetal ultrasound. arXiv:2411.16551 """ # Get the array namespace xp: ModuleType = array_namespace(image, roi, sobel_kernel) # Validate input if image.ndim != 2: raise ValueError(f"Image must be 2D, got {image.ndim}D") # Use provided kernel or create default lateral Sobel filter if sobel_kernel is None: # Define the lateral Sobel filter kernel # This detects edges primarily in the horizontal direction sobel_kernel = xp.asarray([ [1.0, 0.0, -1.0], [2.0, 0.0, -2.0], [1.0, 0.0, -1.0], ]) else: # Validate custom kernel if sobel_kernel.ndim != 2: raise ValueError(f"Sobel kernel must be 2D, got {sobel_kernel.ndim}D") kernel_h, kernel_w = sobel_kernel.shape if kernel_h % 2 == 0 or kernel_w % 2 == 0: raise ValueError(f"Sobel kernel dimensions must be odd, got {kernel_h}x{kernel_w}") # Apply convolution using backend-specific implementations filtered_image = convolve2d(image, sobel_kernel) # Take absolute value (magnitude) magnitude = xp.abs(filtered_image) if roi is not None: magnitude = magnitude[roi] # This gives us a 1D array of just ROI pixels if not reduce_sum: return magnitude # Compute scalar metric if normalize: sharpness = xp.mean(magnitude) else: sharpness = xp.sum(magnitude) return sharpness