Skip to content

Note

Click here to download the full example code

Neuron Types#

This tutorial will show you the different neuron types and how to work with them.

Depending your data/workflows, you will use different representations of neurons. If, for example, you work with light-level data you might end up extracting point clouds or neuron skeletons from image stacks. If, on the other hand, you work with segmented EM data, you will typically work with meshes.

To cater for these different representations, neurons in NAVis come in four flavours:

Neuron type Description Core data
navis.TreeNeuron A hierarchical skeleton consisting
of nodes and edges.
- .nodes: the SWC node table
navis.MeshNeuron A mesh with faces and vertices. - .vertices: (N, 3) array of x/y/z vertex coordinates
- .faces: (M, 3) array of faces
navis.VoxelNeuron An image represented by either a
2d array of voxels or a 3d voxel grid.
- .voxels: (N, 3) array of voxels
- .values: (N, ) array of values (i.e. intensity)
- .grid: (N, M, K) 3D voxelgrid
navis.Dotprops A cloud of points, each with an
associated local vector.
- .points: (N, 3) array of point coordinates
- .vect: (N, 3) array of normalized vectors

Note that functions in NAVis may only work on a subset of neuron types: check out this table in the API reference for details. If necessary, NAVis can help you convert between the different neuron types (see further below)!

Important

In this guide we introduce the different neuron types using data bundled with NAVis. To learn how to load your own neurons into NAVis please see the tutorials on Import/Export.

TreeNeurons#

TreeNeurons represent a neuron as a tree-like "skeleton" - effectively a directed acyclic graph, i.e. they consist of nodes and each node connects to at most one parent. This format is commonly used to describe a neuron's topology and often shared using SWC files.

skeleton

A navis.TreeNeuron is typically loaded from an SWC file via navis.read_swc but you can also constructed one yourself from e.g. pandas.DataFrame or a networkx.DiGraph. See the skeleton I/O tutorial for details.

NAVis ships with a couple example Drosophila neurons from the Janelia hemibrain project published in Scheffer et al. (2020) and available at https://neuprint.janelia.org (see also the neuPrint tutorial):

import navis

# Load one of the example neurons
sk = navis.example_neurons(n=1, kind="skeleton")

# Inspect the neuron
sk
type navis.TreeNeuron
name DA1_lPN_R
id 1734350788
n_nodes 4465
n_connectors 2705
n_branches 599
n_leafs 618
cable_length 266476.875
soma 4177
units 8 nanometer
created_at 2024-10-24 11:20:42.224436
origin /home/runner/work/navis/navis/navis/data/swc/1...
file 1734350788.swc

navis.TreeNeuron stores nodes and other data as attached pandas.DataFrames:

sk.nodes.head()
node_id label x y z radius parent_id type
0 1 0 15784.0 37250.0 28062.0 10.000000 -1 root
1 2 0 15764.0 37230.0 28082.0 18.284300 1 slab
2 3 0 15744.0 37190.0 28122.0 34.721401 2 slab
3 4 0 15744.0 37150.0 28202.0 34.721401 3 slab
4 5 0 15704.0 37130.0 28242.0 34.721401 4 slab

MeshNeurons#

MeshNeurons consist of vertices and faces, and are a typical output of e.g. image segmentation.

mesh

A navis.MeshNeuron can be constructed from any object that has .vertices and .faces properties, a dictionary of vertices and faces or a file that can be parsed by trimesh.load. See the mesh I/O tutorial for details.

Each of the example neurons in NAVis also comes as mesh representation:

m = navis.example_neurons(n=1, kind="mesh")
m
type navis.MeshNeuron
name DA1_lPN_R
id 1734350788
units 8 nanometer
n_vertices 6309
n_faces 13054

navis.MeshNeuron stores vertices and faces as attached numpy arrays:

m.vertices, m.faces

Out:

