Neuron types

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 some functions in navis will work on some but not all neuron types (see this table in the API reference for details). If need be, navis also offers ways to convert between the different neuron types (see further below). For details on how to load/import your neurons into navis please see the other tutorials.

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 1 parent. This format is commonly used to describe a neuron’s topology and often shared using SWC files.

A TreeNeuron is typically constructed from an SWC file (see navis.read_swc()) but you can also use a 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
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 2022-05-09 11:52:08.206993
id 1734350788
origin /Users/philipps/Google Drive/Cloudbox/Github/n...

TreeNeuron stores nodes and other data as attached 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.

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

MeshNeuron stores vertices and faces as attached numpy arrays:

m.vertices, m.faces
(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 are used e.g. for NBLAST. See the dotprops I/O tutorial for details.

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
(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.

VoxelNeuron consist of either a 3d (N, M, K) array (=”grid”) or an 2d (N, 3) array of voxel coordinates. 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
# The grid representation of the neuron
vx.grid.shape
(297, 392, 286)
# The (N, 3) voxel + (N, ) values representation of the neuron
# -> this is the sparse representation of the neuron
vx.voxels.shape, vx.values.shape
((641384, 3), (641384,))

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!

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:

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 make use of or even require them. The most obvious example is probably for plotting.

# Plot neuron including its connectors
# Note we're increasing connector size and decreasing line width
# to illustrate emphasize connectors
fig, ax = navis.plot2d(n, connectors=True, color='k', cn_size=10, lw=.25)
ax.dist = 5
../../_images/neurons_intro_18_0.png

In above plot, red are presynapses (outputs) and cyan 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, navis neurons have a .soma property:

n = navis.example_neurons(1)
n.soma
array([4177], dtype=int32)

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

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

Other neuron types also support soma annotations but they may look slightly different. For a 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
[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
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 space:

m = navis.example_neurons(1, kind='mesh')
m.units
8 nanometer

To assign or change the units simply use a descriptive string:

m.units = '10 micrometers'
m.units
10 micrometer

Tracking units is good practive but can also be very useful: some navis functions let you pass quantities as unit strings:

# Load example neuron in 8x8x8nm
n = navis.example_neurons(1, kind='skeleton')

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

Lists of Neurons

Multiple neurons are typically collected in a NeuronList as container:

# Grab three example TreeNeurons
nl = navis.example_neurons(n=3)
nl
<class 'navis.core.neuronlist.NeuronList'> containing 3 neurons (875.1KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer
2 navis.TreeNeuron DA1_lPN_R 722817260 4332 3136 633 656 274703.37500 None 8 nanometer

A NeuronList works similar to normal lists with a bunch of additional perks:

# Get the first neuron in the list
nl[0]
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

They also allow easy and fast access to data and across all neurons:

# Get the number of nodes in the first skeleton
nl[0].n_nodes
4465
# Use the neuronlist to collect number of nodes across all neurons
nl.n_nodes
array([4465, 4847, 4332])
# Works on any neuron attribute
nl.cable_length
array([266476.88, 304332.66, 274703.38], dtype=float32)

In addition to these attributes, both TreeNeuron and NeuronList have shortcuts (called methods) to other navis functions. These lines of code are equivalent:

sk.reroot(sk.soma, inplace=True)
navis.reroot_skeleton(sk, sk.soma, inplace=True)
sk.plot3d(color='red')
navis.plot3d(sk, color='red')
lh = navis.example_volume('LH')
sk.prune_by_volume(lh, inplace=True)
navis.in_volume(sk, lh, inplace=True)

Note

The inplace parameter is part of many navis functions and works like e.g. in the pandas library:
  • if inplace=True operations are performed on the original

  • if inplace=False (default) operations are performed on a copy of the original which is then returned

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

# Prune neuron to neurpil 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')
4465 nodes before and 344 nodes after pruning

Index by position

NeuronList are designed to behave similar to numpy arrays in that they allow some fancing indexing.

You’ve already seen how to extract a single neuron from a NeuronList using a single integer index. Like for numpy arrays, this also works for list of indices…

nl = navis.example_neurons(n=3)

nl[[0, 2]]
<class 'navis.core.neuronlist.NeuronList'> containing 2 neurons (571.4KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R 1734350788 4465 2705 599 618 266476.875 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R 722817260 4332 3136 633 656 274703.375 None 8 nanometer

… or slices

nl[:2]
<class 'navis.core.neuronlist.NeuronList'> containing 2 neurons (577.6KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Index by attributes

You can index by NeuronList by boolean numpy.arrays - that includes TreeNeuron attributes, e.g. n_nodes, cable_length, soma, etc.

Index using node count:

subset = nl[nl.n_branches > 700]
subset
<class 'navis.core.neuronlist.NeuronList'> containing 1 neurons (303.8KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Here is an example where we subset to neurons that have a soma:

nl.soma
array([array([4177], dtype=int32), array([6], dtype=int32), None],
      dtype=object)
nl.soma != None
array([ True,  True, False])
subset = nl[nl.soma != None]
subset
<class 'navis.core.neuronlist.NeuronList'> containing 2 neurons (577.6KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Index by name

TreeNeuron can (but don’t have to) have names (.name). If you, for example, import neurons from SWC files they will inherit their name from the file by default.

Our example neurons all have the same name, so to demo this feature we will need to make those names unique:

for i, n in enumerate(nl):
    n.name = n.name + str(i + 1)
nl
<class 'navis.core.neuronlist.NeuronList'> containing 3 neurons (875.1KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer
2 navis.TreeNeuron DA1_lPN_R3 722817260 4332 3136 633 656 274703.37500 None 8 nanometer

You can index by single…

nl['DA1_lPN_R1']
<class 'navis.core.neuronlist.NeuronList'> containing 1 neurons (273.8KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.875 [4177] 8 nanometer

… or multiple names:

nl[['DA1_lPN_R1', 'DA1_lPN_R2']]
<class 'navis.core.neuronlist.NeuronList'> containing 2 neurons (577.6KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Using regex

Under the hood navis uses re.fullmatch to match neuron names - so you can use regex!

nl['.*DA1.*']
<class 'navis.core.neuronlist.NeuronList'> containing 3 neurons (875.1KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer
2 navis.TreeNeuron DA1_lPN_R3 722817260 4332 3136 633 656 274703.37500 None 8 nanometer

Index by ID

All neurons have an ID - even if you don’t explicitly assign one, a UUID will assigned under the hood.

nl[0].id
1734350788

Neuron lists can be indexed by their ID (similar to .loc[] in pandas DataFrames) by using the .idx indexer:

nl.idx[1734350908]
type navis.TreeNeuron
name DA1_lPN_R2
id 1734350908
n_nodes 4847
n_connectors 3042
n_branches 735
n_leafs 761
cable_length 304332.65625
soma [6]
units 8 nanometer

Neuron Math

navis implements a very simple and intuitive syntax to add and remove items from a NeuronList:

Addition

To merge two lists in Python, you can simply add them:

[1] + [3]
[1, 3]

NeuronList works exactly the same:

nl[:2] + nl[2:]
<class 'navis.core.neuronlist.NeuronList'> containing 3 neurons (875.1KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer
2 navis.TreeNeuron DA1_lPN_R3 722817260 4332 3136 633 656 274703.37500 None 8 nanometer

This also works on with two single TreeNeurons! You can use that to combine them into a list:

nl[0] + nl[1]
<class 'navis.core.neuronlist.NeuronList'> containing 2 neurons (577.6KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Substraction

To remove an item from a Python list, you would call the .pop() method:

l = [1, 2, 3]
l.pop(2)
l
[1, 2]

For NeuronList you can use substraction:

nl - nl[2]
<class 'navis.core.neuronlist.NeuronList'> containing 2 neurons (577.6KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R1 1734350788 4465 2705 599 618 266476.87500 [4177] 8 nanometer
1 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Bitwise AND

To find the intersection between two lists, you would use sets and the & operator:

set([0, 1, 2]) &  set([2, 3, 4])
{2}

NeuronList work similarly:

nl[[0, 1]] & nl[[1, 2]]
<class 'navis.core.neuronlist.NeuronList'> containing 1 neurons (303.8KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units
0 navis.TreeNeuron DA1_lPN_R2 1734350908 4847 3042 735 761 304332.65625 [6] 8 nanometer

Multiplication and Division

So far, all operations have led to changes in the structure of the NeuronList. Multiplication and division are different! If you multiply/divide a TreeNeuron or NeuronList by a number, you will change the coordinates of nodes and connectors (including radii):

n = nl[0]
n.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
n2 = n / 1000
n2.nodes.head()
node_id label x y z radius parent_id type
0 1 0 15.784 37.250000 28.062000 0.010000 -1 root
1 2 0 15.764 37.230000 28.082001 0.018284 1 slab
2 3 0 15.744 37.189999 28.122000 0.034721 2 slab
3 4 0 15.744 37.150002 28.202000 0.034721 3 slab
4 5 0 15.704 37.130001 28.242001 0.034721 4 slab

Note that this also automatically adjusts the neuron’s units (if it has any):

print('Before:', n.units)
print('After:', n2.units)
Before: 8 nanometer
After: 8.0 micrometer

Comparing Neuron/Lists

NeuronList implements some of the basic arithmetic and comparison operators that you might know from standard lists or numpy.arrays. Most this should be fairly intuitive (I hope) but there are a few things you should be aware of. The following examples will illustrate that.

Comparisons

In Python the == operator compares two elements:

1 == 1
True
2 == 1
False

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

nl[0] == nl[0]
True
nl[0] == nl[1]
False

To find out which attributes are compared, check out:

navis.TreeNeuron.EQ_ATTRIBUTES
['n_nodes',
 'n_connectors',
 'soma',
 'root',
 'n_branches',
 'n_leafs',
 'cable_length',
 'name']

Edit this list to establish your own criteria for equality.

For NeuronList, we do the same comparison pairwise between the neurons in both neuronlists:

nl == nl
True
nl == nl[:2]
False

Because the comparison is done pairwise and in order, shuffling a NeuronList will result in a failed comparison:

nl == nl[[2, 1, 0]]
False

Comparisons are safe against copying but making any changes to the neurons will cause inequality:

nl[0] == nl[0].copy()
True
nl[0] == nl[0].downsample(2, inplace=False)
False

You can also ask if a neuron is in a given NeuronList:

nl[0] in nl
True
nl[0] in nl[1:]
False

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)}')
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)}')
Nodes in node table: 1770
Nodes in graph: 1770

If, however, you make changes to the neurons that do not use built-in functions there is a chance that navis won’t know 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]

print(f'\nNodes in node table after: {n.nodes.shape[0]}')
print(f'Nodes in graph after: {len(n.graph.nodes)}')
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 generates and checks hash values for neurons to detect changes and because here the node table is the master. It would not work the other way around (i.e. changing the graph to change 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:

navis.make_dotprops(x[, k, resample, threshold])

Produce dotprops from neurons or x/y/z points.

navis.skeletonize(x, **kwargs)

Turn neuron into skeleton.

navis.mesh(x, **kwargs)

Generate mesh from object(s).

navis.voxelize(x, pitch[, bounds, counts, ...])

Turn neuron into voxels.

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(.5))

Inspect the results:

# Co-visualize the mesh and the skeleton
fig = navis.plot3d([m, s], color=[(1, 1, 1, .2), 'r'])