#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# File : voxels.py
# License: MIT
# Author : Andrei Leonard Nicusan <a.l.nicusan@bham.ac.uk>
# Date : 20.05.2021
import pickle
import textwrap
import numpy as np
# Plotting is optional
try:
import plotly.graph_objects as go
except ImportError:
pass
try:
import matplotlib
import matplotlib.pyplot as plt
except ImportError:
pass
try:
import pyvista as pv
except ImportError:
pass
[docs]class Voxels:
'''A class managing a 3D voxel space with physical dimensions, including
tools for voxel manipulation and visualisation.
The `.voxels` attribute is simply a `numpy.ndarray[ndim=3, dtype=float64]`.
The `.attrs` dictionary can be used to store extra information.
Attributes
----------
voxels : (M, N, P) np.ndarray[ndim=3, dtype=float64]
The 3D numpy array containing the voxel values. This class assumes a
uniform grid of voxels - that is, the voxel size in each dimension is
constant, but can vary from one dimension to another.
xlim : (2,) np.ndarray[ndim=1, dtype=float64]
The lower and upper boundaries of the voxellised volume in the
x-dimension, formatted as [x_min, x_max].
ylim : (2,) np.ndarray[ndim=1, dtype=float64]
The lower and upper boundaries of the voxellised volume in the
y-dimension, formatted as [y_min, y_max].
zlim : (2,) np.ndarray[ndim=1, dtype=float64]
The lower and upper boundaries of the voxellised volume in the
z-dimension, formatted as [z_min, z_max].
voxel_size : (3,) np.ndarray[ndim=1, dtype=float64]
The lengths of a voxel in the x-, y- and z-dimensions, respectively.
voxel_grids : (3,) list[np.ndarray[ndim=1, dtype=float64]]
A list containing the voxel gridlines in the x-, y-, and z-dimensions.
Each dimension's gridlines are stored as a numpy of the voxel
delimitations, such that it has length (M + 1), where M is the number
of voxels in given dimension.
lower : (3,) np.ndarray[ndim=1, dtype=float64]
The lower left corner of the voxel box; corresponds to
[xlim[0], ylim[0], zlim[0]].
upper : (3,) np.ndarray[ndim=1, dtype=float64]
The upper right corner of the voxel box; corresponds to
[xlim[1], ylim[1], zlim[1]].
attrs : dict[Any, Any]
A dictionary storing any other user-defined information.
See Also
--------
konigcell.Pixels : A class managing a physical 2D pixel space.
konigcell.dynamic3d : Rasterize moving particles' trajectories.
konigcell.static3d : Rasterize static particles' positions.
konigcell.dynamic_prob3d : 3D probability distribution of a quantity.
'''
__slots__ = ("_voxels", "_xlim", "_ylim", "_zlim", "_attrs", "_voxel_size",
"_voxel_grids", "_lower", "_upper")
[docs] def __init__(self, voxels_array, xlim, ylim, zlim, **kwargs):
'''`Voxels` class constructor.
Parameters
----------
voxels_array : 3D numpy.ndarray
A 3D numpy array, corresponding to a pre-defined voxel space.
xlim : (2,) numpy.ndarray
The lower and upper boundaries of the voxellised volume in the
x-dimension, formatted as [x_min, x_max].
ylim : (2,) numpy.ndarray
The lower and upper boundaries of the voxellised volume in the
y-dimension, formatted as [y_min, y_max].
zlim : (2,) numpy.ndarray
The lower and upper boundaries of the voxellised volume in the
z-dimension, formatted as [z_min, z_max].
**kwargs : extra keyword arguments
Extra user-defined attributes to be saved in `.attrs`.
Raises
------
ValueError
If `voxels_array` does not have exactly 3 dimensions or if
`xlim`, `ylim` or `zlim` do not have exactly 2 values each.
'''
# Type-checking inputs
voxels_array = np.asarray(
voxels_array,
order = "C",
dtype = float
)
if voxels_array.ndim != 3:
raise ValueError(textwrap.fill((
"The input `voxels_array` must contain an array-like with "
"exactly three dimensions (i.e. pre-made voxels array). "
f"Received an array with {voxels_array.ndim} dimensions."
)))
xlim = np.asarray(xlim, dtype = float)
if xlim.ndim != 1 or len(xlim) != 2:
raise ValueError(textwrap.fill((
"The input `xlim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the voxel space in the x-dimension. "
f"Received parameter with shape {xlim.shape}."
)))
ylim = np.asarray(ylim, dtype = float)
if ylim.ndim != 1 or len(ylim) != 2:
raise ValueError(textwrap.fill((
"The input `ylim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the voxel space in the y-dimension. "
f"Received parameter with shape {ylim.shape}."
)))
zlim = np.asarray(zlim, dtype = float)
if zlim.ndim != 1 or len(zlim) != 2:
raise ValueError(textwrap.fill((
"The input `zlim` parameter must be a list with exactly "
"two values, corresponding to the minimum and maximum "
"coordinates of the voxel space in the z-dimension. "
f"Received parameter with shape {zlim.shape}."
)))
# Setting class attributes
self._voxels = voxels_array
self._xlim = xlim
self._ylim = ylim
self._zlim = zlim
self._attrs = dict(kwargs)
@property
def voxels(self):
return self._voxels
@property
def xlim(self):
return self._xlim
@property
def ylim(self):
return self._ylim
@property
def zlim(self):
return self._zlim
@property
def voxel_size(self):
# Compute once upon the first access and cache
if not hasattr(self, "_voxel_size"):
self._voxel_size = np.array([
(self._xlim[1] - self._xlim[0]) / self._voxels.shape[0],
(self._ylim[1] - self._ylim[0]) / self._voxels.shape[1],
(self._zlim[1] - self._zlim[0]) / self._voxels.shape[2],
])
return self._voxel_size
@property
def voxel_grids(self):
# Compute once upon the first access and cache
if not hasattr(self, "_voxel_grids"):
self._voxel_grids = [
np.linspace(lim[0], lim[1], self._voxels.shape[i] + 1)
for i, lim in enumerate((self._xlim, self._ylim, self._zlim))
]
return self._voxel_grids
@property
def lower(self):
# Compute once upon the first access and cache
if not hasattr(self, "_lower"):
self._lower = np.array([
self._xlim[0],
self._ylim[0],
self._zlim[0],
])
return self._lower
@property
def upper(self):
# Compute once upon the first access and cache
if not hasattr(self, "_upper"):
self._upper = np.array([
self._xlim[1],
self._ylim[1],
self._zlim[1],
])
return self._upper
@property
def attrs(self):
return self._attrs
[docs] def save(self, filepath):
'''Save a `Voxels` instance as a binary `pickle` object.
Saves the full object state, including the inner `.voxels` NumPy array,
`xlim`, etc. in a fast, portable binary format. Load back the object
using the `load` method.
Parameters
----------
filepath : filename or file handle
If filepath is a path (rather than file handle), it is relative
to where python is called.
Examples
--------
Save a `Voxels` instance, then load it back:
>>> import numpy as np
>>> import konigcell as kc
>>>
>>> grid = np.zeros((64, 48, 32))
>>> voxels = kc.Voxels(grid, [0, 20], [0, 10])
>>> voxels.save("voxels.pickle")
>>> voxels_reloaded = kc.Voxels.load("voxels.pickle")
'''
with open(filepath, "wb") as f:
pickle.dump(self, f)
[docs] @staticmethod
def load(filepath):
'''Load a saved / pickled `Voxels` object from `filepath`.
Most often the full object state was saved using the `.save` method.
Parameters
----------
filepath : filename or file handle
If filepath is a path (rather than file handle), it is relative
to where python is called.
Returns
-------
pept.Voxels
The loaded `pept.Voxels` instance.
Examples
--------
Save a `Voxels` instance, then load it back:
>>> import numpy as np
>>> import konigcell as kc
>>>
>>> grid = np.zeros((64, 48, 32))
>>> voxels = kc.Voxels(grid, [0, 20], [0, 10])
>>> voxels.save("voxels.pickle")
>>> voxels_reloaded = kc.Voxels.load("voxels.pickle")
'''
with open(filepath, "rb") as f:
obj = pickle.load(f)
return obj
[docs] def copy(self, voxels_array = None, xlim = None, ylim = None, zlim = None,
**kwargs):
'''Create a copy of the current `Voxels` instance, optionally with new
`voxels_array`, `xlim` and / or `ylim`.
The extra attributes in `.attrs` are propagated too. Pass new
attributes as extra keyword arguments.
'''
if voxels_array is None:
voxels_array = self.voxels.copy()
if xlim is None:
xlim = self.xlim.copy()
if ylim is None:
ylim = self.ylim.copy()
if zlim is None:
zlim = self.zlim.copy()
# Propagate attributes
kwargs.update(self.attrs)
return Voxels(voxels_array, xlim, ylim, zlim, **kwargs)
[docs] @staticmethod
def zeros(shape, xlim, ylim, zlim, **kwargs):
'''Create a Voxels object filled with zeros.
'''
zero_voxels = np.zeros(shape, dtype = float)
return Voxels(zero_voxels, xlim, ylim, zlim, **kwargs)
[docs] def from_physical(self, locations, corner = False):
'''Transform `locations` from physical dimensions to voxel indices. If
`corner = True`, return the index of the bottom left corner of each
voxel; otherwise, use the voxel centres.
Examples
--------
Create a simple `konigcell.Voxels` grid, spanning [-5, 5] mm in the
X-dimension, [10, 20] mm in the Y-dimension and [0, 10] in Z:
>>> import konigcell as kc
>>> voxels = kc.Voxels.zeros((5, 5, 5), xlim=[-5, 5], ylim=[10, 20],
zlim=[0, 10])
>>> voxels
Voxels
------
xlim = [-5. 5.]
ylim = [10. 20.]
zlim = [10. 20.]
voxels =
(shape: (5, 5, 5))
[[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]
[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]
...
[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]
[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]]
attrs = {}
>>> voxels.voxel_size
array([2., 2., 2.])
Transform physical coordinates to voxel coordinates:
>>> voxels.from_physical([-5, 10, 0], corner = True)
array([0., 0., 0.])
>>> voxels.from_physical([-5, 10, 0])
array([-0.5, -0.5, -0.5])
The voxel coordinates are returned exactly, as real numbers. For voxel
indices, round them into values:
>>> voxels.from_physical([0, 15, 0]).astype(int)
array([2, 2, 0])
Multiple coordinates can be given as a 2D array / list of lists:
>>> voxels.from_physical([[0, 15, 0], [5, 20, 10]])
array([[ 2. , 2. , -0.5],
[ 4.5, 4.5, 4.5]])
'''
offset = 0. if corner else self.voxel_size / 2
return (locations - self.lower - offset) / self.voxel_size
[docs] def to_physical(self, indices, corner = False):
'''Transform `indices` from voxel indices to physical dimensions. If
`corner = True`, return the coordinates of the bottom left corner of
each voxel; otherwise, use the voxel centres.
Examples
--------
Create a simple `konigcell.Voxels` grid, spanning [-5, 5] mm in the
X-dimension, [10, 20] mm in the Y-dimension and [0, 10] in Z:
>>> import konigcell as kc
>>> voxels = kc.Voxels.zeros((5, 5, 5), xlim=[-5, 5], ylim=[10, 20],
zlim=[0, 10])
>>> voxels
Voxels
------
xlim = [-5. 5.]
ylim = [10. 20.]
zlim = [10. 20.]
voxels =
(shape: (5, 5, 5))
[[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]
[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]
...
[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]
[[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]
...
[0. 0. ... 0. 0.]
[0. 0. ... 0. 0.]]]
attrs = {}
>>> voxels.voxel_size
array([2., 2., 2.])
Transform physical coordinates to voxel coordinates:
>>> voxels.to_physical([0, 0, 0], corner = True)
array([-5., 10., 0.])
>>> voxels.to_physical([0, 0, 0])
array([-4., 11., 1.])
Multiple coordinates can be given as a 2D array / list of lists:
>>> voxels.to_physical([[0, 0, 0], [4, 4, 3]])
array([[-4., 11., 1.],
[ 4., 19., 7.]])
'''
offset = 0. if corner else self.voxel_size / 2
return self.lower + indices * self.voxel_size + offset
[docs] def plot(
self,
condition = lambda voxel_data: voxel_data > 0,
ax = None,
alt_axes = False,
):
'''Plot the voxels in this class using Matplotlib.
This plots the centres of all voxels encapsulated in a `pept.Voxels`
instance, colour-coding the voxel value.
The `condition` parameter is a filtering function that should return
a boolean mask (i.e. it is the result of a condition evaluation). For
example `lambda x: x > 0` selects all voxels that have a value larger
than 0.
Parameters
----------
condition : function, default `lambda voxel_data: voxel_data > 0`
The filtering function applied to the voxel data before plotting
it. It should return a boolean mask (a numpy array of the same
shape, filled with True and False), selecting all voxels that
should be plotted. The default, `lambda x: x > 0` selects all
voxels which have a value larger than 0.
ax : mpl_toolkits.mplot3D.Axes3D object, optional
The 3D matplotlib-based axis for plotting. If undefined, new
Matplotlib figure and axis objects are created.
alt_axes : bool, default False
If `True`, plot using the alternative PEPT-style axes convention:
z is horizontal, y points upwards. Because Matplotlib cannot swap
axes, this is achieved by swapping the parameters in the plotting
call (i.e. `plt.plot(x, y, z)` -> `plt.plot(z, x, y)`).
Returns
-------
fig, ax
Matplotlib figure and axes objects.
Notes
-----
Plotting all points is very computationally-expensive for matplotlib.
It is recommended to only plot a couple of samples at a time, or use
Plotly, which is faster.
Examples
--------
Voxellise an array of lines and add them to a `PlotlyGrapher` instance:
>>> import konigcell as kc
>>>
>>> lines = np.array(...) # shape (N, M >= 7)
>>> number_of_voxels = [10, 10, 10]
>>> voxels = kc.Voxels(lines, number_of_voxels)
>>> fig, ax = voxels.plot()
>>> fig.show()
'''
if ax is None:
fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
else:
fig = plt.gcf()
filtered_indices = np.argwhere(condition(self.voxels))
positions = self.voxel_size * (0.5 + filtered_indices) + \
[self.xlim[0], self.ylim[0], self.zlim[0]]
x = positions[:, 0]
y = positions[:, 1]
z = positions[:, 2]
voxel_vals = np.array([
self.voxels[tuple(fi)]
for fi in filtered_indices
])
cmap = plt.cm.magma
color_array = cmap(voxel_vals / voxel_vals.max())
if alt_axes:
ax.scatter(z, x, y, c = color_array, marker = "s")
ax.set_xlabel("z (mm)")
ax.set_ylabel("x (mm)")
ax.set_zlabel("y (mm)")
else:
ax.scatter(x, y, z, c = color_array, marker = "s")
ax.set_xlabel("x (mm)")
ax.set_ylabel("y (mm)")
ax.set_zlabel("z (mm)")
return fig, ax
[docs] def plot_volumetric(
self,
condition = lambda voxels: voxels > 0,
mode = "box",
colorscale = "magma",
):
'''Create a volumetric PyVista plot - check the `mode` argument for
the available types.
Parameters
----------
condition : function, default `lambda voxel_data: voxel_data > 0`
The filtering function applied to the voxel data before plotting
it. It should return a boolean mask (a numpy array of the same
shape, filled with True and False), selecting all voxels that
should be plotted. The default, `lambda x: x > 0` selects all
voxels which have a value larger than 0.
mode : "box", "plane", "slice"
Use a VTK clip box, clip plane or clip slice.
colorscale : str, default "magma"
The PyVista colorscale to use.
Returns
-------
pyvista.Plotter
A PyVista Figure object that can be ``.show()``.
'''
# Type-checking inputs
mode = str(mode).lower()
vox = self.voxels.copy(order = "F")
vox[~(condition(vox))] = 0.
# You need to install PyVista to use this function!
grid = pv.UniformGrid()
grid.dimensions = np.array(vox.shape) + 1
grid.origin = self.lower
grid.spacing = self.voxel_size
grid.cell_data["values"] = vox.flatten(order="F")
# Create PyVista volumetric / voxel plot with an interactive clipper
fig = pv.Plotter()
if mode == "plane":
fig.add_mesh_clip_plane(grid, cmap = colorscale)
elif mode == "box":
fig.add_mesh_clip_box(grid, cmap = colorscale)
elif mode == "slice":
fig.add_mesh(grid.slice_orthogonal(), cmap = colorscale)
else:
raise ValueError(textwrap.fill((
"The input `mode` must be one of 'plane' | 'box' | 'slice'. "
f"Received `mode={mode}`."
)))
return fig
[docs] def vtk(
self,
condition = lambda voxels: voxels != 0.,
):
'''Return a PyVista VTK object, exposing all VTK functionality.
Parameters
----------
condition : function, default `lambda voxel_data: voxel_data > 0`
The filtering function applied to the voxel data before plotting
it. It should return a boolean mask (a numpy array of the same
shape, filled with True and False), selecting all voxels that
should be plotted. The default, `lambda x: x > 0` selects all
voxels which have a value larger than 0.
Returns
-------
pyvista.UniformGrid
A VTK UniformGrid object.
'''
vox = self.voxels.copy(order = "F")
vox[~(condition(vox))] = 0.
# You need to install PyVista to use this function!
grid = pv.UniformGrid()
grid.dimensions = np.array(vox.shape) + 1
grid.origin = self.lower
grid.spacing = self.voxel_size
grid.cell_data["values"] = vox.flatten(order="F")
return grid
[docs] def cube_trace(
self,
index,
color = None,
opacity = 0.4,
colorbar = True,
colorscale = "magma",
):
'''Get the Plotly `Mesh3d` trace for a single voxel at `index`.
This renders the voxel as a cube. While visually accurate, this method
is *very* computationally intensive - only use it for fewer than 100
cubes. For more voxels, use the `voxels_trace` method.
Parameters
----------
index : (3,) tuple
The voxel indices, given as a 3-tuple.
color : str or list-like, optional
Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a
colorbar list. Overrides `colorbar` if set. For more information,
check the Plotly documentation. The default is None.
opacity : float, default 0.4
The opacity of the lines, where 0 is transparent and 1 is fully
opaque.
colorbar : bool, default True
If set to True, will color-code the voxel values. Is overridden if
`color` is set.
colorscale : str, default "Magma"
The Plotly scheme for color-coding the voxel values in the input
data. Typical ones include "Cividis", "Viridis" and "Magma".
A full list is given at `plotly.com/python/builtin-colorscales/`.
Only has an effect if `colorbar = True` and `color` is not set.
Raises
------
ValueError
If `index` does not contain exactly three values.
Notes
-----
If you want to render a small number of voxels as cubes using Plotly,
use the `cubes_traces` method, which creates a list of individual cubes
for all voxels, using this function.
'''
index = np.asarray(index, dtype = int)
if index.ndim != 1 or len(index) != 3:
raise ValueError(textwrap.fill((
"The input `index` must contain exactly three values, "
"corresponding to the x, y, z indices of the voxel to plot. "
f"Received {index}."
)))
xyz = self.voxel_size * index + \
[self.xlim[0], self.ylim[0], self.zlim[0]]
x = np.array([0, 0, 1, 1, 0, 0, 1, 1]) * self.voxel_size[0]
y = np.array([0, 1, 1, 0, 0, 1, 1, 0]) * self.voxel_size[1]
z = np.array([0, 0, 0, 0, 1, 1, 1, 1]) * self.voxel_size[2]
i = np.array([7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2])
j = np.array([3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3])
k = np.array([0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6])
cube = dict(
x = x + xyz[0],
y = y + xyz[1],
z = z + xyz[2],
i = i,
j = j,
k = k,
opacity = opacity,
color = color
)
if colorbar and color is None:
cmap = matplotlib.cm.get_cmap(colorscale)
c = cmap(self.voxels[tuple(index)] / (self.voxels.max() or 1))
cube.update(color = f"rgb({c[0]},{c[1]},{c[2]})")
# You need to install Plotly to use this function!
return go.Mesh3d(cube)
[docs] def cubes_traces(
self,
condition = lambda voxels: voxels > 0,
color = None,
opacity = 0.4,
colorbar = True,
colorscale = "magma",
):
'''Get a list of Plotly `Mesh3d` traces for all voxels selected by the
`condition` filtering function.
The `condition` parameter is a filtering function that should return
a boolean mask (i.e. it is the result of a condition evaluation). For
example `lambda x: x > 0` selects all voxels that have a value larger
than 0.
This renders each voxel as individual cubes. While visually accurate,
this method is *very* computationally intensive - only use it for fewer
than 100 cubes. For more voxels, use the `voxels_trace` method.
Parameters
----------
condition : function, default `lambda voxels: voxels > 0`
The filtering function applied to the voxel data before plotting
it. It should return a boolean mask (a numpy array of the same
shape, filled with True and False), selecting all voxels that
should be plotted. The default, `lambda x: x > 0` selects all
voxels which have a value larger than 0.
color : str or list-like, optional
Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a
colorbar list. Overrides `colorbar` if set. For more information,
check the Plotly documentation. The default is None.
opacity : float, default 0.4
The opacity of the lines, where 0 is transparent and 1 is fully
opaque.
colorbar : bool, default True
If set to True, will color-code the voxel values. Is overridden if
`color` is set.
colorscale : str, default "magma"
The Plotly scheme for color-coding the voxel values in the input
data. Typical ones include "Cividis", "Viridis" and "Magma".
A full list is given at `plotly.com/python/builtin-colorscales/`.
Only has an effect if `colorbar = True` and `color` is not set.
Examples
--------
Plot a `konigcell.Voxels` on a `plotly.graph_objs.Figure`.
>>> import konigcell as kc
>>> voxels = ...
>>> import plotly.graph_objs as go
>>>
>>> fig = go.Figure()
>>> fig.add_traces(voxels.cubes_traces()) # small number of voxels
>>> fig.show()
'''
indices = np.argwhere(condition(self.voxels))
traces = [
self.cube_trace(
i,
color = color,
opacity = opacity,
colorbar = colorbar,
colorscale = colorscale,
) for i in indices
]
return traces
[docs] def scatter_trace(
self,
condition = lambda voxel_data: voxel_data > 0,
size = 4,
color = None,
opacity = 0.4,
colorbar = True,
colorscale = "Magma",
colorbar_title = None,
):
'''Create and return a trace for all the voxels in this class, with
possible filtering.
Creates a `plotly.graph_objects.Scatter3d` object for the centres of
all voxels encapsulated in a `pept.Voxels` instance, colour-coding the
voxel value.
The `condition` parameter is a filtering function that should return
a boolean mask (i.e. it is the result of a condition evaluation). For
example `lambda x: x > 0` selects all voxels that have a value larger
than 0.
Parameters
----------
condition : function, default `lambda voxel_data: voxel_data > 0`
The filtering function applied to the voxel data before plotting
it. It should return a boolean mask (a numpy array of the same
shape, filled with True and False), selecting all voxels that
should be plotted. The default, `lambda x: x > 0` selects all
voxels which have a value larger than 0.
size : float, default 4
The size of the plotted voxel points. Note that due to the large
number of voxels in typical applications, the *voxel centres* are
plotted as square points, which provides an easy to understand
image that is also fast and responsive.
color : str or list-like, optional
Can be a single color (e.g. "black", "rgb(122, 15, 241)") or a
colorbar list. Overrides `colorbar` if set. For more information,
check the Plotly documentation. The default is None.
opacity : float, default 0.4
The opacity of the lines, where 0 is transparent and 1 is fully
opaque.
colorbar : bool, default True
If set to True, will color-code the voxel values. Is overridden if
`color` is set.
colorscale : str, default "Magma"
The Plotly scheme for color-coding the voxel values in the input
data. Typical ones include "Cividis", "Viridis" and "Magma".
A full list is given at `plotly.com/python/builtin-colorscales/`.
Only has an effect if `colorbar = True` and `color` is not set.
colorbar_title : str, optional
If set, the colorbar will have this title above it.
Examples
--------
Voxellise an array of lines and add them to a `PlotlyGrapher` instance:
>>> grapher = PlotlyGrapher()
>>> lines = np.array(...) # shape (N, M >= 7)
>>> number_of_voxels = [10, 10, 10]
>>> voxels = pept.Voxels.from_lines(lines, number_of_voxels)
>>> grapher.add_lines(lines)
>>> grapher.add_trace(voxels.voxels_trace())
>>> grapher.show()
'''
filtered_indices = np.argwhere(condition(self.voxels))
positions = self.voxel_size * (0.5 + filtered_indices) + \
[self.xlim[0], self.ylim[0], self.zlim[0]]
marker = dict(
size = size,
color = color,
symbol = "square",
)
if colorbar and color is None:
voxel_vals = [self.voxels[tuple(fi)] for fi in filtered_indices]
marker.update(colorscale = colorscale, color = voxel_vals)
if colorbar_title is not None:
marker.update(colorbar = dict(title = colorbar_title))
voxels = dict(
x = positions[:, 0],
y = positions[:, 1],
z = positions[:, 2],
opacity = opacity,
mode = "markers",
marker = marker,
)
# You need to install Plotly to use this function!
return go.Scatter3d(voxels)
[docs] def heatmap_trace(
self,
ix = None,
iy = None,
iz = None,
width = 0,
colorscale = "Magma",
transpose = True
):
'''Create and return a Plotly `Heatmap` trace of a 2D slice through the
voxels.
The orientation of the slice is defined by the input `ix` (for the YZ
plane), `iy` (XZ), `iz` (XY) parameters - which correspond to the
voxel index in the x-, y-, and z-dimension. Importantly, at least one
of them must be defined.
Parameters
----------
ix : int, optional
The index along the x-axis of the voxels at which a YZ slice is to
be taken. One of `ix`, `iy` or `iz` must be defined.
iy : int, optional
The index along the y-axis of the voxels at which a XZ slice is to
be taken. One of `ix`, `iy` or `iz` must be defined.
iz : int, optional
The index along the z-axis of the voxels at which a XY slice is to
be taken. One of `ix`, `iy` or `iz` must be defined.
width : int, default 0
The number of voxel layers around the given slice index to collapse
(i.e. accumulate) onto the heatmap.
colorscale : str, default "Magma"
The Plotly scheme for color-coding the voxel values in the input
data. Typical ones include "Cividis", "Viridis" and "Magma".
A full list is given at `plotly.com/python/builtin-colorscales/`.
Only has an effect if `colorbar = True` and `color` is not set.
transpose : bool, default True
Transpose the heatmap (i.e. flip it across its diagonal).
Raises
------
ValueError
If neither of `ix`, `iy` or `iz` was defined.
Examples
--------
Voxellise an array of lines and add them to a `PlotlyGrapher` instance:
>>> lines = np.array(...) # shape (N, M >= 7)
>>> number_of_voxels = [10, 10, 10]
>>> voxels = pept.Voxels(lines, number_of_voxels)
>>> import plotly.graph_objs as go
>>> fig = go.Figure()
>>> fig.add_trace(voxels.heatmap_trace())
>>> fig.show()
'''
if ix is not None:
x = self.voxel_grids[1]
y = self.voxel_grids[2]
z = self.voxels[ix, :, :]
for i in range(1, width + 1):
z = z + self.voxels[ix + i, :, :]
z = z + self.voxels[ix - i, :, :]
elif iy is not None:
x = self.voxel_grids[0]
y = self.voxel_grids[2]
z = self.voxels[:, iy, :]
for i in range(1, width + 1):
z = z + self.voxels[:, iy + i, :]
z = z + self.voxels[:, iy - i, :]
elif iz is not None:
x = self.voxel_grids[0]
y = self.voxel_grids[1]
z = self.voxels[:, :, iz]
for i in range(1, width + 1):
z = z + self.voxels[:, :, iz + i]
z = z + self.voxels[:, :, iz - i]
else:
raise ValueError(textwrap.fill((
"[ERROR]: One of the `ix`, `iy`, `iz` slice indices must be "
"provided."
)))
heatmap = dict(
x = x,
y = y,
z = z,
colorscale = colorscale,
transpose = transpose,
)
# You need to install Plotly to use this function!
return go.Heatmap(heatmap)
def __repr__(self):
# String representation of the class
name = "Voxels"
underline = "-" * len(name)
# Custom printing of the .lines and .samples_indices arrays
with np.printoptions(threshold = 5, edgeitems = 2):
voxels_str = f"{textwrap.indent(str(self.voxels), ' ')}"
# Pretty-printing extra attributes
attrs_str = ""
if self.attrs:
items = []
for k, v in self.attrs.items():
s = f" {k.__repr__()}: {v}"
if len(s) > 75:
s = s[:72] + "..."
items.append(s)
attrs_str = "\n" + "\n".join(items) + "\n"
# Return constructed string
return (
f"{name}\n{underline}\n"
f"xlim = {self.xlim}\n"
f"ylim = {self.ylim}\n"
f"zlim = {self.ylim}\n"
f"voxels = \n"
f" (shape: {self.voxels.shape})\n"
f"{voxels_str}\n"
"attrs = {"
f"{attrs_str}"
"}\n"
)