(TrackedArray([[16384.        , 34792.03125   , 24951.88085938],
              [16384.        , 36872.0625    , 25847.89453125],
              [16384.        , 36872.0625    , 25863.89453125],
              ...,
              [ 5328.08105469, 21400.07617188, 16039.99414062],
              [ 6872.10498047, 19560.04882812, 13903.96191406],
              [ 6872.10498047, 19488.046875  , 13927.96191406]]), TrackedArray([[3888, 3890, 3887],
              [3890, 1508, 3887],
              [1106, 1104, 1105],
              ...,
              [5394, 5426, 5548],
              [5852, 5926, 6017],
              [ 207,  217,  211]]))

Dotprops#

Dotprops represent neurons as point clouds where each point is associated with a vector describing the local orientation. This simple representation often comes from e.g. light-level data or as direvative of skeletons/meshes (see navis.make_dotprops).

dotprops

Dotprops are used e.g. for NBLAST. See the dotprops I/O tutorial for details.

navis.Dotprops consist of .points and associated .vect (vectors). They are typically created from other types of neurons using navis.make_dotprops:

Turn our above skeleton into dotprops

dp = navis.make_dotprops(sk, k=5)
dp
type navis.Dotprops
name DA1_lPN_R
id 1734350788
k 5
units 8 nanometer
n_points 4465
dp.points, dp.vect

Out:

(array([[15784., 37250., 28062.],
       [15764., 37230., 28082.],
       [15744., 37190., 28122.],
       ...,
       [14544., 36430., 28422.],
       [14944., 36510., 28282.],
       [15264., 36870., 28282.]], dtype=float32), array([[-0.3002053 , -0.39364937,  0.8688596 ],
       [-0.10845336, -0.2113751 ,  0.9713694 ],
       [-0.0435693 , -0.45593134,  0.8889479 ],
       ...,
       [-0.38446087,  0.44485292, -0.80888546],
       [-0.9457323 , -0.1827982 , -0.26865458],
       [-0.79947734, -0.5164282 , -0.30681902]], dtype=float32))

Check out the NBLAST tutorial for further details on dotprops!

VoxelNeurons#

VoxelNeurons represent neurons as either 3d image or x/y/z voxel coordinates typically obtained from e.g. light-level microscopy.

voxels

navis.VoxelNeuron consist of either a dense 3d (N, M, K) array (a "grid") or a sparse 2d (N, 3) array of voxel coordinates (COO format). You will probably find yourself loading these data from image files (e.g. .nrrd via navis.read_nrrd()). That said we can also "voxelize" other neuron types to produce VoxelNeurons:

# Load an example mesh
m = navis.example_neurons(n=1, kind="mesh")

# Voxelize:
# - with a 0.5 micron voxel size
# - some Gaussian smoothing
# - use number of vertices (counts) for voxel values
vx = navis.voxelize(m, pitch="0.5 microns", smooth=2, counts=True)
vx
type navis.VoxelNeuron
name DA1_lPN_R
id 1734350788
units 500.0 nanometer
shape (298, 392, 286)
dtype float32

This is the grid representation of the neuron:

vx.grid.shape

Out:

(298, 392, 286)

And this is the (N, 3) voxel coordinates + (N, ) values sparse representation of the neuron:

vx.voxels.shape, vx.values.shape

Out:

((643611, 3), (643611,))

Note

You may have noticed that all neurons share some properties irrespective of their type, for example .id, .name or .units. These properties are optional and can be set when you first create the neuron, or at a later point.

In particular the .id property is important because many functions in NAVis will return results that are indexed by the neurons' IDs. If .id is not set explicitly, it will default to some rather cryptic random UUID - you have been warned! 😉

Neuron meta data#

Connectors#

NAVis was designed with connectivity data in mind! Therefore, each neuron - regardless of type - can have a .connectors table. Connectors are meant to bundle all kinds of connections: pre- & postsynapses, electrical synapses, gap junctions and so on.

A connector table must minimally contain an x/y/z coordinate and a type for each connector. Here is an example of a connector table:

n = navis.example_neurons(1)
n.connectors.head()
connector_id node_id type x y z roi confidence
0 0 1436 pre 6444 21608 14516 LH(R) 0.959
1 1 1436 pre 6457 21634 14474 LH(R) 0.997
2 2 2638 pre 4728 23538 14179 LH(R) 0.886
3 3 1441 pre 5296 22059 16048 LH(R) 0.967
4 4 1872 pre 4838 23114 15146 LH(R) 0.990

