import warnings
from typing import Any, List, Optional
import numpy as np
import PIL
import scipy.ndimage as ndi
import skimage.exposure
import skimage.io
import skimage.measure
from skimage.color import gray2rgb, rgb2gray
from skimage.util import dtype
from morphocut import Node, Output, RawOrVariable, ReturnOutputs, closing_if_closable
[docs]@ReturnOutputs
@Output("mask")
class ThresholdConst(Node):
"""
Calculate a mask by applying a constant threshold.
The result will be `image < threshold`.
Args:
image (np.ndarray or Variable): Image for which the mask is to be calculated.
threshold (Number or Variable): Threshold. Image intensities less than this will be `True` in the
result.
Returns:
Variable[np.ndarray]: Mask.
"""
def __init__(self, image: RawOrVariable, threshold: RawOrVariable):
super().__init__()
self.image = image
self.threshold = threshold
def transform(self, image):
if image.ndim != 2:
raise ValueError("image.ndim needs to be exactly 2.")
mask = image < self.threshold
return mask
[docs]@ReturnOutputs
@Output("rescaled")
class RescaleIntensity(Node):
"""
Rescale the intensities of the image.
.. note::
Uses the skimage library :py:func:`skimage.exposure.rescale_intensity`.
Args:
image (np.ndarray or Variable): An image file to be rescaled.
in_range ((str or 2-tuple) or Variable): min/max as the intensity range.
dtype (str or Variable): min/max of the image's dtype as the intensity range.
Returns:
Variable[np.ndarray]: Image with intensities rescaled.
"""
def __init__(
self, image: RawOrVariable, in_range: RawOrVariable = "image", dtype=None
):
super().__init__()
self.image = image
self.dtype = dtype
self.in_range = in_range
if dtype is not None:
self.out_range = dtype
else:
self.out_range = "dtype"
def transform(self, image, in_range):
image = skimage.exposure.rescale_intensity(
image, in_range=in_range, out_range=self.out_range
)
if self.dtype is not None:
image = image.astype(self.dtype, copy=False)
return image
[docs]class RegionProperties(
skimage.measure._regionprops.RegionProperties # pylint: disable=protected-access
):
"""
Like skimage.measure.RegionProperties but without storing the whole image.
Please refer to `skimage.measure.regionprops` for more information
on the available region properties.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._image = super().image
if self._intensity_image is not None:
self._image_intensity = super().image_intensity
else:
self._image_intensity = None
@property
def image(self):
return self._image
@property
def intensity_image(self):
if self._image_intensity is None:
raise AttributeError("No intensity image specified.")
return self._image_intensity
[docs]@ReturnOutputs
@Output("regionprops")
class FindRegions(Node):
"""
|stream| Find regions in a mask and calculate properties.
For more information see :py:func:`skimage.measure.regionprops`.
.. note::
This Node creates multiple objects per incoming object.
Args:
mask (np.ndarray or Variable): Mask of a given image.
image (np.ndarray or Variable): An image whose mask we have to find region with.
min_area (int): Minimum area of the region. If the area of our prop/region is
smaller than our min_area then it will discard it.
max_area (int): Maximum area of the region. If the area of our prop/region is
bigger than our max_area then it will discard it.
padding (int): Size of the slices/regions of our image.
warn_empty (bool or str or Variable): Warn for empty images (default false).
If a String is supplied, it is used as an identifier for the image.
Example:
.. code-block:: python
mask = ...
regionsprops = FindRegions(mask)
# regionsprops: A skimage.measure.regionsprops object.
"""
def __init__(
self,
mask: RawOrVariable,
image: RawOrVariable = None,
min_area=None,
max_area=None,
padding=0,
warn_empty=False,
):
super().__init__()
self.mask = mask
self.image = image
self.min_area = min_area
self.max_area = max_area
self.padding = padding
self.warn_empty = warn_empty
@staticmethod
def _enlarge_slice(slices, padding):
return tuple(slice(max(0, s.start - padding), s.stop + padding) for s in slices)
def transform_stream(self, stream):
with closing_if_closable(stream):
for obj in stream:
mask, image = self.prepare_input(obj, ("mask", "image"))
labels, nlabels = skimage.measure.label(mask, return_num=True)
objects = ndi.find_objects(labels, nlabels)
for i, slices in enumerate(objects):
if slices is None:
continue
if self.padding:
slices = self._enlarge_slice(slices, self.padding)
props = RegionProperties(slices, i + 1, labels, image, True)
if self.min_area is not None and props.area < self.min_area:
continue
if self.max_area is not None and props.area > self.max_area:
continue
yield self.prepare_output(obj.copy(), props)
if nlabels == 0:
if self.warn_empty is not False:
warn_empty = self.prepare_input(obj, "warn_empty")
if not isinstance(warn_empty, str):
warn_empty = "Image"
warnings.warn(f"{warn_empty} did not contain any objects.")
[docs]@ReturnOutputs
@Output("regionprops")
class ImageProperties(Node):
"""
Calculate region properties for an image containing a single object.
For more information see :py:func:`skimage.measure.regionprops`.
Args:
mask (np.ndarray or Variable): Mask of a given image.
image (np.ndarray or Variable): An image whose mask we have to find region with.
Example:
.. code-block:: python
image = ...
mask = image < 128
regionsprops = ImageProperties(mask, image)
# regionsprops: A skimage.measure.regionsprops object.
"""
def __init__(self, mask: RawOrVariable, image: RawOrVariable = None):
super().__init__()
self.mask = mask
self.image = image
def transform(self, mask: np.ndarray, image: np.ndarray):
return RegionProperties(
tuple(slice(0, s) for s in mask.shape), # Whole mask
True, # Where mask == True
mask,
image,
True,
)
[docs]@ReturnOutputs
class ImageStats(Node):
"""
Parse information from a path
"""
def __init__(self, image: RawOrVariable, name: str = ""):
super().__init__()
self.min = [] # type: List[Any]
self.max = [] # type: List[Any]
self.image = image
self.name = name
def transform(self, image):
self.min.append(np.min(image))
self.max.append(np.max(image))
def after_stream(self):
print("### Range stats ({}) ###".format(self.name))
mean_min = np.mean(self.min)
mean_max = np.mean(self.max)
print("Absolute: ", min(self.min), max(self.max))
print("Average: ", mean_min, mean_max)
[docs]@ReturnOutputs
@Output("image")
class ImageReader(Node):
"""
Read and open the image from a given path.
Use Python Imaging Library `PIL`_ to open the file from a given path.
.. _PIL: https://pillow.readthedocs.io/en/stable/
Args:
fp (file or Variable): A filename (string), pathlib.Path object or file object.
"""
def __init__(self, fp: RawOrVariable):
super().__init__()
self.fp = fp
def transform(self, fp):
return np.array(PIL.Image.open(fp))
[docs]@ReturnOutputs
class ImageWriter(Node):
"""
Write the image into the given directory path.
Use Python Imaging Library `PIL`_ to save the image in a given path.
.. _PIL: https://pillow.readthedocs.io/en/stable/
Args:
fp (file or Variable): A filename (string), pathlib.Path object or file object.
image (np.ndarray or Variable): Image that is to be saved into a given directory.
**kwargs: Arguments for :py:meth:`PIL.Image.Image.save`.
"""
def __init__(self, fp: RawOrVariable, image: RawOrVariable, **kwargs):
super().__init__()
self.fp = fp
self.image = image
self.kwargs = kwargs
def transform(self, fp, image):
img = PIL.Image.fromarray(image)
img.save(fp, **self.kwargs)
[docs]@ReturnOutputs
@Output("image")
class Gray2RGB(Node):
"""
Create an RGB representation of a gray-level image.
.. note::
Uses the skimage library :py:func:`skimage.color.gray2rgb` to convert from Grayscale to RGB.
Args:
image (numpy.ndarray or Variable): Gray-level input image.
Returns:
Variable[numpy.ndarray]: The RGB image:
An array which is the same size as the input array,
but with a channel dimension appended.
"""
def __init__(self, image: RawOrVariable[np.ndarray], keep_dtype=False):
super().__init__()
self.image = image
self.keep_dtype = keep_dtype
def transform(self, image):
result = gray2rgb(image)
if self.keep_dtype:
result = dtype.convert(result, image.dtype)
return result
[docs]@ReturnOutputs
@Output("image")
class RGB2Gray(Node):
"""
Compute luminance of an RGB image using :py:func:`skimage.color.rgb2gray`.
Args:
image (numpy.ndarray or Variable): The image in RGB format.
Returns:
Variable[numpy.ndarray]: The luminance image:
An array which is the same size as the input array,
but with the channel dimension removed and dtype=float.
"""
def __init__(self, image: RawOrVariable[np.ndarray], keep_dtype=False):
super().__init__()
self.image = image
self.keep_dtype = keep_dtype
def transform(self, image: np.ndarray):
if len(image.shape) != 3:
raise ValueError("image.shape != 3 in {!r}".format(self))
result = rgb2gray(image)
if self.keep_dtype:
result = dtype.convert(result, image.dtype)
return result