Skip to content

Working with Annotations

AI-Generated Documentation

This documentation was generated with assistance from AI. While we strive for accuracy, errors may be present. If you find issues, unclear explanations, or have suggestions for improvement, please report them on GitHub.

Annotations are sparse point cloud layers that represent specific features or events in cellular morphology. Unlike the main morphological layers (mesh, graph, skeleton), annotations are designed for discrete features like synapses, spines, or markers that occur at specific locations.

Shared Features

Annotations inherit many common features from the PointMixin class. For information about features, masking, transformations, spatial queries, and cross-layer mapping, see Shared Layer Features.

Annotations vs Layer features

Annotations are sparse point clouds representing discrete features. Layer features are arrays of values attached to every vertex in a layer. Use annotations for sparse features (synapses, spines) and features for properties of existing vertices (radius, compartment).

What are Annotations?

Annotations (PointCloudLayer in annotation context) contain: - Vertices: 3D coordinates of feature locations - features: Metadata about each annotation (confidence, type, size, etc.) - Linkage: Optional connections to morphological layers - Sparse nature: Represent discrete events, not continuous morphology

Inspecting Annotations

Quick Overview with describe()

The describe() method provides a comprehensive summary of annotation layers, showing vertex counts, features, and connections to other layers:

# Individual annotation layer
cell.annotations.synapses.describe()

Output:

# Cell: my_neuron
# Layer: synapses (PointCloudLayer)
├── 23 vertices
├── features: [synapse_type, confidence, size]
└── Links: skeleton → synapses

The output shows: - Cell context: Which cell these annotations belong to - Layer type: Confirms this is a PointCloudLayer (annotation) - Metrics: Vertex count (number of annotation points) - features: Available metadata columns for each annotation - Links: Connections to morphological layers (<-> = bidirectional, = unidirectional)

Annotation Manager Overview

You can inspect all annotations at once:

# All annotations in the cell  
cell.annotations.describe()

Output:

# Annotations (2)
├── synapses (PointCloudLayer)
│   ├── 23 vertices
│   ├── features: [synapse_type, confidence]
│   └── Links: skeleton → synapses
└── spines (PointCloudLayer)
    ├── 47 vertices
    ├── features: [spine_type, head_diameter]
    └── Links: skeleton → spines

This gives you a detailed overview of all annotation layers and their relationships to morphological layers.

Creating Annotations

Basic Point Annotations

import numpy as np
import ossify

# Create a cell with a skeleton
cell = ossify.Cell(name="annotated_cell")
skeleton_vertices = np.array([[0,0,0], [1,0,0], [2,0,0]])
skeleton_edges = np.array([[0,1], [1,2]])
cell.add_skeleton(vertices=skeleton_vertices, edges=skeleton_edges, root=0)

# Add synaptic sites as annotations
synapse_locations = np.array([
    [0.5, 0.1, 0.0],   # Near first edge
    [1.5, -0.1, 0.0],  # Near second edge
    [0.8, 0.2, 0.0],   # Between vertices
])

cell.add_point_annotations(
    name="synapses",
    vertices=synapse_locations,
    spatial_columns=["x", "y", "z"]
)

print(f"Added {len(cell.annotations.synapses.vertices)} synapses")
print(f"Available annotations: {cell.annotations.names}")

Annotations with Metadata

import pandas as pd

# Create annotation data with metadata
synapse_data = pd.DataFrame({
    'x': [0.5, 1.5, 0.8],
    'y': [0.1, -0.1, 0.2], 
    'z': [0.0, 0.0, 0.0],
    'confidence': [0.95, 0.87, 0.92],
    'synapse_type': ['excitatory', 'inhibitory', 'excitatory'],
    'size': [1.2, 0.8, 1.0],
    'partner_id': [12345, 67890, 11111]
})

cell.add_point_annotations(
    name="synapses_detailed",
    vertices=synapse_data,
    spatial_columns=['x', 'y', 'z']
)

# Access annotation metadata
synapses = cell.annotations.synapses_detailed
print(f"Confidence values: {synapses.get_feature('confidence')}")
print(f"Synapse types: {synapses.get_feature('synapse_type')}")

Multiple Annotation Types

# Add different types of annotations
# Presynaptic sites
pre_syn_locations = np.array([[0.2, 0.1, 0.0], [1.8, -0.1, 0.0]])
cell.add_point_annotations(
    name="pre_syn",
    vertices=pre_syn_locations,
    features={"strength": [0.8, 0.6]}
)

# Postsynaptic sites  
post_syn_locations = np.array([[0.7, 0.2, 0.0], [1.3, 0.1, 0.0]])
cell.add_point_annotations(
    name="post_syn", 
    vertices=post_syn_locations,
    features={"receptor_type": ["AMPA", "GABA"]}
)

# Dendritic spines
spine_locations = np.array([[0.3, 0.3, 0.0], [0.9, -0.2, 0.0], [1.7, 0.3, 0.0]])
cell.add_point_annotations(
    name="spines",
    vertices=spine_locations,
    features={
        "spine_type": ["mushroom", "thin", "stubby"],
        "volume": [0.05, 0.02, 0.03]
    }
)

