"""SH Glyph Slicer for Skyline."""
import numpy as np
from dipy.utils.optpkg import optional_package
from dipy.viz.skyline.UI.elements import (
create_numeric_input,
render_group,
thin_slider,
toggle_button,
)
from dipy.viz.skyline.render.renderer import (
Visualization,
slice_slider_bounds,
slice_slider_values_from_state,
slice_state_from_slider_values,
)
from dipy.viz.skyline.render.sh_billboard import sph_glyph_billboard_sliced
fury_trip_msg = (
"Skyline requires Fury version 2.0.0 or higher."
" Please upgrade Fury by `pip install -U fury --pre` to use Skyline."
)
fury, has_fury_v2, _ = optional_package(
"fury",
min_version="2.0.0",
trip_msg=fury_trip_msg,
)
if has_fury_v2:
from fury import apply_transformation
from fury.actor import Group
imgui_bundle, has_imgui, _ = optional_package(
"imgui_bundle", min_version="1.92.600", max_version="1.92.801"
)
if has_imgui:
imgui = imgui_bundle.imgui
[docs]
def create_shm_visualization(
input,
idx,
*,
render_callback=None,
scale=1.3,
l_max=8,
lut_res=8,
use_hermite=True,
mapping_mode="cube",
basis_type="descoteaux07",
color_type="orientation",
mask=None,
sync_callback=None,
):
"""Create SH glyph visualization from input.
Parameters
----------
input : tuple
Tuple of one of the following forms:
- (coeffs, affine, filename, basis_type)
- (coeffs, affine, filename)
- (coeffs, affine)
idx : int
Index used for naming when filename is not provided.
render_callback : callable, optional
Callback function to be called after rendering.
scale : float, optional
Initial per-glyph scale.
l_max : int, optional
Maximum SH order.
lut_res : int, optional
LUT resolution.
use_hermite : bool, optional
Whether to use Hermite analytic normals.
mapping_mode : str, optional
Billboard mapping mode.
basis_type : str, optional
SH basis convention. Ignored if provided in ``input`` as 4th element.
color_type : str, optional
Colour mapping type.
mask : ndarray, optional
Boolean mask of valid voxels.
sync_callback : callable, optional
Callback to trigger when synchronization is available.
Returns
-------
SHGlyph3D
The created SH glyph visualization object.
Raises
------
ValueError
If input is not a tuple of length 2, 3, or 4.
"""
if not isinstance(input, tuple) or len(input) not in (2, 3, 4):
raise ValueError(
"Input must be a tuple containing (coeffs, affine, filename, basis_type), "
"(coeffs, affine, filename), or (coeffs, affine) for SH visualization."
)
if len(input) == 2:
coeffs, affine = input
filename = f"SH_Glyphs_{idx}"
input_basis_type = basis_type
elif len(input) == 3:
coeffs, affine, filename = input
input_basis_type = basis_type
else:
coeffs, affine, filename, input_basis_type = input
return SHGlyph3D(
filename,
coeffs,
affine=affine,
render_callback=render_callback,
scale=scale,
l_max=l_max,
lut_res=lut_res,
use_hermite=use_hermite,
mapping_mode=mapping_mode,
basis_type=input_basis_type,
color_type=color_type,
mask=mask,
sync_callback=sync_callback,
)
def _descoteaux_to_fury_standard(coeffs_4d, sh_order):
"""Convert even-order descoteaux07 SH coefficients to Fury's standard basis.
The legacy descoteaux07 basis uses Im(Y) for m>0 and Re(Y) for m<0, while
FURY uses cos(mφ) for m>0 and sin(|m|φ) for m<0. Coefficients satisfy
``c_fury(l, m) = c_desc(l, -m)``.
Parameters
----------
coeffs_4d : ndarray
Volume storing descoteaux07 coefficients along the last axis.
sh_order : int
Maximum even spherical harmonic order present in the volume.
Returns
-------
ndarray
Array with the same leading shape as ``coeffs_4d`` and
``(sh_order + 1) ** 2`` standard-basis coefficients on the last axis.
"""
n_std = (sh_order + 1) ** 2
out = np.zeros(coeffs_4d.shape[:-1] + (n_std,), dtype=coeffs_4d.dtype)
desc_idx = 0
for l_val in range(0, sh_order + 1, 2):
for m in range(-l_val, l_val + 1):
fury_m = -m
fury_idx = l_val * l_val + l_val + fury_m
out[..., fury_idx] = coeffs_4d[..., desc_idx]
desc_idx += 1
return out
[docs]
class SHSlicer:
"""Represent ``SHSlicer`` in Skyline.
Parameters
----------
coeffs_4d : ndarray
Value for ``coeffs 4d``.
voxel_sizes : tuple(float, float, float), optional
Value for ``voxel sizes``.
scale : float, optional
Value for ``scale``.
l_max : int, optional
Value for ``l max``.
lut_res : int, optional
Value for ``lut res``.
use_hermite : bool, optional
Value for ``use hermite``.
mapping_mode : str, optional
Value for ``mapping mode``.
mask : ndarray, optional
Value for ``mask``.
basis_type : str, optional
Value for ``basis type``.
color_type : str, optional
Value for ``color type``.
"""
def __init__(
self,
coeffs_4d,
*,
voxel_sizes=(1.0, 1.0, 1.0),
scale=1.0,
l_max=8,
lut_res=32,
use_hermite=True,
mapping_mode="cube",
mask=None,
basis_type="standard",
color_type="orientation",
):
"""Represent ``SHSlicer`` in Skyline.
Parameters
----------
coeffs_4d : ndarray
Value for ``coeffs 4d``.
voxel_sizes : tuple(float, float, float), optional
Value for ``voxel sizes``.
scale : float, optional
Value for ``scale``.
l_max : int, optional
Value for ``l max``.
lut_res : int, optional
Value for ``lut res``.
use_hermite : bool, optional
Value for ``use hermite``.
mapping_mode : str, optional
Value for ``mapping mode``.
mask : ndarray, optional
Value for ``mask``.
basis_type : str, optional
Value for ``basis type``.
color_type : str, optional
Value for ``color type``.
"""
if basis_type in ("descoteaux", "descoteaux07"):
coeffs_4d = _descoteaux_to_fury_standard(coeffs_4d, l_max)
basis_type = "standard"
self.coeffs_4d = coeffs_4d
self.shape = coeffs_4d.shape[:3]
self.n_coeffs = coeffs_4d.shape[-1]
self.voxel_sizes = np.array(voxel_sizes, dtype=float)
self.scale = scale
self.l_max = l_max
self.lut_res = lut_res
self.use_hermite = use_hermite
self.mapping_mode = mapping_mode
self.mask = mask
self.basis_type = basis_type
self.color_type = color_type
self._cur = {"x": -1, "y": -1, "z": -1}
self._opacity = 1.0
self.actor = Group()
self._glyph_actor = None
[docs]
def build(self):
"""Handle build for ``SHSlicer``."""
self._glyph_actor = self._build_volume_actor()
if self._glyph_actor is not None:
self.actor.add(self._glyph_actor)
return self.actor
def _build_volume_actor(self):
"""Handle build volume actor for ``SHSlicer``."""
vs = self.voxel_sizes
X, Y, Z = self.shape
flat_coeffs = self.coeffs_4d.reshape(-1, self.n_coeffs)
valid = np.any(flat_coeffs != 0, axis=1)
if self.mask is not None:
valid &= self.mask.ravel()
if not np.any(valid):
return None
ix, iy, iz = np.meshgrid(
np.arange(X, dtype=np.int32),
np.arange(Y, dtype=np.int32),
np.arange(Z, dtype=np.int32),
indexing="ij",
)
voxel_coords = np.column_stack([ix.ravel(), iy.ravel(), iz.ravel()])
centers = voxel_coords.astype(np.float32) * vs[np.newaxis, :]
coeffs_valid = flat_coeffs[valid]
centers_valid = centers[valid]
voxel_valid = voxel_coords[valid]
glyph = sph_glyph_billboard_sliced(
coeffs_valid,
centers_valid,
voxel_valid,
scale=self.scale,
l_max=self.l_max,
color_type=self.color_type,
lut_res=self.lut_res,
use_hermite=self.use_hermite,
mapping_mode=self.mapping_mode,
)
return glyph
[docs]
def set_slice(self, axis, idx):
"""Show slice *idx* on *axis* via uniform update."""
if idx == self._cur[axis]:
return
if self._glyph_actor is not None:
attr = f"active_slice_{axis}"
setattr(self._glyph_actor.material, attr, idx)
self._cur[axis] = idx
[docs]
def hide_axis(self, axis):
"""Hide all slices for *axis*."""
if self._glyph_actor is not None:
setattr(self._glyph_actor.material, f"vis_{axis}", 0)
self._cur[axis] = -1
[docs]
def show_axis(self, axis):
"""Enable axis visibility."""
if self._glyph_actor is not None:
setattr(self._glyph_actor.material, f"vis_{axis}", 1)
[docs]
def set_scale(self, new_scale):
"""Update scale on the actor."""
ratio = float(new_scale) / float(self.scale) if self.scale > 0 else 1.0
if abs(ratio - 1.0) < 1e-6:
return
self.scale = float(new_scale)
a = self._glyph_actor
if a is not None:
a.material.scale = float(new_scale)
a.geometry.normals.data[:, :2] *= ratio
a.geometry.normals.update_full()
[docs]
def set_opacity(self, opacity):
"""Set opacity."""
self._opacity = float(opacity)
a = self._glyph_actor
if a is not None:
a.material.opacity = float(opacity)
a.material.alpha_mode = "blend" if opacity < 1.0 else "solid"
[docs]
class SHGlyph3D(Visualization):
"""Represent ``SHGlyph3D`` in Skyline.
Parameters
----------
name : str
Display name used in the Skyline UI.
coeffs : ndarray
Value for ``coeffs``.
affine : ndarray, optional
Voxel-to-world affine used to position slices in world coordinates.
render_callback : callable, optional
Callback used to request a render/update.
scale : float, optional
Value for ``scale``.
l_max : int, optional
Value for ``l max``.
lut_res : int, optional
Value for ``lut res``.
use_hermite : bool, optional
Value for ``use hermite``.
mapping_mode : str, optional
Value for ``mapping mode``.
basis_type : str, optional
Value for ``basis type``.
color_type : str, optional
Value for ``color type``.
mask : ndarray, optional
Value for ``mask``.
sync_callback : callable, optional
Callback used to synchronize state across views.
"""
def __init__(
self,
name,
coeffs,
*,
affine=None,
render_callback=None,
scale=2.0,
l_max=8,
lut_res=8,
use_hermite=True,
mapping_mode="cube",
basis_type="standard",
color_type="orientation",
mask=None,
sync_callback=None,
):
"""Represent ``SHGlyph3D`` in Skyline.
Parameters
----------
name : str
Display name used in the Skyline UI.
coeffs : ndarray
Value for ``coeffs``.
affine : ndarray, optional
Voxel-to-world affine used to position slices in world coordinates.
render_callback : callable, optional
Callback used to request a render/update.
scale : float, optional
Value for ``scale``.
l_max : int, optional
Value for ``l max``.
lut_res : int, optional
Value for ``lut res``.
use_hermite : bool, optional
Value for ``use hermite``.
mapping_mode : str, optional
Value for ``mapping mode``.
basis_type : str, optional
Value for ``basis type``.
color_type : str, optional
Value for ``color type``.
mask : ndarray, optional
Value for ``mask``.
sync_callback : callable, optional
Callback used to synchronize state across views.
"""
self.affine = affine
default_scale = abs(self.affine[0, 0]) if self.affine is not None else scale
self._voxel_sizes = np.array([1.0, 1.0, 1.0])
self.shape = coeffs.shape[:3]
self._slicer = SHSlicer(
coeffs,
voxel_sizes=self._voxel_sizes,
scale=default_scale,
l_max=l_max,
lut_res=lut_res,
use_hermite=use_hermite,
mapping_mode=mapping_mode,
mask=mask,
basis_type=basis_type,
color_type=color_type,
)
self._slicer.build()
if affine is not None:
self._slicer.actor.transform(self.affine)
super().__init__(name, render_callback)
self._scale = float(default_scale)
self._opacity = 100
self._slice_visibility = [True, True, True]
self._synchronize = True
self._sync_callback = sync_callback
self._last_state = [-1, -1, -1]
lower_bounds = np.zeros(3)
upper_bounds = np.array(coeffs.shape[:3]) - 1
if self.affine is not None:
self.bounds = apply_transformation(
np.array([lower_bounds, upper_bounds]), self.affine
)
self.state = np.asarray(self.bounds).mean(axis=0).astype(int)
else:
self.bounds = np.asarray([lower_bounds, upper_bounds])
self.state = [self.shape[0] // 2, self.shape[1] // 2, self.shape[2] // 2]
self.set_slices()
@property
def actor(self):
"""Handle actor for ``SHGlyph3D``.
Returns
-------
Group
The actor of the SHGlyph3D visualization.
"""
return self._slicer.actor
def _populate_info(self):
"""Handle populate info for ``SHGlyph3D``.
Returns
-------
str
The information of the SHGlyph3D visualization.
"""
info = f"Dimensions: {self.shape}"
info += f"\nSH Coefficients: {self._slicer.n_coeffs}"
info += f"\nSH Order: {self._slicer.l_max}"
if self.affine is not None:
info += f"\nVoxel Sizes: {self._voxel_sizes}"
return info
[docs]
def set_slices(self):
"""Handle set slices for ``SHGlyph3D``."""
if self.affine is not None:
slice_state = np.asarray(self.state[:3], dtype=float)
else:
slice_state = np.clip(
np.asarray(self.state[:3], dtype=float),
0,
np.array(self.shape, dtype=float) - 1,
)
for i, axis in enumerate(("x", "y", "z")):
self._slicer.set_slice(axis, float(slice_state[i]))
self._last_state[i] = self.state[i]
[docs]
def update_state(self, new_state):
"""Handle update state for ``SHGlyph3D``.
Parameters
----------
new_state : array-like
New synchronized state for this visualization.
"""
if self._synchronize:
self.state = new_state[:3]
self.apply_scene_op(self.set_slices)
[docs]
def set_slice_visibility(self):
"""Handle set slice visibility for ``SHGlyph3D``."""
for i, axis in enumerate(("x", "y", "z")):
if self._slice_visibility[i]:
self._slicer.show_axis(axis)
self._last_state[i] = self.state[i]
else:
self._slicer.hide_axis(axis)
self._last_state[i] = -1
if not has_fury_v2:
create_shm_visualization = SHSlicer = SHGlyph3D = fury