#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
# Copyright (c) 2022 Univ. Lyon, ENS de Lyon, UCB Lyon 1, CNRS, INRAe, Inria
# All rights reserved.
# This file is part of the TimageTK library, and is released under the "GPLv3"
# license. Please see the LICENSE.md file that should have been included as
# part of this package.
# ------------------------------------------------------------------------------
"""MultiChannel images module."""
import copy as cp
import numpy as np
from pint import UnitRegistry
from skimage import img_as_float
from skimage import img_as_ubyte
from skimage import img_as_uint
from timagetk.bin.logger import get_logger
from timagetk.components.metadata import ImageMetadata
from timagetk.components.metadata import Metadata
from timagetk.components.metadata import ProcessMetadata
from timagetk.components.spatial_image import DEFAULT_SIZE_UNIT
from timagetk.util import _to_list
from timagetk.util import compute_extent
from timagetk.util import compute_voxelsize
from timagetk.util import dimensionality_test
log = get_logger(__name__)
DEF_4D_AXES = "ZYXC"
DEFAULT_AXIS_ORDER_4D = {ax: ax_id for ax_id, ax in enumerate(DEF_4D_AXES)}
DEF_CHANNEL_COLORS = ['red', 'green', 'blue', 'yellow', 'cyan', 'magenta']
[docs]
def check_images_channels(image, channel_names):
"""Check that there is the same number of images and channel names.
Parameters
----------
image : list of str or list of timagetk.SpatialImage
A list of image names, file names or instances.
channel_names : list of str
A list of channel names.
Raises
------
ValueError
If the length of the two lists is not the same.
"""
try:
assert len(image) == len(channel_names)
except AssertionError:
msg = f"Please provide the same number of images ({len(image)}) and channel names ({len(channel_names)})!"
raise ValueError(msg)
return
[docs]
def check_image_shape(image):
"""Check that all images have the same shape.
Parameters
----------
image : dict of timagetk.SpatialImage
A dictionary of ``SpatialImage``.
Raises
------
ValueError
If not all images are of same shape.
"""
ref_ch = list(image.keys())[0]
ref_shape = image[ref_ch].shape
try:
assert all(ref_shape == img.shape for _, img in image.items())
except AssertionError:
msg = "All images should have the same shape!\n"
for ch, img in image.items():
msg += f" - Channel '{ch}' has a shape of {img.shape}\n"
raise ValueError(msg)
return
[docs]
def check_image_voxelsize(image, rtol=1e-5):
"""Check that all images have the same voxel size.
Parameters
----------
image : dict of SpatialImage
A dictionary of SpatialImage.
rtol : float, optional
The relative tolerance parameter, *i.e.* the maximum relative difference allowed.
Raises
------
ValueError
If not all images are of same voxel size.
"""
ref_ch = list(image.keys())[0]
ref_vxs = image[ref_ch].voxelsize
try:
assert all(np.allclose(ref_vxs, img.voxelsize, rtol=rtol) for _, img in image.items())
except AssertionError as err:
log.error(err)
msg = "All images should have the same voxelsize!\n"
for ch, img in image.items():
msg += f" - Channel '{ch}' has a voxelsize of {img.voxelsize}\n"
raise ValueError(msg)
return
[docs]
def check_image_axes_order(image):
"""Check that all images have the same ``axes_order`` attribute.
Parameters
----------
image : dict of SpatialImage
A dictionary of SpatialImage.
Raises
------
ValueError
If not all images have the same ``axes_order`` attribute.
"""
ref_ch = list(image.keys())[0]
ref_axes_order = image[ref_ch].axes_order_dict
try:
assert all(ref_axes_order == img.axes_order_dict for _, img in image.items())
except AssertionError as err:
log.error(err)
msg = "All images should have the same axes ordering!\n"
for ch, img in image.items():
msg += f" - Channel '{ch}' has an axes ordering of {img.voxelsize}\n"
raise ValueError(msg)
return
[docs]
def chnames_generator(n_imgs):
"""Generate channel names as `CH_i` with `i` in range(n_imgs).
Parameters
----------
n_imgs : int
Number of images or channels to generate names for.
Examples
--------
>>> from timagetk.components.multi_channel import chnames_generator
>>> chnames_generator(5)
['CH_0', 'CH_1', 'CH_2', 'CH_3', 'CH_4']
"""
return [f"CH_{i}" for i in range(n_imgs)]
from collections import UserDict
[docs]
class MultiChannelImage(UserDict):
"""Multichannel image data structure.
Attributes
----------
name : str
The name of the multichannel image.
channel_names : list of str
The list of channel names.
metadata : Metadata
A metadata instance for the multichannel image.
extent : list of float
The physical extent of the image, sorted as (planes,) rows & columns [(Z,) Y, X].
origin : list of int
The coordinates of origin, sorted as (planes,) rows & columns [(Z,) Y, X].
voxelsize : list of float
The size of the pixels or voxels (2D/3D), sorted as (planes,) rows & columns [(Z,) Y, X].
ndim : int
The number of dimensions for each channel.
filepath : str
The directory where to find the image, if any.
filename : str
The file name of the image.
Notes
-----
TODO: Add a ``metric_unit`` attribute and associated methods to handle this!
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_url = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> # IO example #1 - `imread` method can read CZI and return a MultiChannelImage:
>>> im = imread(im_url, channel_names=ch_names)
>>> im.channel_names
>>> isinstance(im, MultiChannelImage)
True
>>> from timagetk.visu.stack import orthogonal_view
>>> orthogonal_view(im.to_blend_image())
>>> # IO example #2 - MultiChannelImage can be instantiated from a filename:
>>> im = MultiChannelImage(im_url, channel_names=ch_names, czi_pattern='.C.ZXY')
>>> print(im)
>>> # Access the channels as you would with a dictionary:
>>> print(im["DII-VENUS-N7"])
SpatialImage object with following metadata:
- shape: (65, 1024, 1024)
- ndim: 3
- dtype: uint16
- origin: [0, 0, 0]
- voxelsize: (6.771450737227882e-07, 2.075664593629475e-07, 2.075664593629475e-07)
- extent: [4.4014429791981236e-05, 0.00021254805438765823, 0.00021254805438765823]
>>> # Access the channels using the `get_channel` method:
>>> ahp6_img = im.get_channel("AHP6")
>>> # Get the name of the multichannel image:
>>> print(im.name)
qDII-CLV3-PIN1-PI-E35-LD-SAM4
>>> # Add or change the name of the multichannel image:
>>> im.set_name("my_experiment1_rep2")
>>> print(im.name)
my_experiment1_rep2
>>> from timagetk.visu.stack import channels_stack_browser
>>> channels_stack_browser(im, im.name)
"""
[docs]
def __init__(self, image, channel_names=None, metadata=None, **kwargs):
"""Constructor.
Parameters
----------
image : str or list of str or list of SpatialImage or dict
If an `str`, should be a multichannel image file name.
If a `list of str`, should be a list of single channel file names.
Lists of SpatialImage are also accepted, they should be of same shape.
If a ``dict``, keys are used as ``channel_names`` and values should be ``SpatialImage``.
channel_names : list of str, optional
List of names to use as channel names.
metadata : dict, optional
Dictionary of image metadata. An empty dictionary by default.
Other Parameters
----------------
name : str
Name of the multichannel image.
czi_pattern : str
Pattern used to read the CZI file, ``".C.ZXY."`` by default.
filepath : str
The directory where to find the image, if any.
filename : str
The file name of the image.
"""
from timagetk.components.image import assert_spatial_image
from timagetk.io.image import imread
if isinstance(image, (list, tuple)):
# Generates channel names if not defined:
if channel_names is None:
channel_names = chnames_generator(len(image))
# Check the number of images and channels:
self._check_images_channels(image, channel_names)
# Load the image if a string (filepath or URL) else assume it's a SpatialImage:
image = [imread(img) if isinstance(img, str) else img for img in image]
# Assert we now have a list of SpatialImage...
[assert_spatial_image(im, 'Channel image #{}') for im in image]
# Generate the {channel_name: image} dictionary:
image = dict(zip(channel_names, image))
else:
assert isinstance(image, dict)
channel_names = list(image.keys())
super().__init__(image)
# -- Make sure images are...
# ... of identical shapes...
self._check_image_shape(image)
# ... have the same voxel sizes...
self._check_image_voxelsize(image)
# ... and axes ordering
self._check_image_axes_order(image)
# -- Initialize attributes:
self.channel_names = None
self.n_ch = None
# - Physical image attributes:
self.voxelsize = None
self.shape = None
self.ndim = None
self.extent = None
self.origin = None
self.dtype = None
self.axes_order = None
# - File attributes:
self.filename = None
self.filepath = None
self.name = None
# - Metadata attributes:
self._metadata = Metadata({})
self.metadata_image = None
self.metadata_acquisition = None
self.metadata_processing = None
# -- Set attributes values:
self.update_attributes(image, channel_names, **kwargs)
def update_attributes(self, image, channel_names, **kwargs):
# -- Add attributes:
self.channel_names = channel_names
self.n_ch = len(self.channel_names)
ref_img = list(image.values())[0]
# - Physical image attributes:
self.voxelsize = ref_img.voxelsize
self.shape = ref_img.shape
self.extent = ref_img.extent
self.origin = ref_img.origin
self.dtype = ref_img.dtype
self.ndim = ref_img.ndim + 1
# - Defines axes order:
axes_order_dict = ref_img.axes_order_dict
# Add `channels` as last axes
axes_order_dict.update({'C': max(axes_order_dict.values()) + 1})
if self.ndim == 4 and axes_order_dict != DEFAULT_AXIS_ORDER_4D:
log.warning(f"You have a non-default 3D multichannel image axes ordering: {axes_order_dict}")
self.axes_order = ref_img.axes_order + 'C'
self.axes_order_dict = axes_order_dict
# - File attributes:
self.filename = kwargs.get("filename", '')
self.filepath = kwargs.get("filepath", '')
self.name = kwargs.get("name", self.filename)
# - Metadata attributes:
metadata = kwargs.get('metadata', self._metadata._dict)
self._metadata = Metadata(metadata)
self.metadata_image = {ch: ImageMetadata(im, origin=self.origin, voxelsize=self.voxelsize) for ch, im in
image.items()}
self.metadata_acquisition = {}
self.metadata_processing = {'class': 'MultiChannelImage'}
[docs]
def __str__(self):
msg = f"MultiChannelImage '{self.name}' has {len(self)} channels: "
msg += ", ".join(self.channel_names)
msg += "\n * "
msg += '\n * '.join(
[f'{ch_name}: {img}' for ch_name, img in self.items()])
return msg
@staticmethod
def _check_images_channels(image, channel_names):
"""Check that we have the same number of images and channel names.
Parameters
----------
image : list
A list of image name or object.
channel_names : list
A list of channel names.
Raises
------
ValueError
If the length of the two lists is not the same.
"""
check_images_channels(image, channel_names)
@staticmethod
def _check_image_shape(image):
"""Check that all images have the same shape.
Parameters
----------
image : dict of SpatialImage
A dictionary of SpatialImage.
Raises
------
ValueError
If not all images are of same shape.
"""
check_image_shape(image)
@staticmethod
def _check_image_voxelsize(image):
"""Check that all images have the same voxel-size.
Parameters
----------
image : list of SpatialImage
A list of SpatialImage.
Raises
------
ValueError
If not all images are of same voxel-size.
"""
check_image_voxelsize(image)
@staticmethod
def _check_image_axes_order(image):
"""Check that all images have the same ``axes_order`` attribute.
Parameters
----------
image : list of SpatialImage
A list of SpatialImage.
Raises
------
ValueError
If not all images have the same ``axes_order`` attribute.
"""
check_image_axes_order(image)
# --------------------------------------------------------------------------
#
# SpatialImage properties:
#
# --------------------------------------------------------------------------
@property
def metadata(self):
"""Get ``MultiChannelImage`` metadata dictionary.
Returns
-------
dict
Metadata dictionary.
"""
md = {}
if isinstance(self.metadata_image, ImageMetadata):
md.update(self.metadata_image.get_dict())
if isinstance(self.metadata_acquisition, Metadata):
acq = self.metadata_acquisition.get_dict()
if acq != {}:
md['acquisition'] = acq
if isinstance(self.metadata_processing, ProcessMetadata):
proc = self.metadata_processing.get_dict()
if proc != {}:
md['processing'] = proc
self._metadata.update(md)
return cp.copy(self._metadata)
@metadata.setter
def metadata(self, img_md):
"""Update ``MultiChannelImage`` metadata dictionary."""
self._metadata.update(img_md)
# --------------------------------------------------------------------------
#
# Conversion:
#
# --------------------------------------------------------------------------
[docs]
def get_array(self, return_order=None):
"""Return a NumPy array.
Parameters
----------
return_order : str, optional
Order of the dimension for returned array, same as the ``axes_order`` attribute by default.
Returns
-------
numpy.ndarray
The array that correspond to the requested axes ordering.
Examples
--------
>>> # Example #1: Use a dummy multichannel image:
>>> from timagetk.array_util import dummy_multichannel_image
>>> img = dummy_multichannel_image()
>>> print(img.axes_order)
>>> arr = img.get_array()
>>> print(arr.shape) # same as the ``axes_order`` attribute
(5, 11, 11, 2)
>>> arr = img.get_array("CXYZ") # specify the axes return order
>>> print(arr.shape)
(2, 11, 11, 5)
>>> # Example #2: Use a real multichannel image (downloaded from Zenodo.org):
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> img = imread(im_fname, channel_names=ch_names)
>>> arr = img.get_array()
>>> print(arr.shape)
(40, 1024, 1024, 4)
>>> arr = img.get_array("CXYZ")
>>> print(arr.shape)
(4, 1024, 1024, 40)
"""
ch_names = self.channel_names
# - Stack the channel along a new axis (in last position):
arr = np.stack([self[ch].get_array() for ch in ch_names], axis=-1)
if return_order is None:
return_order = "".join(list(self.axes_order_dict.keys()))
axis = [self.axes_order_dict[c.upper()] for c in return_order]
log.debug(f"Found axes transposition to apply: {axis}")
arr = arr.transpose(axis)
return arr
# --------------------------------------------------------------------------
#
# Array methods :
#
# --------------------------------------------------------------------------
[docs]
def invert_axis(self, axis):
"""Revert given axis for all channels.
Parameters
----------
axis : str in {'x', 'y', 'z'}
Axis to invert, can be either 'x', 'y' or 'z' (if 3D)
Returns
-------
MultiChannelImage
Image with reverted array for selected axis.
Raises
------
ValueError
If given `axis` is not in {'x', 'y', 'z'} for 3D images or not in
{'x', 'y'} for 2D images.
Example
-------
>>> from timagetk.array_util import random_spatial_image
>>> from timagetk import MultiChannelImage
>>> # Initialize a random (uint8) 3D SpatialImage with one z-slice:
>>> img_a = random_spatial_image((3, 5, 5), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> img_b = random_spatial_image((3, 5, 5), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> img = MultiChannelImage([img_a, img_b])
>>> img.get_channel_names()
['CH_0', 'CH_1']
>>> print(img_a.get_array())
>>> img.invert_axis('z')
>>> print(img['CH_0'].get_array())
"""
for ch in self.channel_names:
self[ch] = self[ch].invert_axis(axis)
return
[docs]
def is_isometric(self):
"""Test image isometry.
Tests if the voxelsize values are the same for every axis or not.
Returns
-------
is_iso : bool
True if the image is isometric, else ``False``.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.is_isometric()
False
"""
vxs = [self.voxelsize[0]] * self.ndim
return np.allclose(vxs, self.voxelsize, atol=1.e-9)
[docs]
def is2D(self):
"""Return True if the SpatialImage is 2D, else ``False``.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.is2D()
False
>>> # Take the first z-slice:
>>> img2d = img.get_slice(0, 'z') # Equivalent to img[0]
>>> img2d.is2D()
True
"""
return self.get_channel(self.get_channel_names()[0]).ndim == 2
[docs]
def is3D(self):
"""Return True if the SpatialImage is 3D, else ``False``.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.is3D()
True
"""
return self.get_channel(self.get_channel_names()[0]).ndim == 3
[docs]
def transpose(self, *axes):
"""Permute image axes to given order, reverse by default.
Parameters
----------
axes : list of int or list of str, optional
By default, reverse the dimensions, otherwise permute the axes according to the values given.
Returns
-------
SpatialImage
The image with permuted axes.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> img = imread(im_fname, channel_names=ch_names)
>>> print(img.shape)
(5, 4, 3)
>>> img_t = img.transpose()
>>> # Transpose update the shape attribute of the image (here reversed):
>>> print(img_t.shape)
(5, 4, 3)
>>> # Transpose update the voxelsize attribute of the image (here reversed):
>>> print(img_t.voxelsize)
[0.53, 0.52, 0.51]
>>> # Transpose update the metadata dictionary of the image:
>>> print(img_t.metadata)
>>> # -- Transpose accept axe names as input:
>>> img_t = img.transpose('xyz')
>>> print(img_t.shape)
(5, 4, 3)
>>> img_t = img.transpose('x', 'y', 'z')
>>> print(img_t.shape)
(5, 4, 3)
"""
ch_imgs = {}
for ch in self.channel_names:
ch_imgs[ch] = self[ch].transpose(*axes)
return MultiChannelImage(ch_imgs, name=self.name,
filename=self.filename,
filepath=self.filepath,
metadata=self.metadata.get_dict())
# --------------------------------------------------------------------------
#
# GETTERs & SETTERs:
#
# --------------------------------------------------------------------------
[docs]
def set_name(self, name):
"""Set the name of the MultiChannelImage."""
self.name = name
[docs]
def set_channel_name(self, old, new):
"""Change the name of a channel."""
self[new] = self.pop(old)
self.channel_names = list(self.keys())
[docs]
def get_channel(self, channel_name):
"""Return the intensity image for given channel.
Parameters
----------
channel_name : str
Name of the channel to returns.
Returns
-------
SpatialImage
The intensity image for given channel.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> im = imread(im_fname, channel_names=ch_names)
>>> # Access the channels as you would with a dictionary:
>>> print(im["CLV3"])
SpatialImage object with following metadata:
- shape: (1024, 1024, 40)
- ndim: 3
- dtype: uint16
- origin: [0, 0, 0]
- voxelsize: [0.208, 0.208, 1.171]
- extent: [212.992, 212.992, 46.84]
>>> # Access the channels using the `get_channel` method:
>>> ahp6_img = im.get_channel("AHP6")
>>> print(ahp6_img)
SpatialImage object with following metadata:
- shape: (1024, 1024, 40)
- ndim: 3
- dtype: uint16
- origin: [0, 0, 0]
- voxelsize: [0.208, 0.208, 1.171]
- extent: [212.992, 212.992, 46.84]
"""
return self[channel_name]
[docs]
def get_channel_names(self):
"""Return the list of channel names.
Returns
-------
list
The list of channel names.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ["qDII", "AHP6", "tagBFP", "CLV3"]
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.get_channel_names()
['qDII', 'AHP6', 'tagBFP', 'CLV3']
"""
return cp.copy(self.channel_names)
[docs]
def get_extent(self, axis=None):
"""Return the get_extent of the given axis.
Parameters
----------
axis : str, in {'x', 'y', 'z'}
Axis for which the get_extent should be returned.
Returns
-------
list of float or float
The get_extent of the axis.
Notes
-----
It is related to the array shape and image voxelsize.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ["qDII", "AHP6", "tagBFP", "CLV3"]
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.get_extent()
[43.337284718258445, 212.3404879282953, 212.3404879282953]
>>> img.get_extent('x')
212.3404879282953
"""
if axis is None:
return cp.copy(self.extent)
else:
return cp.copy(self.extent[self.axes_order_dict[axis.upper()]])
[docs]
def get_voxelsize(self, axis=None):
"""Return the voxelsize of the image or of an axis.
Parameters
----------
axis : {'x', 'y', 'z'}, optional
If specified, axis to use for voxel-size, else the voxel-size of the image.
Returns
-------
list or float
The voxel-size of the image or of the axis (if specified).
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ["qDII", "AHP6", "tagBFP", "CLV3"]
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.get_voxelsize()
[0.6771450737227882, 0.2075664593629475, 0.2075664593629475]
>>> img.get_voxelsize('z')
0.6771450737227882
"""
if axis is None:
return list(cp.copy(self.voxelsize))
else:
try:
assert axis.lower() in ['x', 'y', 'z']
except AssertionError:
raise ValueError(
"Parameter `axis` take values in {'x', 'y', 'z'}!")
except AttributeError:
raise ValueError("Parameter `axis` should be string!")
return cp.copy(self.voxelsize[self.axes_order_dict[axis.upper()]])
[docs]
def get_shape(self, axis=None):
"""Return the shape of the image or of an axis.
Parameters
----------
axis : str in {'x', 'y', 'z', 'c'}, optional
If specified, axis to use for shape dimension, else the shape of the image
Returns
-------
list or int
The shape of the array or an integer if an axis is specified.
Examples
--------
>>> from timagetk import MultiChannelImage
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ["qDII", "AHP6", "tagBFP", "CLV3"]
>>> img = imread(im_fname, channel_names=ch_names)
>>> img.get_shape()
[65, 1024, 1024, 4]
>>> img.get_shape('z')
65
>>> img.get_shape('c')
4
"""
if axis is None:
return list(cp.copy(self.shape)) + [self.n_ch]
else:
try:
assert axis.lower() in ['x', 'y', 'z', 'c']
except AssertionError:
raise ValueError(
"Parameter `axis` take values in {'x', 'y', 'z', 'c'}!")
except AttributeError:
raise ValueError("Parameter `axis` should be string!")
if axis.lower() == 'c':
return self.n_ch
else:
return cp.copy(self.shape[self.axes_order_dict[axis.upper()]])
[docs]
def get_slice(self, slice_id, axis='z'):
"""Return a MultiChannelImage with only one slice for given axis.
Parameters
----------
slice_id : int
Slice to return
axis : int or str in {'x', 'y', 'z'}, optional
Axis to use for slicing, default is 'z'.
Returns
-------
MultiChannelImage
2D MultiChannelImage with only the required slice
Raises
------
ValueError
If the image is not 3D and ``axis='z'``.
If ``slice_id`` does not exist, *i.e.* should satisfy: ``0 < slice_id < max(len(axis))``.
Examples
--------
>>> # Example #1: Use a dummy multichannel image:
>>> from timagetk.array_util import dummy_multichannel_image
>>> img = dummy_multichannel_image()
>>> img_z = img.get_slice(1, 'z')
>>> print(img_z.shape)
(11, 11)
>>> print(img_z.axes_order_dict)
{'Y': 0, 'X': 1, 'C': 2}
>>> # Taking an existing x-slice from a 3D image works fine:
>>> img_x = img.get_slice(3, 'x')
>>> print(img_x.axes_order_dict)
{'Z': 0, 'Y': 1, 'C': 2}
>>> # Example #2: Use a real multichannel image (downloaded from Zenodo.org):
>>> from timagetk.io import imread
>>> im_fname = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ["qDII", "AHP6", "tagBFP", "CLV3"]
>>> img = imread(im_fname, channel_names=ch_names)
>>> # Taking an existing z-slice from a 3D image works fine:
>>> img_z = img.get_slice(1, 'z')
>>> img_z.is2D()
True
>>> img_z.is3D()
False
>>> # Taking an existing x-slice from a 3D image works fine:
>>> img_x = img.get_slice(3, 'x')
>>> print(img_x.axes_order_dict)
{'Z': 0, 'Y': 1, 'C': 2}
>>> # Down sampling x-axis of a 3D image:
>>> img_ds_x2 = img.get_slice(range(0, img.get_shape('x'), 2), 'x')
>>> img_ds_x2.is3D()
True
>>> print(img.get_shape('x'))
1024
>>> print(img_ds_x2.get_shape('x'))
512
>>> print(img.voxelsize)
[1.1712524251243286e-06, 2.075664593629475e-07, 2.075664593629475e-07]
>>> print(img_ds_x2.voxelsize)
[1.1712524251243286e-06, 2.075664593629475e-07, 4.15132918725895e-07]
>>> # Taking an NON-existing z-slice from a 3D image raises an error:
>>> img.get_slice(50, 'z')
>>> # Taking a z-slice from a 2D image raises an error:
>>> img_z.get_slice(5, 'z')
"""
ch_names = self.get_channel_names()
md = self.metadata.get_dict()
name = self.name
filename = self.filename
filepath = self.filepath
ch_sl = {ch: self[ch].get_slice(slice_id, axis=axis) for ch in ch_names}
return MultiChannelImage(ch_sl, metadata=md, name=name, channel_names=ch_names,
filepath=filepath, filename=filename)
[docs]
def to_blend_image(self, **kwargs):
"""Return an RGB BlendImage object used for plotting.
Other Parameters
----------------
colors : list of str, optional
List of colormap to apply to each image
rtype : str in {'uint8', 'uint16', 'float64'}
Type of array to return, `'uint8'` by default.
Returns
-------
BlendImage
The RGB image resulting in the blending of channels.
Examples
--------
>>> from timagetk.io.image import imread
>>> # We use a freely available multichannel image from an online database
>>> url = "https://zenodo.org/record/3737795/files/qDII-CLV3-PIN1-PI-E35-LD-SAM4.czi"
>>> ch_names = ['DII-VENUS-N7', 'pPIN1:PIN1-GFP', 'Propidium Iodide', 'pRPS5a:TagBFP-SV40', 'pCLV3:mCherry-N7']
>>> ch_colors = ['yellow', 'green', 'red', 'blue', 'purple']
>>> mch_img = imread(url, channel_names=ch_names)
>>> rgb_img = mch_img.to_blend_image(colors=ch_colors, rtype='uint8')
>>> from timagetk.visu.stack import orthogonal_view
>>> orthogonal_view(rgb_img)
"""
return BlendImage(
*[self.get_channel(ch) for ch in self.get_channel_names()],
**kwargs)
[docs]
def random_blend_image(size, n_spimg=2, **kwargs):
"""Generate a random ``BlendImage`` from ``SpatialImage``.
Parameters
----------
size : iterable
Shape of the image to generate.
ZYX sorted if 3D else YX sorted.
n_spimg : int, optional
Number of random SpatialImage to generate to create
Other Parameters
----------------
origin : list
Coordinates of the origin in the image, default: [0, 0] or [0, 0, 0]
voxelsize : list
Image voxelsize, default: [1.0, 1.0] or [1.0, 1.0, 1.0]
dtype : str or numpy.dtype
If specified, default is ``np.float32``, change the dtype of the returned image.
Returns
-------
BlendImage
A random blend image from a list of random spatial images.
See Also
--------
timagetk.components.spatial_image.random_spatial_image
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Example 1 - Generates a BlendImage using 3D SpatialImages:
>>> blend_img = random_blend_image([5, 10, 10], voxelsize=[1., 0.5, 0.5])
>>> type(blend_img)
timagetk.components.multi_channel.BlendImage
>>> blend_img.shape # the last dimension is of size 3, for RGB
(5, 10, 10, 3)
>>> blend_img.axes_order # the last dimension is RGB
'ZYXB'
>>> blend_img.is3D()
True
>>> # Example 2 - Generates a BlendImage using 2D SpatialImages:
>>> blend_img = random_blend_image([10, 10], voxelsize=[0.5, 0.5])
>>> blend_img.shape # the last dimension is of size 3, for RGB
(10, 10, 3)
>>> blend_img.is2D()
True
"""
from timagetk.array_util import random_spatial_image as rsi
return BlendImage(*[rsi(size, **kwargs) for n in range(n_spimg)])
[docs]
class BlendImage(np.ndarray):
"""RGB image class used to represent blended images like ``MultiChannelImage``.
Attributes
----------
_extent : list of float
The physical extent of the image, sorted as (planes,) rows & columns [(Z,) Y, X].
_origin : list of int
The coordinates of origin, sorted as (planes,) rows & columns [(Z,) Y, X].
_voxelsize : list of float
The size of the pixels or voxels (2D/3D), sorted as (planes,) rows & columns [(Z,) Y, X].
axes_order : dict
Dictionary of axes orders, *e.g.* {'x': 2, 'y': 1, 'z': 0} by default for 3D image.
"""
[docs]
def __new__(cls, *images, **kwargs):
"""Blend image constructor.
YX sorted if images are 2D, ZYX sorted if images are 3D.
Parameters
----------
images : numpy.ndarray or list of SpatialImage
SpatialImages to blend.
If a ``numpy.ndarray``, last dimension should be of size 3 (RGB dim).
Other Parameters
----------------
axes_order : str
The physical axes ordering to use with this array.
Defaults to ``default_axes_order(array)``.
origin : list of int
The coordinate of the origin for this array.
Defaults to ``default_origin(array)``.
unit : float
The units associated to the voxel-sizes to use with this array.
Defaults to ``DEFAULT_SIZE_UNIT``.
voxelsize : list of float
The voxel-sizes to use with this array.
Defaults to ``default_voxelsize(array)``.
metadata : dict
The dictionary of metadata to use with this array.
Defaults to ``{}``.
colors : list of str
List of color names to apply to each image, they can be one of the 140 standard HTML colors or an
hexadecimal value like `'#ff0000'` for 'red'.
rtype : {'uint8', 'uint16', 'float64'}
Type of RGB array to return, set to the type of the first image on the list by default.
See Also
--------
timagetk.visu.util.DEF_CHANNEL_COLORS
timagetk.visu.util.combine_channels
Examples
--------
>>> from timagetk.components.multi_channel import BlendImage
>>> # Example 1 - Initialize with some random 3D RGB array:
>>> from timagetk.array_util import random_array
>>> rand_image = random_array([7, 15, 15, 3], dtype='uint8')
>>> rand_image.shape
(7, 15, 15, 3)
>>> blend_img = BlendImage(rand_image, axes_order='ZYXB')
>>> blend_img.shape
(7, 15, 15, 3)
>>> blend_img.axes_order
'ZYXB'
>>> blend_img.voxelsize # Unspecified, so set to default value
[1.0, 1.0, 1.0]
>>> # Example 2 - Initialize with some random SpatialImages
>>> from timagetk.array_util import random_spatial_image
>>> imgA = random_spatial_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> imgB = random_spatial_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img = BlendImage(imgA, imgB)
>>> blend_img.shape # the last dimension is of size 3, for RGB
(5, 10, 10, 3)
>>> blend_img.dtype
dtype('uint8')
>>> # Example 3 - Initialize from an RGB numpy.ndarray slice
>>> import numpy as np
>>> rgb_arr2d = np.array(blend_img[3, :, :, :])
>>> type(rgb_arr2d)
numpy.ndarray
>>> blend_img2d = BlendImage(rgb_arr2d, voxelsize=blend_img.voxelsize[1:])
>>> blend_img2d.voxelsize
[0.5, 0.5]
"""
from timagetk.components.spatial_image import default_axes_order
from timagetk.components.spatial_image import default_origin
from timagetk.components.spatial_image import default_voxelsize
from timagetk.components.image import assert_spatial_image
from timagetk.io.image import default_image_attributes
from timagetk.util import check_type
from timagetk.util import clean_type
# If the input tuple is of size 1, reduce it to its first element!
if len(images) == 1:
images = images[0]
log.debug(f"Got an `{clean_type(images)}` instance as `images` input!")
# Get attributes from `BlendImage` if not overriden at instance call:
if isinstance(images, BlendImage):
if kwargs.get('axes_order', None) is None:
kwargs['axes_order'] = images.axes_order
if kwargs.get('origin', None) is None:
kwargs['origin'] = images.origin
if kwargs.get('unit', None) is None:
kwargs['unit'] = images.unit
if kwargs.get('voxelsize', None) is None:
kwargs['voxelsize'] = images.voxelsize
# Get the list of colors to use for blending:
colors = kwargs.get('colors', None)
if colors is None:
colors = DEF_CHANNEL_COLORS
if isinstance(images, np.ndarray):
# Need to check physical axes ordering:
axes_order = kwargs.get('axes_order', None)
if axes_order is None:
axes_order = default_axes_order(images) + 'B'
log.warning(f"Undefined parameter ``axes_order``, using default: {axes_order}")
try:
assert 'B' in axes_order
except AssertionError:
axes_order += 'B'
log.warning("Blend axis ('B') is not defined in parameter ``axes_order``.")
log.info(f"Defaulted to `{axes_order}` physical axes ordering!")
log.debug(f"Got an `{axes_order}` sorted RGB array as input for `blendImage`.")
kwargs['axes_order'] = axes_order
# Check array dimensionality and the size oth the blend axe:
cls._check_input_rgb_array(images, axes_order.index('B'))
# Now "rename" the array & update the missing attributes (if any):
array = images
kwargs |= default_image_attributes(array, **kwargs)
else:
# Check we have enough color to blend each image
try:
assert len(images) <= len(colors)
except AssertionError:
raise ValueError("Not enough colors to blend provided images!")
# Check we have SpatialImage instance:
[assert_spatial_image(img) for img in images]
log.debug("Got a list of `SpatialImage` instance as `images` input!")
# Check images shape & voxelsize:
cls._check_image_shape(images)
cls._check_image_voxelsize(images)
cls._check_image_axes_order(images)
# TODO: we could re-order if not all the same
# TODO: Check units!
# Re-order to given 'axes_order' if any::
axes_order = kwargs.get('axes_order', None)
if axes_order is not None and isinstance(axes_order, str):
axo = axes_order.replace('B', '')
if images[0].is3D():
try:
assert len(axo) == 3
except AssertionError:
raise ValueError("Input `axes_order` do not match for a 3D image!")
else:
try:
assert len(axo) == 2
except AssertionError:
raise ValueError("Input `axes_order` do not match for a 2D image!")
images = [img.transpose(axo) for img in images]
# Combining channel:
rtype = kwargs.get('rtype', images[0].dtype.name)
kwargs['axes_order'] = images[0].axes_order + 'B' # Blend/RGB is last axe
kwargs['origin'] = images[0].origin
kwargs['unit'] = images[0].unit
kwargs['voxelsize'] = images[0].voxelsize
array = _combine_channels(images, colors=colors, rtype=rtype)
# - View casting (see class documentation) of the array as a SpatialImage subclass:
# NOTE: it converts the array to given dtype if not None:
# This call ``__array_finalize__``!
obj = np.asarray(array).view(cls)
# ----------------------------------------------------------------------
# Physical axes ordering:
# ----------------------------------------------------------------------
axes_order = kwargs.get('axes_order', None)
if axes_order is None:
axes_order = default_axes_order(array) + 'B' # Blend/RGB is last axe
log.warning(f"Undefined parameter ``axes_order``, using default: {axes_order}")
else:
log.debug(f"Defined parameter ``axes_order`` as: {axes_order}")
# ----------------------------------------------------------------------
# Origin:
# ----------------------------------------------------------------------
origin = kwargs.get('origin', None)
if origin is None:
origin = default_origin(array)
log.warning(f"Undefined parameter ``origin``, using default: {origin}")
else:
log.debug(f"Defined parameter ``origin`` as: {origin}")
# ----------------------------------------------------------------------
# Size unit:
# ----------------------------------------------------------------------
unit = kwargs.get('unit', None)
if unit is None:
unit = DEFAULT_SIZE_UNIT
log.warning(f"Undefined parameter ``unit``, using default: {unit}")
else:
log.debug(f"Defined parameter ``unit`` as: {unit}")
# ----------------------------------------------------------------------
# Voxel size:
# ----------------------------------------------------------------------
voxelsize = kwargs.get('voxelsize', None)
if voxelsize is None:
voxelsize = default_voxelsize(array)
log.warning(f"No ``voxelsize`` given, using default: {voxelsize}")
else:
voxelsize = list(map(float, voxelsize))
log.debug(f"Defined parameter ``voxelsize`` as: {voxelsize}")
# ----------------------------------------------------------------------
# Metadata:
# ----------------------------------------------------------------------
metadata = kwargs.get('metadata', None)
# - Initialize (empty) or check type of given metadata dictionary:
if metadata is None:
metadata = {}
log.debug("No metadata dictionary provided!")
else:
check_type(metadata, 'metadata', dict)
# - Set 'voxelsize', 'origin', 'unit' & 'extent' hidden attributes:
obj._axes_order = axes_order
obj._voxelsize = voxelsize
obj._origin = origin
obj._extent = compute_extent(voxelsize, array.shape[:-1])
obj._unit = unit
obj._unit_reg = UnitRegistry() # initialize unit registry
return obj
[docs]
def __array_finalize__(self, obj):
"""Mechanism provided by numpy to allow subclasses instanciation.
Parameters
----------
obj : object
The object returned by the __new__ method.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> type(blend_img)
timagetk.components.multi_channel.BlendImage
>>> # Taking the first z-slice automatically return a BlendImage instance:
>>> blend_img2d = blend_img[3, :, :, :][0]
>>> type(blend_img2d)
timagetk.components.multi_channel.BlendImage
"""
self._axes_order = getattr(obj, '_axes_order', None)
self._origin = getattr(obj, '_origin', None)
self._voxelsize = getattr(obj, '_voxelsize', None)
self._unit_reg = getattr(obj, '_unit_reg', None)
self._unit = getattr(obj, '_unit', None)
self._extent = getattr(obj, '_extent', None)
@staticmethod
def _check_input_rgb_array(array, blend_axis_idx):
"""Check the requirements for an RGB array as input."""
# - The last dimension should be the RGB axe of size 3
try:
assert array.shape[blend_axis_idx] == 3
except AssertionError:
msg = f"Input RGB array do NOT have a size 3 axis at the specified index ({blend_axis_idx})!"
msg += f"Got an array of 'shape': {array.shape}"
raise ValueError(msg)
# - Test input array dimensionality, should be of dimension 2 or 3:
try:
assert array.ndim in [3, 4]
except AssertionError:
msg = "Input 'RGB array' must be 2D or 3D (excluding the last axis)!"
for attr in ['ndim', 'shape']:
try:
msg += "Got '{}': {}".format(attr, getattr(array, attr))
except:
pass
raise ValueError(msg)
@staticmethod
def _check_image_axes_order(images):
"""Check that all images have the same physical axes ordering.
Parameters
----------
images : list of SpatialImage
A list of SpatialImage.
Raises
------
ValueError
If not all images have the same physical axes ordering.
"""
check_image_axes_order(dict(zip(chnames_generator(len(images)), images)))
@staticmethod
def _check_image_shape(images):
"""Check that all images have the same shape.
Parameters
----------
images : list of SpatialImage
A list of SpatialImage.
Raises
------
ValueError
If not all images are of same shape.
"""
check_image_shape(dict(zip(chnames_generator(len(images)), images)))
@staticmethod
def _check_image_voxelsize(images):
"""Check that all images have the same voxelsize.
Parameters
----------
images : list of SpatialImage
A list of SpatialImage.
Raises
------
ValueError
If not all images are of same voxelsize.
"""
check_image_voxelsize(dict(zip(chnames_generator(len(images)), images)))
# --------------------------------------------------------------------------
#
# BlendImage properties:
#
# --------------------------------------------------------------------------
@property
def axes_order(self):
"""Get ``BlendImage`` physical axes ordering.
Returns
-------
str
``BlendImage`` physical axes ordering.
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> print(blend_img.axes_order)
ZYX
"""
return cp.copy(self._axes_order)
@property
def axes_order_dict(self):
"""Get ``BlendImage`` physical axes ordering.
Returns
-------
dict
``BlendImage`` physical axes ordering.
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> print(blend_img.axes_order_dict)
{'Z': 0, 'Y': 1, 'X': 2}
"""
return {ax: axi for axi, ax in enumerate(self._axes_order)}
@property
def extent(self):
"""Get ``BlendImage`` physical extent.
Returns
-------
list
``BlendImage`` physical extent.
Notes
-----
It is related to the array shape and image voxelsize.
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> print(blend_img.extent)
[3.0, 2.5, 2.5]
"""
return cp.copy(self._extent)
@extent.setter
def extent(self, img_extent):
"""Set ``BlendImage`` physical extent.
Parameters
----------
img_extent : list
``BlendImage`` new physical extent.
Notes
-----
Do NOT use this method except if you know EXACTLY what you are doing!
This will change voxelsize based on array shape.
Metadata are updated according to the new physical extent and voxelsize.
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5],dtype='uint8')
>>> print(blend_img.voxelsize)
[1.0, 0.5, 0.5]
>>> blend_img.extent = [10., 10., 10.]
>>> print(blend_img.extent)
[10., 10., 10.]
>>> print(blend_img.voxelsize)
[2.0, 1.0, 1.0]
"""
log.warning("You are changing a physical property of the image!")
log.warning("This should not happen!")
log.warning("You better know what you are doing!")
dimensionality_test(self.ndim - 1, img_extent)
# - Update 'extent' hidden attribute:
self._extent = _to_list(img_extent)
# - Update 'voxelsize' hidden attribute:
self._voxelsize = compute_voxelsize(self.get_shape(), self.extent)
log.info("Set extent to '{}'".format(self.extent))
log.info("Changed voxelsize to '{}'".format(self.voxelsize))
return
@property
def origin(self):
"""Get ``BlendImage`` origin.
Returns
-------
list
``BlendImage`` origin coordinates
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5],dtype='uint8')
>>> print(blend_img.origin)
[0, 0, 0]
"""
return cp.copy(self._origin)
@origin.setter
def origin(self, img_origin):
"""Set ``BlendImage`` origin.
Given list should be of same length than the image dimensionality.
Parameters
----------
img_origin : list
``BlendImage`` origin coordinates,
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5],dtype='uint8')
>>> print(blend_img.origin)
[0, 0, 0]
>>> blend_img.origin = [2, 2, 2]
>>> print(blend_img.origin)
[2, 2, 2]
"""
log.warning("You are changing a physical property of the image!")
log.warning("This should not happen!")
log.warning("You better know what you are doing!")
dimensionality_test(self.ndim - 1, img_origin)
# - Update hidden attribute 'origin':
self._origin = _to_list(img_origin)
log.info("Set origin to '{}'".format(self.origin))
return
@property
def unit(self):
"""Return the unit in International System of Units (SI).
Returns
-------
float
The unit in International System of Units (SI).
"""
return cp.copy(self._unit)
@unit.setter
def unit(self, unit):
"""Set the unit of the image size, in International System of Units (SI).
Parameters
----------
unit : int or float
The unit, in meters, associated to the image.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> print(blend_img.get_unit()) # default unit is micrometer
'micrometer'
>>> blend_img.unit = 1e-3 # change to millimeter
>>> print(blend_img.get_unit())
'millimeter'
"""
self._unit = unit
@property
def voxelsize(self):
"""Get ``BlendImage`` voxelsize, sorted as planes, columns & rows.
Returns
-------
list
``BlendImage`` voxelsize
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5],dtype='uint8')
>>> print(blend_img.voxelsize)
[1., 0.5, 0.5]
"""
return cp.copy(self._voxelsize)
@voxelsize.setter
def voxelsize(self, img_vxs):
"""Change ``BlendImage`` voxelsize by preserving 'extent'.
Parameters
----------
img_vxs : list
Image new voxelsize.
Notes
-----
Do NOT use this method except if you know EXACTLY what you are doing!
This will change voxelsize based on array shape.
Metadata are updated according to the new physical extent and voxelsize.
Example
-------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> print(blend_img.voxelsize)
[1., 0.5, 0.5]
>>> print(blend_img.extent)
[5.0, 5.0, 5.0]
>>> # - Change blend_img voxelsize:
>>> blend_img.voxelsize = [0.5, 0.5, 0.5]
>>> print(blend_img.voxelsize)
[0.5, 0.5, 0.5]
>>> print(blend_img.extent)
[2.5, 5.0, 5.0]
"""
log.warning("You are changing a physical property of the image!")
log.warning("This should not happen!")
log.warning("You better know what you are doing!")
dimensionality_test(self.ndim - 1, img_vxs)
# - Update 'voxelsize' hidden attribute:
self._voxelsize = _to_list(img_vxs)
# - Update 'extent' hidden attribute:
self._extent = compute_extent(self.voxelsize, self.get_shape())
log.info("Set voxelsize to '{}'".format(self.voxelsize))
log.info("Changed extent to '{}'".format(self.extent))
return
# --------------------------------------------------------------------------
#
# BlendImage methods :
#
# --------------------------------------------------------------------------
[docs]
def is_isometric(self):
"""Test image isometry.
Tests if the voxelsize values are the same for every axis or not.
Returns
-------
is_iso : bool
True if the image is isometric, else ``False``.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5],dtype='uint8')
>>> blend_img.is_isometric()
False
>>> blend_img.voxelsize = [1., 1., 1.]
>>> blend_img.is_isometric()
True
"""
vxs = [self.voxelsize[0]] * (self.ndim - 1)
return np.allclose(vxs, self.voxelsize, atol=1.e-9)
[docs]
def is2D(self):
"""Return True if the BlendImage is 2D, else ``False``.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.is2D()
False
>>> # Take the first z-slice:
>>> blend_img2d = blend_img.get_slice(0, 'z') # Equivalent to img[0]
>>> blend_img2d.is2D()
True
"""
return self.ndim - 1 == 2
[docs]
def is3D(self):
"""Return True if the BlendImage is 3D, else ``False``.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.is3D()
True
"""
return self.ndim - 1 == 3
# --------------------------------------------------------------------------
#
# GETTERs & SETTERs:
#
# --------------------------------------------------------------------------
def _get_unit(self):
return (self._unit * self._unit_reg.meter).to_compact()
[docs]
def get_unit(self):
"""Get the human-readable image unit in International System of Units (SI)."""
return str(self._get_unit().u)
[docs]
def get_dtype(self):
"""Get the human-readable image type.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.get_dtype()
'uint8'
"""
return self.dtype.name
[docs]
def get_array(self, dtype=None):
"""Return a copy of the image as a ``numpy.ndarray`` object.
Parameters
----------
dtype : str or numpy.dtype, optional
If specified, default is ``None``, change the dtype of the returned array.
Returns
-------
numpy.ndarray
Numpy array linked to the object (Z)YX sorted.
Notes
-----
Changing the `dtype` of the array force the return of a copy.
"""
return np.asarray(self, dtype=dtype).copy()
[docs]
def get_voxelsize(self, axis=None):
"""Return the voxelsize of the image or of an axis.
Parameters
----------
axis : {'x', 'y', 'z'}, optional
If specified, axis to use for voxel-size, else the voxel-size of the image.
Returns
-------
list or float
The voxel-size of the image or of the axis (if specified).
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.get_voxelsize()
[1. , 0.5, 0.5]
>>> blend_img.get_voxelsize('z')
1.0
"""
if axis is None:
return list(cp.copy(self.voxelsize))
else:
try:
assert axis.lower() in ['x', 'y', 'z']
except AssertionError:
raise ValueError(
"Parameter `axis` take values in {'x', 'y', 'z'}!")
except AttributeError:
raise ValueError("Parameter `axis` should be string!")
return cp.copy(self.voxelsize[self.axes_order_dict[axis.upper()]])
[docs]
def get_extent(self, axis=None):
"""Return the get_extent of the given axis.
Parameters
----------
axis : str, in {'x', 'y', 'z'}
Axis for which the get_extent should be returned.
Returns
-------
list of float or float
The get_extent of the axis.
Notes
-----
It is related to the array shape and image voxelsize.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.get_extent()
[4. , 4.5, 4.5]
>>> blend_img.get_extent('x')
4.5
>>> blend_img.get_extent('y')
4.5
>>> blend_img.get_extent('z')
4.0
"""
if axis is None:
return cp.copy(self.extent)
else:
return cp.copy(self.extent[self.axes_order_dict[axis.upper()]])
[docs]
def get_shape(self, axis=None):
"""Return the shape of the image or of an axis.
Parameters
----------
axis : str in {'x', 'y', 'z'}, optional
If specified, axis to use for shape dimension, else the shape of the image
Returns
-------
list or int
The shape of the array or an integer if an axis is specified.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.get_shape()
[3, 5, 5]
>>> blend_img.get_shape('z')
5
>>> blend_img.get_shape('x')
10
"""
if axis is None:
return list(cp.copy(self.shape[:-1]))
else:
try:
assert axis.lower() in ['x', 'y', 'z']
except AssertionError:
raise ValueError(
"Parameter `axis` take values in {'x', 'y', 'z'}!")
except AttributeError:
raise ValueError("Parameter `axis` should be string!")
return cp.copy(self.shape[self.axes_order_dict[axis.upper()]])
[docs]
def get_slice(self, slice_id, axis='z'):
"""Return a ``BlendImage`` with only one slice for given axis.
Parameters
----------
slice_id : int or Iterable
Index of the slice to return.
axis : int or str in {'x', 'y', 'z'}, optional
Axis to use for slicing, default is 'z'.
Returns
-------
BlendImage
2D BlendImage with only the required slice.
Raises
------
ValueError
If the image is not 3D and ``axis='z'``.
If ``slice_id`` does not exist, *i.e.* should satisfy: ``0 < slice_id < max(len(axis))``.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D SpatialImage:
>>> blend_img = random_blend_image((5, 10, 10), voxelsize=[1., 0.5, 0.5], dtype='uint8')
>>> blend_img.axes_order_dict
{'Z': 0, 'Y': 1, 'X': 2, 'B': 3}
>>> blend_img.get_shape()
[5, 10, 10]
>>> # Taking an existing z-slice from a 3D image works fine:
>>> blend_img_z = blend_img.get_slice(1, 'z')
>>> blend_img_z.axes_order_dict
{'Y': 0, 'X': 1, 'B': 2}
>>> blend_img_z.shape
(10, 10, 3)
>>> blend_img_z.get_shape()
[10, 10]
>>> # Taking an existing x-slice from a 3D image works fine:
>>> blend_img_x = blend_img.get_slice(3, 'x')
>>> blend_img_x.get_shape()
[5, 10]
>>> # Down-sampling x-axis of a 3D image:
>>> nx = blend_img.get_shape('x')
>>> img_ds_x2 = blend_img.get_slice(range(0, nx, 2), 'x')
>>> img_ds_x2.get_shape()
[5, 10, 5]
>>> # Taking an NON-existing z-slice from a 3D image raises an error:
>>> blend_img.get_slice(50, 'z')
ValueError: Required z-slice (50) does not exists (max: 5)
>>> # Taking a z-slice from a 2D image raises an error:
>>> blend_img_z.get_slice(5, 'z')
"""
if isinstance(axis, str):
axis_id = self.axes_order_dict[axis.upper()]
else:
axis_id = cp.copy(axis)
axis = {v: k for k, v in self.axes_order_dict.items()}[axis]
if axis.lower() == 'z':
# Make sure we have a 3D image:
try:
assert self.is3D()
except AssertionError:
raise ValueError("Can only extract z-slice from 3D images!")
# If a list with just one integer, ``slice_id`` should be an integer:
if isinstance(slice_id, list) and len(slice_id) == 1:
slice_id = slice_id[0]
# Make sure this slice exists:
max_slice = self.get_shape(axis)
if isinstance(slice_id, int):
try:
assert slice_id <= max_slice
except AssertionError:
msg = "Required {}-slice ({}) does not exists (max: {})"
raise ValueError(msg.format(axis, slice_id, max_slice))
else:
try:
assert np.max(slice_id) <= max_slice
except AssertionError:
msg = "Required {}-slice range ({}-{}) does not exists (max: {})"
raise ValueError(
msg.format(axis, np.min(slice_id), np.max(slice_id), max_slice))
arr = np.take(self, slice_id, axis=axis_id)
vxs = self.voxelsize
ori = self.origin
axo = self.axes_order
# If ``slice_id`` is an integer, one axis will "disappear", its info should thus be removed:
if isinstance(slice_id, int):
# Remove value corresponding to returned slice axis:
vxs.pop(axis_id)
ori.pop(axis_id)
axo = axo.replace(axis.upper(), '')
else:
# Assume we have a constant compression ratio along the selected axis:
ratio = self.get_shape(axis) / arr.shape[axis_id]
vxs[axis_id] = vxs[axis_id] * ratio
axo = axo
log.debug(f"Slice shape: {arr.shape}")
log.debug(f"Slice ``origin``: {ori}")
log.debug(f"Slice ``voxelsize``: {vxs}")
log.debug(f"Slice ``axes_order``: {axo}")
return BlendImage(arr, origin=ori, voxelsize=vxs, axes_order=axo, unit=self.unit)
[docs]
def transpose(self, *axes):
"""Permute image axes to given order, reverse by default.
Parameters
----------
axes : list of int or list of str, optional
By default, reverse the dimensions, otherwise permute the axes according to the values given.
Returns
-------
BlendImage
The image with permuted axes.
Examples
--------
>>> from timagetk.components.multi_channel import random_blend_image
>>> # Initialize a random (uint8) 3D BlendImage:
>>> blend_img = random_blend_image((5, 6, 7), voxelsize=[1., 0.51, 0.52], dtype='uint8')
>>> blend_img.axes_order
'ZYXB'
>>> blend_img_t = blend_img.transpose()
>>> blend_img_t.axes_order
'XYZB'
>>> # Transpose update the shape attribute of the image (here reversed):
>>> print(img_t.get_shape())
[7, 6, 5]
>>> # Transpose update the voxelsize attribute of the image (here reversed):
>>> print(img_t.get_voxelsize())
[0.52, 0.51, 1.0]
>>> # -- Transpose accept axe names as input:
>>> blend_img_t = blend_img.transpose('xzy') # transpose ZYXB to XZYB
>>> blend_img_t.axes_order
'XZYB'
>>> print(img_t.get_shape())
[7, 6, 5]
>>> img_t = blend_img.transpose('x', 'y', 'z') # transpose ZYXB to XYZB
>>> print(img_t.shape)
[7, 6, 5]
"""
# Handle order change for X, Y & Z dims
if axes == ():
# If no axes order is specified, reverse the axes.
axes = list(range(self.ndim - 1))[::-1]
else:
if len(axes) == 1 and isinstance(axes[0], str) and len(axes[0]) == self.ndim - 1:
# If axes are given as str, translate them to "axe ids":
axes = [self.axes_order_dict[axe.upper()] for i, axe in enumerate(axes[0])]
elif isinstance(axes, (list, tuple)) and all(isinstance(axe, str) for axe in axes):
# If axes are given as list of str, translate them to "axe ids":
axes = [self.axes_order_dict[axe.upper()] for axe in axes]
else:
raise ValueError(f"Could not make sense of given `axes`: {axes}")
# Uses ``axes`` to reorder the origin and voxelsize attributes:
ori = [self.origin[axe] for axe in axes]
vxs = [self.voxelsize[axe] for axe in axes]
axo = ''.join([self.axes_order[axe] for axe in axes])
# Transpose the array:
arr = np.transpose(self.get_array(), axes + [max(axes) + 1]) # add the RGB as last dimension
return BlendImage(arr, origin=ori, voxelsize=vxs, axes_order=axo, unit=self.unit)
[docs]
def label_blending(label, image, colors=None, alpha=0.3, bg_label=1, bg_color=(0, 0, 0)):
"""Return an RGB image where color-coded labels are painted over the image.
This is a shorter version of 'skimage.color' module 'label2rgb' function.
Parameters
----------
label : numpy.ndarray, shape (M, N [, P])
Integer array of labels with the same shape as `image`.
image : numpy.ndarray, shape (M, N [, P])
Image used as underlay for labels.
colors : list, optional
List of colors. If the number of labels exceeds the number of colors, then the colors are cycled.
alpha : float [0, 1], optional
Opacity of colorized labels. Ignored if image is ``None``.
bg_label : int, optional
Label that's treated as the background.
bg_color : str or array, optional
Background color. Must be a name in `color_dict` or RGB float values between [0, 1].
Returns
-------
BlendImage
The result of blending a cycling colormap (`colors`) for each distinct value in `label` with the image, at a certain alpha value.
An array of float of shape (M, N, 3)
Notes
-----
Compared to the `label2rgb` function of ``skimage``, we set the `image_alpha` to `1 - alpha`.
Examples
--------
>>> from timagetk import LabelledImage
>>> from timagetk.components.multi_channel import label_blending
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_dataset
>>> from timagetk.visu.stack import rgb_stack_browser
>>> from timagetk.visu.stack import stack_panel
>>> int_path = shared_dataset("sphere", 'intensity')[0]
>>> seg_path = shared_dataset("sphere", 'segmented')[0]
>>> int_img = imread(int_path)
>>> seg_img = LabelledImage(imread(seg_path), not_a_label=0)
>>> blend = label_blending(seg_img, int_img)
>>> rgb_stack_browser(blend, "Intensity & segmentation blending", val_range=[None, None])
>>> stack_panel(blend, step=10, suptitle="Intensity & segmentation blending", val_range=[None, None])
"""
from skimage.color import gray2rgb
from skimage.color import label2rgb
from timagetk.components.image import assert_spatial_image
assert_spatial_image(label)
assert_spatial_image(image)
if label.ndim == 3:
label_rgb = label2rgb(label, colors=colors, alpha=1, bg_label=bg_label,
bg_color=bg_color)
image_rgb = gray2rgb(img_as_float(image))
return BlendImage(label_rgb * alpha + image_rgb * (1 - alpha))
else:
return BlendImage(label2rgb(label, image, colors=colors, alpha=alpha,
bg_label=bg_label, bg_color=bg_color,
image_alpha=1 - alpha))
def _combine_channels(images, colors=None, rtype='uint8'):
"""Return an RGB image combining the given channels after RGB coloring.
Parameters
----------
images : list of numpy.ndarray or list of SpatialImage
List of grayscale images to blend
colors : list of str, optional
List of color names to apply to each image, they can be one of the 140
standard HTML colors or an hexadecimal value like '#ff0000' for 'red'.
rtype : str in {'uint8', 'uint16', 'float64'}
Type of array to return, 'uint8' by default.
Returns
-------
numpy.ndarray
An RGB array obtained by combining each image with their assigned ``colors``, of shape (M[, N][, P], 3).
"""
from skimage.color import gray2rgb
from timagetk.visu.util import sum_no_overflow
from timagetk.visu.util import color_code
try:
assert rtype in ['uint8', 'uint16', 'float64']
except AssertionError:
raise ValueError(f"Return type is wrong: {rtype}.")
else:
log.debug(f"Selected return type: {rtype}!")
if isinstance(images, tuple):
images = list(images)
if not isinstance(images, list):
raise TypeError(f"Got a {type(images)} for `images` instead of a list!")
n_img = len(images)
if colors is None:
colors = DEF_CHANNEL_COLORS
n_colors = len(colors)
try:
assert n_colors >= n_img
except AssertionError:
msg = f"Not enough colors ({n_colors}) to blend images ({n_img})!"
raise ValueError(msg)
ctype = images[0].get_dtype() # current dtype
# Initialize an empty RGB array (of same type than the first image of the list):
blend = np.zeros_like(gray2rgb(images[0]))
log.debug(f"Initialize an empty blend array of shape {blend.shape}!")
log.debug(f"Initialize an empty blend array of type {blend.dtype}!")
# Loop trough channel to "blend":
# 1. to RGB array
# 2. apply colormap
# 3. add them to blend RGB array
for n_img, img in enumerate(images):
log.debug(f"Adding image #{n_img} with '{colors[n_img]}' colormap...")
c_arr = color_code(colors[n_img])
if ctype == 'uint8':
type_conv = img_as_ubyte
elif ctype == 'uint16':
type_conv = img_as_uint
else:
type_conv = img_as_float
log.debug(f"Converted '{colors[n_img]}' colormap to {c_arr} color array")
blend = sum_no_overflow(blend, type_conv(np.multiply(c_arr, img_as_float(gray2rgb(img)))))
# Convert returned type:
if rtype == 'uint8' and ctype != 'uint8':
blend = img_as_ubyte(blend)
elif rtype == 'uint16' and ctype != 'uint16':
blend = img_as_uint(blend)
elif rtype == 'float64' and ctype != 'float64':
blend = img_as_float(blend)
return blend
[docs]
def combine_channels(images, colors=None, rtype='uint8', **kwargs):
"""Return an RGB image combining the given channels after RGB coloring.
Example of available ``colors`` are: 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta'.
Parameters
----------
images : list(array), shape (M[, N][, P])
List of grayscale images to blend
colors : list of str, optional
List of color names to apply to each image, they can be one of the 140
standard HTML colors or an hexadecimal value like '#ff0000' for 'red'.
rtype : str in {'uint8', 'uint16', 'float64'}
Type of array to return, 'uint8' by default.
Returns
-------
BlendImage
An RGB array obtained by combining each image with their assigned ``colors``, of shape (M[, N][, P], 3).
See Also
--------
timagetk.visu.util.DEF_CHANNEL_COLORS
timagetk.visu.util.color_code
Notes
-----
Beware that channels with co-localized signals will lead to "new color" by
combination, *e.g.* by combinations of red and green, yellow will appear.
Matplotlib method ``imshow`` accept ``float`` or ``uint8`` data types for
RGB images.
In this method we use a ``float64`` array to combine the images.
Since this method is mostly used for image representation, it is converted
back to ``uint8`` in order to save some memory.
Examples
--------
>>> from timagetk.synthetic_data.wall_image import example_layered_sphere_wall_image
>>> from timagetk.synthetic_data.nuclei_image import nuclei_image_from_point_positions
>>> from timagetk.components.multi_channel import combine_channels
>>> from timagetk.visu.mplt import grayscale_imshow
>>> from timagetk.visu.stack import orthogonal_view
>>> # EXAMPLE - Synthetic wall (green) & nuclei (red) image
>>> kwargs = {"extent": 50., "voxelsize": (0.5, 0.25, 0.25), "dtype": 'uint8'}
>>> wall_img, pts = example_layered_sphere_wall_image(return_points=True, wall_intensity=250, **kwargs)
>>> nuclei_img = nuclei_image_from_point_positions(pts, nuclei_intensity=250., **kwargs)
>>> blend = combine_channels([nuclei_img, wall_img])
>>> blend.get_dtype()
'uint8'
>>> # Get the middle z-slice and plot it:
>>> z_sh = blend.get_shape('z')
>>> z_sl = z_sh // 2
>>> grayscale_imshow(blend.get_slice(50), title=f"z-slice{z_sl}/{z_sh}")
>>> # Show an orthogonal view:
>>> orthogonal_view(blend, suptitle="Synthetic wall (green) & nuclei (red) image")
"""
return BlendImage(images, colors=colors, rtype=rtype, **kwargs)