"""Tractography layers, clustering, and BUAN coloring for Skyline."""
import colorsys
from pathlib import Path
import time
import numpy as np
from dipy.io.stateful_tractogram import StatefulTractogram
from dipy.io.streamline import save_tractogram
from dipy.io.utils import split_filename_extension
from dipy.segment.clustering import qbx_and_merge
from dipy.stats.analysis import assignment_map
from dipy.tracking.streamline import (
length as streamline_length,
)
from dipy.utils.logging import logger
from dipy.utils.optpkg import optional_package
from dipy.viz.skyline.UI.elements import (
color_picker,
colors_equal,
create_numeric_input,
downloader,
normalize_picker_color,
open_confirmation_dialog,
segmented_switch,
thin_slider,
toggle_button,
two_disk_slider,
uploader,
)
from dipy.viz.skyline.compute import run_async
from dipy.viz.skyline.io import load_npy
from dipy.viz.skyline.render.renderer import Visualization
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 distinguishable_colormap
from fury.actor import Group, streamlines, streamtube
from fury.colormap import line_colors
from fury.ui import TextBlock2D
else:
actor = fury
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
icons_fontawesome_6 = imgui_bundle.icons_fontawesome_6
[docs]
def create_colormap(n, *, hue=(0.0, 0.1), saturation=(0.8, 0.2), value=0.8):
"""Build an RGB lookup table sampled along HSV space.
Parameters
----------
n : int
Number of discrete colors.
hue : tuple of float, optional
Hue endpoints passed through ``np.interp`` over the LUT indices.
saturation : tuple of float, optional
Saturation endpoints interpolated like ``hue``.
value : float, optional
Fixed HSV value (brightness) for every entry.
Returns
-------
ndarray, shape (n, 3)
Float32 RGB colors in ``[0, 1]``.
"""
lut = np.zeros((n, 3), dtype=np.float32)
h = np.interp(np.arange(n), [0, n - 1], hue)
s = np.interp(np.arange(n), [0, n - 1], saturation)
for i in range(n):
r, g, b = colorsys.hsv_to_rgb(h[i], s[i], value)
lut[i] = (r, g, b)
return lut
[docs]
def apply_buan_colors(
streamlines,
buan_pvals,
*,
hue=(0.0, 0.1),
saturation=(0.8, 0.2),
value=0.8,
buan_color_idx=None,
):
"""Assign BUAN p-value-derived RGB colors per streamline or sample.
Parameters
----------
streamlines : list of ndarray
Streamline arrays used for assignment mapping when ``buan_color_idx``
is omitted.
buan_pvals : ndarray
Scalar p-values (or statistics) sampled per correspondence bin.
hue : tuple of float, optional
Passed to :func:`create_colormap`.
saturation : tuple of float, optional
Passed to :func:`create_colormap`.
value : float, optional
Passed to :func:`create_colormap`.
buan_color_idx : ndarray or None, optional
Precomputed indices into the LUT; if None, computed via ``assignment_map``.
Returns
-------
buan_colors : ndarray, shape (N, 3)
Per-streamline RGB colors.
buan_color_idx : ndarray
Integer LUT indices used for coloring.
"""
n = len(buan_pvals)
if n > 1000:
logger.info("Limiting assignment to 1000 bands for performance reasons.")
n = 1000
if buan_color_idx is None:
_, indx = assignment_map(streamlines, streamlines, n)
buan_color_pvals = buan_pvals[indx].astype(np.float32)
buan_color_idx = np.interp(
buan_color_pvals, [buan_pvals.min(), buan_pvals.max()], [0, n - 1]
).astype(int)
lut = create_colormap(n, hue=hue, saturation=saturation, value=value)
buan_colors = lut[buan_color_idx]
return buan_colors, buan_color_idx
[docs]
def create_cluster_help(*, position=(0, 0), size=(200, 180)):
"""Create a 2D text panel summarizing cluster interaction shortcuts.
Parameters
----------
position : tuple of float, optional
Screen-space anchor for the block.
size : tuple of float, optional
Pixel width and height of the background rectangle.
Returns
-------
TextBlock2D
Fury overlay actor ready to be inserted into the scene.
"""
help_text = (
" Cluster Instructions:\n"
" Click to select/deselect.\n"
" 'e' to expand.\n"
" 'c' to collapse.\n"
" 'a' to select all.\n"
" 'd' to deselect all.\n"
" 'h' to hide.\n"
" 's' to show.\n"
)
text_block = TextBlock2D(
text=help_text,
position=position,
vertical_justification="middle",
justification="left",
font_size=16,
color=(1, 1, 1),
bg_color=(0.2, 0.2, 0.2),
size=size,
)
return text_block
[docs]
def create_streamline_visualization(
input,
idx,
*,
is_cluster=False,
thr=15.0,
line_type="Line",
color=(1, 0, 0),
render_callback=None,
colormap=None,
tract_colors=None,
switch_render_callback=None,
loader=None,
size_threshold=None,
length_threshold=None,
buan_pvals_file=None,
async_clustering=True,
):
"""Create streamline visualization from input
Parameters
----------
input : tuple
Tuple of the (sft, filename) or (sft,)
idx : int
Index of the tractogram for naming purposes.
is_cluster : bool, optional
Whether to cluster the streamline.
thr : float, optional
Clustering distance threshold.
line_type : str, optional
The type of line to render ("Line" or "Tube").
color : tuple, optional
Color of the streamline rendering.
render_callback : callable, optional
Callback function to be called after rendering.
colormap : colormap, optional
Colormap for clustering.
tract_colors : variable float or str, optional
Define the colors of the tractograms. Colors can be defined with
3 values and should be between [0-1].
String options are 'random' for random colors for each tractogram,
'direction' for directionally colored streamlines.
For example, a value of (1, 0, 0) would mean the red color.
switch_render_callback : callable, optional
Callback function to switch rendering type, used for cluster visualization.
loader : callable, optional
Callback function to show/hide loader during asynchronous operations.
size_threshold : int, optional
Minimum number of streamlines in a cluster to be visible.
length_threshold : float, optional
Minimum length of streamlines in a cluster to be visible.
buan_pvals_file : str, optional
File path to BUAN p-values for coloring streamlines.
async_clustering : bool, optional
Whether to perform clustering asynchronously. Set to False to block
until clustering completes (used in stealth mode).
Returns
-------
Visualization
The created streamline visualization object.
"""
if not isinstance(input, tuple) or len(input) not in (1, 2):
raise ValueError(
"Input must be a tuple containing (sft, filename) or (sft,) "
"for streamline visualization."
)
if len(input) == 1:
sft = input[0]
filename = f"Streamline_{idx}"
else:
sft, filename = input
if is_cluster:
return ClusterStreamline3D(
filename,
sft,
thr,
line_type=line_type,
render_callback=render_callback,
switch_render_callback=switch_render_callback,
loader=loader,
size_threshold=size_threshold,
length_threshold=length_threshold,
async_clustering=async_clustering,
)
if tract_colors is not None:
if tract_colors == "random":
color = next(colormap)
elif tract_colors == "direction" or len(tract_colors) in [3, 4]:
color = tract_colors
else:
raise ValueError(
"Invalid tract_colors value. Must be 'random', 'direction', "
"or a tuple of 3 or 4 values."
)
return Streamline3D(
filename,
sft,
line_type=line_type,
color=color,
render_callback=render_callback,
switch_render_callback=switch_render_callback,
buan_pvals_file=buan_pvals_file,
loader=loader,
)
[docs]
def create_streamline(lines, *, color=(1, 0, 0), line_type="Line", segments=4):
"""Instantiate Fury line or tube geometry for polyline streamlines.
Parameters
----------
lines : list of ndarray
Each array is a (N, 3) polyline in world space.
color : ndarray, tuple, or str, optional
Per-point, per-line, directional (``"direction"``), or constant RGB colors.
line_type : {"Line", "Tube"}, optional
Primitive style passed to Fury.
segments : int, optional
Tube tessellation segments when ``line_type`` is ``"Tube"``.
Returns
-------
Actor
Fury actor (line or tube container) ready to parent under a ``Group``.
"""
if isinstance(color, str) and color == "direction" and lines:
color = line_colors(lines)
if line_type == "Tube":
if (
isinstance(color, np.ndarray)
and color.ndim == 2
and len(color) != len(lines)
):
points_per_line = [len(line) for line in lines]
if color.shape[0] == sum(points_per_line):
color = np.split(color, np.cumsum(points_per_line)[:-1])
tubes = streamtube(
lines=lines,
radius=0.5,
colors=color,
segments=segments,
)
return tubes
elif line_type == "Line":
if (
isinstance(color, np.ndarray)
and color.ndim == 2
and len(color) == len(lines)
):
color = np.repeat(color, [len(line) for line in lines], axis=0)
lines = streamlines(
lines=lines,
colors=color,
thickness=5,
outline_thickness=0.4,
outline_color=(0.15, 0.15, 0.15),
)
lines.material.aa = True
return lines
[docs]
class Streamline3D(Visualization):
"""Represent ``Streamline3D`` in Skyline.
Parameters
----------
name : str
Display name used in the Skyline UI.
sft : StatefulTractogram
Value for ``sft``.
line_type : str, optional
Value for ``line type``.
color : tuple(float, float, float), optional
Value for ``color``.
render_callback : callable, optional
Callback used to request a render/update.
switch_render_callback : callable, optional
Value for ``switch render callback``.
buan_pvals_file : str, optional
Value for ``buan pvals file``.
loader : callable, optional
Value for ``loader``.
"""
def __init__(
self,
name,
sft,
*,
line_type="Line",
color=(1, 0, 0),
render_callback=None,
switch_render_callback=None,
buan_pvals_file=None,
loader=None,
):
"""Represent ``Streamline3D`` in Skyline.
Parameters
----------
name : str
Display name used in the Skyline UI.
sft : StatefulTractogram
Value for ``sft``.
line_type : str, optional
Value for ``line type``.
color : tuple(float, float, float), optional
Value for ``color``.
render_callback : callable, optional
Callback used to request a render/update.
switch_render_callback : callable, optional
Value for ``switch render callback``.
buan_pvals_file : str, optional
Value for ``buan pvals file``.
loader : callable, optional
Value for ``loader``.
"""
self.sft = sft
self.color = color
self._original_color = color
self._draft_color = color
self._color_picker_open = False
self._color_picker_popup_id = f"streamline_color_picker_popup##{name}"
self._hue_low = 0.0
self._hue_high = 0.1
self._saturation_high = 0.8
self._saturation_low = 0.2
self._value = 0.8
self._line_type = line_type
self._buan_pvals_file = buan_pvals_file
self._buan_pvals_data = None
self._buan_color_idx = None
self._show_line_type_confirmation = False
self._requested_line_type = None
self._apply_line_change_next_frame = False
self._switch_render_callback = switch_render_callback
self._loader = loader
self._create_streamline_actor()
super().__init__(name, render_callback)
if buan_pvals_file is not None:
self.handle_color_change(buan_pvals_file)
def _create_streamline_actor(self):
"""Handle create streamline actor for ``Streamline3D``."""
self._actor = create_streamline(
lines=self.sft.streamlines,
color=self.color,
line_type=self._line_type,
)
@property
def actor(self):
"""Handle actor for ``Streamline3D``.
Returns
-------
Line
Returned value.
"""
return self._actor
def _populate_info(self):
"""Handle populate info for ``Streamline3D``.
Returns
-------
str
The information of the Streamline3D visualization.
"""
np.set_printoptions(precision=2, suppress=True)
info = f"Number of streamlines: {len(self.sft.streamlines)}\n"
info += f"Min Length: {streamline_length(self.sft.streamlines).min():.0f}\n"
info += f"Max Length: {streamline_length(self.sft.streamlines).max():.0f}\n"
np.set_printoptions()
return info
[docs]
def handle_color_change(self, fname):
"""Handle handle color change for ``Streamline3D``.
Parameters
----------
fname : str
Value for ``fname``.
"""
if fname is not None:
self._buan_pvals_file = Path(fname[0]).name
self._buan_pvals_data = load_npy(fname[0])
self.color, self._buan_color_idx = apply_buan_colors(
self.sft.streamlines,
self._buan_pvals_data,
hue=(self._hue_low, self._hue_high),
saturation=(self._saturation_high, self._saturation_low),
value=self._value,
)
self.apply_scene_op(self._create_streamline_actor)
self.render()
def _update_buan_colors_on_sliders(self):
"""Handle update buan colors on sliders for ``Streamline3D``."""
self.color, self._buan_color_idx = apply_buan_colors(
self.sft.streamlines,
self._buan_pvals_data,
buan_color_idx=self._buan_color_idx,
hue=(self._hue_low, self._hue_high),
saturation=(self._saturation_high, self._saturation_low),
value=self._value,
)
self.apply_scene_op(self._create_streamline_actor)
self.render()
[docs]
class ClusterStreamline3D(Visualization):
"""Represent ``ClusterStreamline3D`` in Skyline.
Parameters
----------
name : str
Display name used in the Skyline UI.
sft : StatefulTractogram
Value for ``sft``.
thr : float, optional
Value for ``thr``.
line_type : str, optional
Value for ``line type``.
render_callback : callable, optional
Callback used to request a render/update.
switch_render_callback : callable, optional
Value for ``switch render callback``.
loader : callable, optional
Value for ``loader``.
size_threshold : int, optional
Value for ``size threshold``.
length_threshold : float, optional
Value for ``length threshold``.
async_clustering : bool, optional
Value for ``async clustering``.
"""
def __init__(
self,
name,
sft,
thr,
*,
line_type="Line",
render_callback=None,
switch_render_callback=None,
loader=None,
size_threshold=None,
length_threshold=None,
async_clustering=True,
):
"""Represent ``ClusterStreamline3D`` in Skyline.
Parameters
----------
name : str
Display name used in the Skyline UI.
sft : StatefulTractogram
Value for ``sft``.
thr : float, optional
Value for ``thr``.
line_type : str, optional
Value for ``line type``.
render_callback : callable, optional
Callback used to request a render/update.
switch_render_callback : callable, optional
Value for ``switch render callback``.
loader : callable, optional
Value for ``loader``.
size_threshold : int, optional
Value for ``size threshold``.
length_threshold : float, optional
Value for ``length threshold``.
async_clustering : bool, optional
Value for ``async clustering``.
"""
self.sft = sft
self.thr = thr
self._clusters = []
self._cluster_state = {}
self._sizes = np.asarray([])
self._lengths = np.asarray([])
self._line_type = line_type
self._actor = Group()
self._pending_thr = None
self._thr_changed_at = None
self._switch_render_callback = switch_render_callback
self._recluster_debounce_sec = 0.3
self._loader = loader
self._is_clustering = False
self._queued_recluster = False
self._async_clustering = async_clustering
self.size = size_threshold if size_threshold is not None else 10
self.length = length_threshold if length_threshold is not None else 20.0
super().__init__(name, render_callback=render_callback)
self._perform_clustering()
def _perform_clustering(self):
"""Handle perform clustering for ``ClusterStreamline3D``."""
if self._is_clustering:
self._queued_recluster = True
return
self._is_clustering = True
if self._loader is not None:
self._loader(True, message="Clustering streamlines...")
self.render()
if not self._async_clustering:
result = self._compute_clustering_data(self.thr)
self._apply_clustering_result(result, None)
return
run_async(
self._compute_clustering_data,
self._apply_clustering_result,
self.thr,
)
def _compute_clustering_data(self, thr):
"""Handle compute clustering data for ``ClusterStreamline3D``.
Parameters
----------
thr : float, optional
Value for ``thr``.
Returns
-------
tuple
Returned value.
"""
clusters = qbx_and_merge(self.sft.streamlines, [40, 30, 25, 20, thr])
lengths = np.asarray([streamline_length(c) for c in clusters.centroids])
sizes = np.asarray([len(c) for c in clusters])
colormap = distinguishable_colormap(nb_colors=len(clusters))
if sizes.size:
line_widths = np.interp(sizes, [np.min(sizes), np.max(sizes)], [0.1, 2.0])
else:
line_widths = np.asarray([])
return clusters, lengths, sizes, colormap, line_widths
def _apply_clustering_result(self, result, exception):
"""Handle apply clustering result for ``ClusterStreamline3D``.
Parameters
----------
result : tuple
Clustering outputs ``(clusters, lengths, sizes, colormap, line_widths)``.
exception : Exception or None
Value for ``exception``.
"""
self._is_clustering = False
if exception is not None:
logger.error(f"Error clustering streamlines: {exception}")
else:
clusters, lengths, sizes, colormap, line_widths = result
for actor in list(self._actor.children):
self._actor.remove(actor)
self._cluster_state.clear()
self._clusters = clusters
self._lengths = lengths
self._sizes = sizes
for idx, centroid in enumerate(self._clusters.centroids):
centroid_rep = streamtube(
lines=[centroid],
radius=line_widths[idx],
colors=colormap[idx],
backend="cpu",
opacity=0.5,
)
centroid_rep.add_event_handler(
lambda event: self._toggle_cluster_selection(event.target),
"pointer_down",
)
self._cluster_state[centroid_rep] = {
"cluster": idx,
"size": self._sizes[idx],
"length": self._lengths[idx],
"color": colormap[idx],
"selected": False,
"expanded": False,
"cluster_actor": None,
}
self._actor.add(centroid_rep)
self._refresh_cluster_visibility()
self._info = self._populate_info()
if self._queued_recluster:
self._queued_recluster = False
self._perform_clustering()
return
if self._loader is not None:
self._loader(False)
def _refresh_cluster_visibility(self):
"""Handle refresh cluster visibility for ``ClusterStreamline3D``.
None
"""
for centroid_rep, state in self._cluster_state.items():
is_visible = state["size"] >= self.size and state["length"] >= self.length
if state["expanded"] and state["cluster_actor"] is not None:
state["cluster_actor"].visible = is_visible
else:
centroid_rep.visible = is_visible
# Interaction methods
def _create_cluster_streamlines(self, centroid_rep):
"""Handle create cluster streamlines for ``ClusterStreamline3D``.
Parameters
----------
centroid_rep : str
Value for ``centroid rep``.
Returns
-------
object
Returned value.
"""
state = self._cluster_state[centroid_rep]
cluster_idx = state["cluster"]
cluster_streamlines = self._clusters[cluster_idx]
color = state["color"]
streamline_actor = create_streamline(
lines=cluster_streamlines,
color=color,
line_type=self._line_type,
segments=3,
)
return centroid_rep, streamline_actor
def _selected_unexpanded_clusters(self):
"""Handle selected unexpanded clusters for ``ClusterStreamline3D``.
None
Returns
-------
object
Returned value.
"""
return [
centroid_rep
for centroid_rep, state in self._cluster_state.items()
if state["selected"] and not state["expanded"]
]
def _expand_clusters(self):
"""Handle expand clusters for ``ClusterStreamline3D``.
None
"""
selected_clusters = self._selected_unexpanded_clusters()
if not selected_clusters:
return
for centroid_rep in selected_clusters:
_, streamline_actor = self._create_cluster_streamlines(centroid_rep)
state = self._cluster_state[centroid_rep]
self._actor.add(streamline_actor)
self._actor.remove(centroid_rep)
state["cluster_actor"] = streamline_actor
state["expanded"] = True
def _collapse_clusters(self):
"""Handle collapse clusters for ``ClusterStreamline3D``.
None
"""
for centroid_rep, state in self._cluster_state.items():
if state["selected"] and state["expanded"]:
self._actor.add(centroid_rep)
cluster_actor = state["cluster_actor"]
if cluster_actor is not None and cluster_actor in self._actor.children:
self._actor.remove(cluster_actor)
state["cluster_actor"] = None
state["expanded"] = False
def _select_all_clusters(self):
"""Handle select all clusters for ``ClusterStreamline3D``.
None
"""
for centroid_rep in self._cluster_state:
self._update_cluster_state(centroid_rep, True)
def _deselect_all_clusters(self):
"""Handle deselect all clusters for ``ClusterStreamline3D``.
None
"""
for centroid_rep in self._cluster_state:
self._update_cluster_state(centroid_rep, False)
def _update_cluster_state(self, centroid_rep, selected):
"""Handle update cluster state for ``ClusterStreamline3D``.
Parameters
----------
centroid_rep : str
Value for ``centroid rep``.
selected : bool
Value for ``selected``.
"""
state = self._cluster_state[centroid_rep]
state["selected"] = selected
if selected:
centroid_rep.material.opacity = 1.0
else:
centroid_rep.material.opacity = 0.5
def _toggle_cluster_selection(self, cluster):
"""Handle toggle cluster selection for ``ClusterStreamline3D``.
Parameters
----------
cluster : ClusterCentroid
Value for ``cluster``.
"""
self.apply_scene_op(
self._update_cluster_state,
cluster,
not self._cluster_state[cluster]["selected"],
)
def _hide_deselected_clusters(self):
"""Handle hide deselected clusters for ``ClusterStreamline3D``.
None
"""
for centroid_rep, state in self._cluster_state.items():
if not state["selected"]:
centroid_rep.visible = False
def _show_all_clusters(self):
"""Handle show all clusters for ``ClusterStreamline3D``.
None
"""
for centroid_rep in self._cluster_state:
centroid_rep.visible = True
def _show_all_clusters_and_refresh(self):
"""Handle show all clusters and refresh for ``ClusterStreamline3D``.
None
"""
self._show_all_clusters()
self._refresh_cluster_visibility()
def _apply_cluster_line_type_change(self):
"""Handle apply cluster line type change for ``ClusterStreamline3D``."""
for centroid_rep, state in self._cluster_state.items():
if state["expanded"]:
_, new_actor = self._create_cluster_streamlines(centroid_rep)
self._actor.remove(state["cluster_actor"])
self._actor.add(new_actor)
state["cluster_actor"] = new_actor
def _populate_info(self):
"""Handle populate info for ``ClusterStreamline3D``.
Returns
-------
str
The information of the ClusterStreamline3D visualization.
"""
np.set_printoptions(precision=2, suppress=True)
info = f"Total streamlines: {len(self.sft.streamlines)}\n"
info += f"Number of clusters: {len(self._clusters)}\n"
info += (
f"Max Cluster Size: {(self._sizes.max() if self._sizes.size else 0):.0f}\n"
)
info += (
f"Min Cluster Size: {(self._sizes.min() if self._sizes.size else 0):.0f}\n"
)
info += (
"Max Cluster Length: "
f"{(self._lengths.max() if self._lengths.size else 0):.0f}\n"
)
info += (
"Min Cluster Length: "
f"{(self._lengths.min() if self._lengths.size else 0):.0f}\n"
)
return info
[docs]
def compute_visible_tractogram(self):
"""Handle compute visible tractogram for ``ClusterStreamline3D``.
Returns
-------
StatefulTractogram
The visible tractogram of the ClusterStreamline3D visualization.
"""
visible_streamlines = []
for state in self._cluster_state.values():
if state["selected"]:
cluster_idx = state["cluster"]
cluster_streamlines = self._clusters[cluster_idx]
visible_streamlines.extend(cluster_streamlines)
return StatefulTractogram.from_sft(visible_streamlines, self.sft)
[docs]
def save_tractogram(self, filenames, *, rois=None, shm_coeffs=None):
"""Handle save tractogram for ``ClusterStreamline3D``.
Parameters
----------
filenames : list, optional
Value for ``filenames``.
rois : list, optional
Value for ``rois``.
shm_coeffs : list, optional
Value for ``shm coeffs``.
"""
if filenames:
if isinstance(filenames, (list, tuple)):
filenames = filenames[0]
visible_sft = self.compute_visible_tractogram()
save_tractogram(visible_sft, filenames, bbox_valid_check=False)
[docs]
def handle_key_events(self, event):
"""Handle handle key events for ``ClusterStreamline3D``.
Parameters
----------
event : Event
Interaction event from the renderer callback.
"""
if event.key == "e":
self.apply_scene_op(self._expand_clusters)
elif event.key == "c":
self.apply_scene_op(self._collapse_clusters)
elif event.key == "a":
self.apply_scene_op(self._select_all_clusters)
elif event.key == "d":
self.apply_scene_op(self._deselect_all_clusters)
elif event.key == "h":
self.apply_scene_op(self._hide_deselected_clusters)
elif event.key == "s":
self.apply_scene_op(self._show_all_clusters_and_refresh)
@property
def actor(self):
"""Handle actor for ``ClusterStreamline3D``.
Returns
-------
Group or Line
The actor of the ClusterStreamline3D visualization.
"""
return self._actor
if not has_fury_v2:
(
create_cluster_help,
create_streamline_visualization,
create_streamline,
Streamline3D,
ClusterStreamline3D,
) = (fury,) * 5