"""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,
)