Connector tables aren't just passive meta data: certain functions in NAVis use or even require them. The most obvious example is probably for plotting:

# Plot neuron including its connectors
fig, ax = navis.plot2d(
    n,  # the neuron
    connectors=True,  # plot the neurons' connectors
    color="k",  # make the neuron black
    cn_size=3,  # slightly increase connector size
    view=("x", "-z"),  # set frontal view
    method="2d"  # connectors are better visible in 2d
)

tutorial basic 01 neurons

In above plot, red dots are presynapses (outputs) and cyan dots are postsynapses (inputs).

Somas#

Unless a neuron is truncated, it should have a soma somewhere. Knowing where the soma is can be very useful, e.g. as point of reference for distance calculations or for plotting. Therefore, {{ soma }} neurons have a .soma property:

n = navis.example_neurons(1)
n.soma

Out:

4177

In case of this exemplary navis.TreeNeuron, the .soma points to an ID in the node table. We can also get the position:

n.soma_pos

Out:

array([[14957.1, 36540.7, 28432.4]], dtype=float32)

Other neuron types also support soma annotations but they may look slightly different. For a navis.MeshNeuron, annotating a node position makes little sense. Instead, we track the x/y/z position directly:

m = navis.example_neurons(1, kind="mesh")
m.soma_pos

Out:

array([14957.1, 36540.7, 28432.4])

For the record: .soma / .soma_pos can be set manually like any other property (there are some checks and balances to avoid issues) and can also be None:

# Set the skeleton's soma on node with ID 1
n.soma = 1
n.soma

Out:

1

Drop soma from MeshNeuron

m.soma_pos = None

Units#

NAVis supports assigning units to neurons. The neurons shipping with NAVis, for example, are in 8x8x8nm voxel space1:

m = navis.example_neurons(1, kind="mesh")
print(m.units)

Out:

8 nanometer

To set the neuron's units simply use a descriptive string:

m.units = "10 micrometers"
print(m.units)

Out:

10 micrometer

Note

Setting the units as we did above does not actually change the neuron's coordinates. It merely sets a property that can be used by other functions to interpret the neuron's coordinate space. See below on how to convert the units of a neuron.

Tracking units is good practice in general but is also very useful in a variety of scenarios:

First, certain NAVis functions let you pass quantities as unit strings:

# Load example neuron which is in 8x8x8nm space
n = navis.example_neurons(1, kind="skeleton")

# Resample to 1 micrometer
rs = navis.resample_skeleton(n, resample_to="1 um")

Second, NAVis optionally uses the neuron's units to make certain properties more interpretable. By default, properties like cable length or volume are returned in the neuron's units, i.e. in 8x8x8nm voxel space in our case:

print(n.cable_length)

Out:

266476.88

You can tell NAVis to use the neuron's .units to make these properties more readable:

navis.config.add_units = True
print(n.cable_length)
navis.config.add_units = False  # reset to default

Out:

2.1318150000000005 millimeter

Note

Note that n.cable_length is now a pint.Quantity object. This may make certain operations a bit more cumbersome which is why this feature is optional. You can to a float by calling .magnitude:

n.cable_length.magnitude

Check out Pint's documentation to learn more.

To actually convert the neuron's coordinate space, you have two options:

You can multiply or divide any neuron or NeuronList by a number to change the units:

# Example neuron are in 8x8x8nm voxel space
n = navis.example_neurons(1)
# Multiply by 8 to get to nanometer space
n_nm = n * 8
# Divide by 1000 to get micrometers
n_um = n_nm / 1000

For non-isometric conversions you can pass a vector of scaling factors:

neuron * [4, 4, 40]
Note that for TreeNeurons, this is expected to be scaling factors for (x, y, z, radius).

If your neuron has known units, you can let NAVis do the conversion for you:

n = navis.example_neurons(1)
# Convert to micrometers
n_um = n.convert_units("micrometers")

Addition & Subtraction

Multiplication and division will scale the neuro as you've seen above. Similarly, adding or subtracting to/from neurons will offset the neuron's coordinates:

n = navis.example_neurons(1)