print(f"All annotations: {cell.annotations.names}")

Linking Annotations to Morphological Layers

Manual Linkage Specification

from ossify import Link

# Create annotations with explicit linkage to skeleton
# Link by providing skeleton vertex IDs that annotations map to
mapping_to_skeleton = np.array([0, 1, 0])  # Synapses map to skeleton vertices 0, 1, 0

cell.add_point_annotations(
    name="linked_synapses",
    vertices=synapse_locations,
    linkage=Link(
        mapping=mapping_to_skeleton,
        target="skeleton",           # Target layer
        map_value_is_index=True     # Values are vertex indices
    ),
    features={"confidence": [0.9, 0.8, 0.7]}
)

Automatic Spatial Linkage

# Let ossify automatically find nearest skeleton vertices
cell.add_point_annotations(
    name="auto_linked_synapses",
    vertices=synapse_locations,
    linkage=Link(
        target="skeleton",
        distance_threshold=0.5      # Max distance to link
    ),
    vertices_from_linkage=True     # Use target layer coordinates
)

# The annotation coordinates will be set to the linked skeleton vertices
linked = cell.annotations.auto_linked_synapses
print(f"Linked annotation coordinates: {linked.vertices}")

Working with Annotation Data

Accessing Annotation Properties

synapses = cell.annotations.synapses_detailed

# Basic properties
print(f"Number of annotations: {synapses.n_vertices}")
print(f"Spatial columns: {synapses.spatial_columns}")
print(f"Available features: {synapses.feature_names}")

# Coordinates and data
coordinates = synapses.vertices           # Nx3 coordinate array
full_data = synapses.nodes               # DataFrame with coordinates + features
features_only = synapses.features            # DataFrame with just features

# Individual feature access
confidence = synapses.get_feature('confidence')
types = synapses.get_feature('synapse_type')

Filtering and Queries

# Filter by feature values
high_confidence = synapses.get_feature('confidence') > 0.9
high_conf_annotations = synapses.apply_mask(high_confidence, as_positional=True)

print(f"High confidence annotations: {high_conf_annotations.n_vertices}")

# Filter by type
excitatory_mask = synapses.get_feature('synapse_type') == 'excitatory'
excitatory_synapses = synapses.apply_mask(excitatory_mask, as_positional=True)

# Spatial filtering using KDTree
query_point = [1.0, 0.0, 0.0]
distances, indices = synapses.kdtree.query(query_point, k=2, distance_upper_bound=0.5)

# Get nearby annotations (excluding infinite distances)
nearby_mask = distances < np.inf
nearby_indices = indices[nearby_mask]
nearby_annotations = synapses.apply_mask(nearby_indices, as_positional=True)

Annotation Statistics

# Summary statistics by type
types = synapses.get_feature('synapse_type')
confidence = synapses.get_feature('confidence')

for syn_type in np.unique(types):
    type_mask = types == syn_type
    type_confidence = confidence[type_mask]
    print(f"{syn_type}: count={sum(type_mask)}, mean_confidence={np.mean(type_confidence):.3f}")

# Size distribution
sizes = synapses.get_feature('size')
print(f"Size range: {np.min(sizes):.2f} - {np.max(sizes):.2f}")
print(f"Mean size: {np.mean(sizes):.2f}")

Mapping Annotations to Morphological Layers

Aggregating to Layer Vertices

# Count annotations near each skeleton vertex
if cell.skeleton is not None:
    synapse_counts = cell.skeleton.map_annotations_to_feature(
        annotation="synapses_detailed",
        distance_threshold=0.3,     # Search radius
        agg="count",                # Count annotations
        chunk_size=1000,
        validate=False
    )

    # Add as skeleton feature
    cell.skeleton.add_feature(synapse_counts, name="synapse_count")

    # Aggregate by type
    type_counts = cell.skeleton.map_annotations_to_feature(
        annotation="synapses_detailed",
        distance_threshold=0.3,
        agg={
            "excitatory_count": ("synapse_type", lambda x: sum(x == 'excitatory')),
            "inhibitory_count": ("synapse_type", lambda x: sum(x == 'inhibitory')),
            "mean_confidence": ("confidence", "mean"),
            "total_strength": ("size", "sum")
        }
    )

    cell.skeleton.add_feature(type_counts)
    print(f"Skeleton now has features: {cell.skeleton.feature_names}")

Finding Annotations Linked to Specific Vertices

# Find annotations that map to specific skeleton vertices
target_vertices = cell.skeleton.vertex_index[:2]  # First two skeleton vertices

# Get annotations linked to these vertices
linked_annotation_indices = cell.annotations.synapses_detailed.map_index_to_layer(
    layer="skeleton",
    source_index=target_vertices,
    as_positional=False
)

print(f"Annotations linked to vertices {target_vertices}: {linked_annotation_indices}")

# Reverse mapping: find which skeleton vertex each annotation links to
skeleton_links = cell.annotations.synapses_detailed.map_index_to_layer(
    layer="skeleton",
    as_positional=False
)
print(f"Annotation-to-skeleton mapping: {skeleton_links}")

Annotation Management

