# -*- coding: utf-8 -*-
################################ Begin license #################################
# Copyright (C) Laboratory of Imaging technologies,
# Faculty of Electrical Engineering,
# University of Ljubljana.
#
# This file is part of PyXOpto.
#
# PyXOpto is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyXOpto is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyXOpto. If not, see <https://www.gnu.org/licenses/>.
################################# End license ##################################
from typing import Callable
import matplotlib.pyplot as pp
from matplotlib.widgets import Slider, RadioButtons, CheckButtons
import numpy as np
[docs]class DualSliceView:
def __init__(self, data: np.ndarray,
axis1: int = 0, slices1: np.ndarray = None,
slice1_label: str = None, slice1_valfmt: str = None,
axis2: int = 0, slices2: np.ndarray = None,
slice2_label: str = None, slice2_valfmt: str = None,
logscale: bool or Callable[[np.ndarray], np.ndarray]=False,
autoscale: bool = False,
xlabel: str = None, ylabel: str = None, title: str = None,
**kwargs):
'''
Creates a matplotlib-based slice viewer of 3D data cubes along
one axis.
Parameters
----------
data: np.ndarray
A 3D Data array.
axis1: int
The first axis along which to slice the data.
Assuming that the dimensions of the data are (z, y, x, t), the
produced slices are as follows:
- along = 0; slicing along the z axis, slice dimensions (y, x)
- along = 1; slicing along the y axis, slice dimensions (z, x)
- along = 2; slicing along the x axis, slice dimensions (z, y)
- along = 3; slicing along the x axis, slice dimensions (z, y)
- along = 4; slicing along the t axis, slice dimensions (z, y)
Note that the first dimension of the slice will be shown along the
vertical/y axis of the slice image and the second dimension of the
slice along the horizontal/x axis of the slice image.
slices1: np.ndarray
A vector that defines the positions of slices. If None, a default
sequence of positions from 0 to num_slices - 1 is created.
slice1_label: str
Label of the slider along the axis1 axis.
slice1_valfmt: str
Slice along axis1 - value format string.
axis2: int
The first axis along which to slice the data.
Assuming that the dimensions of the data are (z, y, x, t), the
produced slices are as follows:
- along = 0; slicing along the z axis, slice dimensions (y, x)
- along = 1; slicing along the y axis, slice dimensions (z, x)
- along = 2; slicing along the x axis, slice dimensions (z, y)
- along = 3; slicing along the x axis, slice dimensions (z, y)
- along = 4; slicing along the t axis, slice dimensions (z, y)
Note that the first dimension of the slice will be shown along the
vertical/y axis of the slice image and the second dimension of the
slice along the horizontal/x axis of the slice image.
slices2: np.ndarray
A vector that defines the positions of slices. If None, a default
sequence of positions from 0 to num_slices - 1 is created.
slice2_label: str
Label of the slider along the axis2 axis.
slice2_valfmt: str
Slice along axis2 - value format string.
logscale: bool or Callable[[np.ndarray], np.ndarray]
A bool value enables or disables the logarithmic scaling of the
data that is computed as:
- :code:`np.log10(slice_data + 1)` if data are strictly positive
- :code:`np.log10(slice_dat - data.min() + 1)` if data include negative values
If a callable, all data are scaled with this function.
autoscale: bool
Automatically adjust the intensity range of the image to
the current slice minimum and maximum intensity.
title: str
Slice title. This can be a format string with placeholders for
the slice number "{slice1}" or "{slice2}", total number of slices
"{num_slices1}" od "{num_slices2}" and position of the slice
"{pos1}" or "{pos2}".
xlabel: str
Label of the x axis.
ylabel: str
Label of the y axis.
kwargs: dict
Additional keyword arguments that are passed to pyplot.imshow.
'''
self._axis1 = int(axis1)
self._axis2 = int(axis2)
if self._axis1 == self._axis2:
raise ValueError('Cannot slice along the same axis!')
if slice1_label is None:
slice1_label = 'Slice'
if slice2_label is None:
slice2_label = 'Slice'
if slice1_valfmt is None:
slice1_valfmt = '%.4f'
if slice2_valfmt is None:
slice2_valfmt = '%.4f'
data = np.asarray(data)
if title is None:
title = 'Slice {slice1}/{num_slices1} @ {pos1:.4f}, '\
'{slice2}/{num_slices2} @ {pos2:.4f}'
self._title = None
self._title_fmtstr = title
self._data = data
self._data_range = (data.min(), data.max())
self._slice_all = [slice(0, s) for s in self._data.shape]
self._logmin = np.finfo(float).eps
if slices1 is None:
slices1 = np.arange(data.shape[axis1])
if slices2 is None:
slices2 = np.arange(data.shape[axis2])
self._slices1 = slices1
self._slices2 = slices2
if callable(logscale):
self._scale_fun = logscale
logscale = True
else:
self._scale_fun = self._apply_logscale
self._logscale = bool(logscale)
self._autoscale = bool(autoscale)
self._fig, self._ax_image = pp.subplots()
self._fig.canvas.manager.set_window_title('Slice View')
if 'vmin' in kwargs:
kwargs.pop('vmin')
if 'vmax' in kwargs:
kwargs.pop('vmax')
select = self._slice_all
select[self._axis1] = select[self._axis2] = slice(0, 1)
self._image = self._ax_image.imshow(
np.squeeze(data[tuple(select)]),
vmin=self._data_range[0], vmax=self._data_range[1],
**kwargs
)
pp.colorbar(self._image, orientation='vertical')
pp.subplots_adjust(bottom=0.35, left=0.20)
self._slice2_ax = pp.axes([0.30, 0.16, 0.58, 0.03])
self._slice1_ax = pp.axes([0.30, 0.20, 0.58, 0.03])
self._vmin_ax = pp.axes([0.30, 0.08, 0.58, 0.03])
self._vmax_ax = pp.axes([0.30, 0.03, 0.58, 0.03])
self._scale_ax = pp.axes([0.04, 0.01, 0.15, 0.10])
self._auto_ax = pp.axes([0.04, 0.12, 0.15, 0.08])
self._vmin_slider = Slider(
self._vmin_ax, 'vmin',
valinit=0.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmin_slider.on_changed(self._vmin_changed)
self._vmax_slider = Slider(
self._vmax_ax, 'vmax',
valinit=100.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmax_slider.on_changed(self._vmax_changed)
self._slice1_slider = Slider(
self._slice1_ax, slice1_label, valinit=slices1[0],
valmin=slices1[0], valmax=slices1[-1], valfmt=slice1_valfmt)
self._slice1_slider.on_changed(self._slice1_changed)
self._slice2_slider = Slider(
self._slice2_ax, slice2_label, valinit=slices2[0],
valmin=slices2[0], valmax=slices2[-1], valfmt=slice2_valfmt)
self._slice2_slider.on_changed(self._slice2_changed)
self._scale_radio = RadioButtons(
self._scale_ax, ('lin', 'log'), active=self._scale_active())
self._scale_radio.on_clicked(self._set_scale)
self._auto_button = CheckButtons(
self._auto_ax, ['autoscale'], [self._autoscale])
self._auto_button.on_clicked(self._autoscale_changed)
if self._title_fmtstr is not None:
self._title = self._ax_image.set_title(self._make_title())
if xlabel is not None:
self._ax_image.set_xlabel(xlabel)
if ylabel is not None:
self._ax_image.set_ylabel(ylabel)
self._slice1_changed(0)
self._slice2_changed(0)
def _make_title(self, slice1_index: int = None, slice2_index: int = None):
if slice1_index is None:
slice1_index = int(self._slice1_slider.val)
if slice2_index is None:
slice2_index = int(self._slice2_slider.val)
return self._title_fmtstr.format(
slice1=slice1_index + 1, num_slices1=self._slices1.size,
slice2=slice2_index + 1, num_slices2=self._slices2.size,
pos1=self._slices1[slice1_index], pos2=self._slices2[slice2_index]
)
def _apply_logscale(self, data: np.ndarray) -> np.ndarray:
if self._data_range[0] < 0.0:
return np.log10((1.0 - self._data_range[0]) + data)
else:
return np.log10(1.0 + data)
def _slice1_changed(self, pos):
slice_index = np.argmin(np.abs(self._slices1 - pos))
select = self._slice_all
select[self._axis1] = slice(slice_index, slice_index + 1)
img_slice = np.squeeze(self._data[tuple(select)])
if self._logscale:
img_slice = self._scale_fun(img_slice)
self._image.set_array(img_slice)
if self._autoscale:
self._image.autoscale()
if self._title is not None:
self._title.set_text(self._make_title(slice1_index=slice_index))
self._fig.canvas.draw_idle()
def _slice2_changed(self, pos):
slice_index = np.argmin(np.abs(self._slices2 - pos))
select = self._slice_all
select[self._axis2] = slice(slice_index, slice_index + 1)
img_slice = np.squeeze(self._data[tuple(select)])
if self._logscale:
img_slice = self._scale_fun(img_slice)
self._image.set_array(img_slice)
if self._autoscale:
self._image.autoscale()
if self._title is not None:
self._title.set_text(self._make_title(slice2_index=slice_index))
self._fig.canvas.draw_idle()
def _set_scale(self, val):
if val == 'log':
self._logscale = True
else:
self._logscale = False
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice1_changed(self._slice1_slider.val)
self._slice2_changed(self._slice2_slider.val)
def _scale_active(self):
return {False: 0, True:1}.get(self._logscale, 0)
def _autoscale_changed(self, value):
self._autoscale = not self._autoscale
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice1_changed(self._slice1_slider.val)
self._slice2_changed(self._slice2_slider.val)
def _vmin_changed(self, value):
vmin = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmin = self._scale_fun(vmin)
self._image.set_clim(vmin=vmin)
def _vmax_changed(self, value):
vmax = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmax = self._scale_fun(vmax)
self._image.set_clim(vmax=vmax)
def _get_ax(self):
return self._ax_image
axis = property(_get_ax, None, None, 'Image axis.')
def _get_fig(self):
return self._fig
fig = property(_get_fig, None, None, 'Figure instance.')
[docs] def show(self):
'''
Show the SliceView window.
'''
pp.show()
def show():
pp.show()
[docs]class SliceView:
def __init__(self, data: np.ndarray, axis: int = 0,
slices: np.ndarray = None,
slice_valfmt: str = None, slice_label: str = None,
logscale: bool or Callable[[np.ndarray], np.ndarray]=False,
autoscale: bool = False,
xlabel: str = None, ylabel: str = None, title: str = None,
**kwargs):
'''
Creates a matplotlib-based slice viewer of 3D data cubes along
one axis.
Parameters
----------
data: np.ndarray
A 3D Data array.
axis: int
The axis along which to slice the data.
Assuming that the dimensions of the data are (z, y, x), the
produced slices are as follows:
- along = 0; slicing along the z axis, slice dimensions (y, x)
- along = 1; slicing along the y axis, slice dimensions (z, x)
- along = 2; slicing along the x axis, slice dimensions (z, y)
Note that the first dimension of the slice will be shown along the
vertical/y axis of the slice image and the second dimension of the
slice along the horizontal/x axis of the slice image.
slices: np.ndarray
A vector that defines the positions of slices. If None, a default
sequence of positions from 0 to num_slices - 1 is created.
slice_valfmt: str
Slice along axis1 - value format string.
slice_label: str
Label of the slider along the axis1 axis.
logscale: bool or Callable[[np.ndarray], np.ndarray]
A bool value enables or disables the logarithmic scaling of the
data that is computed as:
- :code:`np.log10(slice_data + 1)` if data are strictly positive
- :code:`np.log10(slice_dat - data.min() + 1)` if data include negative values
If a callable, all data are scaled with this function.
autoscale: bool
Automatically adjust the intensity range of the image to
the current slice minimum and maximum intensity.
title: str
Slice title. This can be a format string with placeholders for
the slice number "{slice}", total number of slices "{num_slices}"
and position of the slice "{pos}".
xlabel: str
Label of the x axis.
ylabel: str
Label of the y axis.
kwargs: dict
Additional keyword arguments that are passed to pyplot.imshow.
'''
self._axis = int(axis)
data = np.asarray(data)
if title is None:
title = 'Slice {slice}/{num_slices} @ {pos:.4f}'
if slice_valfmt is None:
slice_valfmt = '%.4f'
if slice_label is None:
slice_label = 'Slice'
self._title = None
self._title_fmtstr = title
if axis != 0:
order = {0:(0, 1, 2), 1:(1, 0, 2), 2:(2, 0, 1)}.get(axis)
data = np.transpose(data, order)
self._data = data
self._data_range = (data.min(), data.max())
self._logmin = np.finfo(float).eps
if slices is None:
slices = np.arange(data.shape[axis])
self._slices = slices
if callable(logscale):
self._scale_fun = logscale
logscale = True
else:
self._scale_fun = self._apply_logscale
self._logscale = bool(logscale)
self._autoscale = bool(autoscale)
self._fig, self._ax_image = pp.subplots()
self._fig.canvas.manager.set_window_title('Slice View')
if 'vmin' in kwargs:
kwargs.pop('vmin')
if 'vmax' in kwargs:
kwargs.pop('vmax')
self._image = self._ax_image.imshow(
data[0], vmin=self._data_range[0], vmax=self._data_range[1],
**kwargs
)
pp.colorbar(self._image, orientation='vertical')
pp.subplots_adjust(bottom=0.30, left=0.20)
self._slice_ax = pp.axes([0.30, 0.16, 0.58, 0.03])
self._vmin_ax = pp.axes([0.30, 0.08, 0.58, 0.03])
self._vmax_ax = pp.axes([0.30, 0.03, 0.58, 0.03])
self._scale_ax = pp.axes([0.04, 0.01, 0.15, 0.10])
self._auto_ax = pp.axes([0.04, 0.12, 0.15, 0.08])
self._vmin_slider = Slider(
self._vmin_ax, 'vmin',
valinit=0.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmin_slider.on_changed(self._vmin_changed)
self._vmax_slider = Slider(
self._vmax_ax, 'vmax',
valinit=100.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmax_slider.on_changed(self._vmax_changed)
self._slice_slider = Slider(
self._slice_ax, slice_label, valinit=slices[0],
valmin=slices[0], valmax=slices[-1], valfmt=slice_valfmt)
self._slice_slider.on_changed(self._slice_changed)
self._scale_radio = RadioButtons(
self._scale_ax, ('lin', 'log'), active=self._scale_active())
self._scale_radio.on_clicked(self._set_scale)
self._auto_button = CheckButtons(
self._auto_ax, ['autoscale'], [self._autoscale])
self._auto_button.on_clicked(self._autoscale_changed)
if self._title_fmtstr is not None:
self._title = self._ax_image.set_title(self._make_title())
if xlabel is not None:
self._ax_image.set_xlabel(xlabel)
if ylabel is not None:
self._ax_image.set_ylabel(ylabel)
self._slice_changed(0)
def _make_title(self, slice_index=0):
return self._title_fmtstr.format(
slice=slice_index + 1, num_slices=self._slices.size,
pos=self._slices[slice_index]
)
def _apply_logscale(self, data: np.ndarray) -> np.ndarray:
if self._data_range[0] < 0.0:
return np.log10((1.0 - self._data_range[0]) + data)
else:
return np.log10(1.0 + data)
def _slice_changed(self, pos):
slice_index = np.argmin(np.abs(self._slices - pos))
img_slice = self._data[slice_index]
if self._logscale:
img_slice = self._scale_fun(img_slice)
self._image.set_array(img_slice)
if self._autoscale:
self._image.autoscale()
if self._title is not None:
self._title.set_text(self._make_title(slice_index))
self._fig.canvas.draw_idle()
def _set_scale(self, val):
if val == 'log':
self._logscale = True
else:
self._logscale = False
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice_changed(self._slice_slider.val)
def _scale_active(self):
return {False: 0, True:1}.get(self._logscale, 0)
def _autoscale_changed(self, value):
self._autoscale = not self._autoscale
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice_changed(self._slice_slider.val)
def _vmin_changed(self, value):
vmin = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmin = self._scale_fun(vmin)
self._image.set_clim(vmin=vmin)
def _vmax_changed(self, value):
vmax = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmax = self._scale_fun(vmax)
self._image.set_clim(vmax=vmax)
def _get_ax(self):
return self._ax_image
axis = property(_get_ax, None, None, 'Image axis.')
def _get_fig(self):
return self._fig
fig = property(_get_fig, None, None, 'Figure instance.')
[docs] def show(self):
'''
Show the SliceView window.
'''
pp.show()
def show():
pp.show()
[docs]class SliceViewCyl:
def __init__(self, data: np.ndarray, axis: int = 0,
slices: np.ndarray = None, polar=False,
logscale: bool or Callable[[np.ndarray], np.ndarray]=False,
autoscale: bool = False,
xlabel: str = None, ylabel: str = None, title: str = None,
**kwargs):
'''
Creates a matplotlib-based slice viewer of 3D data cubes along
one axis.
Parameters
----------
data: np.ndarray
A 3D Data array.
axis: int
The axis along which to slice the data.
Assuming that the dimensions of the data are (z, fi, r), the
produced slices are as follows:
- along = 0; slicing along the z axis, slice dimensions (fi, r)
- along = 1; slicing along the fi axis, slice dimensions (z, r)
- along = 2; slicing along the z axis, slice dimensions (z, fi)
polar: bool
Set to True if the slice is 'r-fi'. Note that the slice data is
expected in order (..., r, fi, ...).
slices: np.ndarray
A vector that defines the positions of slices. If None, a default
sequence of positions from 0 to num_slices - 1 is created.
logscale: bool or Callable[[np.ndarray], np.ndarray]
A bool value enables or disables the logarithmic scaling of the
data that is computed as:
- :code:`np.log10(slice_data + 1)` if data are strictly positive
- :code:`np.log10(slice_dat - data.min() + 1)` if data include negative values
If a callable, all data are scaled with this function.
autoscale: bool
Automatically adjust the intensity range of the image to
the current slice minimum and maximum intensity.
title: str
Slice title. This can be a format string with placeholders for
the slice number "{slice}", total number of slices "{num_slices}"
and position of the slice "{pos}".
xlabel: str
Label of the x axis.
ylabel: str
Label of the y axis.
kwargs: dict
Additional keyword arguments that are passed to pyplot.imshow or
pyplot.pcolormesh.
'''
self._axis = int(axis)
data = np.asarray(data)
if title is None:
title = 'Slice {slice}/{num_slices} @ {pos:.4f}'
self._title = None
self._title_fmtstr = title
if axis != 0:
order = {0:(0, 1, 2), 1:(1, 0, 2), 2:(2, 0, 1)}.get(axis)
data = np.transpose(data, order)
self._data = data
self._data_range = (data.min(), data.max())
self._logmin = np.finfo(float).eps
if slices is None:
slices = np.arange(data.shape[axis])
self._slices = slices
if callable(logscale):
self._scale_fun = logscale
logscale = True
else:
self._scale_fun = self._apply_logscale
self._logscale = bool(logscale)
self._autoscale = bool(autoscale)
self._polar_axis = bool(polar)
subplot_kw = {}
if self._polar_axis:
subplot_kw = {'projection': 'polar'}
self._fig, self._ax_image = pp.subplots(subplot_kw=subplot_kw)
self._fig.canvas.manager.set_window_title('Cylindrical slice View')
if 'vmin' in kwargs:
kwargs.pop('vmin')
if 'vmax' in kwargs:
kwargs.pop('vmax')
R = Fi = None
if 'R' in kwargs:
R = kwargs.pop('R')
if 'Fi' in kwargs:
Fi = kwargs.pop('Fi')
if self._polar_axis:
if R is None or Fi is None:
fi = np.linspace(0, 2*np.pi, data.shape[2])
r = np.arange(data.shape[1], dtype=np.float64)
R, Fi = np.meshgrid(r, fi, indexing='ij')
self._image = pp.pcolormesh(
Fi, R, data[0],
vmin=self._data_range[0], vmax=self._data_range[1])
else:
self._image = self._ax_image.imshow(
data[0], vmin=self._data_range[0], vmax=self._data_range[1],
**kwargs
)
pp.colorbar(self._image, orientation='vertical')
pp.subplots_adjust(bottom=0.30, left=0.20)
self._slice_ax = pp.axes([0.30, 0.16, 0.58, 0.03])
self._vmin_ax = pp.axes([0.30, 0.08, 0.58, 0.03])
self._vmax_ax = pp.axes([0.30, 0.03, 0.58, 0.03])
self._scale_ax = pp.axes([0.04, 0.01, 0.15, 0.10])
self._auto_ax = pp.axes([0.04, 0.12, 0.15, 0.08])
self._vmin_slider = Slider(
self._vmin_ax, 'vmin',
valinit=0.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmin_slider.on_changed(self._vmin_changed)
self._vmax_slider = Slider(
self._vmax_ax, 'vmax',
valinit=100.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmax_slider.on_changed(self._vmax_changed)
self._slice_slider = Slider(
self._slice_ax, 'Slice', valinit=slices[0],
valmin=slices[0], valmax=slices[-1], valfmt='%.4f')
self._slice_slider.on_changed(self._slice_changed)
self._scale_radio = RadioButtons(
self._scale_ax, ('lin', 'log'), active=self._scale_active())
self._scale_radio.on_clicked(self._set_scale)
self._auto_button = CheckButtons(
self._auto_ax, ['autoscale'], [self._autoscale])
self._auto_button.on_clicked(self._autoscale_changed)
if self._title_fmtstr is not None:
self._title = self._ax_image.set_title(self._make_title())
if xlabel is not None:
self._ax_image.set_xlabel(xlabel)
if ylabel is not None:
self._ax_image.set_ylabel(ylabel)
self._slice_changed(0)
def _make_title(self, slice_index=0):
return self._title_fmtstr.format(
slice=slice_index + 1, num_slices=self._slices.size,
pos=self._slices[slice_index]
)
def _apply_logscale(self, data: np.ndarray) -> np.ndarray:
if self._data_range[0] < 0.0:
return np.log10((1.0 - self._data_range[0]) + data)
else:
return np.log10(1.0 + data)
def _slice_changed(self, pos):
slice_index = np.argmin(np.abs(self._slices - pos))
img_slice = self._data[slice_index]
if self._logscale:
img_slice = self._scale_fun(img_slice)
if self._polar_axis:
self._image.set_array(img_slice.flatten())
else:
self._image.set_array(img_slice)
if self._autoscale:
self._image.autoscale()
if self._title is not None:
self._title.set_text(self._make_title(slice_index))
self._fig.canvas.draw_idle()
def _set_scale(self, val):
if val == 'log':
self._logscale = True
else:
self._logscale = False
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice_changed(self._slice_slider.val)
def _scale_active(self):
return {False: 0, True:1}.get(self._logscale, 0)
def _autoscale_changed(self, value):
self._autoscale = not self._autoscale
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice_changed(self._slice_slider.val)
def _vmin_changed(self, value):
vmin = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmin = self._scale_fun(vmin)
self._image.set_clim(vmin=vmin)
def _vmax_changed(self, value):
vmax = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmax = self._scale_fun(vmax)
self._image.set_clim(vmax=vmax)
def _get_ax(self):
return self._ax_image
axis = property(_get_ax, None, None, 'Image axis.')
def _get_fig(self):
return self._fig
fig = property(_get_fig, None, None, 'Figure instance.')
[docs] def show(self):
'''
Show the SliceView window.
'''
pp.show()
def show():
pp.show()
[docs]class DualSliceViewCyl:
def __init__(self, data: np.ndarray,
axis1: int = 0, slices1: np.ndarray = None,
slice1_label: str = None, slice1_valfmt: str = None,
axis2: int = 0, slices2: np.ndarray = None,
slice2_label: str = None, slice2_valfmt: str = None,
polar: bool = False,
logscale: bool or Callable[[np.ndarray], np.ndarray]=False,
autoscale: bool = False,
xlabel: str = None, ylabel: str = None, title: str = None,
**kwargs):
'''
Creates a matplotlib-based slice viewer of 3D data cubes along
one axis.
Parameters
----------
data: np.ndarray
A 3D Data array.
axis1: int
The axis along which to slice the data.
Assuming that the dimensions of the data are (z, fi, r), the
produced slices are as follows:
- along = 0; slicing along the z axis, slice dimensions (fi, r)
- along = 1; slicing along the fi axis, slice dimensions (z, r)
- along = 2; slicing along the z axis, slice dimensions (z, fi)
slices1: np.ndarray
A vector that defines the positions of slices along axis1.
If None, a default sequence of positions
from 0 to num_slices - 1 is created.
slice1_label: str
Label of the slider along the axis1 axis.
slice1_valfmt: str
Slice along axis1 - value format string.
axis2: int
The axis along which to slice the data.
Assuming that the dimensions of the data are (z, fi, r), the
produced slices are as follows:
- along = 0; slicing along the z axis, slice dimensions (fi, r)
- along = 1; slicing along the fi axis, slice dimensions (z, r)
- along = 2; slicing along the z axis, slice dimensions (z, fi)
slices2: np.ndarray
A vector that defines the positions of slices along axis2.
If None, a default sequence of positions
from 0 to num_slices - 1 is created.
slice2_label: str
Label of the slider along the axis2 axis.
slice2_valfmt: str
Slice along axis1 - value format string.
polar: bool
Set to True if the slice is 'r-fi'. Note that the slice data is
expected in order (..., r, fi, ...).
logscale: bool or Callable[[np.ndarray], np.ndarray]
A bool value enables or disables the logarithmic scaling of the
data that is computed as:
- :code:`np.log10(slice_data + 1)` if data are strictly positive
- :code:`np.log10(slice_dat - data.min() + 1)` if data include negative values
If a callable, all data are scaled with this function.
autoscale: bool
Automatically adjust the intensity range of the image to
the current slice minimum and maximum intensity.
title: str
Slice title. This can be a format string with placeholders for
the slice number "{slice}", total number of slices "{num_slices}"
and position of the slice "{pos}".
xlabel: str
Label of the x axis.
ylabel: str
Label of the y axis.
kwargs: dict
Additional keyword arguments that are passed to pyplot.imshow or
pyplot.pcolormesh.
'''
self._axis1 = int(axis1)
self._axis2 = int(axis2)
if self._axis1 == self._axis2:
raise ValueError('Cannot slice along the same axis!')
if slice1_label is None:
slice1_label = 'Slice'
if slice2_label is None:
slice2_label = 'Slice'
if slice1_valfmt is None:
slice1_valfmt = '%.4f'
if slice2_valfmt is None:
slice2_valfmt = '%.4f'
if title is None:
title = 'Slice {slice1}/{num_slices1} @ {pos1:.4f}, '\
'{slice2}/{num_slices2} @ {pos2:.4f}'
self._title = None
self._title_fmtstr = title
self._logmin = np.finfo(float).eps
if slices1 is None:
slices1 = np.arange(data.shape[axis1])
if slices2 is None:
slices2 = np.arange(data.shape[axis2])
self._slices1 = slices1
self._slices2 = slices2
self._data = data
self._data_range = (data.min(), data.max())
self._slice_all = [slice(0, s) for s in self._data.shape]
if callable(logscale):
self._scale_fun = logscale
logscale = True
else:
self._scale_fun = self._apply_logscale
self._logscale = bool(logscale)
self._autoscale = bool(autoscale)
self._polar_axis = bool(polar)
subplot_kw = {}
if self._axis2 == 0:
subplot_kw = {'projection': 'polar'}
self._fig, self._ax_image = pp.subplots(subplot_kw=subplot_kw)
self._fig.canvas.manager.set_window_title('Cylindrical slice View')
if 'vmin' in kwargs:
kwargs.pop('vmin')
if 'vmax' in kwargs:
kwargs.pop('vmax')
R = Fi = None
if 'R' in kwargs:
R = kwargs.pop('R')
if 'Fi' in kwargs:
Fi = kwargs.pop('Fi')
select = self._slice_all
select[self._axis1] = select[self._axis2] = slice(0, 1)
data_slice = np.squeeze(data[tuple(select)])
if self._polar_axis:
if R is None or Fi is None:
fi = np.linspace(0, 2*np.pi, data.shape[2])
r = np.arange(data.shape[1], dtype=np.float64)
R, Fi = np.meshgrid(r, fi, indexing='ij')
self._image = pp.pcolormesh(
Fi, R, data_slice,
vmin=self._data_range[0], vmax=self._data_range[1])
else:
self._image = self._ax_image.imshow(
data_slice,
vmin=self._data_range[0], vmax=self._data_range[1],
**kwargs
)
pp.colorbar(self._image, orientation='vertical')
pp.subplots_adjust(bottom=0.35, left=0.20)
self._slice2_ax = pp.axes([0.30, 0.16, 0.58, 0.03])
self._slice1_ax = pp.axes([0.30, 0.20, 0.58, 0.03])
self._vmin_ax = pp.axes([0.30, 0.08, 0.58, 0.03])
self._vmax_ax = pp.axes([0.30, 0.03, 0.58, 0.03])
self._scale_ax = pp.axes([0.04, 0.01, 0.15, 0.10])
self._auto_ax = pp.axes([0.04, 0.12, 0.15, 0.08])
self._vmin_slider = Slider(
self._vmin_ax, 'vmin',
valinit=0.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmin_slider.on_changed(self._vmin_changed)
self._vmax_slider = Slider(
self._vmax_ax, 'vmax',
valinit=100.0, valmin=0.0, valmax=100.0, valfmt='%4.1f %%')
self._vmax_slider.on_changed(self._vmax_changed)
self._slice1_slider = Slider(
self._slice1_ax, slice1_label, valinit=slices1[0],
valmin=slices1[0], valmax=slices1[-1], valfmt=slice1_valfmt)
self._slice1_slider.on_changed(self._slice1_changed)
self._slice2_slider = Slider(
self._slice2_ax, 'Slice', valinit=slices2[0],
valmin=slices2[0], valmax=slices2[-1], valfmt=slice2_valfmt)
self._slice2_slider.on_changed(self._slice2_changed)
self._scale_radio = RadioButtons(
self._scale_ax, ('lin', 'log'), active=self._scale_active())
self._scale_radio.on_clicked(self._set_scale)
self._auto_button = CheckButtons(
self._auto_ax, ['autoscale'], [self._autoscale])
self._auto_button.on_clicked(self._autoscale_changed)
if self._title_fmtstr is not None:
self._title = self._ax_image.set_title(self._make_title())
if xlabel is not None:
self._ax_image.set_xlabel(xlabel)
if ylabel is not None:
self._ax_image.set_ylabel(ylabel)
self._slice1_changed(0)
self._slice2_changed(0)
def _make_title(self, slice1_index: int = None, slice2_index: int = None):
if slice1_index is None:
slice1_index = int(self._slice1_slider.val)
if slice2_index is None:
slice2_index = int(self._slice2_slider.val)
return self._title_fmtstr.format(
slice1=slice1_index + 1, num_slices1=self._slices1.size,
slice2=slice2_index + 1, num_slices2=self._slices2.size,
pos1=self._slices1[slice1_index], pos2=self._slices2[slice2_index]
)
def _apply_logscale(self, data: np.ndarray) -> np.ndarray:
if self._data_range[0] < 0.0:
return np.log10((1.0 - self._data_range[0]) + data)
else:
return np.log10(1.0 + data)
def _slice1_changed(self, pos):
slice_index = np.argmin(np.abs(self._slices1 - pos))
select = self._slice_all
select[self._axis1] = slice(slice_index, slice_index + 1)
img_slice = np.squeeze(self._data[tuple(select)])
if self._logscale:
img_slice = self._scale_fun(img_slice)
if self._polar_axis:
self._image.set_array(img_slice.flatten())
else:
self._image.set_array(img_slice)
if self._autoscale:
self._image.autoscale()
if self._title is not None:
self._title.set_text(self._make_title(slice1_index=slice_index))
self._fig.canvas.draw_idle()
def _slice2_changed(self, pos):
slice_index = np.argmin(np.abs(self._slices2 - pos))
select = self._slice_all
select[self._axis2] = slice(slice_index, slice_index + 1)
img_slice = np.squeeze(self._data[tuple(select)])
if self._logscale:
img_slice = self._scale_fun(img_slice)
if self._polar_axis:
self._image.set_array(img_slice.flatten())
else:
self._image.set_array(img_slice)
if self._autoscale:
self._image.autoscale()
if self._title is not None:
self._title.set_text(self._make_title(slice2_index=slice_index))
self._fig.canvas.draw_idle()
def _set_scale(self, val):
if val == 'log':
self._logscale = True
else:
self._logscale = False
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice_changed(self._slice_slider.val)
def _scale_active(self):
return {False: 0, True:1}.get(self._logscale, 0)
def _autoscale_changed(self, value):
self._autoscale = not self._autoscale
if not self._autoscale:
self._vmin_changed(self._vmin_slider.val)
self._vmax_changed(self._vmax_slider.val)
self._slice_changed(self._slice_slider.val)
def _vmin_changed(self, value):
vmin = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmin = self._scale_fun(vmin)
self._image.set_clim(vmin=vmin)
def _vmax_changed(self, value):
vmax = self._data_range[0] + \
(self._data_range[1] - self._data_range[0])*(value*0.01)
if self._logscale:
vmax = self._scale_fun(vmax)
self._image.set_clim(vmax=vmax)
def _get_ax(self):
return self._ax_image
axis = property(_get_ax, None, None, 'Image axis.')
def _get_fig(self):
return self._fig
fig = property(_get_fig, None, None, 'Figure instance.')
[docs] def show(self):
'''
Show the SliceView window.
'''
pp.show()
if __name__ == '__main__':
data = np.random.randint(0, 255, (101,101,101, 20)).astype(np.float64)
sv = DualSliceView(
data,
axis1=2, slices1=np.linspace(0, 1, 101), slice1_label='Time',
axis2=3, slices2=np.linspace(0, 2, 20),
cmap='gray')
pp.show()
exit(0)
data = np.random.randint(0, 255, (20,256,256)).astype(np.float64)
for i in range(data.shape[0]):
data[i] = data[i]*(1.0 - float(i)/data.shape[0])
sv = SliceView(data, slices=np.linspace(0, 1, 20), cmap='gray')
pp.show()