Source code for fastplotlib.graphics.features._volume

from itertools import product
from math import ceil

import numpy as np
import pygfx

from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance

VOLUME_RENDER_MODES = {
    "mip": pygfx.VolumeMipMaterial,
    "minip": pygfx.VolumeMinipMaterial,
    "iso": pygfx.VolumeIsoMaterial,
    "slice": pygfx.VolumeSliceMaterial,
}


[docs] class TextureArrayVolume(GraphicFeature): """ Manages an array of Textures representing chunks of an image. Chunk size is the GPU's max texture limit. Creates and manages multiple pygfx.Texture objects. """ event_info_spec = [ { "dict key": "key", "type": "slice, index, numpy-like fancy index", "description": "key at which image data was sliced/fancy indexed", }, { "dict key": "value", "type": "np.ndarray | float", "description": "new data values", }, ] def __init__(self, data, isolated_buffer: bool = True): super().__init__() data = self._fix_data(data) shared = pygfx.renderers.wgpu.get_shared() self._texture_size_limit = shared.device.limits["max-texture-dimension-3d"] if isolated_buffer: # useful if data is read-only, example: memmaps self._value = np.zeros(data.shape, dtype=data.dtype) self.value[:] = data[:] else: # user's input array is used as the buffer self._value = data # data start indices for each Texture self._row_indices = np.arange( 0, ceil(self.value.shape[1] / self._texture_size_limit) * self._texture_size_limit, self._texture_size_limit, ) self._col_indices = np.arange( 0, ceil(self.value.shape[2] / self._texture_size_limit) * self._texture_size_limit, self._texture_size_limit, ) self._zdim_indices = np.arange( 0, ceil(self.value.shape[0] / self._texture_size_limit) * self._texture_size_limit, self._texture_size_limit, ) shape = (self.zdim_indices.size, self.row_indices.size, self.col_indices.size) # buffer will be an array of textures self._buffer: np.ndarray[pygfx.Texture] = np.empty(shape=shape, dtype=object) self._iter = None # iterate through each chunk of passed `data` # create a pygfx.Texture from this chunk for _, buffer_index, data_slice in self: texture = pygfx.Texture(self.value[data_slice], dim=3) self.buffer[buffer_index] = texture @property def value(self) -> np.ndarray: """The full array that represents all the data within this TextureArray""" return self._value
[docs] def set_value(self, graphic, value): self[:] = value
@property def buffer(self) -> np.ndarray[pygfx.Texture]: """array of buffers that are mapped to the GPU""" return self._buffer @property def row_indices(self) -> np.ndarray: """ row indices that are used to chunk the big data array into individual Textures on the GPU """ return self._row_indices @property def col_indices(self) -> np.ndarray: """ column indices that are used to chunk the big data array into individual Textures on the GPU """ return self._col_indices @property def zdim_indices(self) -> np.ndarray: """ z dimension indices that are used to chunk the big data array into individual Textures on the GPU """ return self._zdim_indices def _fix_data(self, data): if data.ndim not in (3, 4): raise ValueError( "Volume Image data must be 3D with or without an RGB(A) dimension, i.e. " "it must be of shape [z, rows, cols], [z, rows, cols, 3] or [z, rows, cols, 4]" ) # let's just cast to float32 always return data.astype(np.float32) def __iter__(self): self._iter = product( enumerate(self.zdim_indices), enumerate(self.row_indices), enumerate(self.col_indices), ) return self def __next__( self, ) -> tuple[pygfx.Texture, tuple[int, int, int], tuple[slice, slice, slice]]: """ Iterate through each Texture within the texture array Returns ------- Texture, tuple[int, int], tuple[slice, slice] | Texture: pygfx.Texture | tuple[int, int]: chunk index, i.e corresponding index of ``self.buffer`` array | tuple[slice, slice]: data slice of big array in this chunk and Texture """ # chunk indices ( (chunk_z, data_z_start), (chunk_row, data_row_start), (chunk_col, data_col_start), ) = next(self._iter) # indices for to self.buffer for this chunk chunk_index = (chunk_z, chunk_row, chunk_col) # stop indices of big data array for this chunk z_stop = min(self.value.shape[0], data_z_start + self._texture_size_limit) row_stop = min(self.value.shape[1], data_row_start + self._texture_size_limit) col_stop = min(self.value.shape[2], data_col_start + self._texture_size_limit) # zdim, row and column slices that slice the data for this chunk from the big data array data_slice = ( slice(data_z_start, z_stop), slice(data_row_start, row_stop), slice(data_col_start, col_stop), ) # texture for this chunk texture = self.buffer[chunk_index] return texture, chunk_index, data_slice def __getitem__(self, item): return self.value[item] @block_reentrance def __setitem__(self, key, value): self.value[key] = value for texture in self.buffer.ravel(): texture.update_range((0, 0, 0), texture.size) event = GraphicFeatureEvent("data", info={"key": key, "value": value}) self._call_event_handlers(event) def __len__(self): return self.buffer.size
def create_volume_material_kwargs(graphic, mode: str): kwargs = { "clim": (graphic.vmin, graphic.vmax), "map": graphic._texture_map, "interpolation": graphic.interpolation, "pick_write": True, } if mode == "iso": more_kwargs = { attr: getattr(graphic, attr) for attr in [ "threshold", "step_size", "substep_size", "emissive", "shininess", ] } elif mode == "slice": more_kwargs = {"plane": graphic.plane} else: more_kwargs = {} kwargs.update(more_kwargs) return kwargs
[docs] class VolumeRenderMode(GraphicFeature): """Volume rendering mode, controls world object material""" event_info_spec = [ { "dict key": "value", "type": "str", "description": "volume rendering mode that has been set", }, ] def __init__(self, value: str): self._validate(value) self._value = value super().__init__() @property def value(self) -> str: return self._value def _validate(self, value): if value not in VOLUME_RENDER_MODES.keys(): raise ValueError( f"Given render mode: {value} is invalid. Valid render modes are: {VOLUME_RENDER_MODES.keys()}" ) @block_reentrance def set_value(self, graphic, value: str): self._validate(value) VolumeMaterialCls = VOLUME_RENDER_MODES[value] kwargs = create_volume_material_kwargs(graphic, mode=value) new_material = VolumeMaterialCls(**kwargs) # since the world object is a group for volume_tile in graphic.world_object.children: volume_tile.material = new_material # so we have one place to reference it graphic._material = new_material self._value = value event = GraphicFeatureEvent(type="mode", info={"value": value}) self._call_event_handlers(event)
[docs] class VolumeIsoThreshold(GraphicFeature): """Isosurface threshold""" event_info_spec = [ { "dict key": "value", "type": "float", "description": "new isosurface threshold", }, ] def __init__(self, value: float): self._value = value super().__init__() @property def value(self) -> float: return self._value @block_reentrance def set_value(self, graphic, value: float): graphic._material.threshold = value self._value = graphic._material.threshold event = GraphicFeatureEvent(type="threshold", info={"value": value}) self._call_event_handlers(event)
[docs] class VolumeIsoStepSize(GraphicFeature): """Isosurface step_size""" event_info_spec = [ { "dict key": "value", "type": "float", "description": "new isosurface step_size", }, ] def __init__(self, value: float): self._value = value super().__init__() @property def value(self) -> float: return self._value @block_reentrance def set_value(self, graphic, value: float): graphic._material.step_size = value self._value = graphic._material.step_size event = GraphicFeatureEvent(type="step_size", info={"value": value}) self._call_event_handlers(event)
[docs] class VolumeIsoSubStepSize(GraphicFeature): """Isosurface substep_size""" event_info_spec = [ { "dict key": "value", "type": "float", "description": "new isosurface step_size", }, ] def __init__(self, value: float): self._value = value super().__init__() @property def value(self) -> float: return self._value @block_reentrance def set_value(self, graphic, value: float): graphic._material.substep_size = value self._value = graphic._material.substep_size event = GraphicFeatureEvent(type="step_size", info={"value": value}) self._call_event_handlers(event)
[docs] class VolumeIsoEmissive(GraphicFeature): """Isosurface emissive color""" event_info_spec = [ { "dict key": "value", "type": "pygfx.Color", "description": "new isosurface emissive color", }, ] def __init__(self, value: pygfx.Color | str | tuple | np.ndarray): self._value = pygfx.Color(value) super().__init__() @property def value(self) -> pygfx.Color: return self._value @block_reentrance def set_value(self, graphic, value: pygfx.Color | str | tuple | np.ndarray): graphic._material.emissive = value self._value = graphic._material.emissive event = GraphicFeatureEvent(type="emissive", info={"value": value}) self._call_event_handlers(event)
[docs] class VolumeIsoShininess(GraphicFeature): """Isosurface shininess""" event_info_spec = [ { "dict key": "value", "type": "int", "description": "new isosurface shininess", }, ] def __init__(self, value: int): self._value = value super().__init__() @property def value(self) -> int: return self._value @block_reentrance def set_value(self, graphic, value: float): graphic._material.shininess = value self._value = graphic._material.shininess event = GraphicFeatureEvent(type="shininess", info={"value": value}) self._call_event_handlers(event)
[docs] class VolumeSlicePlane(GraphicFeature): """Volume plane""" event_info_spec = [ { "dict key": "value", "type": "tuple[float, float, float, float]", "description": "new plane slice", }, ] def __init__(self, value: tuple[float, float, float, float]): self._value = value super().__init__() @property def value(self) -> tuple[float, float, float, float]: return self._value @block_reentrance def set_value(self, graphic, value: tuple[float, float, float, float]): graphic._material.plane = value self._value = graphic._material.plane event = GraphicFeatureEvent(type="plane", info={"value": value}) self._call_event_handlers(event)