Source code for towbintools.classification.qc_tools

from typing import Optional
from typing import Union

import numpy as np
import pandas as pd
from csbdeep.utils import normalize
from skimage.measure import regionprops_table
from skimage.measure._regionprops import RegionProperties

from towbintools.foundation.image_handling import pad_images_to_same_dim
from towbintools.foundation.image_handling import read_tiff_file
from towbintools.foundation.image_quality import normalized_variance_measure


[docs] def get_all_skimage_regionprops(mask_only: bool = False) -> list[str]: """ Return all available scikit-image region-property names. Excludes image-array and coordinates properties. When ``mask_only`` is ``True``, intensity-based properties are also excluded so that the result can be used on masks without an intensity image. Parameters: mask_only (bool, optional): If ``True``, omit intensity-based properties (e.g. ``intensity_mean``, ``moments_weighted``). (default: False) Returns: list[str]: Names of the available region properties. """ features = [ attr for attr in dir(RegionProperties) if not attr.startswith("_") and isinstance(getattr(RegionProperties, attr), property) if "image" not in attr and "coords" not in attr ] # remove intensity based features if mask_only is True if mask_only: intensity_features = [ "centroid_weighted", "centroid_weighted_local", "intensity_min", "intensity_max", "intensity_mean", "intensity_median", "intensity_std", "moments_weighted", "moments_weighted_central", "moments_weighted_hu", "moments_weighted_normalized", ] features = [f for f in features if f not in intensity_features] return features
[docs] def compute_qc_features( mask: Union[str, np.ndarray], image: Union[str, np.ndarray, None], features: Optional[list[str]] = None, channels: Optional[list[int]] = None, ) -> Optional[pd.DataFrame]: """ Compute quality-control features from a mask and an optional intensity image. Accepts file paths or NumPy arrays for both ``mask`` and ``image``. Computes scikit-image region properties for the largest foreground object, plus a normalized-variance focus measure when an intensity image is provided. Parameters: mask (str or np.ndarray): Binary mask (path to TIFF or array). Pixels > 0 are treated as foreground. image (str, np.ndarray, or None): Intensity image (path to TIFF, array, or ``None`` for mask-only features). features (list[str], optional): Region-property names to compute. If ``None``, all properties returned by :func:`get_all_skimage_regionprops` are used. (default: None) channels (list[int], optional): Channel indices to load when ``image`` is a file path. (default: None) Returns: pd.DataFrame or None: DataFrame of computed features, or ``None`` if the mask is empty or an error occurs during extraction. """ try: if isinstance(image, str): image = read_tiff_file(image, channels_to_keep=channels) # skimage expects the channel dimension last, in our case it's first if the image is not a stack if image.ndim > 3: raise ValueError( "Input image has more than 3 dimensions, which is currently not supported." ) if isinstance(mask, str): mask = read_tiff_file(mask) mask = (mask > 0).astype(np.uint8) if np.max(mask) == 0: return None if features is None: features = get_all_skimage_regionprops( mask_only=image is None, ) if image is not None: # normalize image image = normalize(image, 1, 99, axis=None) if image.shape[-2:] != mask.shape[-2:]: image, mask = pad_images_to_same_dim(image, mask) if image.ndim == 3: image = np.transpose(image, (1, 2, 0)) props = regionprops_table( mask, intensity_image=image, properties=features, ) props_df = pd.DataFrame(props) other_features = { "NORMALIZED_VARIANCE_MEASURE": normalized_variance_measure(image), } other_features_df = pd.DataFrame([other_features]) props_df = pd.concat([props_df, other_features_df], axis=1) else: props = regionprops_table( mask, properties=features, ) props_df = pd.DataFrame(props) return props_df except Exception as e: print(f"Error during feature extraction: {e}") return None