Source code for brainrender.actors.volume

"""Volume actor for rendering 3D numpy arrays as surfaces or volumes."""

from pathlib import Path
from typing import Any

import numpy as np
import numpy.typing as npt
from loguru import logger
from vedo import Volume as VedoVolume

from brainrender.actor import Actor


[docs] class Volume(Actor): """ Render a 3D numpy array as a surface mesh or vedo Volume. By default the volume is represented as an isosurface. """ def __init__( self, griddata: npt.NDArray | VedoVolume | str | Path, voxel_size: int = 1, cmap: str = "bwr", min_quantile: float | None = None, min_value: float | None = None, name: str | None = None, br_class: str | None = None, as_surface: bool = True, **volume_kwargs: Any, ) -> None: """ Parameters ---------- griddata 3D array with grid data. Can also be a vedo Volume or a path to a ``.npy`` file. voxel_size Size of each voxel in microns. Default 1. cmap Colormap name. Default ``"bwr"``. min_quantile Percentile threshold for isosurface extraction. min_value Hard value threshold for isosurface extraction. name Actor name. Default ``"Volume"``. br_class Brainrender class type. Default ``"Volume"``. as_surface If True, return an isosurface mesh instead of the full volume. Default True. **volume_kwargs Keyword arguments forwarded to vedo's Volume class. """ logger.debug("Creating a Volume actor") # Create mesh color = volume_kwargs.pop("c", "viridis") if isinstance(griddata, np.ndarray): # create volume from data mesh = self._from_numpy( griddata, voxel_size, color, **volume_kwargs ) elif isinstance(griddata, (str, Path)): # create from .npy file mesh = self._from_file( griddata, voxel_size, color, **volume_kwargs ) else: mesh = griddata # assume a vedo Volume was passed if as_surface: # Get threshold if min_quantile is None and min_value is None: th = 0 elif min_value is not None: th = min_value else: th = np.percentile(griddata.ravel(), min_quantile) mesh = mesh.legosurface(vmin=th) mesh.cmap(cmap) Actor.__init__( self, mesh, name=name or "Volume", br_class=br_class or "Volume" ) def _from_numpy( self, griddata: npt.NDArray, voxel_size: int, color: str, **volume_kwargs: Any, ) -> VedoVolume: """ Create a vedo Volume from a 3D numpy array. Parameters ---------- griddata 3D array with volume data. voxel_size Size of each voxel in microns. color Colormap name to apply. **volume_kwargs Keyword arguments forwarded to vedo's Volume class. Returns ------- VedoVolume A vedo volume created from the input 3D array. """ vvol = VedoVolume( griddata, spacing=[voxel_size, voxel_size, voxel_size], **volume_kwargs, ) vvol.cmap(color) # The transformation below is ALREADY applied # to vedo.Volume instances in render.py # so we should not apply it here. # Flip volume so that it's oriented as in the atlas # vvol.permute_axes(2, 1, 0) # mtx = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]] # vvol.apply_transform(mtx) return vvol def _from_file( self, filepath: str | Path, voxel_size: int, color: str, **volume_kwargs: Any, ) -> VedoVolume: """ Load a ``.npy`` file and return a vedo Volume. Parameters ---------- filepath Path to the ``.npy`` file. voxel_size Size of each voxel in microns. color Colormap name to apply. **volume_kwargs Keyword arguments forwarded to vedo's Volume class. Returns ------- VedoVolume Raises ------ FileExistsError If the file does not exist. ValueError If the file is not a ``.npy`` file. """ filepath = Path(filepath) if not filepath.exists(): raise FileExistsError( f"Loading volume from file, file not found: {filepath}" ) if not filepath.suffix == ".npy": raise ValueError( "Loading volume from file only accepts .npy files" ) return self._from_numpy( np.load(str(filepath)), voxel_size, color, **volume_kwargs )