# This script is part of navis (http://www.github.com/navis-org/navis).
# Copyright (C) 2018 Philipp Schlegel
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
""" Interface with Blender. Unlike other moduls of navis, this module is
not automatically imported as it only works from within Blender.
"""
# Important bit of advice:
# Avoid operators ("bpy.ops.") as much as possible:
# They cause scene updates which will exponentially slow down processing
import colorsys
import json
import math
import os
import time
import uuid
import pandas as pd
import numpy as np
import seaborn as sns
import trimesh as tm
from .. import core, utils, config
from ..plotting.colors import eval_color
logger = config.get_logger(__name__)
try:
import bpy
import bmesh
import mathutils
except ImportError:
logger.error('Unable to load Blender API - this module only works from '
'within Blender!')
except BaseException:
raise
[docs]class Handler:
"""Class that interfaces with scene in Blender.
Parameters
----------
scaling : float, optional
scaling factor between navis and Blender coordinates.
Notes
-----
(1) The handler adds neurons and keeps track of them in the scene.
(2) If you request a list of objects via its attributes (e.g. ``Handler.neurons``)
or via :func:`~navis.interfaces.blender.Handler.select`, a :class:`~navis.interfaces.blender.ObjectList`
is returned. This class lets you change basic parameters of your selected
neurons.
Attributes
----------
neurons : returns list containing all neurons
connectors : returns list containing all connectors
soma : returns list containing all somata
selected : returns list containing selected objects
presynapses : returns list containing all presynapses
postsynapses : returns list containing all postsynapses
gapjunctions : returns list containing all gap junctions
abutting : returns list containing all abutting connectors
all : returns list containing all objects
Examples
--------
>>> # This example assumes you have alread imported and set up navis
>>> # b3d module has to be imported explicitly
>>> from navis import b3d
>>> # Get some neurons (you have already set up a remote instance?)
>>> nl = navis.example_neurons()
>>> # Initialize handler
>>> h = b3d.Handler()
>>> # Add neurons
>>> h.add(nl)
>>> # Assign colors to all neurons
>>> h.colorize()
>>> # Select all somas and change color to black
>>> h.soma.color(0, 0, 0)
>>> # Clear scene
>>> h.clear()
>>> # Add only soma
>>> h.add(nl, neurites=False, connectors=False)
"""
cn_dict = {
0: dict(name='presynapses',
color=(1, 0, 0)),
1: dict(name='postsynapses',
color=(0, 0, 1)),
2: dict(name='gapjunction',
color=(0, 1, 0)),
3: dict(name='abutting',
color=(1, 0, 1))
} # : defines default colours/names for different connector types
# Some synonyms
cn_dict['pre'] = cn_dict[0]
cn_dict['post'] = cn_dict[1]
cn_dict['gap'] = cn_dict['gapjunction'] = cn_dict[2]
cn_dict['abutting'] = cn_dict[3]
# Some other parameters
cn_dict['display'] = 'lines' # "lines" or "spheres", overriden if MeshNeuron
cn_dict['size'] = 0.01 # sets size of spheres only
defaults = dict(bevel_depth=0.007,
bevel_resolution=5,
resolution_u=10)
[docs] def __init__(self,
scaling=1 / 10000,
axes_order=[0, 1, 2],
ax_translate=[1, 1, 1]):
self.scaling = scaling
self.cn_dict = Handler.cn_dict
self.axes_order = axes_order
self.ax_translate = ax_translate
def _selection_helper(self, type):
return [ob.name for ob in bpy.data.objects if 'type' in ob and ob['type'] == type]
def _cn_selection_helper(self, cn_type):
return [ob.name for ob in bpy.data.objects if 'type' in ob and ob['type'] == 'CONNECTORS' and ob['cn_type'] == cn_type]
def __getattr__(self, key):
if key == 'neurons' or key == 'neuron' or key == 'neurites':
return ObjectList(self._selection_helper('NEURON'))
elif key == 'connectors' or key == 'connector':
return ObjectList(self._selection_helper('CONNECTORS'))
elif key == 'soma' or key == 'somas':
return ObjectList(self._selection_helper('SOMA'))
elif key == 'selected':
return ObjectList([ob.name for ob in bpy.context.selected_objects if 'navis_object' in ob])
elif key == 'visible':
objects = [o for o in self.neurons if not o.hide]
return ObjectList(objects)
elif key == 'presynapses':
return ObjectList(self._cn_selection_helper(0))
elif key == 'postsynapses':
return ObjectList(self._cn_selection_helper(1))
elif key == 'gapjunctions':
return ObjectList(self._cn_selection_helper(2))
elif key == 'abutting':
return ObjectList(self._cn_selection_helper(3))
elif key == 'all':
return self.neurons + self.connectors + self.soma
else:
raise AttributeError('Unknown attribute ' + key)
[docs] def add(self, x, neurites=True, soma=True, connectors=True, redraw=False,
use_radii=False, skip_existing=False, downsample=False,
collection=None, **kwargs):
"""Add neuron(s) to scene.
Parameters
----------
x : TreeNeuron | MeshNeuron | NeuronList | core.Volume
Objects to import into Blender.
neurites : bool, optional
Plot neurites. TreeNeurons only.
soma : bool, optional
Plot somas. TreeNeurons only.
connectors : bool, optional
Plot connectors. Uses a defaults dictionary to set
color/type. See Examples on how to change.
redraw : bool, optional
If True, will redraw window after each neuron. This
will slow down loading!
use_radii : bool, optional
If True, will use node radii. For TreeNeurons only.
skip_existing : bool, optional
If True, will skip neurons that are already loaded.
downsample : False | int, optional
If integer < 1, will downsample neurites upon import.
Preserves branch point/roots. TreeNeurons only.
collection : str, optional
Only for Blender >2.8: add object(s) to given collection.
If collection does not exist, will be created.
Examples
--------
Add one of the example neurons:
>>> h = navis.interfaces.blender.Handler()
>>> n = navis.example_neurons(1)
>>> h.add(n, connectors=True)
Change connector settings:
>>> h.cn_dict['display'] = 'sphere'
>>> h.cn_dict[0]['color'] = (1, 1, 0)
"""
start = time.time()
if skip_existing:
exists = [ob.get('id', None) for ob in bpy.data.objects]
if isinstance(x, (core.BaseNeuron, core.NeuronList)):
if redraw:
print('Set "redraw=False" to vastly speed up import!')
if isinstance(x, core.BaseNeuron):
x = [x]
wm = bpy.context.window_manager
wm.progress_begin(0, len(x))
for i, n in enumerate(x):
# Skip existing if applicable
if skip_existing and n.id in exists:
continue
self._create_neuron(n,
neurites=neurites,
soma=soma,
connectors=connectors,
collection=collection,
downsample=downsample,
use_radii=use_radii)
if redraw:
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
wm.progress_update(i)
wm.progress_end()
elif isinstance(x, tm.Trimesh):
self._create_volume(x, collection=collection)
elif isinstance(x, np.ndarray):
self._create_scatter(x, collection=collection, **kwargs)
elif isinstance(x, core.Dotprops):
self._create_dotprops(x, collection=collection, **kwargs)
else:
raise AttributeError(f'Unable add data type of type {type(x)}')
print(f'Import done in {time.time()-start:.2f}s')
return
[docs] def clear(self):
"""Clear all neurons """
self.all.delete()
def _create_scatter2(self, x, collection=None, **kwargs):
"""Create scatter by reusing mesh data.
This generate an individual objects for each data point. This is slower!
"""
if x.ndim != 2 or x.shape[1] != 3:
raise ValueError('Array must be of shape N,3')
# Get & scale coordinates and invert y
coords = x.astype(float)[:, self.axes_order]
coords *= float(self.scaling)
coords *= self.ax_translate
verts, faces = calc_sphere(kwargs.get('size', 0.02),
kwargs.get('sp_res', 7),
kwargs.get('sp_res', 7))
mesh = bpy.data.meshes.new(kwargs.get('name', 'scatter'))
mesh.from_pydata(verts, [], faces)
mesh.polygons.foreach_set('use_smooth', [True] * len(mesh.polygons))
objects = []
for i, co in enumerate(coords):
obj = bpy.data.objects.new(kwargs.get('name', 'scatter') + str(i),
mesh)
obj.location = co
obj.show_name = False
objects.append(obj)
# Link to scene and add to group
group_name = kwargs.get('name', 'scatter')
if group_name != 'scatter' and group_name in bpy.data.groups:
group = bpy.data.groups[group_name]
else:
group = bpy.data.groups.new(group_name)
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
for obj in objects:
col.objects.link(obj)
group.objects.link(obj)
return
def _create_scatter(self, x, collection=None, **kwargs):
"""Create scatter."""
if x.ndim != 2 or x.shape[1] != 3:
raise ValueError('Array must be of shape N,3')
# Get & scale coordinates and invert y
coords = x.astype(float)[:, self.axes_order]
coords *= float(self.scaling)
coords *= self.ax_translate
# Generate a base sphere
base_sphere = tm.creation.uv_sphere(radius=kwargs.get('size', 0.02),
count=[kwargs.get('sp_res', 7),
kwargs.get('sp_res', 7)])
base_verts, base_faces = base_sphere.vertices, base_sphere.faces
# Repeat sphere vertices
sp_verts = np.tile(base_verts.T, coords.shape[0]).T
# Add coords offsets to each sphere
offsets = np.repeat(coords, base_verts.shape[0], axis=0)
sp_verts += offsets
# Repeat sphere faces and offset vertex indices
sp_faces = np.tile(base_faces.T, coords.shape[0]).T
face_offsets = np.repeat(np.arange(coords.shape[0]),
base_faces.shape[0], axis=0)
face_offsets *= base_verts.shape[0]
sp_faces += face_offsets.reshape((face_offsets.size, 1))
# Generate mesh
mesh = bpy.data.meshes.new(kwargs.get('name', 'scatter'))
mesh.from_pydata(sp_verts, [], sp_faces.tolist())
mesh.polygons.foreach_set('use_smooth', [True] * len(mesh.polygons))
obj = bpy.data.objects.new(kwargs.get('name', 'scatter'), mesh)
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
col.objects.link(obj)
obj.location = (0, 0, 0)
obj.show_name = False
return obj
def _create_neuron(self, x, neurites=True, soma=True, connectors=True,
use_radii=False, downsample=False, collection=None):
"""Create neuron object."""
mat_name = (f'M#{x.id}')[:59]
mat = bpy.data.materials.get(mat_name,
bpy.data.materials.new(mat_name))
if isinstance(x, core.TreeNeuron):
if neurites:
self._create_skeleton(x, mat,
use_radii=use_radii,
downsample=downsample,
collection=collection)
if soma and not isinstance(x.soma, type(None)):
self._create_soma(x, mat, collection=collection)
elif isinstance(x, core.MeshNeuron):
self._create_mesh(x, mat, collection=collection)
else:
raise TypeError(f'Expected Mesh/TreeNeuron, got "{type(x)}"')
if connectors and x.has_connectors:
self._create_connectors(x, collection=collection)
return
def _create_mesh(self, x, mat, collection=None):
"""Create mesh from MeshNeuron."""
name = getattr(x, 'name', '')
# Make copy of vertices as we are potentially modifying them
verts = x.vertices.copy()
# Convert to Blender space
verts = verts * self.scaling
verts = verts[:, self.axes_order]
verts *= self.ax_translate
me = bpy.data.meshes.new(f'{name} mesh')
ob = bpy.data.objects.new(f"#{x.id} - {name}", me)
ob.location = (0, 0, 0)
ob.show_name = True
ob['type'] = 'NEURON'
ob['navis_object'] = True
ob['id'] = str(x.id)
blender_verts = verts.tolist()
me.from_pydata(list(blender_verts), [], list(x.faces))
me.update()
me.polygons.foreach_set('use_smooth', [True] * len(me.polygons))
ob.active_material = mat
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
col.objects.link(ob)
def _create_skeleton(self, x, mat, use_radii=False, downsample=False,
collection=None):
"""Create neuron branches."""
name = getattr(x, 'name', '')
cu = bpy.data.curves.new(f"{name} mesh", 'CURVE')
ob = bpy.data.objects.new(f"#{x.id} - {name}", cu)
ob.location = (0, 0, 0)
ob.show_name = True
ob['type'] = 'NEURON'
ob['navis_object'] = True
ob['id'] = str(x.id)
cu.dimensions = '3D'
cu.fill_mode = 'FULL'
cu.bevel_resolution = self.defaults.get('bevel_resolution', 5)
cu.resolution_u = self.defaults.get('resolution_u', 10)
if use_radii:
cu.bevel_depth = 1
else:
cu.bevel_depth = self.defaults.get('bevel_depth', 0.007)
# DO NOT touch this: lookup via dict is >10X faster!
tn_coords = {r.node_id: (r.x * self.scaling,
r.y * self.scaling,
r.z * self.scaling) for r in x.nodes.itertuples()}
if use_radii:
tn_radii = {r.node_id: r.radius * self.scaling for r in x.nodes.itertuples()}
for s in x.segments:
if isinstance(downsample, int) and downsample > 1:
mask = np.zeros(len(s), dtype=bool)
mask[downsample::downsample] = True
keep = np.isin(s, x.nodes[x.nodes['type'] != 'slab'].node_id.values)
s = np.array(s)[mask | keep]
sp = cu.splines.new('POLY')
coords = np.array([tn_coords[tn] for tn in s])
coords = coords[:, self.axes_order]
coords *= self.ax_translate
# Add points
sp.points.add(len(coords) - 1)
# Add this weird fourth coordinate
coords = np.c_[coords, [0] * coords.shape[0]]
# Set point coordinates
sp.points.foreach_set('co', coords.ravel())
sp.points.foreach_set('weight', s)
if use_radii:
r = [tn_radii[tn] for tn in s]
sp.points.foreach_set('radius', r)
ob.active_material = mat
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
col.objects.link(ob)
return
def _create_dotprops(self, x, scale_vect=1, collection=None):
"""Create neuron branches."""
# Generate uuid
object_id = str(uuid.uuid4())
mat_name = (f'M#{object_id}')[:59]
mat = bpy.data.materials.get(mat_name,
bpy.data.materials.new(mat_name))
cu = bpy.data.curves.new(f"{getattr(x, 'dotprop', '')} mesh", 'CURVE')
ob = bpy.data.objects.new(f"#{object_id} - {getattr(x, 'neuron_name', '')}",
cu)
ob.location = (0, 0, 0)
ob.show_name = True
ob['type'] = 'DOTPROP'
ob['navis_object'] = True
ob['id'] = object_id
cu.dimensions = '3D'
cu.fill_mode = 'FULL'
cu.bevel_resolution = 5
cu.bevel_depth = 0.007
# Prepare lines - this is based on nat:::plot3d.dotprops
halfvect = (np.vstack(x.vector) / 2 * scale_vect)
starts = (np.vstack(x.point) - halfvect)
ends = (np.vstack(x.point) + halfvect)
halfvect *= self.scaling
starts *= self.scaling
ends *= self.scaling
halfvect = halfvect[:, self.axes_order] * self.ax_translate
starts = starts[:, self.axes_order] * self.ax_translate
ends = ends[:, self.axes_order] * self.ax_translate
segments = list(zip(starts, ends))
for s in segments:
sp = cu.splines.new('POLY')
# Add points
sp.points.add(1)
# Add this weird fourth coordinate
coords = np.c_[s, [0, 0]]
# Set point coordinates
sp.points.foreach_set('co', coords.ravel())
ob.active_material = mat
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
col.objects.link(ob)
return
def _create_soma(self, x, mat, collection=None):
"""Create soma."""
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
for s in utils.make_iterable(x.soma):
s = x.nodes[x.nodes.node_id == s]
loc = s[['x', 'y', 'z']].values
loc = loc[:, self.axes_order]
loc *= self.scaling
loc *= self.ax_translate
rad = s.radius * self.scaling
mesh = bpy.data.meshes.new(f'Soma of #{x.id} - mesh')
soma_ob = bpy.data.objects.new(f'Soma of #{x.id}', mesh)
soma_ob.location = loc[0]
# Construct the bmesh cube and assign it to the blender mesh.
bm = bmesh.new()
# Blender 3.0 uses `radius` instead of `diameter`
try:
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=rad * 2)
except:
bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, diameter=rad)
bm.to_mesh(mesh)
bm.free()
mesh.polygons.foreach_set('use_smooth', [True] * len(mesh.polygons))
soma_ob.name = f'Soma of #{x.id}'
soma_ob['type'] = 'SOMA'
soma_ob['navis_object'] = True
soma_ob['id'] = str(x.id)
soma_ob.active_material = mat
# Add the object into the scene.
col.objects.link(soma_ob)
return
def _create_connectors(self, x, collection=None):
"""Create connectors."""
if not x.has_connectors:
return
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
for t in x.connectors['type'].unique():
con = x.connectors[x.connectors.type == t]
# See if we have pre-defined names/colors for this
settings = self.cn_dict.get(t, {'name': t, 'color': (0, 0, 0)})
if con.empty:
continue
# Get & scale coordinates and invert y
cn_coords = con[['x', 'y', 'z']].values.astype(float)
ob_name = f'{settings["name"]} of {x.id}'
# Only plot as lines if this is a TreeNeuron
if self.cn_dict.get('display', 'lines') == 'lines' and isinstance(x, core.TreeNeuron):
cn_coords = cn_coords[:, self.axes_order]
cn_coords *= float(self.scaling)
cn_coords *= self.ax_translate
tn_coords = x.nodes.set_index('node_id').loc[con.node_id.values,
['x', 'y', 'z']].values.astype(float)
tn_coords = tn_coords[:, self.axes_order]
tn_coords *= float(self.scaling)
tn_coords *= self.ax_translate
# Add 4th coordinate for blender
cn_coords = np.c_[cn_coords, [0] * con.shape[0]]
tn_coords = np.c_[tn_coords, [0] * con.shape[0]]
# Combine cn and tn coords in pairs
# This will have to be transposed to get pairs of cn and tn
# (see below)
coords = np.dstack([cn_coords, tn_coords])
cu = bpy.data.curves.new(ob_name + ' mesh', 'CURVE')
ob = bpy.data.objects.new(ob_name, cu)
cu.dimensions = '3D'
cu.fill_mode = 'FULL'
cu.bevel_resolution = 0
cu.bevel_depth = 0.007
cu.resolution_u = 0
for cn in coords:
sp = cu.splines.new('POLY')
# Add a second point
sp.points.add(1)
# Move points
sp.points.foreach_set('co', cn.T.ravel())
col.objects.link(ob)
else:
ob = self._create_scatter(cn_coords,
collection=collection,
size=self.cn_dict.get('size', 0.01))
ob.name = ob_name
ob['type'] = 'CONNECTORS'
ob['navis_object'] = True
ob['cn_type'] = t
ob['id'] = str(x.id)
ob.location = (0, 0, 0)
ob.show_name = False
mat_name = f'{settings["name"]} of #{str(x.id)}'
mat = bpy.data.materials.get(mat_name,
bpy.data.materials.new(mat_name))
mat.diffuse_color = eval_color(settings['color'],
color_range=1,
force_alpha=True)
ob.active_material = mat
return
def _create_volume(self, volume, collection=None):
"""Create mesh from volume.
Parameters
----------
volume : core.Volume | dict
Must contain 'faces', 'vertices'
"""
mesh_name = str(getattr(volume, 'name', 'mesh'))
verts = volume.vertices.copy()
# Convert to Blender space
verts = verts * self.scaling
verts = verts[:, self.axes_order]
verts *= self.ax_translate
blender_verts = verts.tolist()
me = bpy.data.meshes.new(mesh_name + '_mesh')
ob = bpy.data.objects.new(mesh_name, me)
scn = bpy.context.scene
scn.collection.objects.link(ob)
me.from_pydata(list(blender_verts), [], list(volume.faces))
me.update()
me.polygons.foreach_set('use_smooth', [True] * len(me.polygons))
[docs] def select(self, x, *args):
"""Select given neurons.
Parameters
----------
x : list of neuron IDs | Neuron/List | pd Dataframe
Returns
-------
:class:`navis.b3d.ObjectList` : containing requested neurons
Examples
--------
>>> selection = Handler.select([123456, 7890])
>>> # Get only connectors
>>> cn = selection.connectors
>>> # Hide everything else
>>> cn.hide_others()
>>> # Change color of presynapses
>>> selection.presynapses.color(0, 1, 0)
"""
ids = utils.eval_id(x)
if not ids:
logger.error('No ids found.')
names = []
for ob in bpy.data.objects:
ob.select_set(False)
if 'id' in ob:
if ob['id'] in ids:
ob.select_set(True)
names.append(ob.name)
return ObjectList(names, handler=self)
[docs] def color(self, r, g, b):
"""Assign color to all neurons.
Parameters
----------
r : float
Red value, range 0-1
g : float
Green value, range 0-1
b : float
Blue value, range 0-1
Notes
-----
This will only change color of neurons, if you want to change
color of e.g. connectors, use:
>>> handler.connectors.color(r, g, b)
"""
self.neurons.color(r, g, b)
[docs] def colorize(self):
"""Randomly colorize ALL neurons.
Notes
-----
This will only change color of neurons, if you want to change
color of e.g. connectors, use:
>>> handler.connectors.colorize()
"""
self.neurons.colorize()
[docs] def emit(self, v):
"""Change emit value."""
self.neurons.emit(v)
[docs] def use_transparency(self, v):
"""Change transparency (True/False)."""
self.neurons.use_transparency(v)
[docs] def alpha(self, v):
"""Change alpha (0-1)."""
self.neurons.alpha(v)
[docs] def bevel(self, r):
"""Change bevel of ALL neurons.
Parameters
----------
r : float
New bevel radius
Notes
-----
This will only change bevel of neurons, if you want to change
bevel of e.g. connectors, use:
>>> handler.connectors.bevel(.02)
"""
self.neurons.bevel_depth(r)
[docs] def hide(self):
"""Hide all neuron-related objects."""
self.all.hide()
[docs] def unhide(self):
"""Unide all neuron-related objects."""
self.all.unhide()
class ObjectList:
"""Collection of Blender objects.
Notes
-----
1. ObjectLists should normally be constructed via the handler
(see :class:`navis.b3d.Handler`)!
2. List works with object NAMES to prevent Blender from crashing when
trying to access neurons that do not exist anymore. This also means
that changing names manually will compromise a object list.
3. Accessing a neuron list's attributes (see below) return another
``ObjectList`` class which you can use to manipulate the new
subselection.
Attributes
----------
neurons : returns list containing just neurons
connectors : returns list containing all connectors
soma : returns list containing all somata
presynapses : returns list containing all presynapses
postsynapses : returns list containing all postsynapses
gapjunctions : returns list containing all gap junctions
abutting : returns list containing all abutting connectors
id : returns list of IDs
Examples
--------
>>> # b3d module has to be import explicitly
>>> from navis import b3d
>>> nl = navis.example_neurons()
>>> handler = b3d.Handler()
>>> handler.add(nl)
>>> # Select only neurons on the right
>>> right = handler.select('annotation:uPN right')
>>> # This can be nested to change e.g. color of all right presynases
>>> handler.select('annotation:uPN right').presynapses.color(0, 1, 0)
"""
def __init__(self, object_names, handler=None):
if not isinstance(object_names, list):
object_names = [object_names]
self.object_names = object_names
self.handler = handler
def __getattr__(self, key):
if key in ['neurons', 'neuron', 'neurites']:
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'NEURON'])
elif key in ['connectors', 'connector']:
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'CONNECTORS'])
elif key in ['soma', 'somas']:
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'SOMA'])
elif key == 'presynapses':
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'CONNECTORS' and bpy.data.objects[n]['cn_type'] == 0])
elif key == 'postsynapses':
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'CONNECTORS' and bpy.data.objects[n]['cn_type'] == 1])
elif key == 'gapjunctions':
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'CONNECTORS' and bpy.data.objects[n]['cn_type'] == 2])
elif key == 'abutting':
return ObjectList([n for n in self.object_names if n in bpy.data.objects and bpy.data.objects[n]['type'] == 'CONNECTORS' and bpy.data.objects[n]['cn_type'] == 3])
elif key.lower() in ['id', 'ids']:
return [bpy.data.objects[n]['id'] for n in self.object_names if n in bpy.data.objects]
else:
raise AttributeError('Unknown attribute ' + key)
def __getitem__(self, key):
if isinstance(key, int) or isinstance(key, slice):
return ObjectList(self.object_names[key], handler=self.handler)
else:
raise Exception('Unable to index non-integers.')
def __str__(self):
return self.__repr__()
def __repr__(self):
self._repr = pd.DataFrame([[n, n in bpy.data.objects] for n in self.object_names],
columns=['name', 'still_exists']
)
return str(self._repr)
def __len__(self):
return len(self.object_names)
def __add__(self, to_add):
if not isinstance(to_add, ObjectList):
raise AttributeError('Can only merge other object lists')
print(to_add.object_names)
return ObjectList(list(set(self.object_names + to_add.object_names)),
handler=self.handler)
@property
def objects(self):
"""Objects in this list."""
objects = []
for n in self.object_names:
if n in bpy.data.objects:
objects.append(bpy.data.objects[n])
return objects
def add_to_collection(self, collection, unlink_from_other=False):
if not collection:
col = bpy.context.scene.collection
elif collection in bpy.data.collections:
col = bpy.data.collections[collection]
else:
col = bpy.data.collections.new(collection)
bpy.context.scene.collection.children.link(col)
for ob in self.objects:
if ob.name not in col.objects:
col.objects.link(ob)
if unlink_from_other:
if ob.name in bpy.context.scene.collection.objects:
bpy.context.scene.collection.objects.unlink(ob)
for col2 in bpy.data.collections:
if col2 == col:
continue
if ob.name in col2.objects:
col2.objects.unlink(ob)
[docs] def select(self, unselect_others=True):
"""Select objects in 3D viewer
Parameters
----------
unselect_others : bool, optional
If False, will not unselect other objects.
"""
for ob in bpy.data.objects:
if ob.name in self.object_names:
ob.select_set(True)
elif unselect_others:
ob.select_set(False)
[docs] def color(self, r, g, b, a=1):
"""Assign color to all objects in the list.
Parameters
----------
r : float
Red value, range 0-1
g : float
Green value, range 0-1
b : float
Blue value, range 0-1
a : float
Alpha value, range 0-1
"""
for ob in self.objects:
ob.active_material.diffuse_color = eval_color((r, g, b, a),
color_range=1,
force_alpha=True)
[docs] def colorize(self, groups=None, palette='hls'):
"""Assign colors across the color spectrum.
Parameters
----------
groups : dict, optional
A dictionary mapping either neuron ID (always str!) or
object name to a group (str). Neurons of the same group will
receive the same color.
palette : str
Name of a seaborn color palette.
"""
objects = self.objects
if isinstance(groups, type(None)):
colors = sns.color_palette(palette, len(objects))
cmap = dict(zip(objects, colors))
elif isinstance(groups, dict):
# Make sure keys are strings
groups = {str(k): v for k, v in groups.items()}
# Get unique groups & create a color map
groups_uni = list(set(list(groups.values())))
colors = sns.color_palette(palette, len(groups_uni))
groups_cmap = dict(zip(groups_uni, colors))
# Make the actual color map
cmap = {}
for ob in objects:
# Get the group either by name or ID
g = groups.get(ob.name, groups.get(ob.get('id'), None))
cmap[ob] = groups_cmap.get(g, (.1, .1, .1))
else:
raise TypeError(f'`groups` must be either None or dict, got {type(groups)}')
for ob in objects:
try:
ob.active_material.diffuse_color = eval_color(cmap[ob],
color_range=1,
force_alpha=True)
except BaseException:
logger.warning(f'Error changing color of object "{ob}"')
[docs] def emit(self, e):
"""Change emit value."""
for ob in self.objects:
ob.active_material.emit = e
[docs] def use_transparency(self, t):
"""Change transparency (True/False)."""
for ob in self.objects:
ob.active_material.use_transparency = t
[docs] def alpha(self, a):
"""Change alpha (0-1)."""
for ob in self.objects:
ob.active_material.alpha = a
[docs] def bevel(self, r):
"""Change bevel radius of objects.
Parameters
----------
r : float
New bevel radius.
"""
for ob in self.objects:
if ob.type == 'CURVE':
ob.data.bevel_depth = r
[docs] def hide(self, viewport=True, render=False):
"""Hide objects."""
for ob in self.objects:
if viewport:
ob.hide_set(True)
if render:
ob.hide_render = True
[docs] def unhide(self, viewport=True, render=False):
"""Unhide objects."""
for ob in self.objects:
if viewport:
ob.hide_set(False)
if render:
ob.hide_render = False
[docs] def hide_others(self):
"""Hide everything BUT these objects."""
for ob in bpy.data.objects:
if ob.name in self.object_names:
ob.hide = False
else:
ob.hide = True
[docs] def delete(self):
"""Delete neurons in the selection."""
self.select(unselect_others=True)
bpy.ops.object.delete()
def calc_sphere(radius, nrPolar, nrAzimuthal):
"""Calculate vertices and faces for a sphere."""
dPolar = math.pi / (nrPolar - 1)
dAzimuthal = 2.0 * math.pi / (nrAzimuthal)
# 1/2: vertices
verts = []
currV = mathutils.Vector((0.0, 0.0, radius)) # top vertex
verts.append(currV)
for iPolar in range(1, nrPolar - 1): # regular vertices
currPolar = dPolar * float(iPolar)
currCosP = math.cos(currPolar)
currSinP = math.sin(currPolar)
for iAzimuthal in range(nrAzimuthal):
currAzimuthal = dAzimuthal * float(iAzimuthal)
currCosA = math.cos(currAzimuthal)
currSinA = math.sin(currAzimuthal)
currV = mathutils.Vector((currSinP * currCosA,
currSinP * currSinA,
currCosP)) * radius
verts.append(currV)
currV = mathutils.Vector((0.0, 0.0, - radius)) # bottom vertex
verts.append(currV)
# 2/2: faces
faces = []
for iAzimuthal in range(nrAzimuthal): # top faces
iNextAzimuthal = iAzimuthal + 1
if iNextAzimuthal >= nrAzimuthal:
iNextAzimuthal -= nrAzimuthal
faces.append([0, iAzimuthal + 1, iNextAzimuthal + 1])
for iPolar in range(nrPolar - 3): # regular faces
iAzimuthalStart = iPolar * nrAzimuthal + 1
for iAzimuthal in range(nrAzimuthal):
iNextAzimuthal = iAzimuthal + 1
if iNextAzimuthal >= nrAzimuthal:
iNextAzimuthal -= nrAzimuthal
faces.append([iAzimuthalStart + iAzimuthal,
iAzimuthalStart + iAzimuthal + nrAzimuthal,
iAzimuthalStart + iNextAzimuthal + nrAzimuthal,
iAzimuthalStart + iNextAzimuthal])
iLast = len(verts) - 1
iAzimuthalStart = iLast - nrAzimuthal
for iAzimuthal in range(nrAzimuthal): # bottom faces
iNextAzimuthal = iAzimuthal + 1
if iNextAzimuthal >= nrAzimuthal:
iNextAzimuthal -= nrAzimuthal
faces.append([iAzimuthalStart + iAzimuthal,
iLast,
iAzimuthalStart + iNextAzimuthal])
return np.vstack(verts), faces