# Convert to microns
n_um = n.convert_units("micrometers")

# Add 100 micrometers along all axes to the neuron
n_offset = n + 100

# Subtract 100 micrometers along just one axis
n_offset = n - [0, 0, 100]#

Operating on neurons#

Above we've already seen examples of passing neurons to functions - for example navis.plot2d(n).

For some NAVis functions, neurons offer have shortcut "methods":

import navis
sk = navis.example_neurons(1, kind='skeleton')

sk.reroot(sk.soma, inplace=True)  # reroot the neuron to its soma

lh = navis.example_volume('LH')
sk.prune_by_volume(lh, inplace=True)  # prune the neuron to a volume#

sk.plot3d(color='red')  # plot the neuron in 3d
import navis
sk = navis.example_neurons(1, kind='skeleton')

navis.reroot_skeleton(sk, sk.soma, inplace=True)  # reroot the neuron to its soma

lh = navis.example_volume('LH')
navis.in_volume(sk, lh, inplace=True)  # prune the neuron to a volume

navis.plot3d(sk, color='red')  # plot the neuron in 3d

Note

In some cases the shorthand methods might offer only a subset of the full function's functionality.

The inplace parameter#

The inplace parameter is part of many NAVis functions and works like e.g. in the pandas library:

  • if inplace=True operations are performed directly on the input neuron(s)
  • if inplace=False (default) a modified copy of the input is returned and the input is left unchanged

If you know you don't need the original, you can use inplace=True to save memory (and a bit of time):

# Load a neuron
n = navis.example_neurons(1)
# Load an example neuropil
lh = navis.example_volume("LH")

# Prune neuron to neuropil but leave original intact
n_lh = n.prune_by_volume(lh, inplace=False)

print(f"{n.n_nodes} nodes before and {n_lh.n_nodes} nodes after pruning")

Out:

4465 nodes before and 344 nodes after pruning

All neurons are equal...#

... but some are more equal than others.

In Python the == operator compares two objects:

1 == 1

Out:

True
2 == 1

Out:

False

For NAVis neurons this is comparison done by looking at the neurons' attribues: morphologies (soma & root nodes, cable length, etc) and meta data (name).

n1, n2 = navis.example_neurons(n=2)
n1 == n1

Out:

True
n1 == n2

Out:

False

To find out which attributes are compared, check out the neuron's .EQ_ATTRIBUTES property:

navis.TreeNeuron.EQ_ATTRIBUTES

Out:

['n_nodes', 'n_connectors', 'soma', 'root', 'n_branches', 'n_leafs', 'cable_length', 'name']

Edit this list to establish your own criteria for equality.

Making custom changes#

Under the hood NAVis calculates certain properties when you load a neuron: e.g. it produces a graph representation (.graph or .igraph) and a list of linear segments (.segments) for TreeNeurons. These data are attached to a neuron and are crucial for many functions. Therefore NAVis makes sure that any changes to a neuron automatically propagate into these derived properties. See this example:

n = navis.example_neurons(1, kind="skeleton")

print(f"Nodes in node table: {n.nodes.shape[0]}")
print(f"Nodes in graph: {len(n.graph.nodes)}")

Out:

Nodes in node table: 4465
Nodes in graph: 4465

Making changes will cause the graph representation to be regenerated:

n.prune_by_strahler(1, inplace=True)

print(f"Nodes in node table: {n.nodes.shape[0]}")
print(f"Nodes in graph: {len(n.graph.nodes)}")

Out:

Nodes in node table: 1761
Nodes in graph: 1761

If, however, you make changes to the neurons that do not use built-in functions there is a chance that NAVis won't realize that things have changed and properties need to be regenerated!

n = navis.example_neurons(1)

print(f"Nodes in node table before: {n.nodes.shape[0]}")
print(f"Nodes in graph before: {len(n.graph.nodes)}")

# Truncate the node table by 55 nodes
n.nodes = n.nodes.iloc[:-55].copy()

print(f"\nNodes in node table after: {n.nodes.shape[0]}")
print(f"Nodes in graph after: {len(n.graph.nodes)}")

Out:

Nodes in node table before: 4465
Nodes in graph before: 4465

