#!/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.
# ------------------------------------------------------------------------------
"""Regroup plotting function for browsing stack images."""
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Slider
from pkg_resources import parse_version
from skimage import img_as_ubyte
from timagetk.bin.logger import get_logger
from timagetk.components.multi_channel import BlendImage
from timagetk.components.multi_channel import DEF_CHANNEL_COLORS
from timagetk.components.multi_channel import MultiChannelImage
from timagetk.components.multi_channel import chnames_generator
from timagetk.components.multi_channel import combine_channels
from timagetk.components.spatial_image import SpatialImage
from timagetk.visu.mplt import image_plot
from timagetk.visu.util import convert_str_range
log = get_logger(__name__)
[docs]
def slider(label, mini, maxi, init, step=1, fmt="%1.0f"):
"""Matplotlib slider creation.
Parameters
----------
label : str
Name of the slider
mini : int
Min value of the slider
maxi : int
Max value of the slider
init : int
Initial value of the slider
step : int, optional
Step value of the slider
fmt : str, optional
Formatting of the displayed value selected by the slider
Notes
-----
The parameter `step` is not accessible for matplotlib version before 2.2.2.
Returns
-------
matplotlib.widgets.Slider
A matplotlib slider to use in figures to select values
"""
from matplotlib import __version__
axcolor = 'lightgoldenrodyellow'
rect = [0.25, 0.1, 0.65, 0.03] # [left, bottom, width, height]
if parse_version(__version__) >= parse_version("2.2"):
axz = plt.axes(rect, facecolor=axcolor)
zs = Slider(axz, label=label, valmin=mini, valmax=maxi, valstep=step,
closedmax=True, valinit=init, valfmt=fmt)
else:
axz = plt.axes(rect, axisbg=axcolor)
zs = Slider(axz, label=label, valmin=mini, valmax=maxi, valstep=step,
closedmax=True, valinit=init, valfmt=fmt)
return zs
[docs]
def stack_browser(image, title="", cmap='gray', val_range=None, axis='z', **kwargs):
"""Image stack browser, move along given axis, slice by slice.
Use matplotlib widget (GUI agnostic).
Parameters
----------
image : timagetk.components.spatial_image.SpatialImage
3D image to browse
title : str, optional
Title to give to the figure, *e.g.* the file name, default is empty
cmap : matplotlib.colors.ListedColormap or str, optional
Colormap to use, see the notes for advised colormaps
val_range : list(int, int), {'type', 'auto'}, optional
Minimum and maximum values to use for the colormap, can be given as a list of length 2 values.
If None (default), set to 'auto' for a ``LabelledImage``, set to 'type' for a ``SpatialImage``.
See the "Notes" section for detailled explanations.
axis : str in ['x', 'y', 'z'], optional
Axis to use for slicing
Other Parameters
----------------
init_slice : int
Slice id to use as starting point, default to ``0``.
Should be inferior to max slice number for given axis
extent : list of float
If provided (default, ``None``), set the extent of the displayed image.
By default, use the real unit extent of the SpatialImage istance.
colorbar : bool
If ``True`` (default ``False``), add a colorbar to the figure.
Notes
-----
We advise to use the **sequential** colormaps, such as:
- *Sequential (2)* [mplt_cmap_sequential]_: `'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', 'summer', 'Wistia'`.
- *Perceptually Uniform Sequential* [mplt_cmap_sequential2]_: `'viridis', 'plasma', 'inferno', 'magma', 'cividis'`.
To understand the differences in "perception" induced by the different colormaps, see: [mplt_cmap_perception]_
Accepted str for `val_range` can be:
- 'auto': get the min and max value of the image;
- 'type': get the maximum range from the `image.dtype`, *e.g.* 'uint8'=[0, 255];
Examples
--------
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_dataset
>>> from timagetk.visu.stack import stack_browser
>>> fname = shared_dataset("p58", "intensity")[0]
>>> img = imread(fname)
>>> stack_browser(img, fname)
>>> stack_browser(img, fname, axis='x')
>>> from timagetk import LabelledImage
>>> from timagetk.visu.util import greedy_colormap
>>> fname = shared_dataset("p58", "segmented")[0]
>>> img = imread(fname, LabelledImage, not_a_label=0)
>>> stack_browser(img, fname, cmap=greedy_colormap(img), init_slice=80)
>>> from timagetk.visu.stack import stack_browser
>>> from timagetk.visu.util import greedy_colormap
>>> from timagetk.array_util import dummy_labelled_image_3D
>>> img = dummy_labelled_image_3D((0.2, 0.5, 0.5)) # ZYX sorted voxel-sizes
>>> cmap = greedy_colormap(img)
>>> stack_browser(img, "Dummy", cmap=cmap , init_slice=2)
References
----------
.. [1] https://matplotlib.org/tutorials/colors/colormaps.html#sequential
.. [2] https://matplotlib.org/tutorials/colors/colormaps.html#sequential2
.. [3] https://matplotlib.org/tutorials/colors/colormaps.html#mycarta-banding
"""
from matplotlib.colors import BoundaryNorm
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.25) # save some space for the slider
norm, bounds = None, None
if isinstance(cmap, str):
if val_range is None:
val_range = 'type'
if isinstance(val_range, str):
val_range = convert_str_range(image, val_range)
mini, maxi = val_range
if cmap == 'glasbey':
from timagetk.visu.util import get_glasbey
bkgd = getattr(image, 'background', 1)
nal = getattr(image, 'not_a_label', 0)
cmap = get_glasbey(np.unique(image), not_a_label=nal, background=bkgd)
else:
from matplotlib import colormaps as cm
cmap = cm.get_cmap(cmap)
else:
# Assume its a valid matplotlib ListedColormap
mini, maxi = int(image.min()), int(image.max())
values_spread = maxi - mini + 1
try:
assert values_spread == cmap.N
except AssertionError:
raise ValueError(f"Number of colors {cmap.N} and values {values_spread} does not match!")
else:
val_range = [None, None]
bounds = np.linspace(mini - 0.5, maxi + 0.5, cmap.N + 1)
norm = BoundaryNorm(bounds, cmap.N)
# - Get the slice to represent:
init_slice = kwargs.get('init_slice', 0)
ax, l = image_plot(image.get_slice(init_slice, axis), ax, cmap=cmap, val_range=val_range, norm=norm)
plt.title(title)
if kwargs.get('colorbar', False):
if bounds is not None:
if maxi < 100:
ticks_step = 1
while len(bounds) / ticks_step > 20:
ticks_step += 1
else:
ticks_step = 10
while len(bounds) / ticks_step > 20:
ticks_step += 10
fig.colorbar(l, ax=ax, ticks=bounds[::ticks_step][:-1] + 0.5, boundaries=bounds)
else:
fig.colorbar(l, ax=ax)
max_slice = image.get_shape(axis) - 1
zs = slider(label='{}-slice'.format(axis), mini=0, maxi=max_slice, init=init_slice, step=1)
def update(val):
slice_id = int(zs.val)
l.set_data(image.get_slice(slice_id, axis).get_array())
fig.canvas.draw_idle()
zs.on_changed(update)
plt.show()
return zs
[docs]
def stack_browser_threshold(image, title="", cmap='gray', val_range=None, axis='z', **kwargs):
"""Stack browser, by slices along given axis.
Use matplotlib widget (GUI agnostic).
Parameters
----------
image : timagetk.SpatialImage
3D image to browse
title : str, optional
Title to give to the figure, *e.g.* the file name, default is empty
cmap : str, optional
Colormap to use, see the notes for advised colormaps
val_range : list(int, int), {'type', 'auto'}, optional
Minimum and maximum values to use for the colormap, can be given as a list of length 2 values.
If None (default), set to 'auto' for a ``LabelledImage``, set to 'type' for a ``SpatialImage``.
See notes for more explanations.
axis : str in ['x', 'y', 'z'], optional
Axis to use for slicing
Other Parameters
----------------
init_slice : int
Slice id to use as starting point, should be inferior to max slice number for given axis
extent : list of float
If provided (default, ``None``), set the extent of the displayed image.
By default, use the real unit extent of the SpatialImage istance.
Notes
-----
We advise to use the **sequential** colormaps, such as:
- *Sequential (2)* [mplt_cmap_sequential]_: `'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', 'summer', 'Wistia'`.
- *Perceptually Uniform Sequential* [mplt_cmap_sequential2]_: `'viridis', 'plasma', 'inferno', 'magma', 'cividis'`.
To understand the differences in "perception" induced by the different colormaps, see: [mplt_cmap_perception]_
Accepted str for `val_range` can be:
- 'auto': get the min and max value of the image;
- 'type': get the maximum range from the `image.dtype`, *e.g.* 'uint8'=[0, 255];
Examples
--------
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_data
>>> from timagetk.visu.stack import stack_browser_threshold
>>> fname = 'p58-t0-a0.lsm'
>>> img = imread(shared_data(fname, "p58"))
>>> b = stack_browser_threshold(img, fname) # do not forget to assign to a variable because "you need to keep the sliders around globally"
References
----------
.. [mplt_cmap_sequential] https://matplotlib.org/tutorials/colors/colormaps.html#sequential
.. [mplt_cmap_sequential2] https://matplotlib.org/tutorials/colors/colormaps.html#sequential2
.. [mplt_cmap_perception] https://matplotlib.org/tutorials/colors/colormaps.html#mycarta-banding
"""
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.25) # save some space for the slider
if val_range is None:
val_range = 'type'
if isinstance(val_range, str):
val_range = convert_str_range(image, val_range)
from timagetk.components.labelled_image import LabelledImage
from timagetk.visu.util import get_glasbey
if cmap == 'glasbey' and isinstance(image, LabelledImage):
cmap = get_glasbey(image.labels(), not_a_label=image.not_a_label, background=getattr(image, 'background', 1))
# - Get the slice to represent:
ax, l = image_plot(image.get_slice(0, axis), ax, cmap=cmap, val_range=val_range)
plt.title(title)
if isinstance(cmap, str):
plt.colorbar(l, ax=ax)
ax_slice = plt.axes([0.25, 0.1, 0.65, 0.03])
ax_threshold = plt.axes([0.25, 0.15, 0.65, 0.03])
max_slice = image.get_shape(axis) - 1
zs = Slider(ax=ax_slice, label='{}-slice'.format(axis), valmin=0, valmax=max_slice,
valinit=kwargs.get('init_slice', 0), valstep=1)
init_threshold = kwargs.get('init_threshold', (val_range[1] - val_range[0]) // 2)
th_step = kwargs.get('th_step', 0.05 if image.dtype == 'float' else 1)
mask = SpatialImage(image.get_slice(0, axis) > init_threshold, voxelsize=image.voxelsize)
from matplotlib.colors import ListedColormap
cm = ListedColormap([[1., 0., 0., 1.], [0., 0., 0., 0.]])
ax, l_th = image_plot(mask, ax, cmap=cm, val_range=[0, 1])
ths = Slider(ax=ax_threshold, label='Threshold', valmin=val_range[0], valmax=val_range[1], valinit=init_threshold,
valstep=th_step)
def update(val):
# Get slice id and update:
slice_id = int(zs.val)
l.set_data(image.get_slice(slice_id, axis).get_array())
# Get threshold value and update
if isinstance(th_step, int):
threshold = int(ths.val)
else:
threshold = float(ths.val)
l_th.set_data(image.get_slice(slice_id, axis).get_array() > threshold)
fig.canvas.draw_idle()
zs.on_changed(update)
ths.on_changed(update)
plt.show()
return zs, ths
[docs]
def rgb_stack_browser(image, title="", cmap='gray', val_range=None, axis='z', **kwargs):
"""RGB stack browser, along last physical dimension.
Use matplotlib widget (GUI agnostic).
Parameters
----------
image : timagetk.BlendImage or timagetk.MultiChannelImage
An RGB 3D array to browse along its last "physical" dimension ``P``, *i.e.* not the RGB values.
title : str, optional
Title to give to the figure, *e.g.* the file name, default is empty
cmap : str, optional
Colormap to use, see the notes for advised colormaps
val_range : list(int, int), {'type', 'auto'}, optional
Minimum and maximum values to use for the colormap, can be given as a list of length 2 values.
If None (default), set to 'auto' for a ``LabelledImage``, set to 'type' for a ``SpatialImage``. See notes for more explanations.
axis : str in ['x', 'y', 'z'], optional
Axis to use for slicing
Other Parameters
----------------
init_slice : int
Slice id to use as starting point, should be inferior to max slice number for given axis
extent : list of float
If provided (default, ``None``), set the extent of the displayed image.
By default, use the real unit extent of the SpatialImage istance.
Notes
-----
As Numpy and Matplotlib have different axis order convention, we transpose the
first two axis of the given numpy array to display the first axis (rows, 'X')
horizontally per the usual plotting convention.
If specified, the ``xy_ratio`` is also inverted to match that modification.
Examples
--------
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_data
>>> from timagetk.visu.stack import rgb_stack_browser
>>> from skimage.color import gray2rgb
>>> fname = 'p58-t0-a0.lsm'
>>> img = imread(shared_data(fname, "p58"))
>>> rgb_img = gray2rgb(img)
>>> # imread return XYZ SpatialImage, the browser will move along the z-plane:
>>> rgb_stack_browser(rgb_img,"z-plane browsing")
>>> # transposing Y & Z axis, the browser will now move along the y-plane:
>>> rgb_stack_browser(rgb_img.transpose((0,2,1,3)),"y-plane browsing")
>>> # Better example with an actual RGB image
>>> from timagetk.components.multi_channel import label_blending
>>> from timagetk.tasks.segmentation import watershed_segmentation
>>> from timagetk.components.multi_channel import label_blending
>>> img = imread(shared_data("p58-t0-a0.lsm", "p58"))
>>> vx, vy, vz = img.voxelsize
>>> seg_img = watershed_segmentation(img, hmin=10)
>>> blend = label_blending(seg_img, img)
>>> b = rgb_stack_browser(blend,"Intensity & segmentation blending",xy_ratio=vx/vy)
"""
assert image.ndim == 4
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.25) # save some space for the slider
# - Get the first slice to represent:
ax, l = image_plot(image.get_slice(0, axis), ax, cmap=cmap, val_range=val_range)
plt.title(title)
max_slice = image.get_shape(axis) - 1
zs = slider(label='{}-slice'.format(axis), mini=0, maxi=max_slice,
init=kwargs.get('init_slice', 0), step=1)
def update(val):
slice_id = int(zs.val)
l.set_data(image.get_slice(slice_id, axis).get_array())
fig.canvas.draw_idle()
zs.on_changed(update)
plt.show()
return zs
[docs]
def channels_stack_browser(images, channel_names=None, colors=DEF_CHANNEL_COLORS, title="", axis='z', **kwargs):
"""Multi channel stack browser, by slices along given axis.
Use matplotlib widget (GUI agnostic).
Parameters
----------
images : list of SpatialImage, MultiChannelImage
List of 3D SpatialImage to browse
channel_names : list of str, optional
List of channel names
colors : list of str in ``DEF_CHANNEL_COLORS``, optional
List of color to use, see ``combine_channels`` for known names
title : str, optional
Title to give to the figure, *e.g.* the file name, default is empty
axis : str, in ['x', 'y', 'z']
Axis to use for slicing
Other Parameters
----------------
init_slice : int
Slice id to use as starting point, should be inferior to max slice number for given axis
extent : list of float
If provided (default, ``None``), set the extent of the displayed image.
By default, use the real unit extent of the SpatialImage istance.
See Also
--------
visu.util.DEF_CHANNEL_COLORS: the list of available channel colors.
Examples
--------
>>> from timagetk.io.util import shared_data
>>> from timagetk.io import imread
>>> from timagetk.algorithms.blockmatching import blockmatching
>>> from timagetk.visu.stack import channels_stack_browser
>>> from timagetk.algorithms.trsf import apply_trsf
>>> from timagetk.algorithms.quaternion import centered_rotation_trsf
>>> # EXAMPLE: Visualize the registration of a floating image (green) on a reference image (red)
>>> float_img = imread(shared_data('p58-t0-a0.lsm', "p58"))
>>> ref_img = imread(shared_data('p58-t0-a1.lsm', "p58"))
>>> trsf_z = centered_rotation_trsf(ref_img, -90, 'z') # Initialize registration with a -90° rotation along Z
>>> trsf = blockmatching(float_img, ref_img, method='affine', init_trsf=trsf_z) # Intensity registration
>>> res_img = apply_trsf(float_img, trsf, template_img=ref_img) # Apply trnsformation to floating image
>>> b = channels_stack_browser([res_img, ref_img], ['t0 on t1', 't1'], ['green', 'red'], "affine registration")
"""
from matplotlib.widgets import CheckButtons
from skimage.color import gray2rgb
if isinstance(images, MultiChannelImage):
channel_names = images.channel_names
images = [images[ch] for ch in channel_names]
else:
if channel_names is None:
channel_names = chnames_generator(len(images))
assert len(images) == len(channel_names)
assert len(channel_names) <= len(colors)
# -- Variables definition:
n_channels = len(images)
visible = [True] * n_channels
used_colors = colors
max_slice = images[0].get_shape(axis) - 1
def selected_channels(images, visible):
# exchange x & y axis for representation: numpy and matplotlib do not have the same the same coordinates conventions!!!
return [img for n, img in enumerate(images) if visible[n]]
# -- Create matplotlib figure:
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.2, bottom=0.25)
# -- Initialise blending for z-slice=0
if isinstance(images, list):
blend = combine_channels(selected_channels(images, visible), colors)
else:
blend = images
ax, l = image_plot(blend.get_slice(0, axis), ax)
plt.title(title)
# -- Slider: z-slice selection
zs = slider(label='{}-slice'.format(axis), mini=0, maxi=max_slice, init=kwargs.get('init_slice', 0), step=1)
def update(val):
"""Update the blend image associated to new `zs.val`."""
slice_id = int(zs.val)
if sum(visible) == 0:
blend = np.zeros_like(gray2rgb(images[0].get_slice(0, axis), alpha=False), dtype=images[0].dtype)
else:
blend = combine_channels(selected_channels(images, visible), used_colors).get_slice(slice_id, axis)
l.set_data(blend)
fig.canvas.draw_idle()
zs.on_changed(update)
# -- CheckButtons: channels selection:
rax = plt.axes([0.05, 0.4, 0.1, 0.15])
check = CheckButtons(rax, channel_names, visible)
def channel_select(ch_name):
"""Update the blend image associated to new channels `visible`."""
slice_id = int(zs.val)
index = channel_names.index(ch_name)
if visible[index]:
visible[index] = False
else:
visible[index] = True
used_colors = [colors[n] for n in range(n_channels) if visible[n]]
if sum(visible) == 0:
blend = np.zeros_like(gray2rgb(images[0].get_slice(0, axis), alpha=False), dtype=np.uint8)
else:
blend = combine_channels(selected_channels(images, visible), used_colors).get_slice(slice_id, axis)
l.set_data(blend)
fig.canvas.draw_idle()
check.on_clicked(channel_select)
plt.show()
return zs, check
[docs]
def stack_panel(image, axis='z', step=1, start=0, stop=-1, **kwargs):
"""Create a panel of slices taken from `image` along given `axis`.
Parameters
----------
image : timagetk.components.spatial_image.SpatialImage or timagetk.components.spatial_image.LabelledImage or timagetk.components.multi_channel.BlendImage or timagetk.components.multi_channel.MultiChannelImage
Image to represent as a panel of slices
axis : str, optional
Axis along which to move to get the slices to display, default is 'z'
step : int, optional
Slices step, default is 1
start : int, optional
Starting slice, default is first
stop : int, optional
Stopping slice, default is last
Other Parameters
----------------
suptitle : str
A general title placed above the sub-figures titles, usually used when a list of images is given
max_per_line : int
Number of figure per line when using more than one images. Defaults to ``4``.
thumb_size : float
Image size in inch (default=5.)
val_range : {'auto', 'type'} or list of int, optional
Define the range of values used by the colormap. Defaults to ``'type'``.
See the "Notes" section for detailled explanations.
cmap : str
Colormap to use, see the ``stack_browser`` notes for advised colormaps
figname : str, optional
If provided (default is empty), the image will be saved under this filename.
Raises
------
ValueError
If given `start` value is above `axis` max dimension - `step`
Notes
-----
Accepted ``val_range`` may be:
- **auto**, get the min and max value of the image;
- **type** (default), get the maximum range from the `image.dtype`, *e.g.* 'uint8'=[0, 255];
- length-2 list of value, *e.g.* [10, 200];
Examples
--------
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_dataset
>>> from timagetk.visu.stack import stack_panel
>>> img = imread(shared_dataset("p58", "intensity")[0])
>>> stack_panel(img, axis="z", step=5)
>>> stack_panel(img, axis="z", step=5, suptitle=img.filename)
>>> stack_panel(img, axis="x", step=10, suptitle=img.filename)
>>> 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)
>>> stack_panel(blend, step=10, suptitle="Intensity & segmentation blending")
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_dataset
>>> from timagetk.algorithms.blockmatching import blockmatching
>>> from timagetk.algorithms.trsf import apply_trsf
>>> from timagetk.visu.stack import stack_panel
>>> int_imgs = shared_dataset("p58", "intensity")
>>> flo_img = imread(int_imgs[0])
>>> ref_img = imread(int_imgs[1])
>>> rigid_trsf = blockmatching(flo_img, ref_img, method='rigid')
>>> reg_img = apply_trsf(flo_img, rigid_trsf, template_img=ref_img, interpolation='linear')
>>> from timagetk import MultiChannelImage
>>> mc = MultiChannelImage([reg_img, ref_img], channel_names=['Registered', 'Reference'])
>>> stack_panel(mc, axis="z", step=mc.get_shape("z")//5)
"""
assert image.is3D()
max_sl = image.get_shape(axis)
if stop == -1:
stop = max_sl - 1
if stop > max_sl:
msg = "Given `stop` () is above max dimension along {}-axis, set to {}"
log.error(msg.format(stop, axis, max_sl))
if start > max_sl - step:
msg = "Given `start` () is above {}-axis max dimension - `step`!"
raise ValueError(msg.format(start, axis))
if kwargs.get('thumb_size', None) is None:
kwargs.update({'thumb_size': 2.})
if kwargs.get('max_per_line', None) is None:
kwargs.update({'max_per_line': 6})
slices = np.arange(start, stop, step)
stack = []
for sl in slices:
im_sl = image.get_slice(int(sl), axis=axis)
stack.append(im_sl)
fname = kwargs.pop('figname', "")
if 'cmap' not in kwargs:
kwargs['cmap'] = 'gray'
kwargs['title'] = [f"{axis}-slice {sl}/{max_sl}" for sl in slices]
kwargs['no_show'] = True # to disable call to `plt.show` in `_multiple_plots`
from timagetk.visu.mplt import _multiple_plots
from timagetk.visu.mplt import image_plot
fig, axes = _multiple_plots(image_plot, stack, cbar=False, **kwargs)
try:
assert image.filename != ""
except:
fig.suptitle("Stack panel")
else:
fig.suptitle(f"{image.filename} - Stack panel")
if fname != "":
plt.savefig(fname)
else:
plt.show()
return
[docs]
def orthogonal_view(image, x_slice=None, y_slice=None, z_slice=None, suptitle="", figname="", cmap='gray', **kwargs):
"""Orthogonal representation of an image by three slices along each axes.
Slice numbering starts at 0 (like indexing).
Parameters
----------
image : numpy.ndarray or timagetk.components.spatial_image.SpatialImage or timagetk.components.multi_channel.MultiChannelImage or timagetk.components.multi_channel.BlendImage
Image from which to extract the slice.
x_slice : int, optional
Value defining the slice to represent in x direction. By default, the middle of the axis.
y_slice : int, optional
Value defining the slice to represent in y direction. By default, the middle of the axis.
z_slice : int, optional
Value defining the slice to represent in z direction. By default, the middle of the axis.
suptitle : str, optional
If provided (default is empty), add this string of characters as title.
figname : str or pathlib.Path, optional
If provided (default is empty), the image will be saved under this filename.
cmap : str
Colormap to use, see the notes of ``stack_browser`` for advised colormaps.
Other Parameters
----------------
val_range : {'type', 'auto'} or list of int
Define the range of values used by the colormap. Defaults to ``'type'``.
See the "Notes" section for detailled explanations.
figsize : tuple
Lenght two tuple defining desired figure size. Defaults to ``(10, 10)``.
dpi : int
The resolution in dots per inch. Defaults to ``96``.
Notes
-----
Accepted ``val_range`` may be:
- **auto**, get the min and max value of the image;
- **type** (default), get the maximum range from the `image.dtype`, *e.g.* 'uint8'=[0, 255];
- length-2 list of value, *e.g.* [10, 200];
Examples
--------
>>> from timagetk.io import imread
>>> from timagetk.io.util import shared_dataset
>>> from timagetk.visu.stack import orthogonal_view
>>> # EXAMPLE 1 - Orthogonal view of a grayscale intensity image:
>>> image = imread(shared_dataset("p58", "intensity")[0])
>>> orthogonal_view(image, suptitle=image.filename)
>>> # EXAMPLE 2 - Orthogonal view of a labelled image:
>>> image = imread(shared_dataset("p58", "segmented")[0])
>>> orthogonal_view(image, cmap='glasbey', val_range='auto', suptitle=image.filename)
"""
# Transform the ``MultiChannelImage`` into a ``BlendImage`` (RGB array):
if isinstance(image, MultiChannelImage):
image = BlendImage([image[ch] for ch in image.get_channel_names()])
x_sh, y_sh, z_sh = image.get_shape()[::-1]
x_ext, y_ext, z_ext = image.get_extent()[::-1]
x_vxs, y_vxs, z_vxs = image.get_voxelsize()[::-1]
if x_slice is None:
x_slice = x_sh // 2
if y_slice is None:
y_slice = y_sh // 2
if z_slice is None:
z_slice = z_sh // 2
if isinstance(image, (SpatialImage, BlendImage)):
x_sl = image.get_slice(x_slice, 'x').transpose('yz')
y_sl = image.get_slice(y_slice, 'y')
z_sl = image.get_slice(z_slice, 'z')
else:
raise TypeError(f"Unknown image type to `orthogonal_view`: {type(image)}")
# If RGB image is unsigned 16bits, convert it to 8bits for representation:
if isinstance(image, BlendImage) and image.dtype.name == 'uint16':
log.debug("Converting 'uint16' image to 'uint8' for display.")
x_sl = BlendImage(img_as_ubyte(x_sl), voxelsize=x_sl.voxelsize, axes_order=x_sl.axes_order)
y_sl = BlendImage(img_as_ubyte(y_sl), voxelsize=y_sl.voxelsize, axes_order=y_sl.axes_order)
z_sl = BlendImage(img_as_ubyte(z_sl), voxelsize=z_sl.voxelsize, axes_order=z_sl.axes_order)
val_range = kwargs.pop('val_range', 'type')
if kwargs.get('figsize', None) is None:
kwargs['figsize'] = (10, 10)
if kwargs.get('dpi', None) is None:
kwargs['dpi'] = 96
fig = plt.figure(**kwargs)
gs = fig.add_gridspec(2, 2, width_ratios=[x_ext, z_ext], height_ratios=[y_ext, z_ext], hspace=0.2, wspace=0.01)
# -- Plot the z-slice / xy-plane image (GREEN):
xy_im = fig.add_subplot(gs[0, 0])
xy_im.plot([x_slice * x_vxs, x_slice * x_vxs], [0, y_sh * y_vxs], color='red') # the x-slice position
xy_im.plot([0, x_sh * x_vxs], [y_slice * y_vxs, y_slice * y_vxs], color='blue') # the y-slice position
xy_im, xy_fig = image_plot(z_sl, xy_im, cmap=cmap, val_range=val_range)
xy_im.set_title('z-slice {}/{}'.format(z_slice, z_sh))
# -- Plot the y-slice / xz-plane image (BLUE):
xz_im = plt.subplot(gs[1, 0])
xz_im.plot([x_slice * x_vxs, x_slice * x_vxs], [0, y_sh * y_vxs], color='red')
xz_im.plot([0, x_sh * x_vxs], [z_slice * z_vxs, z_slice * z_vxs], color='green')
xz_im, xz_fig = image_plot(y_sl, xz_im, cmap=cmap, val_range=val_range)
xz_im.set_title('y-slice {}/{}'.format(y_slice, y_sh))
xz_im.axes.xaxis.set_ticklabels([])
# -- Plot the x-slice / yz-plane image (RED):
yz_im = plt.subplot(gs[0, 1])
yz_im.plot([0, z_sh * z_vxs], [y_slice * y_vxs, y_slice * y_vxs], color='blue')
yz_im.plot([z_slice * z_vxs, z_slice * z_vxs], [0, y_sh * y_vxs], color='green')
yz_im, yz_fig = image_plot(x_sl, yz_im, cmap=cmap, val_range=val_range)
yz_im.set_title('x-slice {}/{}'.format(x_slice, x_sh))
yz_im.axes.yaxis.set_ticklabels([])
# Change the line width and colors of the image border to match slices positions:
colors = ["green", "blue", "red"]
for n, ax in enumerate([xy_im, xz_im, yz_im]):
for axis in ['top', 'bottom', 'left', 'right']:
ax.spines[axis].set_linewidth(2)
ax.spines[axis].set_color(colors[n])
# Add suptitle:
plt.suptitle(suptitle)
# plt.subplots_adjust(wspace=0.01)
# plt.subplots_adjust(hspace=0.09)
if figname != "":
plt.savefig(figname)
else:
plt.show()
return