Skip to content

Neuroglancer Export

This guide shows how to export muDM data to the Neuroglancer precomputed format and how to serve it to an external Neuroglancer viewer.

There are two distinct paths, and choosing the right one matters:

  1. Python writer subsystem (mudm_tools.neuroglancer) — a toolkit of small writers for skeletons, annotations (Point / LineString), legacy single-resolution meshes, and segment properties, plus helpers to build a Neuroglancer viewer state and a shareable URL. Use this for neuron morphologies (SWC skeletons) and point/line annotations.
  2. Scalable Rust mesh path (mudm_tools._rs.StreamingTileGenerator) — generate_neuroglancer (legacy single-res mesh) and generate_neuroglancer_multilod (multi-LOD Draco, with optional sharding). Use this for large mesh corpora that need level-of-detail and object-store-scale deployment. See the 3D Tiling guide for how to feed the generator.

The bundled viewers do NOT render Neuroglancer

muDM ships its own 2D and 3D viewers, but they do not render Neuroglancer precomputed data. Neuroglancer export targets an external Neuroglancer client (the public demo at https://neuroglancer-demo.appspot.com, or your own instance). mudm-serve only serves the files under a /neuroglancer/ route; the rendering happens in the external Neuroglancer app.

Package names

The writers live in mudm_tools.neuroglancer. The compiled mesh generator is mudm_tools._rs.StreamingTileGenerator (never mudm._rs). Geometry types (MuDMFeature, MuDMFeatureCollection) come from the sibling core package mudm (mudm.model).


Path 1: The Python writer subsystem

Quick start: SWC neurons to precomputed skeletons

The fastest way to see something is the bundled swc_to_neuroglancer example. It converts one or more SWC files into a precomputed skeleton source, attaches segment properties, prints a ready-to-open Neuroglancer URL, and serves the files over a CORS-enabled HTTP server.

# Convert + serve (CORS server on :9000)
uv run python -m mudm_tools.examples.swc_to_neuroglancer neuron.swc

# Several neurons at once
uv run python -m mudm_tools.examples.swc_to_neuroglancer n1.swc n2.swc n3.swc

# Export only, no server
uv run python -m mudm_tools.examples.swc_to_neuroglancer neuron.swc --no-serve

# Custom output dir + port
uv run python -m mudm_tools.examples.swc_to_neuroglancer neuron.swc -o out -p 8080
Flag Default Meaning
--output-dir, -o neuroglancer_output Root output directory
--port, -p 9000 Port for the CORS HTTP server
--no-serve off Export only, do not start the server

The example writes skeletons to <output-dir>/skeletons/, prints the viewer URL, then serves on http://localhost:<port>. Copy the printed URL into your browser.

Where to get SWC files

NeuroMorpho.Org hosts 200,000+ reconstructions. Search and download .swc, then point the example at the file.

Writing skeletons directly

write_skeleton writes one precomputed skeleton segment. Call it repeatedly with distinct segment_ids into the same directory to build a multi-skeleton source — the info file is rewritten on each call.

from pathlib import Path

from mudm_tools.swc import _parse_swc
from mudm_tools.neuroglancer import write_skeleton

morphology = _parse_swc("neuron.swc")  # mudm_tools.swc.NeuronMorphology

write_skeleton(Path("out/skeletons"), segment_id=1, morphology=morphology)

The signature:

def write_skeleton(
    output_dir: str | Path,
    segment_id: int,
    morphology: NeuronMorphology,
    *,
    transform: Optional[AffineTransform] = None,
    include_radius: bool = True,
    include_type: bool = True,
    segment_properties: Optional[str] = None,
) -> Path
Parameter Default Notes
output_dir Created with parents
segment_id Binary written to a file named str(segment_id)
morphology A mudm_tools.swc.NeuronMorphology
transform None A mudm.transforms.AffineTransform, embedded as a 12-float row-major upper 3×4
include_radius True Emit a per-vertex radius attribute
include_type True Emit a per-vertex SWC type attribute
segment_properties None Relative path to a segment_properties dir to reference in info

Edges are derived from each sample's parent (parent index → child index).

Unit scaling

write_skeleton builds its info without the micrometre→nanometre transform. If your coordinates are in µm and you want Neuroglancer to treat them as nm, pass an explicit transform, or build the info yourself with build_skeleton_info(scale_um_to_nm=True) and write it after your skeletons (see the SWC example, which rewrites info to attach segment_properties).

Segment properties

write_segment_properties writes a neuroglancer_segment_properties source from each feature's .properties dict. Numeric columns become type number (uint32 if all values are ints, else float32); everything else becomes a label. Values are ordered to match segment_ids; missing values become an empty string.

from mudm_tools.neuroglancer.properties_writer import write_segment_properties

write_segment_properties(
    "out/skeletons/seg_props",
    features=features,         # Sequence[MuDMFeature]
    segment_ids=[1, 2, 3],     # one id per feature
)

Use features_to_segment_properties(features, segment_ids) if you want the info dict without writing it.

To wire the property source into the skeleton info, rebuild and rewrite info with a relative path:

import json
from mudm_tools.neuroglancer.skeleton_writer import build_skeleton_info

info = build_skeleton_info(segment_properties="seg_props")
(skel_dir / "info").write_text(json.dumps(info.to_info_dict(), indent=2))

Annotations: to_neuroglancer and write_annotations

to_neuroglancer is NOT a one-call full exporter

to_neuroglancer only handles Point features (→ point_annotations/ subdir) and LineString features (→ line_annotations/ subdir). It does not export skeletons or meshes — for those, call write_skeleton or write_mesh directly. Features whose geometry is neither Point nor LineString are silently ignored.

from mudm_tools.neuroglancer import to_neuroglancer

result = to_neuroglancer(collection, "out/mixed")
# result["paths"] -> {"point_annotations": <dir>, "line_annotations": <dir>}

Signature:

def to_neuroglancer(
    data: Union[MuDMFeature, MuDMFeatureCollection],
    output_dir: str | Path,
    *,
    base_url: Optional[str] = None,
) -> dict[str, Any]

to_neuroglancer always returns {"paths": {layer_name: dir}}. If you pass base_url and at least one annotation layer was written, it also returns "viewer_state" (a Neuroglancer state dict) and "viewer_url" (an encoded URL).

Two different base_urls

to_neuroglancer's base_url is the precomputed data source base. Annotation layers are added as precomputed://{base_url}/point_annotations and precomputed://{base_url}/line_annotations. This is distinct from viewer_state_to_url's base_url, which is the Neuroglancer viewer instance (default https://neuroglancer-demo.appspot.com).

For finer control, call write_annotations directly:

def write_annotations(
    output_dir: str | Path,
    features: Sequence[MuDMFeature],
    annotation_type: Literal["point", "line"],
) -> Path

This produces {output_dir}/info (@type neuroglancer_annotations_v1), an empty {output_dir}/by_id/ dir, and a single spatial chunk {output_dir}/spatial0/0_0_0.

Line annotation semantics

For annotation_type="line", each consecutive pair of LineString coordinates becomes one LINE annotation — an N-vertex line yields N−1 segments, all sharing the feature's enumerate() index (0-based) as their annotation id. Annotation ids are the feature index, not a property. Missing z defaults to 0.0.

A complete in-memory example (adapted from neuroglancer_export.py):

from pathlib import Path
from mudm.model import MuDMFeature, MuDMFeatureCollection
from mudm_tools.neuroglancer import to_neuroglancer, write_skeleton

out = Path("out/mixed")

# Skeletons are written directly (NOT via to_neuroglancer)
write_skeleton(out / "skeletons", segment_id=1, morphology=neuron_a)
write_skeleton(out / "skeletons", segment_id=2, morphology=neuron_b)

collection = MuDMFeatureCollection(
    type="FeatureCollection",
    features=[
        MuDMFeature(
            type="Feature",
            geometry={"type": "Point", "coordinates": [500, 500, 200]},
            properties={"label": "pyramidal_soma"},
        ),
        MuDMFeature(
            type="Feature",
            geometry={
                "type": "LineString",
                "coordinates": [[500, 500, 200], [650, 550, 200], [800, 600, 200]],
            },
            properties={"label": "connecting_fiber"},
        ),
    ],
)

result = to_neuroglancer(collection, out)
# -> writes out/point_annotations/ and out/line_annotations/

Run all three patterns from the bundled example:

uv run python -m mudm_tools.examples.neuroglancer_export --output-dir neuroglancer_output
Flag Default Meaning
--output-dir neuroglancer_output Root output directory

Legacy single-resolution meshes

The Python mesh writer emits the legacy single-LOD mesh format (@type neuroglancer_legacy_mesh). Write the info once with write_mesh_info, then one segment at a time with write_mesh.

import numpy as np
from mudm_tools.neuroglancer import write_mesh, write_mesh_info

write_mesh_info("out/meshes", segment_properties="segment_properties")
write_mesh("out/meshes", segment_id=1, vertices=verts, indices=faces)
def write_mesh_info(output_dir: str | Path, segment_properties: Optional[str] = None) -> Path
def write_mesh(output_dir: str | Path, segment_id: int, vertices: np.ndarray, indices: np.ndarray) -> Path

write_mesh writes {output_dir}/{segment_id} (binary, via mesh_to_binary) plus {output_dir}/{segment_id}:0 (a JSON fragment manifest {"fragments": ["{segment_id}"]}). It does not write info — call write_mesh_info separately. vertices are Nx3 float32 and indices are Mx3 uint32.

Choosing a mesh path

write_mesh is the legacy single-resolution path. For large meshes that need genuine level-of-detail, use the scalable Rust generate_neuroglancer_multilod instead. Do not conflate the two — they emit different @types and different on-disk layouts.

Low-level binary codecs are also exported: mesh_to_binary(vertices, indices) -> bytes and its inverse decode_mesh_binary(data) -> (vertices, indices). For point/line annotation bytes, see points_to_annotation_binary and lines_to_annotation_binary.

Building a viewer state and URL

The viewer-state helpers in mudm_tools.neuroglancer.state build a plain JSON state dict and a shareable fragment URL using only json + urllib.parse. They do not depend on the official neuroglancer Python package and do not run a viewer server.

from mudm_tools.neuroglancer.state import (
    build_skeleton_layer,
    build_annotation_layer,
    build_viewer_state,
    viewer_state_to_url,
)

layer = build_skeleton_layer(
    "neurons",
    "precomputed://http://localhost:9000/skeletons",
)
layer["segments"] = ["1", "2"]  # pre-select segments so they render immediately

state = build_viewer_state([layer], position=[650.0, 550.0, 200.0])
url = viewer_state_to_url(state)
print(url)  # https://neuroglancer-demo.appspot.com/#!<encoded-state>
Function Signature Purpose
build_skeleton_layer (name, source_url, *, use_radius=True) A segmentation layer for a skeleton source. With use_radius=True, attaches a GLSL skeletonRendering shader mapping the radius attribute to line width.
build_annotation_layer (name, source_url) An annotation layer pointing at a precomputed:// annotation source.
build_viewer_state (layers, position=None, projection_scale=None, layout="3d") A full viewer-state dict. position sets the camera, projection_scale the zoomFactor, layout defaults to "3d" (use "4panel" for all views).
viewer_state_to_url (state, base_url="https://neuroglancer-demo.appspot.com") Encodes the state as {base_url}/#!{encoded}.

Pre-selecting segments

build_skeleton_layer returns a plain dict; callers commonly set layer["segments"] = [...] afterward so the listed segments are visible on load.

Python writer API reference

to_neuroglancer

to_neuroglancer(
    data: Union[MuDMFeature, MuDMFeatureCollection],
    output_dir: str | Path,
    *,
    base_url: Optional[str] = None,
) -> dict[str, Any]

Export MuDM data to Neuroglancer precomputed format.

Auto-dispatches based on geometry type: - Point -> point annotations - LineString -> line annotations

For skeleton export, call write_skeleton() directly.

Parameters:

Name Type Description Default
data Union[MuDMFeature, MuDMFeatureCollection]

A MuDMFeature or MuDMFeatureCollection.

required
output_dir str | Path

Root output directory.

required
base_url Optional[str]

Optional Neuroglancer instance URL for viewer state.

None

Returns:

Type Description
dict[str, Any]

Dict with keys: paths (written dirs), viewer_state (if base_url).

Source code in src/mudm_tools/neuroglancer/writer.py
def to_neuroglancer(
    data: Union[MuDMFeature, MuDMFeatureCollection],
    output_dir: str | Path,
    *,
    base_url: Optional[str] = None,
) -> dict[str, Any]:
    """Export MuDM data to Neuroglancer precomputed format.

    Auto-dispatches based on geometry type:
    - Point -> point annotations
    - LineString -> line annotations

    For skeleton export, call ``write_skeleton()`` directly.

    Args:
        data: A MuDMFeature or MuDMFeatureCollection.
        output_dir: Root output directory.
        base_url: Optional Neuroglancer instance URL for viewer state.

    Returns:
        Dict with keys: ``paths`` (written dirs), ``viewer_state`` (if base_url).
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    if isinstance(data, MuDMFeatureCollection):
        features = list(data.features)
    else:
        features = [data]

    point_features: list[MuDMFeature] = []
    line_features: list[MuDMFeature] = []

    for feat in features:
        geom = feat.geometry
        if isinstance(geom, Point):
            point_features.append(feat)
        elif isinstance(geom, LineString):
            line_features.append(feat)

    paths: dict[str, str] = {}
    layers: list[dict[str, Any]] = []

    if point_features:
        ann_dir = out / "point_annotations"
        write_annotations(ann_dir, point_features, "point")
        paths["point_annotations"] = str(ann_dir)
        if base_url:
            layers.append(
                build_annotation_layer(
                    "point_annotations",
                    f"precomputed://{base_url}/point_annotations",
                )
            )

    if line_features:
        ann_dir = out / "line_annotations"
        write_annotations(ann_dir, line_features, "line")
        paths["line_annotations"] = str(ann_dir)
        if base_url:
            layers.append(
                build_annotation_layer(
                    "line_annotations",
                    f"precomputed://{base_url}/line_annotations",
                )
            )

    result: dict[str, Any] = {"paths": paths}

    if base_url and layers:
        state = build_viewer_state(layers)
        result["viewer_state"] = state
        result["viewer_url"] = viewer_state_to_url(state, base_url)

    return result

write_annotations

write_annotations(
    output_dir: str | Path,
    features: Sequence[MuDMFeature],
    annotation_type: Literal["point", "line"],
) -> Path

Write annotations to Neuroglancer precomputed directory.

Creates

{output_dir}/info — JSON info file {output_dir}/by_id/ — empty dir (required by Neuroglancer) {output_dir}/spatial0/0_0_0 — binary annotation data

Parameters:

Name Type Description Default
output_dir str | Path

Directory to write to.

required
features Sequence[MuDMFeature]

MuDM features with Point or LineString geometry.

required
annotation_type Literal['point', 'line']

"point" or "line".

required

Returns:

Type Description
Path

Path to the output directory.

Source code in src/mudm_tools/neuroglancer/annotation_writer.py
def write_annotations(
    output_dir: str | Path,
    features: Sequence[MuDMFeature],
    annotation_type: Literal["point", "line"],
) -> Path:
    """Write annotations to Neuroglancer precomputed directory.

    Creates:
        {output_dir}/info                — JSON info file
        {output_dir}/by_id/              — empty dir (required by Neuroglancer)
        {output_dir}/spatial0/0_0_0      — binary annotation data

    Args:
        output_dir: Directory to write to.
        features: MuDM features with Point or LineString geometry.
        annotation_type: "point" or "line".

    Returns:
        Path to the output directory.
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    lower, upper = _compute_bounds(features, annotation_type)

    # Chunk size = full extent
    extent = [high - low if high > low else 1.0 for low, high in zip(lower, upper)]

    # Build info — Neuroglancer expects UPPERCASE annotation_type
    ng_type = _NG_ANNOTATION_TYPE[annotation_type]
    spatial = AnnotationSpatialEntry(
        chunk_size=extent,
        grid_shape=[1, 1, 1],
        key="spatial0",
    )
    info = AnnotationInfo(
        annotation_type=ng_type,  # type: ignore[arg-type]
        lower_bound=lower,
        upper_bound=upper,
        spatial=[spatial],
    )
    (out / "info").write_text(json.dumps(info.to_info_dict(), indent=2))

    # Create by_id directory (Neuroglancer checks for it)
    (out / "by_id").mkdir(parents=True, exist_ok=True)

    # Build binary
    if annotation_type == "point":
        points: list[Tuple[float, float, float]] = []
        ids: list[int] = []
        for i, feat in enumerate(features):
            if isinstance(feat.geometry, Point):
                c = feat.geometry.coordinates
                z = float(c[2]) if len(c) > 2 else 0.0
                points.append((float(c[0]), float(c[1]), z))
                ids.append(i)
        binary = points_to_annotation_binary(points, ids)
    else:  # line
        line_data: list[Tuple[float, float, float, float, float, float]] = []
        ids = []
        for i, feat in enumerate(features):
            if isinstance(feat.geometry, LineString):
                coords = feat.geometry.coordinates
                # Each consecutive pair of coordinates forms a line segment
                for j in range(len(coords) - 1):
                    c0, c1 = coords[j], coords[j + 1]
                    z0 = float(c0[2]) if len(c0) > 2 else 0.0
                    z1 = float(c1[2]) if len(c1) > 2 else 0.0
                    line_data.append(
                        (
                            float(c0[0]),
                            float(c0[1]),
                            z0,
                            float(c1[0]),
                            float(c1[1]),
                            z1,
                        )
                    )
                    ids.append(i)
        binary = lines_to_annotation_binary(line_data, ids)

    # Write spatial chunk
    spatial_dir = out / "spatial0"
    spatial_dir.mkdir(parents=True, exist_ok=True)
    (spatial_dir / "0_0_0").write_bytes(binary)

    return out

write_skeleton

write_skeleton(
    output_dir: str | Path,
    segment_id: int,
    morphology: NeuronMorphology,
    *,
    transform: Optional[AffineTransform] = None,
    include_radius: bool = True,
    include_type: bool = True,
    segment_properties: Optional[str] = None,
) -> Path

Write a single skeleton segment to the precomputed directory.

Creates

{output_dir}/info — JSON info file {output_dir}/{segment_id} — binary skeleton data

Parameters:

Name Type Description Default
output_dir str | Path

Directory to write to (created if needed).

required
segment_id int

Numeric ID for this segment.

required
morphology NeuronMorphology

The neuron morphology data.

required
transform Optional[AffineTransform]

Optional affine transform.

None
include_radius bool

Include radius attribute.

True
include_type bool

Include type attribute.

True
segment_properties Optional[str]

Optional path to segment_properties.

None

Returns:

Type Description
Path

Path to the output directory.

Source code in src/mudm_tools/neuroglancer/skeleton_writer.py
def write_skeleton(
    output_dir: str | Path,
    segment_id: int,
    morphology: NeuronMorphology,
    *,
    transform: Optional[AffineTransform] = None,
    include_radius: bool = True,
    include_type: bool = True,
    segment_properties: Optional[str] = None,
) -> Path:
    """Write a single skeleton segment to the precomputed directory.

    Creates:
        {output_dir}/info          — JSON info file
        {output_dir}/{segment_id}  — binary skeleton data

    Args:
        output_dir: Directory to write to (created if needed).
        segment_id: Numeric ID for this segment.
        morphology: The neuron morphology data.
        transform: Optional affine transform.
        include_radius: Include radius attribute.
        include_type: Include type attribute.
        segment_properties: Optional path to segment_properties.

    Returns:
        Path to the output directory.
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    # Write info JSON
    info = build_skeleton_info(
        transform=transform,
        include_radius=include_radius,
        include_type=include_type,
        segment_properties=segment_properties,
    )
    info_path = out / "info"
    info_path.write_text(json.dumps(info.to_info_dict(), indent=2))

    # Write binary skeleton
    binary = neuron_to_skeleton_binary(
        morphology,
        include_radius=include_radius,
        include_type=include_type,
    )
    seg_path = out / str(segment_id)
    seg_path.write_bytes(binary)

    return out

build_skeleton_info

build_skeleton_info(
    *,
    transform: Optional[AffineTransform] = None,
    scale_um_to_nm: bool = False,
    include_radius: bool = True,
    include_type: bool = True,
    segment_properties: Optional[str] = None,
) -> SkeletonInfo

Build a SkeletonInfo config for the info JSON file.

Parameters:

Name Type Description Default
transform Optional[AffineTransform]

Optional affine transform to embed.

None
scale_um_to_nm bool

If True and no explicit transform, embed a 1000× scaling transform (micrometers → nanometers). Useful for SWC files whose coordinates are in µm.

False
include_radius bool

Include radius vertex attribute.

True
include_type bool

Include type vertex attribute.

True
segment_properties Optional[str]

Optional relative path to segment_properties dir.

None

Returns:

Type Description
SkeletonInfo

A SkeletonInfo model ready to serialize.

Source code in src/mudm_tools/neuroglancer/skeleton_writer.py
def build_skeleton_info(
    *,
    transform: Optional[AffineTransform] = None,
    scale_um_to_nm: bool = False,
    include_radius: bool = True,
    include_type: bool = True,
    segment_properties: Optional[str] = None,
) -> SkeletonInfo:
    """Build a SkeletonInfo config for the info JSON file.

    Args:
        transform: Optional affine transform to embed.
        scale_um_to_nm: If True and no explicit transform, embed a
            1000× scaling transform (micrometers → nanometers). Useful
            for SWC files whose coordinates are in µm.
        include_radius: Include radius vertex attribute.
        include_type: Include type vertex attribute.
        segment_properties: Optional relative path to segment_properties dir.

    Returns:
        A SkeletonInfo model ready to serialize.
    """
    vertex_attributes: list[VertexAttributeInfo] = []
    if include_radius:
        vertex_attributes.append(VertexAttributeInfo(id="radius", data_type="float32"))
    if include_type:
        vertex_attributes.append(VertexAttributeInfo(id="type", data_type="float32"))

    ng_transform: Optional[list[float]] = None
    if transform is not None:
        ng_transform = affine_to_ng_transform(transform)
    elif scale_um_to_nm:
        # Row-major 3×4: scale XYZ by 1000 (µm→nm), no translation
        ng_transform = [1000, 0, 0, 0, 0, 1000, 0, 0, 0, 0, 1000, 0]

    return SkeletonInfo(
        transform=ng_transform,
        vertex_attributes=vertex_attributes,
        segment_properties=segment_properties,
    )

neuron_to_skeleton_binary

neuron_to_skeleton_binary(
    morphology: NeuronMorphology,
    *,
    include_radius: bool = True,
    include_type: bool = True,
) -> bytes

Encode a NeuronMorphology as Neuroglancer precomputed skeleton binary.

Parameters:

Name Type Description Default
morphology NeuronMorphology

The neuron morphology to encode.

required
include_radius bool

Include per-vertex radius attribute.

True
include_type bool

Include per-vertex SWC type attribute.

True

Returns:

Type Description
bytes

Raw bytes in Neuroglancer skeleton binary format.

Source code in src/mudm_tools/neuroglancer/skeleton_writer.py
def neuron_to_skeleton_binary(
    morphology: NeuronMorphology,
    *,
    include_radius: bool = True,
    include_type: bool = True,
) -> bytes:
    """Encode a NeuronMorphology as Neuroglancer precomputed skeleton binary.

    Args:
        morphology: The neuron morphology to encode.
        include_radius: Include per-vertex radius attribute.
        include_type: Include per-vertex SWC type attribute.

    Returns:
        Raw bytes in Neuroglancer skeleton binary format.
    """
    tree = morphology.tree
    num_verts = len(tree)

    # Build vertex positions (x, y, z interleaved)
    positions: list[float] = []
    for sample in tree:
        positions.extend([sample.x, sample.y, sample.z])

    # Build edge list — map sample.id → index, then parent edges
    id_to_idx = {sample.id: i for i, sample in enumerate(tree)}
    edges: list[int] = []
    for sample in tree:
        if sample.parent != -1 and sample.parent in id_to_idx:
            edges.extend([id_to_idx[sample.parent], id_to_idx[sample.id]])
    num_edges = len(edges) // 2

    # Pack binary
    buf = bytearray()
    buf += pack_uint32(num_verts)
    buf += pack_uint32(num_edges)
    buf += pack_float32_array(positions)
    buf += pack_uint32_array(edges)

    if include_radius:
        radii = [sample.r for sample in tree]
        buf += pack_float32_array(radii)

    if include_type:
        types = [float(sample.type) for sample in tree]
        buf += pack_float32_array(types)

    return bytes(buf)

affine_to_ng_transform

affine_to_ng_transform(
    transform: AffineTransform,
) -> list[float]

Convert a 4×4 row-major affine to Neuroglancer's 12-element format.

Neuroglancer expects 12 floats representing the upper 3×4 sub-matrix in row-major order (3 rows of 4 values each): [r00, r01, r02, tx, r10, r11, r12, ty, r20, r21, r22, tz]

Internally, Neuroglancer loads these into a column-major mat4 and transposes, which produces the correct 4×4 affine.

Source code in src/mudm_tools/neuroglancer/skeleton_writer.py
def affine_to_ng_transform(transform: AffineTransform) -> list[float]:
    """Convert a 4×4 row-major affine to Neuroglancer's 12-element format.

    Neuroglancer expects 12 floats representing the upper 3×4 sub-matrix
    in row-major order (3 rows of 4 values each):
        [r00, r01, r02, tx, r10, r11, r12, ty, r20, r21, r22, tz]

    Internally, Neuroglancer loads these into a column-major mat4 and
    transposes, which produces the correct 4×4 affine.
    """
    m = transform.matrix
    result: list[float] = []
    for row in range(3):
        for col in range(4):
            result.append(m[row][col])
    return result

write_segment_properties

write_segment_properties(
    output_dir: str | Path,
    features: Sequence[MuDMFeature],
    segment_ids: Sequence[int],
) -> Path

Write segment_properties info JSON to disk.

Creates {output_dir}/info with the segment properties.

Parameters:

Name Type Description Default
output_dir str | Path

Directory to write to (created if needed).

required
features Sequence[MuDMFeature]

MuDM features.

required
segment_ids Sequence[int]

Segment IDs corresponding to each feature.

required

Returns:

Type Description
Path

Path to the output directory.

Source code in src/mudm_tools/neuroglancer/properties_writer.py
def write_segment_properties(
    output_dir: str | Path,
    features: Sequence[MuDMFeature],
    segment_ids: Sequence[int],
) -> Path:
    """Write segment_properties info JSON to disk.

    Creates ``{output_dir}/info`` with the segment properties.

    Args:
        output_dir: Directory to write to (created if needed).
        features: MuDM features.
        segment_ids: Segment IDs corresponding to each feature.

    Returns:
        Path to the output directory.
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    info_dict = features_to_segment_properties(features, segment_ids)
    (out / "info").write_text(json.dumps(info_dict, indent=2))

    return out

features_to_segment_properties

features_to_segment_properties(
    features: Sequence[MuDMFeature],
    segment_ids: Sequence[int],
) -> dict[str, Any]

Build segment_properties inline dict from MuDM features.

Each feature's properties dict is read. All unique property keys across features become columns. Values are ordered to match segment_ids.

Parameters:

Name Type Description Default
features Sequence[MuDMFeature]

MuDM features with properties dicts.

required
segment_ids Sequence[int]

Numeric segment IDs matching each feature.

required

Returns:

Type Description
dict[str, Any]

A dict matching the neuroglancer_segment_properties info schema.

Source code in src/mudm_tools/neuroglancer/properties_writer.py
def features_to_segment_properties(
    features: Sequence[MuDMFeature],
    segment_ids: Sequence[int],
) -> dict[str, Any]:
    """Build segment_properties inline dict from MuDM features.

    Each feature's ``properties`` dict is read. All unique property keys
    across features become columns. Values are ordered to match
    ``segment_ids``.

    Args:
        features: MuDM features with properties dicts.
        segment_ids: Numeric segment IDs matching each feature.

    Returns:
        A dict matching the ``neuroglancer_segment_properties`` info schema.
    """
    ids = [str(sid) for sid in segment_ids]

    # Collect all property keys and determine types
    all_keys: list[str] = []
    seen: set[str] = set()
    for feat in features:
        if feat.properties:
            for k in feat.properties:
                if k not in seen:
                    all_keys.append(k)
                    seen.add(k)

    fields: list[SegmentPropertyField] = []
    for key in all_keys:
        values: list[Any] = []
        for feat in features:
            val = feat.properties.get(key) if feat.properties else None
            values.append(val if val is not None else "")

        # Determine field type
        if all(isinstance(v, (int, float)) for v in values if v != ""):
            field_type = "number"
            # Neuroglancer requires data_type for number properties
            if all(isinstance(v, int) for v in values if v != ""):
                data_type = "uint32"
            else:
                data_type = "float32"
            fields.append(
                SegmentPropertyField(
                    id=key,
                    type=cast(Literal["label", "number", "string", "tags"], field_type),
                    data_type=cast(
                        Literal["float32", "int8", "uint8", "int16", "uint16", "int32", "uint32"],
                        data_type,
                    ),
                    values=values,
                )
            )
        else:
            field_type = "label"
            fields.append(
                SegmentPropertyField(
                    id=key,
                    type=cast(Literal["label", "number", "string", "tags"], field_type),
                    values=values,
                )
            )

    inline = SegmentPropertiesInline(ids=ids, properties=fields)
    info = SegmentPropertiesInfo(inline=inline)
    return info.to_info_dict()

write_mesh

write_mesh(
    output_dir: str | Path,
    segment_id: int,
    vertices: ndarray,
    indices: ndarray,
) -> Path

Write a single mesh segment to the precomputed directory.

Creates

{output_dir}/{segment_id} — binary mesh data {output_dir}/{segment_id}:0 — JSON fragment manifest

Parameters:

Name Type Description Default
output_dir str | Path

Directory to write to (created if needed).

required
segment_id int

Numeric ID for this segment.

required
vertices ndarray

Nx3 float32 vertex positions.

required
indices ndarray

Mx3 uint32 triangle face indices.

required

Returns:

Type Description
Path

Path to the binary mesh file.

Source code in src/mudm_tools/neuroglancer/mesh_writer.py
def write_mesh(
    output_dir: str | Path,
    segment_id: int,
    vertices: np.ndarray,
    indices: np.ndarray,
) -> Path:
    """Write a single mesh segment to the precomputed directory.

    Creates:
        {output_dir}/{segment_id}    — binary mesh data
        {output_dir}/{segment_id}:0  — JSON fragment manifest

    Args:
        output_dir: Directory to write to (created if needed).
        segment_id: Numeric ID for this segment.
        vertices: Nx3 float32 vertex positions.
        indices: Mx3 uint32 triangle face indices.

    Returns:
        Path to the binary mesh file.
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    # Write binary mesh
    binary = mesh_to_binary(vertices, indices)
    seg_path = out / str(segment_id)
    seg_path.write_bytes(binary)

    # Write fragment manifest JSON
    # The ":0" file tells Neuroglancer which fragment files to load.
    # For single-resolution legacy mesh, there's one fragment per segment.
    manifest = {"fragments": [str(segment_id)]}
    manifest_path = out / f"{segment_id}:0"
    manifest_path.write_text(json.dumps(manifest))

    return seg_path

write_mesh_info

write_mesh_info(
    output_dir: str | Path,
    segment_properties: Optional[str] = None,
) -> Path

Write the Neuroglancer mesh info JSON file.

Creates {output_dir}/info with the legacy mesh type.

Parameters:

Name Type Description Default
output_dir str | Path

Directory to write to (created if needed).

required
segment_properties Optional[str]

Optional relative path to segment_properties dir.

None

Returns:

Type Description
Path

Path to the output directory.

Source code in src/mudm_tools/neuroglancer/mesh_writer.py
def write_mesh_info(
    output_dir: str | Path,
    segment_properties: Optional[str] = None,
) -> Path:
    """Write the Neuroglancer mesh info JSON file.

    Creates ``{output_dir}/info`` with the legacy mesh type.

    Args:
        output_dir: Directory to write to (created if needed).
        segment_properties: Optional relative path to segment_properties dir.

    Returns:
        Path to the output directory.
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    info = MeshInfo(segment_properties=segment_properties)
    (out / "info").write_text(json.dumps(info.to_info_dict(), indent=2))
    return out

mesh_to_binary

mesh_to_binary(
    vertices: ndarray, indices: ndarray
) -> bytes

Encode mesh data as Neuroglancer legacy mesh binary.

Parameters:

Name Type Description Default
vertices ndarray

Nx3 float32 array of vertex positions.

required
indices ndarray

Mx3 uint32 array of triangle face indices (or flat 1D).

required

Returns:

Type Description
bytes

Raw bytes in Neuroglancer legacy mesh binary format.

Source code in src/mudm_tools/neuroglancer/mesh_writer.py
def mesh_to_binary(
    vertices: np.ndarray,
    indices: np.ndarray,
) -> bytes:
    """Encode mesh data as Neuroglancer legacy mesh binary.

    Args:
        vertices: Nx3 float32 array of vertex positions.
        indices: Mx3 uint32 array of triangle face indices (or flat 1D).

    Returns:
        Raw bytes in Neuroglancer legacy mesh binary format.
    """
    verts = np.ascontiguousarray(vertices, dtype=np.float32).ravel()
    idx = np.ascontiguousarray(indices, dtype=np.uint32).ravel()
    num_vertices = len(verts) // 3

    buf = bytearray()
    buf += pack_uint32(num_vertices)
    buf += pack_float32_array(verts.tolist())
    buf += pack_uint32_array(idx.tolist())
    return bytes(buf)

decode_mesh_binary

decode_mesh_binary(data: bytes) -> tuple[ndarray, ndarray]

Decode Neuroglancer legacy mesh binary back to arrays.

Parameters:

Name Type Description Default
data bytes

Raw bytes in Neuroglancer legacy mesh binary format.

required

Returns:

Type Description
tuple[ndarray, ndarray]

(vertices, indices) — Nx3 float32 and Mx3 uint32 arrays.

Source code in src/mudm_tools/neuroglancer/mesh_writer.py
def decode_mesh_binary(data: bytes) -> tuple[np.ndarray, np.ndarray]:
    """Decode Neuroglancer legacy mesh binary back to arrays.

    Args:
        data: Raw bytes in Neuroglancer legacy mesh binary format.

    Returns:
        (vertices, indices) — Nx3 float32 and Mx3 uint32 arrays.
    """
    import struct

    offset = 0
    (num_vertices,) = struct.unpack_from("<I", data, offset)
    offset += 4

    n_floats = num_vertices * 3
    verts = np.frombuffer(data, dtype=np.float32, count=n_floats, offset=offset)
    offset += n_floats * 4

    remaining = len(data) - offset
    n_indices = remaining // 4
    indices = np.frombuffer(data, dtype=np.uint32, count=n_indices, offset=offset)

    return verts.reshape(-1, 3).copy(), indices.reshape(-1, 3).copy()

fragments_to_mesh

fragments_to_mesh(
    fragments: Sequence[dict],
    world_bounds: tuple[
        float, float, float, float, float, float
    ],
) -> tuple[ndarray, ndarray]

Merge clipped fragments into a single mesh with world coordinates.

Takes fragment dicts (from the streaming pipeline) with keys: xy, z, ring_lengths, geom_type — all in normalized [0,1]^3 space — and unprojections them to world coordinates, performing vertex deduplication.

Parameters:

Name Type Description Default
fragments Sequence[dict]

Sequence of fragment dicts with normalized geometry.

required
world_bounds tuple[float, float, float, float, float, float]

(xmin, ymin, zmin, xmax, ymax, zmax) world bounds.

required

Returns:

Type Description
tuple[ndarray, ndarray]

(vertices, indices) — Nx3 float32 positions and Mx3 uint32 indices.

Source code in src/mudm_tools/neuroglancer/mesh_writer.py
def fragments_to_mesh(
    fragments: Sequence[dict],
    world_bounds: tuple[float, float, float, float, float, float],
) -> tuple[np.ndarray, np.ndarray]:
    """Merge clipped fragments into a single mesh with world coordinates.

    Takes fragment dicts (from the streaming pipeline) with keys:
    ``xy``, ``z``, ``ring_lengths``, ``geom_type`` — all in normalized
    [0,1]^3 space — and unprojections them to world coordinates,
    performing vertex deduplication.

    Args:
        fragments: Sequence of fragment dicts with normalized geometry.
        world_bounds: (xmin, ymin, zmin, xmax, ymax, zmax) world bounds.

    Returns:
        (vertices, indices) — Nx3 float32 positions and Mx3 uint32 indices.
    """
    xmin, ymin, zmin, xmax, ymax, zmax = world_bounds
    dx = xmax - xmin if xmax != xmin else 1.0
    dy = ymax - ymin if ymax != ymin else 1.0
    dz = zmax - zmin if zmax != zmin else 1.0

    vertex_map: dict[tuple[float, float, float], int] = {}
    positions: list[list[float]] = []
    all_indices: list[int] = []

    for frag in fragments:
        xy = frag["xy"]
        z = frag["z"]
        ring_lengths = frag.get("ring_lengths", [])
        n_verts = len(z)

        if not ring_lengths:
            ring_lengths = [n_verts]

        offset = 0
        for rl in ring_lengths:
            nv = 3 if rl >= 4 else rl
            if nv < 3 or offset + 2 >= n_verts:
                offset += rl
                continue

            tri_indices = []
            for vi_off in range(3):
                vi = offset + vi_off
                # Unproject to world coordinates
                wx = xmin + xy[vi * 2] * dx
                wy = ymin + xy[vi * 2 + 1] * dy
                wz = zmin + z[vi] * dz

                # Round to float32 precision for dedup
                wx = float(np.float32(wx))
                wy = float(np.float32(wy))
                wz = float(np.float32(wz))

                key = (wx, wy, wz)
                if key not in vertex_map:
                    vertex_map[key] = len(positions)
                    positions.append([wx, wy, wz])
                tri_indices.append(vertex_map[key])

            # Skip degenerate triangles
            if len(set(tri_indices)) == 3:
                all_indices.extend(tri_indices)

            offset += rl

    if not positions:
        return np.zeros((0, 3), dtype=np.float32), np.zeros((0, 3), dtype=np.uint32)

    verts = np.array(positions, dtype=np.float32)
    idx = np.array(all_indices, dtype=np.uint32).reshape(-1, 3)
    return verts, idx

points_to_annotation_binary

points_to_annotation_binary(
    points: Sequence[Tuple[float, float, float]],
    annotation_ids: Sequence[int],
) -> bytes

Encode point annotations as Neuroglancer binary.

Parameters:

Name Type Description Default
points Sequence[Tuple[float, float, float]]

Sequence of (x, y, z) coordinates.

required
annotation_ids Sequence[int]

Unique ID for each annotation.

required

Returns:

Type Description
bytes

Raw bytes in Neuroglancer annotation binary format.

Source code in src/mudm_tools/neuroglancer/annotation_writer.py
def points_to_annotation_binary(
    points: Sequence[Tuple[float, float, float]],
    annotation_ids: Sequence[int],
) -> bytes:
    """Encode point annotations as Neuroglancer binary.

    Args:
        points: Sequence of (x, y, z) coordinates.
        annotation_ids: Unique ID for each annotation.

    Returns:
        Raw bytes in Neuroglancer annotation binary format.
    """
    count = len(points)
    buf = bytearray()
    buf += pack_uint64(count)

    coords: list[float] = []
    for x, y, z in points:
        coords.extend([x, y, z])
    buf += pack_float32_array(coords)

    buf += pack_uint64_array(list(annotation_ids))
    return bytes(buf)

lines_to_annotation_binary

lines_to_annotation_binary(
    lines: Sequence[
        Tuple[float, float, float, float, float, float]
    ],
    annotation_ids: Sequence[int],
) -> bytes

Encode line annotations as Neuroglancer binary.

Each line is (x1, y1, z1, x2, y2, z2).

Parameters:

Name Type Description Default
lines Sequence[Tuple[float, float, float, float, float, float]]

Sequence of 6-tuples (start_xyz + end_xyz).

required
annotation_ids Sequence[int]

Unique ID for each annotation.

required

Returns:

Type Description
bytes

Raw bytes in Neuroglancer annotation binary format.

Source code in src/mudm_tools/neuroglancer/annotation_writer.py
def lines_to_annotation_binary(
    lines: Sequence[Tuple[float, float, float, float, float, float]],
    annotation_ids: Sequence[int],
) -> bytes:
    """Encode line annotations as Neuroglancer binary.

    Each line is (x1, y1, z1, x2, y2, z2).

    Args:
        lines: Sequence of 6-tuples (start_xyz + end_xyz).
        annotation_ids: Unique ID for each annotation.

    Returns:
        Raw bytes in Neuroglancer annotation binary format.
    """
    count = len(lines)
    buf = bytearray()
    buf += pack_uint64(count)

    coords: list[float] = []
    for line in lines:
        coords.extend(line)
    buf += pack_float32_array(coords)

    buf += pack_uint64_array(list(annotation_ids))
    return bytes(buf)

build_skeleton_layer

build_skeleton_layer(
    name: str, source_url: str, *, use_radius: bool = True
) -> dict[str, Any]

Build a Neuroglancer layer dict for a skeleton source.

Parameters:

Name Type Description Default
name str

Layer display name.

required
source_url str

precomputed:// URL to the skeleton data.

required
use_radius bool

If True, include a skeleton shader that reads the radius vertex attribute to set line width. Adds a radiusScale slider control in the Neuroglancer UI.

True

Returns:

Type Description
dict[str, Any]

Layer dict suitable for the viewer state layers list.

Source code in src/mudm_tools/neuroglancer/state.py
def build_skeleton_layer(
    name: str,
    source_url: str,
    *,
    use_radius: bool = True,
) -> dict[str, Any]:
    """Build a Neuroglancer layer dict for a skeleton source.

    Args:
        name: Layer display name.
        source_url: ``precomputed://`` URL to the skeleton data.
        use_radius: If True, include a skeleton shader that reads the
            radius vertex attribute to set line width. Adds a
            ``radiusScale`` slider control in the Neuroglancer UI.

    Returns:
        Layer dict suitable for the viewer state ``layers`` list.
    """
    layer: dict[str, Any] = {
        "type": "segmentation",
        "source": source_url,
        "name": name,
        "selectedAlpha": 0,
        "notSelectedAlpha": 0,
    }
    if use_radius:
        layer["skeletonRendering"] = {
            "mode2d": "lines",
            "mode3d": "lines",
            "shader": _RADIUS_SHADER,
        }
    return layer

build_annotation_layer

build_annotation_layer(
    name: str, source_url: str
) -> dict[str, Any]

Build a Neuroglancer layer dict for an annotation source.

Parameters:

Name Type Description Default
name str

Layer display name.

required
source_url str

precomputed:// URL to the annotation data.

required

Returns:

Type Description
dict[str, Any]

Layer dict suitable for the viewer state layers list.

Source code in src/mudm_tools/neuroglancer/state.py
def build_annotation_layer(
    name: str,
    source_url: str,
) -> dict[str, Any]:
    """Build a Neuroglancer layer dict for an annotation source.

    Args:
        name: Layer display name.
        source_url: ``precomputed://`` URL to the annotation data.

    Returns:
        Layer dict suitable for the viewer state ``layers`` list.
    """
    return {
        "type": "annotation",
        "source": source_url,
        "name": name,
    }

build_viewer_state

build_viewer_state(
    layers: List[dict[str, Any]],
    position: Optional[List[float]] = None,
    projection_scale: Optional[float] = None,
    layout: str = "3d",
) -> dict[str, Any]

Build a complete Neuroglancer viewer state dict.

Parameters:

Name Type Description Default
layers List[dict[str, Any]]

List of layer dicts (from build_*_layer functions).

required
position Optional[List[float]]

Optional 3D camera position [x, y, z] in nm.

None
projection_scale Optional[float]

Camera zoom (field of view in nm). Larger = more zoomed out.

None
layout str

Viewer layout. "3d" for 3D only, "4panel" for all views.

'3d'

Returns:

Type Description
dict[str, Any]

Viewer state dict that can be serialized to JSON.

Source code in src/mudm_tools/neuroglancer/state.py
def build_viewer_state(
    layers: List[dict[str, Any]],
    position: Optional[List[float]] = None,
    projection_scale: Optional[float] = None,
    layout: str = "3d",
) -> dict[str, Any]:
    """Build a complete Neuroglancer viewer state dict.

    Args:
        layers: List of layer dicts (from build_*_layer functions).
        position: Optional 3D camera position [x, y, z] in nm.
        projection_scale: Camera zoom (field of view in nm). Larger = more zoomed out.
        layout: Viewer layout. ``"3d"`` for 3D only, ``"4panel"`` for all views.

    Returns:
        Viewer state dict that can be serialized to JSON.
    """
    state: dict[str, Any] = {"layers": layers, "layout": layout}
    if position is not None:
        nav: dict[str, Any] = {
            "pose": {"position": {"voxelCoordinates": position}},
        }
        if projection_scale is not None:
            nav["zoomFactor"] = projection_scale
        state["navigation"] = nav
    elif projection_scale is not None:
        state["navigation"] = {"zoomFactor": projection_scale}
    return state

viewer_state_to_url

viewer_state_to_url(
    state: dict[str, Any],
    base_url: str = "https://neuroglancer-demo.appspot.com",
) -> str

Encode a viewer state dict into a Neuroglancer URL.

Parameters:

Name Type Description Default
state dict[str, Any]

Viewer state dict.

required
base_url str

Base Neuroglancer instance URL.

'https://neuroglancer-demo.appspot.com'

Returns:

Type Description
str

Full URL with JSON-encoded fragment.

Source code in src/mudm_tools/neuroglancer/state.py
def viewer_state_to_url(
    state: dict[str, Any],
    base_url: str = "https://neuroglancer-demo.appspot.com",
) -> str:
    """Encode a viewer state dict into a Neuroglancer URL.

    Args:
        state: Viewer state dict.
        base_url: Base Neuroglancer instance URL.

    Returns:
        Full URL with JSON-encoded fragment.
    """
    fragment = json.dumps(state, separators=(",", ":"))
    # URL-encode the JSON so browsers don't mangle special characters
    # ({, }, ", [, ] etc.). Neuroglancer decodes before JSON.parse.
    encoded = urllib.parse.quote(fragment, safe=",:/@")
    return f"{base_url}/#!{encoded}"

MeshInfo

Bases: BaseModel

Info JSON for a precomputed:// legacy mesh source.

SkeletonInfo

Bases: BaseModel

Info JSON for a precomputed:// skeleton source.

VertexAttributeInfo

Bases: BaseModel

A per-vertex attribute in a skeleton (e.g. radius, type).

AnnotationInfo

Bases: BaseModel

Info JSON for a precomputed:// annotation source.

SegmentPropertiesInfo

Bases: BaseModel

Info JSON for neuroglancer_segment_properties.

For the rest of the model classes (AnnotationDimension, AnnotationSpatialEntry, AnnotationPropertySpec, AnnotationRelationship, SegmentPropertiesInline, SegmentPropertyField), see the Python API reference.


Path 2: The scalable Rust mesh generator

For large mesh corpora, drive mudm_tools._rs.StreamingTileGenerator. You ingest geometry (OBJ files, Parquet meshes, or projected feature dicts), the generator accumulates octree-clipped fragments on disk, and a generate_* call emits the chosen Neuroglancer format. See the 3D Tiling guide for ingestion details.

Not autodoc-able

StreamingTileGenerator is a compiled Rust pyclass (mudm_tools._rs), so its methods cannot be introspected by mkdocstrings. The signatures below are transcribed by hand from the source.

Legacy mesh: generate_neuroglancer

def generate_neuroglancer(
    self,
    output_dir: str,
    world_bounds: tuple[float, float, float, float, float, float],  # xmin,ymin,zmin,xmax,ymax,zmax
) -> int

Emits a segment-centric neuroglancer_legacy_mesh source: one mesh per feature at max_zoom (the finest level). It writes output_dir/info (@type neuroglancer_legacy_mesh), a {segment_id} binary mesh, a {segment_id}:0 JSON fragment manifest, and segment_properties/info built from tags. Geometry is merged with float32-bit vertex dedup. Returns the number of segments written.

Multi-LOD Draco meshes: generate_neuroglancer_multilod

def generate_neuroglancer_multilod(
    self,
    output_dir: str,
    world_bounds: tuple[float, float, float, float, float, float],
    vertex_quantization_bits: int = 10,
    max_memory_bytes: int = 0,
    sharded: bool = False,
    minishard_bits: int = 6,
    shard_bits: int = 0,
) -> int

Emits a neuroglancer_multilod_draco source. Unlike the legacy path, this uses all zoom levels: LOD 0 = max_zoom (finest), LOD N = zoom 0 (coarsest). Fragment positions are sorted in Morton / Z-curve order. The info carries vertex_quantization_bits, an identity transform, lod_scale_multiplier 1.0, and a segment_properties reference. Returns the number of segments written.

Parameter Default Notes
output_dir Output directory
world_bounds (xmin, ymin, zmin, xmax, ymax, zmax); degenerate axes default span to 1.0
vertex_quantization_bits 10 Per-tile position quantization (qmax = 2^bits − 1)
max_memory_bytes 0 Per-path memory ceiling. 0 uses the generator's resolved self.max_memory_bytes. The ceiling derives a feature-bucket count k = min(ceil(est_resident_bytes / budget), 256); peak resident ≈ corpus/k. A huge ceiling yields k == 1 (whole-corpus, byte-identical path).
sharded False When True, emit neuroglancer_uint64_sharded_v1 .shard files (see below)
minishard_bits 6 Sharding spec minishard_bits (used only when sharded=True)
shard_bits 0 Sharding spec shard_bits (used only when sharded=True)
from mudm_tools._rs import StreamingTileGenerator, scan_obj_bounds

paths = ["mesh_a.obj", "mesh_b.obj"]
bounds = scan_obj_bounds(paths)

gen = StreamingTileGenerator(min_zoom=0, max_zoom=4)
gen.add_obj_files(paths, bounds, [{"name": "a"}, {"name": "b"}])

# Loose multi-LOD output (default)
n = gen.generate_neuroglancer_multilod("out/ng_multilod", bounds)
print(f"{n} segments written")

Loose vs. sharded layout

By default (sharded=False), the generator writes loose per-segment files: a {seg_id}.index binary manifest plus a {seg_id} file of concatenated Draco fragments. This read is feature-bucketed and memory-bounded.

With sharded=True, per-segment (manifest, fragment bytes) pairs are accumulated and packed into neuroglancer_uint64_sharded_v1 .shard files. The info file gains a sharding block (hash murmurhash3_x86_128, minishard_index_encoding raw, data_encoding raw). Loose per-segment files are not written in sharded mode.

Sharded mode holds the whole corpus in RAM

Sharded packing happens after the bucket loop, so sharded=True does not preserve the per-bucket memory bound — it holds the entire Neuroglancer mesh corpus in memory. Treat it as an opt-in deploy / object-store-scale path, and provision RAM accordingly. The default loose format keeps the memory bound.

gen.generate_neuroglancer_multilod("out/ng", world_bounds)
out/ng/
  info                    # @type neuroglancer_multilod_draco
  {seg_id}.index          # per-segment binary manifest
  {seg_id}                # concatenated Draco fragments
  segment_properties/info
gen.generate_neuroglancer_multilod(
    "out/ng",
    world_bounds,
    sharded=True,
    minishard_bits=6,
    shard_bits=0,
)
out/ng/
  info                    # @type neuroglancer_multilod_draco (+ "sharding" block)
  *.shard                 # neuroglancer_uint64_sharded_v1 files
  segment_properties/info

Output structure reference

# write_skeleton (repeated per segment_id into one dir)
{output_dir}/
  info                 # JSON, @type neuroglancer_skeletons
  {segment_id}         # u32 nverts, u32 nedges, f32 verts[N*3], u32 edges[E*2], [f32 radii[N]], [f32 types[N]]
  seg_props/info       # optional, from write_segment_properties

# write_annotations / to_neuroglancer
{output_dir}/          # to_neuroglancer makes point_annotations/ and/or line_annotations/ subdirs
  info                 # JSON, @type neuroglancer_annotations_v1
  by_id/               # empty dir (required by Neuroglancer)
  spatial0/0_0_0       # u64 count, f32 coords[count*D] (D=3 point, 6 line), u64 ids[count]

# write_mesh_info + write_mesh (legacy_mesh)
{output_dir}/
  info                 # JSON, @type neuroglancer_legacy_mesh
  {segment_id}         # u32 nverts, f32 verts[N*3], u32 idx[M*3]
  {segment_id}:0       # JSON fragment manifest {"fragments": ["{segment_id}"]}
  segment_properties/info   # optional

# StreamingTileGenerator.generate_neuroglancer_multilod (multilod_draco)
{output_dir}/
  info                 # JSON, @type neuroglancer_multilod_draco (+ "sharding" when sharded=True)
  {seg_id}.index       # loose mode only: binary manifest
  {seg_id}             # loose mode only: concatenated Draco fragments
  *.shard              # sharded=True only (neuroglancer_uint64_sharded_v1)
  segment_properties/info

Serving for an external Neuroglancer client

Neuroglancer runs in the browser and fetches data over HTTP, so the server must send CORS headers (Access-Control-Allow-Origin: *). You have two options.

Option A: mudm-serve (/neuroglancer/ route)

The installed console script mudm-serve (mudm_tools.serve:main) serves precomputed files under a /neuroglancer/ route. A request to /neuroglancer/{pyramid_id}/... maps to {tiles_base}/{pyramid_id}/neuroglancer/... on disk, where tiles_base is the tiles directory you serve.

uv run mudm-serve

This is intended for an external Neuroglancer client — point a precomputed:// source at the served URL. See the CLI reference for the full mudm-serve options.

External viewer only

mudm-serve does not render Neuroglancer. Open the served precomputed:// source in a real Neuroglancer instance.

Option B: the standalone example servers

Both ship as runnable example modules and use Python's http.server with a CORS subclass.

# Auto-detect skeleton/annotation layers and print a viewer URL per export
uv run python -m mudm_tools.examples.neuroglancer_serve neuroglancer_output

neuroglancer_serve walks each subdirectory, reads its info @type to classify it (neuroglancer_skeletonssegmentation, neuroglancer_annotations_v1annotation), infers segment ids from the binary files, builds a viewer state, and prints a ready-to-open Neuroglancer URL.

Flag Default Meaning
directory Directory of precomputed data (positional)
--port 9000 Port to serve on
--neuroglancer-url https://neuroglancer-demo.appspot.com Neuroglancer instance for the printed URLs
uv run python -m mudm_tools.examples.swc_to_neuroglancer neuron.swc

swc_to_neuroglancer converts, prints the URL, and serves in a single command (drop --no-serve to keep the server running).


See also

  • 3D Tiling — how to ingest geometry into StreamingTileGenerator before calling generate_neuroglancer / generate_neuroglancer_multilod.
  • CLI referencemudm-serve options and the /neuroglancer/ route.
  • Python API reference — the full mudm_tools.neuroglancer writer and model API.