Source code for fastplotlib.graphics.image_volume

from typing import *

import numpy as np
import pygfx

from ..utils import quick_min_max
from ._base import Graphic
from .features import (
    TextureArrayVolume,
    ImageCmap,
    ImageVmin,
    ImageVmax,
    ImageInterpolation,
    ImageCmapInterpolation,
    VolumeRenderMode,
    VolumeIsoThreshold,
    VolumeIsoStepSize,
    VolumeIsoSubStepSize,
    VolumeIsoEmissive,
    VolumeIsoShininess,
    VolumeSlicePlane,
    VOLUME_RENDER_MODES,
    create_volume_material_kwargs,
)


class _VolumeTile(pygfx.Volume):
    """
    Similar to pygfx.Volume, only difference is that it modifies the pick_info
    by adding the data row start indices that correspond to this chunk of the big Volume
    """

    def __init__(
        self,
        geometry,
        material,
        data_slice: tuple[slice, slice, slice],
        chunk_index: tuple[int, int, int],
        **kwargs,
    ):
        super().__init__(geometry, material, **kwargs)

        self._data_slice = data_slice
        self._chunk_index = chunk_index

    def _wgpu_get_pick_info(self, pick_value):
        pick_info = super()._wgpu_get_pick_info(pick_value)

        data_z_start, data_row_start, data_col_start = (
            self.data_slice[0].start,
            self.data_slice[1].start,
            self.data_slice[2].start,
        )

        # add the actual data row and col start indices
        x, y, z = pick_info["index"]
        x += data_col_start
        y += data_row_start
        z += data_z_start
        pick_info["index"] = (x, y, z)

        xp, yp, zp = pick_info["voxel_coord"]
        xp += data_col_start
        yp += data_row_start
        zp += data_z_start
        pick_info["voxel_coord"] = (xp, yp, zp)

        # add row chunk and col chunk index to pick_info dict
        return {
            **pick_info,
            "data_slice": self.data_slice,
            "chunk_index": self.chunk_index,
        }

    @property
    def data_slice(self) -> tuple[slice, slice, slice]:
        return self._data_slice

    @property
    def chunk_index(self) -> tuple[int, int, int]:
        return self._chunk_index


