diff --git a/brainrender/actor.py b/brainrender/actor.py index 213525e2..2e93e35a 100644 --- a/brainrender/actor.py +++ b/brainrender/actor.py @@ -1,7 +1,11 @@ from io import StringIO +from typing import Optional 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 from vedo import Sphere, Text3D @@ -201,6 +205,31 @@ def make_silhouette(self): return sil + def mirror( + self, + axis: str, + origin: Optional[npt.NDArray] = None, + 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 + """ + 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, *args): """ Print some useful characteristics to console. diff --git a/examples/mirror_actors.py b/examples/mirror_actors.py new file mode 100644 index 00000000..352de5c3 --- /dev/null +++ b/examples/mirror_actors.py @@ -0,0 +1,68 @@ +from pathlib import Path + +import numpy as np +from myterial import orange +from rich import print + +from brainrender import Scene +from brainrender._io import load_mesh_from_file +from brainrender.actor import Actor +from brainrender.actors import Neuron, Points + +neuron_file = Path(__file__).parent.parent / "resources" / "neuron1.swc" +obj_file = Path(__file__).parent.parent / "resources" / "CC_134_1_ch1inj.obj" +probe_striatum = ( + Path(__file__).parent.parent / "resources" / "probe_1_striatum.npy" +) + +print(f"[{orange}]Running example: {Path(__file__).name}") + +# Create a brainrender scene +scene = Scene(title="mirrored actors") + +# Add the neuron +neuron_original = Neuron(neuron_file) +scene.add(neuron_original) + +# Add a mesh from a file +scene.add(obj_file, color="tomato") + +# Add a probe from a file +scene.add( + Points( + np.load(probe_striatum), + name="probe_1", + colors="darkred", + radius=50, + ), + color="darkred", + radius=50, +) + +# Add mirrored objects +axis = "frontal" +atlas_center = scene.root.center + +neuron_mirrored = Neuron(neuron_file) +neuron_mirrored.mirror(axis, origin=atlas_center, atlas=scene.atlas) +scene.add(neuron_mirrored) + +mesh_mirrored = Actor( + load_mesh_from_file(obj_file, color="tomato"), + name=obj_file.name, + br_class="from file", +) +mesh_mirrored.mirror(axis, origin=atlas_center, atlas=scene.atlas) +scene.add(mesh_mirrored) + +mirrored_probe = Points( + np.load(probe_striatum), + name="probe_1", + colors="darkred", + radius=50, +) +mirrored_probe.mirror(axis, origin=atlas_center, atlas=scene.atlas) +scene.add(mirrored_probe) + +# Render! +scene.render() diff --git a/tests/test_actor.py b/tests/test_actor.py index c792c4df..3f703418 100644 --- a/tests/test_actor.py +++ b/tests/test_actor.py @@ -1,10 +1,24 @@ +from pathlib import Path + +import pytest +from brainglobe_space import AnatomicalSpace from rich import print as rprint from vedo import Mesh from brainrender import Scene +from brainrender._io import load_mesh_from_file from brainrender.actor import Actor +@pytest.fixture +def mesh_actor(): + resources_dir = Path(__file__).parent.parent / "resources" + data_path = resources_dir / "CC_134_1_ch1inj.obj" + obj_mesh = load_mesh_from_file(data_path, color="tomato") + + return Actor(obj_mesh, name=data_path.name, br_class="from file") + + def test_actor(): s = Scene() @@ -18,3 +32,105 @@ def test_actor(): assert s.alpha() == s.mesh.alpha() assert s.name == "root" assert s.br_class == "brain region" + + +@pytest.mark.parametrize( + "axis, expected_ind", + [ + ("z", 2), + ("y", 1), + ("x", 0), + ("frontal", 2), + ("vertical", 1), + ("sagittal", 0), + ], +) +def test_mirror_origin(mesh_actor, axis, expected_ind): + original_center = mesh_actor.center + mesh_actor.mirror(axis) + new_center = mesh_actor.center + + assert new_center[expected_ind] == -original_center[expected_ind] + + +@pytest.mark.parametrize( + "axis, expected_ind", + [ + ("z", 2), + ("y", 1), + ("x", 0), + ("frontal", 2), + ("vertical", 1), + ("sagittal", 0), + ], +) +def test_mirror_around_root(mesh_actor, axis, expected_ind): + scene = Scene() + root_center = scene.root.center + + original_center = mesh_actor.center + mesh_actor.mirror(axis, origin=root_center) + new_center = mesh_actor.center + + # The new center should be the same distance from the root center as the original center + expected_location = ( + -(original_center[expected_ind] - root_center[expected_ind]) + + root_center[expected_ind] + ) + + assert new_center[expected_ind] == pytest.approx( + expected_location, abs=1e-3 + ) + + +@pytest.mark.parametrize( + "axis, expected_ind", + [ + ("z", 2), + ("y", 1), + ("x", 0), + ("frontal", 1), + ("vertical", 0), + ("sagittal", 2), + ], +) +def test_mirror_custom_space(mesh_actor, axis, expected_ind): + scene = Scene() + scene.atlas.space = AnatomicalSpace("sra") + + original_center = mesh_actor.center + mesh_actor.mirror(axis, atlas=scene.atlas) + new_center = mesh_actor.center + + assert new_center[expected_ind] == -original_center[expected_ind] + + +@pytest.mark.parametrize( + "axis, expected_ind", + [ + ("z", 2), + ("y", 1), + ("x", 0), + ("frontal", 1), + ("vertical", 0), + ("sagittal", 2), + ], +) +def test_mirror_custom_space_around_root(mesh_actor, axis, expected_ind): + scene = Scene() + scene.atlas.space = AnatomicalSpace("sra") + root_center = scene.root.center + + original_center = mesh_actor.center + mesh_actor.mirror(axis, origin=root_center, atlas=scene.atlas) + new_center = mesh_actor.center + + # The new center should be the same distance from the root center as the original center + expected_location = ( + -(original_center[expected_ind] - root_center[expected_ind]) + + root_center[expected_ind] + ) + + assert new_center[expected_ind] == pytest.approx( + expected_location, abs=1e-3 + )