Adding and Removing Annotations

# Add more annotations
additional_spines = np.array([[0.4, 0.4, 0.0], [1.6, -0.3, 0.0]])
cell.add_point_annotations(
    name="additional_spines",
    vertices=additional_spines,
    features={"spine_type": ["thin", "mushroom"]}
)

# Remove annotation layer
cell.remove_annotation("additional_spines")
print(f"Remaining annotations: {cell.annotations.names}")

# Clear all annotations
original_names = cell.annotations.names.copy()
for name in original_names:
    cell.remove_annotation(name)
print(f"Annotations after clearing: {cell.annotations.names}")

Copying and Transforming Annotations

# Recreate some annotations for demonstration
cell.add_point_annotations("test_synapses", vertices=synapse_locations)

# Copy annotation layer
synapses_copy = cell.annotations.test_synapses.copy()

# Transform annotation coordinates
def shift_up(vertices):
    return vertices + [0, 0, 1.0]  # Move up by 1 unit

transformed_synapses = cell.annotations.test_synapses.transform(
    shift_up, 
    inplace=False
)
print(f"Original Z coords: {cell.annotations.test_synapses.vertices[:, 2]}")
print(f"Transformed Z coords: {transformed_synapses.vertices[:, 2]}")

Advanced Annotation Analysis

Density Analysis

# Recreate detailed synapses for analysis
cell.add_point_annotations("synapses", vertices=synapse_data)

# Calculate annotation density along skeleton
if cell.skeleton is not None:
    # Density per unit length
    synapse_density = cell.skeleton.map_annotations_to_feature(
        annotation="synapses",
        distance_threshold=0.5,
        agg="density",              # Annotations per unit cable length
        chunk_size=1000
    )

    cell.skeleton.add_feature(synapse_density, name="synapse_density")

    # Total density
    total_cable = cell.skeleton.cable_length()
    total_synapses = len(cell.annotations.synapses.vertices)
    overall_density = total_synapses / total_cable
    print(f"Overall synapse density: {overall_density:.3f} synapses/unit")

Spatial Clustering

# Find clusters of annotations
from sklearn.cluster import DBSCAN

coordinates = cell.annotations.synapses.vertices
clustering = DBSCAN(eps=0.3, min_samples=2).fit(coordinates)
cluster_features = clustering.features_

# Add cluster information
cell.annotations.synapses.add_feature(cluster_features, name="cluster")

# Analyze clusters
n_clusters = len(set(cluster_features)) - (1 if -1 in cluster_features else 0)
n_noise = list(cluster_features).count(-1)
print(f"Number of clusters: {n_clusters}")
print(f"Number of noise points: {n_noise}")

Co-localization Analysis

# Analyze spatial relationship between different annotation types
if "pre_syn" in cell.annotations.names and "post_syn" in cell.annotations.names:
    pre_coords = cell.annotations.pre_syn.vertices
    post_coords = cell.annotations.post_syn.vertices

    # Find nearest post-synaptic site for each pre-synaptic site
    from scipy.spatial.distance import cdist
    distances = cdist(pre_coords, post_coords)
    nearest_post = np.argmin(distances, axis=1)
    min_distances = np.min(distances, axis=1)

    # Add co-localization info
    cell.annotations.pre_syn.add_feature(nearest_post, name="nearest_post_idx")
    cell.annotations.pre_syn.add_feature(min_distances, name="distance_to_post")

    print(f"Mean pre-post distance: {np.mean(min_distances):.3f}")

Key Annotation Methods

Annotation Creation

  • cell.add_point_annotations(name, vertices, spatial_columns=None, features=None, linkage=None, vertices_from_linkage=False) - Add annotation layer

Annotation Management

  • cell.remove_annotation(name) - Remove annotation layer
  • cell.annotations.names - List of annotation names
  • cell.annotations[name] - Access specific annotation

Annotation Properties

  • annotation.n_vertices - Number of annotations
  • annotation.vertices - Coordinate array
  • annotation.nodes - DataFrame with coordinates and features
  • annotation.features - DataFrame with just features
  • annotation.feature_names - Available feature names

Data Access and Analysis

  • annotation.get_feature(key) - Get feature values
  • annotation.add_feature(feature, name=None) - Add new features
  • annotation.apply_mask(mask, as_positional=False) - Filter annotations
  • annotation.kdtree - Spatial queries

Cross-layer Integration

  • layer.map_annotations_to_feature(annotation, distance_threshold, agg="count"/"density", ...) - Aggregate to layer vertices
  • annotation.map_index_to_layer(layer, source_index=None, as_positional=False) - Find layer mappings

Linkage

  • Link(mapping, target, map_value_is_index=True) - Explicit linkage
  • Link(target, distance_threshold) - Automatic spatial linkage

Additional Features

For comprehensive information about vertex access, transformations, spatial queries, and other shared functionality, see Shared Layer Features.

When to Use Annotations

  • Sparse features like synapses, spines, or markers
  • Event locations that don't correspond to morphological vertices
  • Features with rich metadata (confidence, type, size, etc.)
  • Analysis requiring spatial aggregation to morphological layers
  • Co-localization analysis between different feature types