Skip to content
147 changes: 106 additions & 41 deletions brainrender/actor.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Actor class and label utilities for brainrender scenes."""

from io import StringIO
from typing import Optional

Expand Down Expand Up @@ -28,16 +30,29 @@ def make_actor_label(
zoffset=0,
):
"""
Adds a 2D text anchored to a point on the actor's mesh
to label what the actor is

:param kwargs: keyword arguments can be passed to determine
text appearance and location:
- size: int, text size. Default 300
- color: str, text color. A list of colors can be passed
if None a gray color is used. Default None.
- xoffset, yoffset, zoffset: integers that shift the label position
- radius: radius of sphere used to denote label anchor. Set to 0 or None to hide.
Create 3D text labels anchored to actor meshes.

Parameters
----------
atlas : BrainGlobeAtlas
Atlas used for hemisphere queries and point mirroring.
actors : Actor or list of Actor
Actors to label.
labels : str or list of str
Label string(s) for each actor.
size : int, optional
Text size. Default 300.
color : str or list or None, optional
Label colour. Defaults to dark grey if None.
radius : int or None, optional
Radius of the anchor sphere. Set to None to hide. Default 100.
xoffset, yoffset, zoffset : int, optional
Positional offsets for the label. Defaults are 0, -500, 0.

Returns
-------
list
Flat list of vedo Text3D and Sphere objects.
"""
offset = [-yoffset, -zoffset, xoffset]
default_offset = np.array([0, -200, 100])
Expand Down Expand Up @@ -79,6 +94,29 @@ def make_actor_label(


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.
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved

Parameters
----------
mesh : vedo.Mesh
The 3D mesh for this actor.
name : str, optional
Actor name. Default ``"Actor"``.
br_class : str, optional
Brainrender class type. Default ``"None"``.
is_text : bool, optional
Whether this actor is a 2D text annotation. Default False.
color : str or None, optional
Colour to apply to the mesh on construction.
alpha : float or None, optional
Transparency to apply to the mesh on construction.
"""

