Source code for k3d.factory.volumetric

"""Factory functions for volumetric and voxel-based objects."""

import warnings
from typing import Any, Optional, Tuple, Union
from typing import Dict as TypingDict
from typing import List as TypingList

import numpy as np

from ..helpers import check_attribute_color_range
from ..objects import MIP, MarchingCubes, SparseVoxels, Volume, VolumeSlice, VoxelChunk, Voxels, VoxelsGroup
from ..transform import process_transform_arguments
from .common import _default_color, default_colormap, nice_colors

# Type aliases for better readability
ArrayLike = Union[TypingList, np.ndarray, Tuple]
ColorMap = Union[TypingList[TypingList[float]], TypingDict[str, Any], np.ndarray]
ColorRange = TypingList[float]
OpacityFunction = TypingList[float]


[docs] def volume( volume: ArrayLike, color_map: Optional[ColorMap] = None, opacity_function: Optional[OpacityFunction] = None, color_range: ColorRange = None, samples: float = 512.0, alpha_coef: float = 50.0, gradient_step: float = 0.005, roughness: float = 0.25, metalness: float = 0.0, shadow: str = "off", interpolation: bool = True, shadow_delay: int = 500, shadow_res: int = 128, focal_length: float = 0.0, focal_plane: float = 100.0, ray_samples_count: int = 16, mask: ArrayLike = None, mask_opacities: ArrayLike = None, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> Volume: if color_range is None: color_range = [] if mask is None: mask = [] if mask_opacities is None: mask_opacities = [] if color_map is None: color_map = default_colormap color_range = ( check_attribute_color_range(volume, color_range) if type(color_range) is not dict else color_range ) if opacity_function is None: if type(color_map) is dict: values = np.concatenate( [np.asarray(frame, np.float32)[::4] for frame in color_map.values()] ) else: values = np.asarray(color_map, np.float32)[::4] opacity_function = [np.min(values), 0.0, np.max(values), 1.0] return process_transform_arguments( Volume( volume=volume, color_map=color_map, opacity_function=opacity_function, color_range=color_range, compression_level=compression_level, samples=samples, alpha_coef=alpha_coef, gradient_step=gradient_step, roughness=roughness, metalness=metalness, interpolation=interpolation, shadow=shadow, shadow_delay=shadow_delay, shadow_res=shadow_res, focal_plane=focal_plane, focal_length=focal_length, mask=mask, mask_opacities=mask_opacities, name=name, group=group, custom_data=custom_data, ray_samples_count=ray_samples_count, ), **kwargs, )
[docs] def mip( volume: ArrayLike, color_map: Optional[ColorMap] = None, opacity_function: Optional[OpacityFunction] = None, color_range: ColorRange = None, samples: float = 512.0, gradient_step: float = 0.005, roughness: float = 0.25, metalness: float = 0.0, interpolation: bool = True, mask: ArrayLike = None, mask_opacities: ArrayLike = None, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> MIP: if color_range is None: color_range = [] if mask is None: mask = [] if mask_opacities is None: mask_opacities = [] if color_map is None: color_map = default_colormap color_range = ( check_attribute_color_range(volume, color_range) if type(color_range) is not dict else color_range ) if opacity_function is None: # color_map may be a TimeSeries dict, which cannot be sliced; derive the default # ramp from the union of its frames in that case. if type(color_map) is dict: values = np.concatenate( [np.asarray(frame, np.float32)[::4] for frame in color_map.values()] ) else: values = np.asarray(color_map, np.float32)[::4] opacity_function = [np.min(values), 0.0, np.max(values), 1.0] return process_transform_arguments( MIP( volume=volume, color_map=color_map, opacity_function=opacity_function, color_range=color_range, samples=samples, gradient_step=gradient_step, roughness=roughness, metalness=metalness, interpolation=interpolation, mask=mask, mask_opacities=mask_opacities, name=name, group=group, custom_data=custom_data, compression_level=compression_level, ), **kwargs, )
[docs] def volume_slice( volume: ArrayLike = None, color_map: Optional[ColorMap] = None, color_range: ColorRange = None, opacity_function: OpacityFunction = None, opacity: float = 1.0, mask: ArrayLike = None, active_masks: ArrayLike = None, color_map_masks: Optional[ColorMap] = None, mask_opacity: float = 0.5, slice_x: int = -1, slice_y: int = -1, slice_z: int = 0, interpolation: int = 1, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> VolumeSlice: if volume is None: volume = [] if color_range is None: color_range = [] if opacity_function is None: opacity_function = [] if mask is None: mask = [] if active_masks is None: active_masks = [] if color_map is None: color_map = default_colormap if color_map_masks is None: color_map_masks = nice_colors color_map = ( np.array(color_map, np.float32) if type(color_map) is not dict else color_map ) if len(volume) > 0: color_range = check_attribute_color_range(volume, color_range) # createCanvasGradient2d writes a fixed alpha, so a transfer function has no channel to # apply to once there are two. Saying so beats dropping it without a word. if isinstance(volume, (list, tuple)) and len(volume) > 1 and len(opacity_function) > 0: warnings.warn( "opacity_function is ignored for a multi-channel volume_slice: the 2D colormap " "carries colour only", stacklevel=2, ) return process_transform_arguments( VolumeSlice( volume=volume, color_map=color_map, color_range=color_range, opacity_function=opacity_function, opacity=opacity, slice_x=slice_x, slice_y=slice_y, slice_z=slice_z, interpolation=interpolation, mask=mask, mask_opacity=mask_opacity, active_masks=active_masks, color_map_masks=color_map_masks, name=name, group=group, custom_data=custom_data, compression_level=compression_level, ), **kwargs, )
[docs] def voxels( voxels: ArrayLike, color_map: Optional[ColorMap] = None, wireframe: bool = False, outlines: bool = True, outlines_color: int = 0, opacity: float = 1.0, roughness: float = 0.4, metalness: float = 0.0, bounds: Optional[ArrayLike] = None, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> Voxels: if color_map is None: color_map = nice_colors if bounds is not None: kwargs["bounds"] = bounds else: max_z, max_y, max_x = np.shape(voxels) kwargs["bounds"] = np.array([0, max_x, 0, max_y, 0, max_z]) return process_transform_arguments( Voxels( voxels=voxels, color_map=color_map, wireframe=wireframe, outlines=outlines, outlines_color=outlines_color, opacity=opacity, roughness=roughness, metalness=metalness, name=name, group=group, custom_data=custom_data, compression_level=compression_level, ), **kwargs, )
[docs] def sparse_voxels( sparse_voxels: ArrayLike, space_size: ArrayLike, color_map: Optional[ColorMap] = None, wireframe: bool = False, outlines: bool = True, outlines_color: int = 0, opacity: float = 1.0, roughness: float = 0.4, metalness: float = 0.0, bounds: Optional[ArrayLike] = None, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> SparseVoxels: if color_map is None: color_map = nice_colors assert ( isinstance(space_size, (tuple, list, np.ndarray)) and np.shape(space_size) == (3,) and all(d > 0 for d in space_size) ) return process_transform_arguments( SparseVoxels( sparse_voxels=sparse_voxels, space_size=space_size, color_map=color_map, wireframe=wireframe, outlines=outlines, outlines_color=outlines_color, opacity=opacity, roughness=roughness, metalness=metalness, name=name, group=group, custom_data=custom_data, compression_level=compression_level, ), **kwargs, )
[docs] def voxels_group( space_size: ArrayLike, voxels_group: TypingList[TypingDict[str, Any]] = None, chunks_ids: TypingList[int] = None, color_map: Optional[ColorMap] = None, wireframe: bool = False, outlines: bool = True, outlines_color: int = 0, opacity: float = 1.0, roughness: float = 0.4, metalness: float = 0.0, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> VoxelsGroup: if voxels_group is None: voxels_group = [] if chunks_ids is None: chunks_ids = [] if color_map is None: color_map = nice_colors for g in voxels_group: g["coord"] = np.array(g["coord"]) g["voxels"] = np.array(g["voxels"]) if "multiple" not in g: g["multiple"] = 1 return process_transform_arguments( VoxelsGroup( voxels_group=voxels_group, chunks_ids=chunks_ids, space_size=space_size, color_map=color_map, wireframe=wireframe, outlines=outlines, outlines_color=outlines_color, opacity=opacity, roughness=roughness, metalness=metalness, name=name, group=group, custom_data=custom_data, compression_level=compression_level, ), **kwargs, )
[docs] def marching_cubes( scalar_field: ArrayLike, level: float, color: int = _default_color, attribute: ArrayLike = None, color_map: Optional[ColorMap] = None, color_range: ColorRange = None, opacity_function: OpacityFunction = None, wireframe: bool = False, flat_shading: bool = True, roughness: float = 0.4, metalness: float = 0.0, shininess: float = None, opacity: float = 1.0, spacings_x: ArrayLike = None, spacings_y: ArrayLike = None, spacings_z: ArrayLike = None, name: Optional[str] = None, group: Optional[str] = None, custom_data: Optional[TypingDict[str, Any]] = None, compression_level: int = 0, **kwargs: Any, ) -> MarchingCubes: if attribute is None: attribute = [] if color_range is None: color_range = [] if opacity_function is None: opacity_function = [] if spacings_x is None: spacings_x = [] if spacings_y is None: spacings_y = [] if spacings_z is None: spacings_z = [] if color_map is None: color_map = default_colormap attribute = ( np.array(attribute, np.float32) if type(attribute) is not dict else attribute ) color_range = check_attribute_color_range(attribute, color_range) return process_transform_arguments( MarchingCubes( scalar_field=scalar_field, spacings_x=spacings_x, spacings_y=spacings_y, spacings_z=spacings_z, color=color, attribute=attribute, color_map=color_map, color_range=color_range, opacity_function=opacity_function, level=level, wireframe=wireframe, flat_shading=flat_shading, roughness=roughness, metalness=metalness, shininess=shininess, opacity=opacity, name=name, group=group, custom_data=custom_data, compression_level=compression_level, ), **kwargs, )
[docs] def voxel_chunk( voxels: ArrayLike, coord: ArrayLike, multiple: int = 1, compression_level: int = 0 ) -> VoxelChunk: """Create a VoxelChunk object for selective updating voxels. Parameters ---------- voxels : array_like Array of voxel data. coord : array_like Coordinates for the chunk. multiple : int, optional Multiple factor, by default 1. compression_level : int, optional Compression level for the chunk, by default 0. Returns ------- VoxelChunk VoxelChunk object. """ return VoxelChunk( voxels=np.array(voxels, np.uint8), coord=np.array(coord, np.uint32), multiple=multiple, compression_level=compression_level, )