Source code for fury.actor.slicer

"""Slicer actor for Fury."""

import numpy as np

from fury.actor import (
    Actor,
    Group,
    Mesh,
    Volume,
    set_group_opacity,
    set_group_visibility,
    show_slices,
)
from fury.geometry import buffer_to_geometry
from fury.lib import (
    Geometry,
    MeshPhongShader,
    Texture,
    VolumeSliceMaterial,
    WorldObject,
    gfx_wgpu,
    register_wgpu_render_function,
)
from fury.material import (
    SphGlyphMaterial,
    VectorFieldArrowMaterial,
    VectorFieldLineMaterial,
    VectorFieldThinLineMaterial,
    _create_vector_field_material,
    validate_opacity,
)
import fury.primitive as fp
from fury.shader import (
    SphGlyphComputeShader,
    VectorFieldArrowShader,
    VectorFieldComputeShader,
    VectorFieldShader,
    VectorFieldThinShader,
)
from fury.utils import (
    create_sh_basis_matrix,
    get_lmax,
    get_n_coeffs,
)


[docs] def data_slicer( data, *, value_range=None, opacity=1.0, interpolation="linear", visibility=(True, True, True), initial_slices=None, alpha_mode="auto", depth_write=False, ): """ Visualize a 3D volume data as a slice. Parameters ---------- data : ndarray, shape (X, Y, Z) or (X, Y, Z, 3) The 3D volume data to be sliced. value_range : tuple, optional The minimum and maximum values for the color mapping. If None, the range is determined from the data. opacity : float, optional The opacity of the slice. Takes values from 0 (fully transparent) to 1 (opaque). interpolation : str, optional The interpolation method for the slice. Options are 'linear' and 'nearest'. visibility : tuple, optional A tuple of three boolean values indicating the visibility of the slices in the x, y, and z dimensions, respectively. initial_slices : tuple, optional A tuple of three initial slice positions in the x, y, and z dimensions, respectively. If None, the slices are initialized to the middle of the volume. alpha_mode : str, optional The alpha mode for the material. Please see the below link for details: https://docs.pygfx.org/stable/_autosummary/materials/pygfx.materials.Material.html#pygfx.materials.Material.alpha_mode. depth_write : bool, optional Whether to write depth information for the material. Returns ------- Group An actor containing the generated slice with the specified properties. """ if value_range is None: value_range = (np.min(data), np.max(data)) if visibility is None: visibility = (True, True, True) if data.ndim < 3 or data.ndim > 4: raise ValueError( "Input data must be 3-dimensional or " "4-dimensional with last dimension of size 3." ) elif data.ndim == 4 and (data.shape[-1] != 3 and data.shape[-1] != 4): raise ValueError("Last dimension must be of size 3 or 4.") opacity = validate_opacity(opacity) data = data.astype(np.float32) data = np.swapaxes(data, 0, 2) data_shape = data.shape if initial_slices is None: initial_slices = ( data_shape[2] // 2, data_shape[1] // 2, data_shape[0] // 2, ) texture = Texture(data, dim=3) slices = [] for dim in [0, 1, 2]: # XYZ abcd = [0, 0, 0, 0] abcd[dim] = -1 abcd[-1] = data_shape[2 - dim] // 2 mat = VolumeSliceMaterial( abcd, clim=value_range, interpolation=interpolation, pick_write=True, alpha_mode=alpha_mode, depth_write=depth_write, ) geo = Geometry(grid=texture) plane = Volume(geo, mat) slices.append(plane) obj = Group(name="Slicer") obj.add(*slices) set_group_visibility(obj, visibility) show_slices(obj, initial_slices) set_group_opacity(obj, opacity) return obj
[docs] class VectorField(WorldObject, Actor): """ Class to visualize a vector field. Parameters ---------- field : ndarray, shape {(X, Y, Z, N, 3), (X, Y, Z, 3)} The vector field data, where X, Y, Z represent the position in 3D, N is the number of vectors per voxel, and 3 represents the vector actor_type : str, optional The type of vector field visualization. Options are "thin_line", "line", and "arrow". cross_section : list or tuple, shape (3,), optional A list or tuple representing the cross section dimensions. If None, the cross section will be ignored and complete field will be shown. colors : tuple, optional Color for the vectors. If None, the color will used from the orientation. scales : {float, ndarray}, shape {(X, Y, Z, N) or (X, Y, Z)}, optional Scale factor for the vectors. If ndarray, it should match the shape of the field. opacity : float, optional Takes values from 0 (fully transparent) to 1 (opaque). thickness : float, optional The thickness of the lines in the vector field visualization. Only applicable for "line" and "arrow" types. visibility : tuple, optional A tuple of three boolean values indicating the visibility of the slices in the x, y, and z dimensions, respectively. """
[docs] def __init__( self, field, *, actor_type="thin_line", cross_section=None, colors=None, scales=1.0, opacity=1.0, thickness=1.0, visibility=None, ): """Initialize a vector field.""" super().__init__() if not (field.ndim == 5 or field.ndim == 4): raise ValueError( "Field must be 5D or 4D, " f"but got {field.ndim}D with shape {field.shape}" ) total_vectors = np.prod(field.shape[:-1]) if field.shape[-1] != 3: raise ValueError( f"Field must have last dimension as 3, but got {field.shape[-1]}" ) self.vectors = field.reshape(total_vectors, 3).astype(np.float32) self.field_shape = field.shape[:3] if field.ndim == 4: self.vectors_per_voxel = 1 else: self.vectors_per_voxel = field.shape[3] if isinstance(scales, (int, float)): self.scales = np.full((total_vectors, 1), scales, dtype=np.float32) elif scales.shape != field.shape[:-1]: raise ValueError( "Scales must match the shape of the field (X, Y, Z, N) or (X, Y, Z)," f" but got {scales.shape}" ) else: self.scales = scales.reshape(total_vectors, 1).astype(np.float32) pnts_per_vector = 2 pts = np.zeros((total_vectors * pnts_per_vector, 3), dtype=np.float32) pts[0] = self.field_shape if colors is None: colors = np.asarray((0, 0, 0), dtype=np.float32) else: colors = np.asarray(colors, dtype=np.float32) colors = np.tile(colors, (total_vectors * pnts_per_vector, 1)) self.geometry = buffer_to_geometry(positions=pts, colors=colors) self.material = _create_vector_field_material( (0, 0, 0), visibility=visibility, material=actor_type, thickness=thickness, opacity=opacity, ) if cross_section is None: self.cross_section = np.asarray([-2, -2, -2], dtype=np.int32) else: self.cross_section = cross_section
[docs] def get_bounding_box(self): """ Get the bounding box of the vector field. Returns ------- list A list containing two elements, each a list of three floats representing the minimum and maximum coordinates of the bounding box. """ return [ [0, 0, 0], [self.field_shape[0] - 1, self.field_shape[1] - 1, self.field_shape[2] - 1], ]
@property def cross_section(self): """ Get the cross section of the vector field. Returns ------- ndarray The cross section of the vector field. """ return self.material.cross_section @cross_section.setter def cross_section(self, value): """ Set the cross section of the vector field. Parameters ---------- value : {list, tuple, ndarray} The cross section in world-space coordinates. When this actor is part of a chunked group its ``local.position`` offset is already baked in, so the caller always works in the same coordinate frame as the full field. """ if not isinstance(value, (list, tuple, np.ndarray)): raise ValueError( "Cross section must be a list, tuple, or ndarray, " f"but got {type(value)}" ) if len(value) != 3: raise ValueError(f"Cross section must have length 3, but got {len(value)}") value = np.asarray(value, dtype=np.float32) bounds = self.bounds if hasattr(self, "bounds") else self.get_bounding_box() world_min = np.asarray(bounds[0], dtype=np.float32) world_max = np.asarray(bounds[1], dtype=np.float32) value = np.maximum(world_min, value) value = np.minimum(world_max, value) self.material.cross_section = value.astype(np.int32) @property def visibility(self): """ Get the visibility of the vector field. Returns ------- tuple A tuple of three boolean values indicating the visibility of the slices in the x, y, and z dimensions, respectively. """ return self.material.visibility @visibility.setter def visibility(self, value): """ Set the visibility of the vector field. Parameters ---------- value : tuple A tuple of three boolean values indicating the visibility of the slices in the x, y, and z dimensions, respectively. """ if not isinstance(value, (list, tuple)) or len(value) != 3: raise ValueError("Visibility must be a tuple of three boolean values.") self.material.visibility = value
def _max_voxels_per_chunk(*, max_buffer_size, max_workgroups): """ Compute the maximum number of voxels that fit in one VectorField chunk. Two independent device limits constrain how large a single VectorField actor may be: * **Storage-buffer binding size** – the largest individual buffer that the GPU accepts. The bottleneck buffer holds two float32 (x, y, z) vertices per vector (start + end point), so it costs ``n_vectors × 2 × 3 × 4`` bytes. * **Compute workgroup dispatch dimension** – the compute pass dispatches ``ceil(n_voxels / workgroup_size)`` workgroups along the X axis. WebGPU limits this to ``max-compute-workgroups-per-dimension`` (typically 65 535). Parameters ---------- max_buffer_size : int Device limit ``max-storage-buffer-binding-size`` in bytes. max_workgroups : int Device limit ``max-compute-workgroups-per-dimension``. Returns ------- int Maximum number of voxels per chunk that satisfies both limits. """ _VECTOR_FIELD_WORKGROUP_SIZE = 64 from_buffer = max_buffer_size // (2 * 3 * 4) from_dispatch = max_workgroups * _VECTOR_FIELD_WORKGROUP_SIZE return min(from_buffer, from_dispatch) def _create_chunked_vector_field( field, *, group_name, scales, actor_params, chunk_actor_postprocess=None, ): """ Create a VectorField group, chunking when required by device limits. Parameters ---------- field : ndarray, shape {(X, Y, Z, N, 3), (X, Y, Z, 3)} Vector field data. group_name : str Name for the returned Group. scales : {float, ndarray} Scalar value or per-voxel scales array. actor_params : dict Keyword arguments passed to each VectorField actor. chunk_actor_postprocess : callable, optional Callback invoked as ``fn(actor, x_start)`` for each chunk actor after chunk positioning, used for chunk-specific updates. Returns ------- Group A Group containing one or more VectorField actors. """ wgpu_device = gfx_wgpu.get_shared().device max_buffer_size = wgpu_device.limits.get( "max-storage-buffer-binding-size", 256 * 1024 * 1024 ) max_workgroups = wgpu_device.limits.get( "max-compute-workgroups-per-dimension", 65535 ) total_voxels = int(np.prod(field.shape[:3])) max_voxels = _max_voxels_per_chunk( max_buffer_size=max_buffer_size, max_workgroups=max_workgroups, ) group = Group(name=group_name) if total_voxels <= max_voxels: actor = VectorField(field, scales=scales, **actor_params) group.add(actor) return group voxels_per_x_slice = int(np.prod(field.shape[1:3])) chunk_x = max(1, max_voxels // voxels_per_x_slice) x_size = field.shape[0] n_chunks = int(np.ceil(x_size / chunk_x)) for i in range(n_chunks): x_start = i * chunk_x x_end = min(x_start + chunk_x, x_size) chunk_field = field[x_start:x_end] if isinstance(scales, np.ndarray): chunk_scales = scales[x_start:x_end] else: chunk_scales = scales actor = VectorField(chunk_field, scales=chunk_scales, **actor_params) actor.local.position = [x_start, 0, 0] if chunk_actor_postprocess is not None: chunk_actor_postprocess(actor, x_start) group.add(actor) return group
[docs] def vector_field( field, *, actor_type="thin_line", colors=None, scales=1.0, opacity=1.0, thickness=1.0, ): """ Visualize a vector field with different features. Parameters ---------- field : ndarray, shape {(X, Y, Z, N, 3), (X, Y, Z, 3)} The vector field data, where X, Y, Z represent the position in 3D, N is the number of vectors per voxel, and 3 represents the vector actor_type : str, optional The type of vector field visualization. Options are "thin_line", "line", and "arrow". colors : tuple, optional Color for the vectors. If None, the color will used from the orientation. scales : {float, ndarray}, shape {(X, Y, Z, N) or (X, Y, Z)}, optional Scale factor for the vectors. If ndarray, it should match the shape of the field. opacity : float, optional Takes values from 0 (fully transparent) to 1 (opaque). thickness : float, optional The thickness of the lines in the vector field visualization. Only applicable for "line" and "arrow" types. Returns ------- Group A Group of VectorField chunks. """ actor_params = { "actor_type": actor_type, "colors": colors, "opacity": opacity, "thickness": thickness, } return _create_chunked_vector_field( field, group_name="VectorField", scales=scales, actor_params=actor_params, )
[docs] def vector_field_slicer( field, *, actor_type="thin_line", cross_section=None, colors=None, scales=1.0, opacity=1.0, thickness=1.0, visibility=(True, True, True), ): """ Visualize a vector field with different features. Parameters ---------- field : ndarray, shape {(X, Y, Z, N, 3), (X, Y, Z, 3)} The vector field data, where X, Y, Z represent the position in 3D, N is the number of vectors per voxel, and 3 represents the vector actor_type : str, optional The type of vector field visualization. Options are "thin_line", "line", and "arrow". cross_section : list or tuple, shape (3,), optional A list or tuple representing the cross section dimensions. If None, the cross section will be ignored and complete field will be shown. colors : tuple, optional Color for the vectors. If None, the color will used from the orientation. scales : {float, ndarray}, shape {(X, Y, Z, N) or (X, Y, Z)}, optional Scale factor for the vectors. If ndarray, it should match the shape of the field. opacity : float, optional Takes values from 0 (fully transparent) to 1 (opaque). thickness : float, optional The thickness of the lines in the vector field visualization. Only applicable for "line" and "arrow" types. visibility : tuple, optional A tuple of three boolean values indicating the visibility of the slices in the x, y, and z dimensions, respectively. Returns ------- VectorField or Group A single VectorField when the data fits within the device limits, or a Group of VectorField chunks otherwise. """ if cross_section is None: cross_section = np.asarray(field.shape[:3], dtype=np.int32) // 2 cross_section = np.asarray(cross_section, dtype=np.int32) actor_params = { "actor_type": actor_type, "cross_section": cross_section, "colors": colors, "opacity": opacity, "thickness": thickness, "visibility": visibility, } return _create_chunked_vector_field( field, group_name="VectorFieldSlicer", scales=scales, actor_params=actor_params, chunk_actor_postprocess=lambda actor, _: setattr( actor, "cross_section", cross_section.copy() ), )
@register_wgpu_render_function(VectorField, VectorFieldThinLineMaterial) def register_vector_field_thin_shaders(wobject): """ Register PeaksActor shaders. Parameters ---------- wobject : VectorField The vector field object to register shaders for. Returns ------- tuple A tuple containing the compute shader and the render shader. """ compute_shader = VectorFieldComputeShader(wobject) render_shader = VectorFieldThinShader(wobject) return compute_shader, render_shader @register_wgpu_render_function(VectorField, VectorFieldLineMaterial) def register_vector_field_shaders(wobject): """ Register PeaksActor shaders. Parameters ---------- wobject : VectorField The vector field object to register shaders for. Returns ------- tuple A tuple containing the compute shader and the render shader. """ compute_shader = VectorFieldComputeShader(wobject) render_shader = VectorFieldShader(wobject) return compute_shader, render_shader @register_wgpu_render_function(VectorField, VectorFieldArrowMaterial) def register_vector_field_arrow_shaders(wobject): """ Register PeaksActor shaders. Parameters ---------- wobject : VectorField The vector field object to register shaders for. Returns ------- tuple A tuple containing the compute shader and the render shader. """ compute_shader = VectorFieldComputeShader(wobject) render_shader = VectorFieldArrowShader(wobject) return compute_shader, render_shader
[docs] class SphGlyph(Mesh): """ Visualize a spherical harmonic glyph with different features. Parameters ---------- coeffs : ndarray, shape (X, Y, Z, N) The spherical harmonics coefficients. X, Y, Z denotes the position and N represents the number of coefficients. sphere : tuple Vertices and faces of the sphere to use for the glyph. basis_type : str, optional The type of basis to use for the spherical harmonics. Options are 'standard', 'descoteaux07'. color_type : str, optional The type of color mapping to use for the spherical glyph. Options are 'sign' and 'orientation'. shininess : float, optional The shininess of the material for the spherical glyph. """
[docs] def __init__( self, coeffs, sphere, *, basis_type="standard", color_type="sign", shininess=50, ): """Visualize a spherical harmonic glyph with different features.""" super().__init__() if not isinstance(coeffs, np.ndarray): raise TypeError("The attribute 'coeffs' must be a numpy ndarray.") elif coeffs.ndim != 4: raise ValueError( ( "The attribute 'coeffs' must be a 4D numpy ndarray " "with shape (X, Y, Z, N)." ) ) elif coeffs.shape[-1] < 1: raise ValueError( "The last dimension of 'coeffs' must be greater than 0, " f"but got {coeffs.shape[-1]}" ) if not isinstance(sphere, tuple): raise TypeError( "The attribute 'sphere' must be a tuple containing vertices and faces." ) elif ( len(sphere) != 2 or not isinstance(sphere[0], np.ndarray) or not isinstance(sphere[1], np.ndarray) ): raise TypeError( "The attribute 'sphere' must be a tuple containing two numpy ndarrays " "(vertices, faces)." ) self.n_coeff = coeffs.shape[-1] self.data_shape = coeffs.shape[:3] self.basis_type = basis_type self._l_max = get_lmax(self.n_coeff, basis_type=basis_type) self.color_type = 0 if color_type == "sign" else 1 vertices, faces = sphere[0], sphere[1] positions = np.tile(vertices, (np.prod(self.data_shape), 1)).astype(np.float32) positions[0] = np.asarray(self.data_shape) self.scaled_vertices = np.zeros_like(positions, dtype=np.float32) self.vertices_per_glyph = vertices.shape[0] self.faces_per_glyph = faces.shape[0] self.indices = faces.reshape(-1).astype(np.int32) indices = np.tile(faces, (np.prod(self.data_shape), 1)).astype(np.int32) self.radii = np.zeros((self.vertices_per_glyph,), dtype=np.float32) for i in range(0, indices.shape[0], faces.shape[0]): start = i end = start + faces.shape[0] indices[start:end] += (i // faces.shape[0]) * self.vertices_per_glyph self.geometry = buffer_to_geometry( positions=positions.astype("float32"), indices=indices.astype("int32"), colors=np.ones_like(positions, dtype="float32"), normals=np.zeros_like(positions).astype("float32"), ) self.material = SphGlyphMaterial( n_coeffs=self.n_coeff, color_mode="vertex", flat_shading=False, shininess=shininess, specular="#494949", side="front", ) B_mat = create_sh_basis_matrix(vertices, self._l_max) self.sh_coeff = coeffs.reshape(-1).astype("float32") self.sf_func = B_mat.reshape(-1).astype("float32") self.sphere = vertices.astype("float32")
@property def l_max(self): """ Get the maximum degree of the spherical harmonics. Returns ------- int The maximum degree of the spherical harmonics used in the glyph. """ return self._l_max @l_max.setter def l_max(self, value): """ Set the maximum degree of the spherical harmonics. Parameters ---------- value : int The maximum degree of the spherical harmonics to set. Raises ------ ValueError If the provided value is not a positive integer. """ if not isinstance(value, int) or value < 0: raise ValueError("The attribute 'l_max' must be a positive integer.") self._l_max = value self.material.n_coeffs = get_n_coeffs(value, basis_type=self.basis_type) @property def scale(self): """ Get the scale of the spherical glyph. Returns ------- float The scale of the spherical glyph. """ return self.material.scale @scale.setter def scale(self, value): """ Set the scale of the spherical glyph. Parameters ---------- value : float The scale of the spherical glyph to set. """ self.material.scale = value
[docs] def sph_glyph( coeffs, *, sphere=None, basis_type="standard", color_type="sign", shininess=50 ): """ Visualize a spherical harmonic glyph with different features. Parameters ---------- coeffs : ndarray, shape (X, Y, Z, N) The spherical harmonics coefficients. X, Y, Z denotes the position and N represents the number of coefficients. sphere : {str, tuple}, optional The name of the sphere to use or a tuple containing the phi and theta segments for a custom sphere. Available options for the named spheres: * 'symmetric362' * 'symmetric642' * 'symmetric724' * 'repulsion724' * 'repulsion100' * 'repulsion200' basis_type : str, optional The type of basis to use for the spherical harmonics. Options are 'standard', 'descoteaux07'. color_type : str, optional The type of color mapping to use for the spherical glyph. Options are 'sign' and 'orientation'. shininess : float, optional The shininess of the material for the spherical glyph. Returns ------- SphGlyph A spherical glyph object. """ if not isinstance(coeffs, np.ndarray): raise TypeError("The attribute 'coeffs' must be a numpy ndarray.") elif coeffs.ndim != 4: raise ValueError( "The attribute 'coeffs' must be a 4D numpy ndarray with shape (X, Y, Z, N)." ) if sphere is None: sphere = "symmetric362" if isinstance(sphere, str): sphere = fp.prim_sphere(name=sphere) elif ( isinstance(sphere, tuple) and isinstance(sphere[0], int) and isinstance(sphere[1], int) ): sphere = fp.prim_sphere(gen_faces=True, phi=sphere[0], theta=sphere[1]) else: raise TypeError( "The attribute 'sphere' must be a string or tuple containing two integers." ) if not isinstance(basis_type, str): raise TypeError("The attribute 'basis_type' must be a string.") elif basis_type not in ["standard", "descoteaux07"]: raise ValueError( "The attribute 'basis_type' must be either 'standard' or 'descoteaux07'." ) if not isinstance(color_type, str): raise TypeError("The attribute 'color_type' must be a string.") if color_type not in ["sign", "orientation"]: raise ValueError( "The attribute 'color_type' must be either 'sign' or 'orientation'." ) if not isinstance(shininess, (int, float)): raise TypeError("The attribute 'shininess' must be an integer or float.") obj = SphGlyph( coeffs=coeffs, sphere=sphere, basis_type=basis_type, color_type=color_type, shininess=shininess, ) return obj
@register_wgpu_render_function(SphGlyph, SphGlyphMaterial) def register_glyph_shaders(wobject): """ Register Glyph shaders. Parameters ---------- wobject : VectorField The vector field object to register shaders for. Returns ------- tuple A tuple containing the compute shader and the render shader. """ compute_shader = SphGlyphComputeShader(wobject) render_shader = MeshPhongShader(wobject) return compute_shader, render_shader