Features and Data on Layers
Every layer in ossify — mesh, graph, skeleton, or annotation — carries vertex-level features: arrays of data attached to each vertex. Features are how you store and access properties like radius, compartment labels, volume, quality scores, and anything else you compute or import. This page covers the operations that work the same way across all layer types.
Setup
The snippets below use a small cell with a skeleton, a mesh and a graph, all linked together so the cross-layer examples have somewhere to map to:
import numpy as np
import ossify
skeleton_vertices = np.array([[0.0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0]])
skeleton_edges = np.array([[1, 0], [2, 1], [3, 2]])
cell = ossify.Cell(name="layer_features_example")
cell.add_skeleton(
vertices=skeleton_vertices,
edges=skeleton_edges,
spatial_columns=["x", "y", "z"],
root=0,
)
# Two mesh vertices per skeleton vertex.
mesh_vertices = np.repeat(skeleton_vertices, 2, axis=0)
mesh_vertices[:, 1] += 0.1
cell.add_mesh(
vertices=mesh_vertices,
faces=np.array([[0, 1, 2], [2, 3, 4], [4, 5, 6]]),
spatial_columns=["x", "y", "z"],
linkage=ossify.Link(mapping=np.repeat(np.arange(4), 2), target="skeleton"),
)
# A graph layer, also linked to the skeleton.
cell.add_graph(
vertices=skeleton_vertices,
edges=skeleton_edges,
spatial_columns=["x", "y", "z"],
linkage=ossify.Link(mapping=np.arange(4), target="skeleton"),
)
Common Properties
Every layer has these basic properties:
# Assuming you have a layer (mesh, graph, skeleton, or point cloud)
layer = cell.skeleton # or cell.mesh, cell.graph, etc.
# Basic properties
print(f"Layer name: {layer.name}")
print(f"Number of vertices: {layer.n_vertices}")
print(f"Bounding box: {layer.bbox}")
print(f"Spatial columns: {layer.spatial_columns}")
print(f"Available features: {layer.feature_names}")
Vertex Data Access
All layers provide consistent access to vertex data:
# Vertex coordinates
vertices_array = layer.vertices # Nx3 numpy array
vertices_df = layer.vertex_df # Pandas DataFrame with index
vertex_indices = layer.vertex_index # Array of vertex indices
vertex_index_map = layer.vertex_index_map # Dict mapping indices to positions
# Complete node data (coordinates + features)
full_data = layer.nodes # DataFrame with all data
features_only = layer.features # DataFrame with just features
Working with features
All layers support adding and accessing vertex features:
# Add features using arrays
quality_values = np.array([0.9, 0.8, 0.7, 0.6])
layer.add_feature(quality_values, name="quality")
# Add multiple features at once
new_features = {
"region": [0, 0, 1, 1],
"processed": [True, True, False, False]
}
layer.add_feature(new_features)
# Access feature data
quality = layer.get_feature("quality")
all_features = layer.features
print(f"Available features: {layer.feature_names}")
Spatial Queries
All layers provide KDTree functionality for spatial searches:
# Get KDTree for spatial queries
kdtree = layer.kdtree
# Find nearest vertices to a point
query_point = [0.5, 0.5, 0.5]
distances, indices = kdtree.query(query_point, k=2) # 2 nearest
# Get vertex indices (not positional indices)
nearest_vertex_ids = layer.vertex_index[indices]
print(f"Nearest vertices: {nearest_vertex_ids}")
print(f"Distances: {distances}")
Copying and Transformations
All layers support copying and spatial transformations:
# Create a deep copy
layer_copy = layer.copy()
# Apply spatial transformation using a function
def scale_and_translate(vertices):
scaled = vertices * 2.0 # Scale by 2
translated = scaled + [10, 0, 0] # Translate
return translated
# Transform (returns new layer)
transformed_layer = layer.transform(scale_and_translate, inplace=False)
# Transform in place
layer.transform(scale_and_translate, inplace=True)
# An array argument is replacement coordinates, of the same shape as the
# layer's vertices -- not an affine matrix. Express an affine as a callable.
layer.transform(lambda vertices: vertices + np.array([5.0, 0.0, 0.0]), inplace=True)
Masking and Filtering
All layers support masking to create filtered subsets:
# Create boolean mask
mask = layer.get_feature("quality") > 0.7
# Apply mask (creates new layer)
filtered_layer = layer.apply_mask(mask, as_positional=False, return_cell=False)
# Use as context manager for temporary filtering
# Masking a layer that belongs to a Cell yields a Cell, so reach through it.
with layer.mask_context(mask, return_cell=False) as filtered_layer:
# Work with filtered data
result = filtered_layer.n_vertices
# Original layer unchanged
# Mask using vertex indices instead of boolean
vertex_indices = layer.vertex_index[:10] # First 10 vertices
subset_layer = layer.apply_mask(
vertex_indices, as_positional=False, return_cell=False
)
# Mask using positional indices
positional_mask = np.array([0, 1, 3]) # Specific positions
subset_layer = layer.apply_mask(
positional_mask, as_positional=True, return_cell=False
)
Cross-Layer Mapping
When layers are part of a cell, you can map data between them:
Index Mapping
# Map indices one-to-one between layers
target_indices = layer.map_index_to_layer(
layer="skeleton", # Target layer
source_index=None, # None = all vertices
as_positional=False, # Use vertex indices
validate=False # Check for ambiguous mappings
)
# Map a region to all corresponding vertices
all_target_indices = layer.map_region_to_layer(
layer="mesh",
source_index=layer.vertex_index[:5], # First 5 vertices
as_positional=False
)
# Map each vertex to lists of all corresponding vertices
mapping_dict = layer.map_index_to_layer_region(
layer="graph",
source_index=layer.vertex_index[:3],
as_positional=False
)
print(f"Vertex mappings: {mapping_dict}")
feature Mapping
# Map features from one layer to another
mapped_features = layer.map_features_to_layer(
features="quality", # feature to map
layer="skeleton", # Target layer
agg="mean" # Aggregation method
)
# Map multiple features with different aggregations
mapped_features = layer.map_features_to_layer(
features=["quality", "region"],
layer="mesh",
agg={
"quality": "mean",
"region": "majority" # Custom aggregation function
}
)
# Use the mapping
# The mapped columns keep their source names, so rename them if the target
# layer already carries features by those names.
target_layer = cell.layers["mesh"]
target_layer.add_feature(mapped_features.add_suffix("_from_skeleton"))
Mask Mapping
# Map a boolean mask to another layer
source_mask = layer.get_feature("processed") == True
target_mask = layer.map_mask_to_layer("mesh", source_mask)
# Apply the mapped mask to target layer
target_layer = cell.layers["mesh"]
filtered_target = target_layer.apply_mask(
target_mask, as_positional=True, return_cell=False
)
Vertex Index vs Positional Index
Understanding the difference between vertex indices and positional indices is crucial:
# Vertex indices: The actual IDs/features of vertices
vertex_ids = layer.vertex_index # e.g., [100, 205, 350, 401]
# Positional indices: Array positions (0, 1, 2, 3...)
# Used for indexing into arrays like layer.vertices
# Convert between them
vertex_index_map = layer.vertex_index_map # Dict: {100: 0, 205: 1, 350: 2, 401: 3}
# Most functions have as_positional parameter
distances = layer.distance_to_root(
vertices=layer.vertex_index[:2], # Vertex indices
as_positional=False # Specify we're using vertex indices
)
distances = layer.distance_to_root(
vertices=[0, 1], # Positional indices
as_positional=True # Specify we're using positions
)
Finding Unmapped Vertices
Find vertices that don't have mappings to other layers:
# Find vertices with no mapping to any other layer
unmapped = layer.get_unmapped_vertices()
# Find vertices with no mapping to specific layers
unmapped_to_mesh = layer.get_unmapped_vertices(target_layers="mesh")
# A layer cannot be its own mapping target, so name the others.
unmapped_to_multiple = layer.get_unmapped_vertices(
target_layers=["mesh", "graph"]
)
# Remove unmapped vertices
clean_layer = layer.mask_out_unmapped(target_layers="mesh", return_cell=False)
# Remove vertices unmapped to any other layer
fully_clean = layer.mask_out_unmapped(return_cell=False) # all other layers
Key Shared Methods Reference
Basic Properties
layer.name- Layer namelayer.n_vertices- Number of verticeslayer.bbox- Bounding boxlayer.spatial_columns- List of coordinate column nameslayer.feature_names- List of available features
Data Access
layer.vertices- Vertex coordinates as numpy arraylayer.vertex_df- Vertices as indexed DataFramelayer.vertex_index- Array of vertex indiceslayer.vertex_index_map- Dict mapping indices to positionslayer.nodes- Complete DataFrame with coordinates and featureslayer.features- DataFrame of all features
features
layer.add_feature(feature, name=None)- Add vertex featureslayer.get_feature(key)- Get feature array
Spatial Operations
layer.kdtree- KDTree for spatial querieslayer.transform(transform, inplace=False)- Apply transformationlayer.copy()- Create deep copy
Masking
layer.apply_mask(mask, as_positional=False, self_only=False)- Create masked subsetlayer.mask_context(mask)- Temporary masking context managerlayer.get_unmapped_vertices(target_layers=None)- Find unmapped verticeslayer.mask_out_unmapped(target_layers=None, self_only=False)- Remove unmapped vertices
Cross-layer Mapping
layer.map_index_to_layer(layer, source_index=None, as_positional=False, validate=False)- Map indices 1:1layer.map_region_to_layer(layer, source_index=None, as_positional=False)- Map region to regionlayer.map_index_to_layer_region(layer, source_index=None, as_positional=False)- Map to listslayer.map_features_to_layer(features, layer, agg="mean")- Map features between layerslayer.map_mask_to_layer(layer, mask)- Map boolean mask