Source code for nanomesh.image._image

from __future__ import annotations

import operator
import os
from pathlib import Path
from typing import Any, Callable, Dict, Union

import numpy as np
from skimage import filters

from .._doc import DocFormatterMeta, doc
from ..io import load_vol

_threshold_dispatch = {
    'isodata': filters.threshold_isodata,
    'li': filters.threshold_li,
    'local': filters.threshold_local,
    'mean': filters.threshold_mean,
    'minimum': filters.threshold_minimum,
    'multiotsu': filters.threshold_multiotsu,
    'niblack': filters.threshold_niblack,
    'otsu': filters.threshold_otsu,
    'sauvola': filters.threshold_sauvola,
    'triangle': filters.threshold_triangle,
    'yen': filters.threshold_yen,
}


def _normalize_values(image: np.ndarray):
    """Rescale values to 0.0 to 1.0.

    Parameters
    ----------
    image : (m, n) numpy.ndarray
        Input image.

    Returns
    -------
    out : (m, n) np.ndarray
        Normalized image
    """
    out = (image - image.min()) / (image.max() - image.min())
    return out


[docs]@doc(prefix='Generic image class', shape='') class Image(object, metaclass=DocFormatterMeta): """{prefix}. Depending on the dimensions of the image data, the appropriate subclass will be chosen if possible. Parameters ---------- image : {shape}numpy.array N-dimensional numpy array containing image data. Attributes ---------- image : {shape}numpy.ndarray The raw image data """ _registry: Dict[int, Any] = {} def __init_subclass__(cls, ndim: int, **kwargs): super().__init_subclass__(**kwargs) cls._registry[ndim] = cls def __new__(cls, image: np.ndarray): subclass = cls._registry.get(image.ndim, cls) return super().__new__(subclass) def __init__(self, image: np.ndarray): self.image = image def __repr__(self): """Canonical string representation.""" return (f'{self.__class__.__name__}(shape={self.image.shape}, ' f'range=({self.image.min()},{self.image.max()}), ' f'dtype={self.image.dtype})') def __eq__(self, other): if isinstance(other, self.__class__): return np.all(other.image == self.image) elif isinstance(other, np.ndarray): return np.all(other == self.image) else: return False def __gt__(self, other): return self._compare(other, op=operator.gt) def __lt__(self, other): return self._compare(other, op=operator.lt) def __ge__(self, other): return self._compare(other, op=operator.ge) def __le__(self, other): return self._compare(other, op=operator.le) def _compare(self, other, *, op: Callable): """Helper function to implement overload functions. Parameters ---------- other : Other instance, can be a numpy array. op : callable Operator (see :mod:`operator` module). Returns ------- {classname} Image with boolean data. """ this = self.image if isinstance(other, self.__class__): other = other.image return self.__class__(op(this, other))
[docs] def astype(self, dtype: str | np.dtype) -> np.ndarray: """Shortcut for :meth:`np.ndarray.astype`.""" return self.__class__(self.image.astype(dtype))
[docs] @classmethod def load(cls, filename: os.PathLike, **kwargs) -> 'Image': """Load the data. Supported filetypes: `.npy`, `.vol`. For memory mapping, use `mmap_mode='r'`. Memory-mapped files are used for accessing small segments of large files on disk, without reading the entire file into memory. Note that this can still result in some slow / unexpected behaviour with some operations. More info: :func:`numpy.memmap` Parameters ---------- filename : PathLike Name of the file to load. **kwargs These parameters are passed on to data readers. Returns ------- {classname} Instance of :class:`{classname}`. Raises ------ IOError Raised if the file extension is unknown. """ filename = Path(filename) suffix = filename.suffix.lower() if suffix == '.npy': array = np.load(filename, **kwargs) elif suffix == '.vol': array = load_vol(filename, **kwargs) else: raise IOError(f'Unknown file extension: {suffix}') return cls(array)
[docs] def to_sitk_image(self): """Return instance of :class:`SimpleITK.Image` from :meth:`{classname}.image`.""" import SimpleITK as sitk return sitk.GetImageFromArray(self.image)
[docs] @classmethod def from_sitk_image(cls, sitk_image) -> 'Image': """Return instance from :class:`SimpleITK.Image`.""" import SimpleITK as sitk image = sitk.GetArrayFromImage(sitk_image) return cls(image)
[docs] def save(self, filename: os.PathLike): """Save the data. Supported filetypes: `.npy`. Parameters ---------- filename : Pathlike Name of the file to save to. """ np.save(filename, self.image)
[docs] def apply(self, function: Callable, **kwargs): """Apply function to :attr:`{classname}.image` array. Return an instance of :class:`{classname}` if the result is of the same dimensions, otherwise return the result of the operation. Parameters ---------- function : callable Function to apply to :attr:`{classname}.image`. **kwargs Keyword arguments to pass to `function`. Returns ------- {classname} New instance of :class:`{classname}`. """ ret = function(self.image, **kwargs) if isinstance(ret, np.ndarray) and (ret.ndim == self.image.ndim): return self.__class__(ret) return ret
[docs] def gaussian(self, sigma: int = 1, **kwargs): """Apply Gaussian blur to image. Parameters ---------- sigma : int Standard deviation for Gaussian kernel. **kwargs These parameters are passed to :func:`skimage.filters.gaussian`. Returns ------- {classname} New instance of :class:`{classname}`. """ from skimage import filters return self.apply(filters.gaussian, sigma=sigma, **kwargs)
[docs] def digitize(self, bins: Union[list, tuple], **kwargs): """Digitize image. For more info see :func:`numpy.digitize`. Parameters ---------- bins : list, tuple List of bin values. Must be monotonic and one-dimensional. **kwargs These parameters are passed to :func:`numpy.digitize`. Returns ------- {classname} New instance of :class:`{classname}`. """ return self.apply(np.digitize, bins=bins, **kwargs)
[docs] def normalize_values(self): """Rescale values to 0.0 to 1.0. Returns ------- out : {classname} Normalized image """ return self.apply(_normalize_values)
[docs] def invert_contrast(self): """Invert the contrast of the image. Returns ------- out : {classname} Inverted image """ return self.apply(lambda arr: arr.max() - arr)
[docs] def binary_digitize(self, threshold: Union[float, str] = None): """Convert into a binary image. Parameters ---------- threshold : float, optional Threshold used for segmentation. If given as a string, apply corresponding theshold via :meth:`{classname}.threshold`. Defaults to `median`. Returns ------- {classname} New instance of :class:`{classname}`. """ if not threshold: threshold_value = np.median(self.image) elif isinstance(threshold, str): threshold_value = self.threshold(threshold) else: threshold_value = threshold return self.apply(np.digitize, bins=[threshold_value])
[docs] def threshold(self, method: str = 'otsu', **kwargs) -> float: """Compute threshold value using given method. For more info, see :mod:`skimage.filters` Parameters ---------- method : str Thresholding method to use. Defaults to `otsu`. **kwargs These parameters are passed to threshold method. Returns ------- threshold : float Threshold value. """ try: func = _threshold_dispatch[method] except KeyError: raise KeyError( f'`method` must be one of {_threshold_dispatch.keys()}') return self.apply(func, **kwargs)
[docs] def fft(self) -> 'Image': """Apply fourier transform to image. Returns ------- {classname} Real component of fourier transform with the zero-frequency component shifted to the center of the spectrum. """ fourier = np.fft.fftn(self.image) shifted = np.abs(np.fft.fftshift(fourier)) return self.__class__(shifted)