Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Masking #172

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft

Masking #172

wants to merge 6 commits into from

Conversation

schlegelp
Copy link
Collaborator

@schlegelp schlegelp commented Oct 24, 2024

This PR adds an interface for masking neurons for analyses/processing:

  • masking/unmasking for:
    • skeleton
    • dotprops
    • meshes
    • voxels (not sure this is strictly necessary)
  • NeuronMask context manager
  • update API documentation
  • add tutorials
  • a generalizable solution for destructive operations (e.g. prune twigs)

The interface currently looks like this:

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

# Generate a mask
n.mask(mask=n.nodes.y > 25000)
# ... do your thing here 
n.unmask(reset=True)

# Alternatively:
with navis.NeuronMask(n, mask=n.nodes.y > 25000):
   # ... do your thing here

A few additional notes:

  • mask can be a boolean array, a neuron property (e.g. a column in the node table), or a function that takes a neuron and returns one of the first two
  • in my head, the NeuronMask context manager is the primary interface but people can also mess around with .mask() + .unmask() if they need more control
  • Neurons also have a .apply_mask() method that makes the mask permanent.
  • under the hood we copy the data when masking a neuron but that can be switched off if a user knows the data won't be modified

Modifying masked neurons

By default, unmasking will reset the neuron to its original state. Users can use e.g. .unmask(reset=False) or NeuronMask(..., reset=False) to instead re-combine the masked and unmasked data. That works well for non-destructive operations such as e.g. smoothing or downsampling where the boundary vertices/nodes are not modified:

n = navis.example_neurons(1, kind='skeleton')
# Label axon and dendrite
navis.split_axon_dendrite(n, label_only=True)
# Downsample only the axon 
with navis.NeuronMask(n, mask=lambda x: x.nodes.compartment == 'axon', reset_neurons=False):
   navis.downsample_neuron(n, 5, inplace=True)
Screenshot 2024-10-24 at 13 18 57

Destructive operations (e.g. navis.prune_twigs) are more tricky and I think we may end up having to adjust those functions to deal with masked neurons as special cases. To give you an example: skeletonization of neurons often leaves ugly spikes where the soma is. These could be removed by masking the neuron to within a given radius of the soma and removing all twigs below a given threshold. However, with the current implementation the actual neurites coming off the soma would also look awfully like twigs and might incorrectly get pruned off - which in turn will break the connectivity of the skeleton. Perfectly solvable problem but I'm still thinking about a generalizable approach that would minimize maintenance burden.

Comments/thoughts, @ceesem @bdpedigo?

- add mask/unmask and apply_mask methods to TreeNeuron, MeshNeuron and Dotprops
- add is_masked property for all neurons
- add `navis.NeuronMask` class
- add __length__ to all neurons
- dotprops: clear `_tree` with temporary attributes
- fix NeuronMask doctest
- TreeNeuron.un/mask: make sure to re-classify
- TreeNeuron.unmask: fix re-connecting
@bdpedigo
Copy link
Contributor

hi @schlegelp - this looks really cool and I'm sure will be useful to have in Navis!

I've also been working on generalizing code for doing these kinds of masking operations to arbitrary collections of morphology representations that all have some kind of spatial connection to each other: https://bdpedigo.github.io/morphlink/examples/microns_example/#masking-and-filtering still mostly at the stage of me trying to get feedback on an interface that feels good.

More and more I am finding places where I have a collection of arbitrary representations, say:

  • a mesh
  • a downsampled mesh
  • a skeleton
  • pre/post synapses
  • SegCLR embeddings - xyz points with a vector attached
  • ...whatever I feel like on a given day

Since I keep ending up with these arbitrary collections of layers, I am trying to divorce the code that dictates how to map between layers and transfer things like masks between them from the code that does other computations on whatever representation I have.

Anyway - I don't think helpful here since it looks like you already have it working, but would be interested in seeing whether we could make the kinds of masking operations like you have here and are in meshparty more general. Hopefully that could also be a framework for building more specific neuron classes with specific features on top

@schlegelp
Copy link
Collaborator Author

Cool! I saw you making the morphlink repo. I definitely want to take it for a spin and see if I can use it in navis. The current syncing between mesh and skeleton works but it would be nice if I could let morphlink do some of the heavy lifting.

FYI: unfortunately, there already is a MorphLink package on PyPI :/

- add .view() methods to neuron objects
- by default, do not copy data
- bits and pieces
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants