Source code for brainrender.actor

"""Actor class and label utilities for brainrender scenes."""

from __future__ import annotations

from io import StringIO
from typing import Any, Self

import numpy as np
import numpy.typing as npt
import pyinspect as pi
from brainglobe_atlasapi import BrainGlobeAtlas
from brainglobe_space import AnatomicalSpace
from myterial import amber, orange, salmon
from rich.console import Console, ConsoleOptions, RenderResult
from vedo import Mesh, Sphere, Text3D

from brainrender._utils import listify

# transform matrix to fix labels orientation
label_mtx = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]


[docs] def make_actor_label( atlas: BrainGlobeAtlas, actors: Actor | list[Actor], labels: str | list[str], size: int = 300, color: str | list[float] | None = None, radius: int | None = 100, xoffset: int = 0, yoffset: int = -500, zoffset: int = 0, ) -> list[Text3D | Sphere]: """ Create 3D text labels anchored to actor meshes. Parameters ---------- atlas Atlas used for hemisphere queries and point mirroring. actors Actors to label. labels Label string(s) for each actor. size Text size. Default 300. color Label colour. Defaults to dark grey if None. radius Radius of the anchor sphere. Set to None to hide. Default 100. xoffset Positional offset along x. Default 0. yoffset Positional offset along y. Default -500. zoffset Positional offset along z. Default 0. Returns ------- list Flat list of vedo Text3D and Sphere objects. """ offset = [-yoffset, -zoffset, xoffset] default_offset = np.array([0, -200, 100]) new_actors = [] for _, (actor, label) in enumerate(zip(listify(actors), listify(labels))): # Get label color if color is None: color = [0.2, 0.2, 0.2] # Get mesh's highest point points = actor.mesh.vertices.copy() point = points[np.argmin(points[:, 1]), :] point += np.array(offset) + default_offset point[2] = -point[2] try: if atlas.hemisphere_from_coords(point, as_string=True) == "left": point = atlas.mirror_point_across_hemispheres(point) except IndexError: pass # Create label txt = Text3D( label, point * np.array([-1, -1, -1]), s=size, c=color, depth=0.1 ) new_actors.append(txt.rotate_x(180).rotate_y(180)) # Mark a point on Mesh that corresponds to the label location if radius is not None: pt = actor.closest_point(point) pt[2] = -pt[2] sphere = Sphere(pt, r=radius, c=color, res=8) sphere.ancor = pt new_actors.append(sphere) sphere.compute_normals() return new_actors
[docs] class Actor: """ Represents any object displayed in a brainrender scene. Wraps a vedo Mesh with a name and class type, and provides helpers for creating silhouettes and 3D labels. Unknown attribute lookups are delegated to the underlying mesh. Parameters ---------- mesh The 3D mesh for this actor. name Actor name. Default ``"Actor"``. br_class Brainrender class type. Default ``"None"``. is_text Whether this actor is a 2D text annotation. Default False. color Colour to apply to the mesh on construction. alpha Transparency to apply to the mesh on construction. """ _needs_label: bool = False # needs to make a label _needs_silhouette: bool = False # needs to make a silhouette _is_transformed: bool = ( False # has been transformed to correct axes orientation ) _is_added: bool = False # has the actor been added to the scene already labels: list[Actor] = [] silhouette: Actor | None = None def __init__( self, mesh: Mesh, name: str | None = None, br_class: str | None = None, is_text: bool = False, color: str | None = None, alpha: float | None = None, ) -> None: self.mesh = mesh self.name = name or Actor self.br_class = br_class or "None" self.is_text = is_text if color: self.mesh.c(color) if alpha: self.mesh.alpha(alpha) def __getattr__(self, attr: str) -> Any: """ Delegate unknown attribute lookups to the underlying mesh. Raises ------ AttributeError If the attribute is not found on any mesh object. """ if "mesh" not in self.__dict__.keys(): raise AttributeError( f"Actor does not have attribute {attr}" ) # pragma: no cover # some attributes should be from .mesh, others from ._mesh mesh_attributes = ("center_of_mass",) if attr in mesh_attributes: if hasattr(self.__dict__["mesh"], attr): return getattr(self.__dict__["mesh"], attr) else: try: return getattr(self.__dict__["_mesh"], attr) except KeyError: # no ._mesh, use .mesh if hasattr(self.__dict__["mesh"], attr): return getattr(self.__dict__["mesh"], attr) raise AttributeError( f"Actor does not have attribute {attr}" ) # pragma: no cover def __repr__(self) -> str: # pragma: no cover return f"brainrender.Actor: {self.name}-{self.br_class}" def __str__(self) -> str: buf = StringIO() _console = Console(file=buf, force_jupyter=False) _console.print(self) return buf.getvalue() @property def center(self) -> npt.NDArray[np.float64]: """ Return the mesh's centre of mass coordinates. Returns ------- numpy.ndarray Array of shape (3,) with (x, y, z) coordinates. """ return self.mesh.center_of_mass()
[docs] @classmethod def make_actor(cls, mesh: Mesh, name: str, br_class: str) -> Self: """ Construct an Actor from an existing mesh. Parameters ---------- mesh Mesh to wrap. name Actor name. br_class Brainrender class type. Returns ------- Actor """ return cls(mesh, name=name, br_class=br_class)
[docs] def make_label(self, atlas: BrainGlobeAtlas) -> list[Actor]: """ Create 3D label actors anchored to this actor's mesh. Parameters ---------- atlas Atlas used for hemisphere queries and mirroring. Returns ------- list of Actor Label actors (text and optional sphere markers). """ labels = make_actor_label( atlas, self, self._label_str, **self._label_kwargs ) self._needs_label = False lbls = [ Actor.make_actor(label, self.name, "label") for label in labels ] self.labels = lbls return lbls
[docs] def make_silhouette(self) -> Actor: """ Create a silhouette actor outlining this actor's mesh. Returns ------- Actor Silhouette actor with brainrender class ``"silhouette"``. """ lw = self._silhouette_kwargs["lw"] color = self._silhouette_kwargs["color"] sil = self._mesh.silhouette().lw(lw).c(color) name = f"{self.name} silhouette" sil = Actor.make_actor(sil, name, "silhouette") sil._is_transformed = True self._needs_silhouette = False self.silhouette = sil return sil
[docs] def mirror( self, axis: str, origin: npt.NDArray | None = None, atlas: BrainGlobeAtlas | None = None, ) -> None: """ Mirror the actor's mesh across the given axis. Accepts Cartesian axes (``'x'``, ``'y'``, ``'z'``) or anatomical plane names (``'sagittal'``, ``'vertical'``, ``'frontal'``). If an anatomical name is used without an atlas, ``'asr'`` space is assumed. The mesh is updated in place. Parameters ---------- axis Axis or anatomical plane to mirror across. origin Centre of the mirroring operation. Default None. atlas Atlas used to resolve anatomical axis names. Default None. """ if axis in ["sagittal", "vertical", "frontal"]: anatomical_space = atlas.space if atlas else AnatomicalSpace("asr") axis_ind = anatomical_space.get_axis_idx(axis) axis = "x" if axis_ind == 0 else "y" if axis_ind == 1 else "z" self.mesh = self.mesh.mirror(axis, origin)
def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: """ Print some useful characteristics to console. """ rep = pi.Report( title="[b]brainrender.Actor: ", color=salmon, accent=orange, ) rep.add(f"[b {orange}]name:[/b {orange}][{amber}] {self.name}") rep.add(f"[b {orange}]type:[/b {orange}][{amber}] {self.br_class}") rep.line() rep.add( f"[{orange}]center of mass:[/{orange}][{amber}] {self.mesh.center_of_mass().astype(np.int32)}" ) rep.add( f"[{orange}]number of vertices:[/{orange}][{amber}] {self.mesh.npoints}" ) rep.add( f"[{orange}]dimensions:[/{orange}][{amber}] {np.array(self.mesh.bounds()).astype(np.int32)}" ) rep.add(f"[{orange}]color:[/{orange}][{amber}] {self.mesh.color()}") yield "\n" yield rep