_needs_label = False # needs to make a label
_needs_silhouette = False # needs to make a silhouette
_is_transformed = False # has been transformed to correct axes orientation
Expand All @@ -96,21 +134,6 @@ def __init__(
color=None,
alpha=None,
):
"""
Actor class representing anything shown in a brainrender scene.
Methods in brainrender.actors are used to creates actors specific
for different data types.

An actor has a mesh, a name and a brainrender class type.
It also has methods to create a silhouette or a label.

:param mesh: instance of vedo.Mesh
:param name: str, actor name
:param br_class: str, name of brainrender actors class
:param is_text: bool, is it a 2d text or annotation?
:param color: str, name or hex code of color to assign to actor's mesh
:param alpha: float, transparency to assign to actor's mesh
"""
self.mesh = mesh
self.name = name or "Actor"
self.br_class = br_class or "None"
Expand All @@ -123,8 +146,12 @@ def __init__(

def __getattr__(self, attr):
"""
If an unknown attribute is called, try `self.mesh.attr`
to get the mesh's attribute
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(
Expand Down Expand Up @@ -161,21 +188,48 @@ def __str__(self):
@property
def center(self):
"""
Returns the coordinates of the mesh's center
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()

@classmethod
def make_actor(cls, mesh, name, br_class):
"""
Make an actor from a given mesh
Construct an Actor from an existing mesh.

Parameters
----------
mesh : vedo.Mesh
Mesh to wrap.
name : str
Actor name.
br_class : str
Brainrender class type.

Returns
-------
Actor
"""
return cls(mesh, name=name, br_class=br_class)

def make_label(self, atlas):
"""
Create a new Actor with a sphere and a text
labelling this actor
Create 3D label actors anchored to this actor's mesh.

Parameters
----------
atlas : BrainGlobeAtlas
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
Expand All @@ -190,7 +244,12 @@ def make_label(self, atlas):

def make_silhouette(self):
"""
Create a new silhouette actor outlining this actor
Create a silhouette actor outlining this actor's mesh.

Returns
-------
Actor
Silhouette actor with brainrender class ``"silhouette"``.
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved
"""
lw = self._silhouette_kwargs["lw"]
color = self._silhouette_kwargs["color"]
Expand All @@ -212,15 +271,21 @@ def mirror(
atlas: Optional[BrainGlobeAtlas] = None,
):
"""
Mirror the actor's mesh around the specified axis, using the
parent_center as the center of the mirroring operation. The axes can
be specified using an abbreviation, e.g. 'x' for the x-axis, or anatomical
convention e.g. 'sagittal'. If an atlas is provided, then the anatomical
space of the atlas is used, otherwise `asr` is assumed.

:param axis: str, axis around which to mirror the mesh
:param origin: np.ndarray, center of the mirroring operation
:param atlas: BrainGlobeAtlas, atlas object to use for anatomical space
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 : str
Axis or anatomical plane to mirror across.
origin : numpy.ndarray or None, optional
Centre of the mirroring operation. Default None.
atlas : BrainGlobeAtlas or None, optional
Atlas used to resolve anatomical axis names. Default None.
"""
if axis in ["sagittal", "vertical", "frontal"]:
anatomical_space = atlas.space if atlas else AnatomicalSpace("asr")
Expand Down
79 changes: 60 additions & 19 deletions brainrender/atlas.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Atlas subclass adding region and plane Actor support for scenes."""

import numpy as np
from brainglobe_atlasapi.bg_atlas import BrainGlobeAtlas
from loguru import logger
Expand All @@ -10,15 +12,18 @@


class Atlas(BrainGlobeAtlas):
def __init__(self, atlas_name=None, check_latest=True):
"""
Brainrender's Atlas class subclasses BrainGlobeAtlas
to add methods to get regions meshes as Actors
and to get a plane at a given point and normal.
"""
Subclass of BrainGlobeAtlas with helpers.
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved

:param atlas_name: str, atlas name from brainglobe's atlas API atlases
:param check_latest: bool, if True checks that the atlas is the latest version
"""
Parameters
----------
atlas_name : str or None, optional
Falls back to ``settings.DEFAULT_ATLAS`` if None.
check_latest : bool, optional
Check for the latest atlas version. Default True.
"""

def __init__(self, atlas_name=None, check_latest=True):
atlas_name = atlas_name or settings.DEFAULT_ATLAS
self.atlas_name = atlas_name
logger.debug(f"Generating ATLAS: {atlas_name}")
Expand All @@ -32,7 +37,7 @@ def __init__(self, atlas_name=None, check_latest=True):
@property
def zoom(self):
"""
Returns the best camera zoom given the atlas resolution
Returns the best camera zoom given the atlas resolution.
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved
"""
res = np.max(self.metadata["resolution"])

Expand All @@ -46,18 +51,36 @@ def zoom(self):

def _get_region_color(self, region):
"""
Gets the rgb color of a region in the atlas
Gets the rgb color of a region in the atlas.
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved

Parameters
----------
region : str or int

Returns
-------
list of float
"""
return [
x / 255 for x in self._get_from_structure(region, "rgb_triplet")
]

def get_region(self, *regions, alpha=1, color=None):
"""
Get brain regions meshes as Actors
:param regions: str with names of brain regions in the atlas
:param alpha: float
:param color: str
Get brain regions meshes as Actors.

Parameters
----------
*regions : str
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved
Region acronyms or IDs.
alpha : float, optional
Mesh transparency. Default 1.
color : str or None, optional
Uses atlas RGB colour if None.

Returns
-------
Actor or list of Actor or None
"""
if not regions:
return None
Expand Down Expand Up @@ -113,11 +136,29 @@ def get_plane(
orthogonally to the vector norm and of width and height
Comment thread
AlgoFoe marked this conversation as resolved.
Resolved
sx, sy.

:param pos: 3-tuple or list with x,y,z, coords of point the plane goes through
:param norm: 3-tuple with plane's normal vector (optional)
:param sx, sy: int, width and height of the plane
:param plane: "sagittal", "horizontal", or "frontal"
:param color, alpha: plane color and transparency
Parameters
----------
pos : array-like or None, optional
(x, y, z) the plane passes through. Defaults to root centre of mass.
norm : array-like or None, optional
Normal vector. Derived from *plane* if not given.
plane : str or None, optional
``"sagittal"``, ``"horizontal"``, or ``"frontal"``.
sx, sy : int or None, optional
Width and height. Inferred from root bounds if None.
color : str, optional
Default ``"lightgray"``.
alpha : float, optional
Default 0.25.

Returns
-------
Actor

Raises
------
ValueError
If *plane* has no matching normal in the atlas space.
"""
axes_pairs = dict(sagittal=(0, 1), horizontal=(2, 0), frontal=(2, 1))

Expand Down
Loading