[docs] class ImageVolumeGraphic(Graphic): _features = { "data": TextureArrayVolume, "cmap": ImageCmap, "vmin": ImageVmin, "vmax": ImageVmax, "interpolation": ImageInterpolation, "cmap_interpolation": ImageCmapInterpolation, "mode": VolumeRenderMode, "threshold": VolumeIsoThreshold, "step_size": VolumeIsoStepSize, "substep_size": VolumeIsoSubStepSize, "emissive": VolumeIsoEmissive, "shininess": VolumeIsoShininess, "plane": VolumeSlicePlane, } def __init__( self, data: Any, mode: str = "mip", vmin: float = None, vmax: float = None, cmap: str = "plasma", interpolation: str = "linear", cmap_interpolation: str = "linear", plane: tuple[float, float, float, float] = (0, 0, -1, 0), threshold: float = 0.5, step_size: float = 1.0, substep_size: float = 0.1, emissive: str | tuple | np.ndarray = (0, 0, 0), shininess: int = 30, isolated_buffer: bool = True, **kwargs, ): """ Create an ImageVolumeGraphic. Parameters ---------- data: array-like array-like, usually numpy.ndarray, must support ``memoryview()``. Shape must be [n_planes, n_rows, n_cols] for grayscale, or [n_planes, n_rows, n_cols, 3 | 4] for RGB(A) mode: str, default "mip" render mode, one of "mip", "minip", "iso" or "slice" vmin: float lower contrast limit vmax: float upper contrast limit cmap: str, default "plasma" colormap for grayscale volumes interpolation: str, default "linear" interpolation method for sampling pixels cmap_interpolation: str, default "linear" interpolation method for sampling from colormap plane: (float, float, float, float), default (0, 0, -1, 0) Slice volume at this plane. Sets (a, b, c, d) in the equation the defines a plane: ax + by + cz + d = 0. Used only if `mode` = "slice" threshold : float, default 0.5 The threshold texture value at which the surface is rendered. Used only if `mode` = "iso" step_size : float, default 1.0 The size of the initial ray marching step for the initial surface finding. Smaller values will result in more accurate surfaces but slower rendering. Used only if `mode` = "iso" substep_size : float, default 0.1 The size of the raymarching step for the refined surface finding. Smaller values will result in more accurate surfaces but slower rendering. Used only if `mode` = "iso" emissive : Color, default (0, 0, 0, 1) The emissive color of the surface. I.e. the color that the object emits even when not lit by a light source. This color is added to the final color and unaffected by lighting. The alpha channel is ignored. Used only if `mode` = "iso" shininess : int, default 30 How shiny the specular highlight is; a higher value gives a sharper highlight. Used only if `mode` = "iso" isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then set the data, useful if the data arrays are ready-only such as memmaps. If False, the input array is itself used as the buffer - useful if the array is large. kwargs additional keyword arguments passed to :class:`.Graphic` """ valid_modes = VOLUME_RENDER_MODES.keys() if mode not in valid_modes: raise ValueError( f"invalid mode specified: {mode}, valid modes are: {valid_modes}" ) super().__init__(**kwargs) world_object = pygfx.Group() if isinstance(data, TextureArrayVolume): # share existing buffer self._data = data else: # create new texture array to manage buffer # texture array that manages the textures on the GPU that represent this image volume self._data = TextureArrayVolume(data, isolated_buffer=isolated_buffer) if (vmin is None) or (vmax is None): _vmin, _vmax = quick_min_max(self.data.value) if vmin is None: vmin = _vmin if vmax is None: vmax = _vmax # other graphic features self._vmin = ImageVmin(vmin) self._vmax = ImageVmax(vmax) self._interpolation = ImageInterpolation(interpolation) # TODO: I'm assuming RGB volume images aren't supported??? # use TextureMap for grayscale images self._cmap = ImageCmap(cmap) self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) self._texture_map = pygfx.TextureMap( self._cmap.texture, filter=self._cmap_interpolation.value, wrap="clamp-to-edge", ) self._plane = VolumeSlicePlane(plane) self._threshold = VolumeIsoThreshold(threshold) self._step_size = VolumeIsoStepSize(step_size) self._substep_size = VolumeIsoSubStepSize(substep_size) self._emissive = VolumeIsoEmissive(emissive) self._shininess = VolumeIsoShininess(shininess) material_kwargs = create_volume_material_kwargs(graphic=self, mode=mode) VolumeMaterialCls = VOLUME_RENDER_MODES[mode] self._material = VolumeMaterialCls(**material_kwargs) self._mode = VolumeRenderMode(mode) # iterate through each texture chunk and create # a _VolumeTile, offset the tile using the data indices for texture, chunk_index, data_slice in self._data: # create a _VolumeTile using the texture for this chunk vol = _VolumeTile( geometry=pygfx.Geometry(grid=texture), material=self._material, data_slice=data_slice, # used to parse pick_info chunk_index=chunk_index, ) # row and column start index for this chunk data_z_start = data_slice[0].start data_row_start = data_slice[1].start data_col_start = data_slice[2].start # offset tile position using the indices from the big data array # that correspond to this chunk vol.world.z = data_z_start vol.world.x = data_col_start vol.world.y = data_row_start world_object.add(vol) self._set_world_object(world_object) @property def data(self) -> TextureArrayVolume: """Get or set the image data""" return self._data @data.setter def data(self, data): self._data[:] = data @property def mode(self) -> str: """Get or set the volume rendering mode""" return self._mode.value @mode.setter def mode(self, mode: str): self._mode.set_value(self, mode) @property def cmap(self) -> str: """Get or set colormap name""" return self._cmap.value @cmap.setter def cmap(self, name: str): self._cmap.set_value(self, name) @property def vmin(self) -> float: """Get or set the lower contrast limit""" return self._vmin.value @vmin.setter def vmin(self, value: float): self._vmin.set_value(self, value) @property def vmax(self) -> float: """Get or set the upper contrast limit""" return self._vmax.value @vmax.setter def vmax(self, value: float): self._vmax.set_value(self, value) @property def interpolation(self) -> str: """Get or set the image data interpolation method""" return self._interpolation.value @interpolation.setter def interpolation(self, value: str): self._interpolation.set_value(self, value) @property def cmap_interpolation(self) -> str: """Get or set the cmap interpolation method""" return self._cmap_interpolation.value @cmap_interpolation.setter def cmap_interpolation(self, value: str): self._cmap_interpolation.set_value(self, value) @property def plane(self) -> tuple[float, float, float, float]: """Get or set displayed plane in the volume. Valid only for `slice` render mode.""" return self._plane.value @plane.setter def plane(self, value: tuple[float, float, float, float]): if self.mode != "slice": raise TypeError("`plane` property is only valid for `slice` render mode.") self._plane.set_value(self, value) @property def threshold(self) -> float: """Get or set isosurface threshold, only for `iso` mode""" return self._threshold.value @threshold.setter def threshold(self, value: float): if self.mode != "iso": raise TypeError( "`threshold` property is only used for `iso` rendering mode" ) self._threshold.set_value(self, value) @property def step_size(self) -> float: """Get or set isosurface step_size, only for `iso` mode""" return self._step_size.value @step_size.setter def step_size(self, value: float): if self.mode != "iso": raise TypeError( "`step_size` property is only used for `iso` rendering mode" ) self._step_size.set_value(self, value) @property def substep_size(self) -> float: """Get or set isosurface substep_size, only for `iso` mode""" return self._substep_size.value @substep_size.setter def substep_size(self, value: float): if self.mode != "iso": raise TypeError( "`substep_size` property is only used for `iso` rendering mode" ) self._substep_size.set_value(self, value) @property def emissive(self) -> pygfx.Color: """Get or set isosurface emissive color, only for `iso` mode. Pass a <str> color, RGBA array or pygfx.Color""" return self._emissive.value @emissive.setter def emissive(self, value: pygfx.Color | str | tuple | np.ndarray): if self.mode != "iso": raise TypeError("`emissive` property is only used for `iso` rendering mode") self._emissive.set_value(self, value) @property def shininess(self) -> int: """Get or set isosurface shininess""" return self._shininess.value @shininess.setter def shininess(self, value: int): if self.mode != "iso": raise TypeError( "`shininess` property is only used for `iso` rendering mode" ) self._shininess.set_value(self, value)
[docs] def reset_vmin_vmax(self): """ Reset the vmin, vmax by *estimating* it from the data Returns ------- None """ vmin, vmax = quick_min_max(self.data.value) self.vmin = vmin self.vmax = vmax