Nodes in node table after: 4410
Nodes in graph after: 4410

Here, the changes to the node table automatically triggered a regeneration of the graph. This works because NAVis checks hash values of neurons and in this instance it detected that the node node table - which represents the core data for TreeNeurons - had changed. It would not work the other way around: changing the graph does not trigger changes in the node table.

Again: as long as you are using built-in functions, you don't have to worry about this. If you do run some custom manipulation of neurons be aware that you might want to make sure that the data structure remains intact. If you ever need to manually trigger a regeneration you can do so like this:

Clear temporary attributes of the neuron

n._clear_temp_attr()

Converting neuron types#

NAVis provides a couple functions to move between neuron types:

In particular meshing and skeletonizing are non-trivial and you might have to play around with the parameters to optimize results with your data! Let's demonstrate on some example:

# Start with a mesh neuron
m = navis.example_neurons(1, kind="mesh")

# Skeletonize the mesh
s = navis.skeletonize(m)

# Make dotprops (this works from any other neuron type
dp = navis.make_dotprops(s, k=5)

# Voxelize the mesh
vx = navis.voxelize(m, pitch="2 microns", smooth=1, counts=True)

# Mesh the voxels
mm = navis.mesh(vx.threshold(0.5))

Out:

/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/skeletor/skeletonize/wave.py:198: DeprecationWarning:

Graph.clusters() is deprecated; use Graph.connected_components() instead

/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/skeletor/skeletonize/wave.py:228: DeprecationWarning:

Graph.shortest_paths() is deprecated; use Graph.distances() instead

Inspect the results:

# Co-visualize the mesh and the skeleton
navis.plot3d(
    [m, s],
    color=[(0.7, 0.7, 0.7, 0.2), "r"],  # transparent mesh, skeleton in red
    radius=False,  # False so that skeleton is drawn as a line
)

Co-visualize the mesh and the dotprops

navis.plot3d(
    [m, dp],
    color=[(0.7, 0.7, 0.7, 0.2), "r"],  # transparent mesh, dotprops in red
)

Co-visualize the mesh and the dotprops (note that plotly is not great at visualizing voxels)

navis.plot3d([m * 8, vx])

Co-visualize the original mesh and the meshed voxels

navis.plot3d([vx, mm], fig_autosize=True)

Neuron attributes#

This is a selection of neuron class (i.e. navis.TreeNeuron, navis.MeshNeuron, etc.) attributes.

All neurons have this:

  • name: a name
  • id: a (hopefully unique) identifier - defaults to random UUID if not set explicitly
  • bbox: Bounding box of neuron
  • units (optional): spatial units (e.g. "1 micrometer" or "8 nanometer" voxels)
  • connectors (optional): connector table

Only for TreeNeurons:

  • nodes: node table
  • cable_length: cable length(s)
  • soma: node ID(s) of soma (if applicable)
  • root: root node ID(s)
  • segments: list of linear segments
  • graph: NetworkX graph representation of the neuron
  • igraph: iGraph representation of the neuron (if library available)

Only for MeshNeurons:

  • vertices/faces: vertices and faces
  • volume: volume of mesh
  • soma_pos: x/y/z position of soma

Only for VoxelNeurons:

  • voxels: (N, 3) sparse representation
  • grid: (N, M, K) voxel grid representation

Only for Dotprops:

  • points (N, 3) x/y/z points
  • vect: (N, 3) array of the vector associated with each point

All above attributes can be accessed via NeuronLists containing the neurons. In addition you can also get:

  • is_mixed: returns True if list contains more than one neuron type
  • is_degenerated: returns True if list contains neurons with non-unique IDs
  • types: tuple with all types of neurons in the list
  • shape: size of neuronlist (N, )

All attributes and methods are accessible through auto-completion.

What next?#

Total running time of the script: ( 0 minutes 2.096 seconds)

Download Python source code: tutorial_basic_01_neurons.py

Download Jupyter notebook: tutorial_basic_01_neurons.ipynb

Gallery generated by mkdocs-gallery


  1. The example neurons are from the Janelia hemibrain connectome project which as imaged at 8x8x8nm resolution.