MASCAF (Mesh and Skeleton Cable Fitting) is a Python package for fitting cable-graph morphology (such as an SWC model) to a closed triangle mesh using a 3D curve skeleton as geometric guidance.
This workflow was developed to create neuronal morphology models for multi-compartmental simulation from 3D surface meshes. MASCAF is especially useful for complex morphologies, including non-tree structures, where the output must preserve graph topology before eventual SWC export.
- Surface mesh — Typically a watertight OBJ, or anything trimesh can load.
- Mesh skeletonization — MASCAF includes an internal CGAL integration for skeletonization, but you can also use CGALLab or any other external method as long as the output matches the expected polylines format:
.polylines.txt- GraphML (
.graphml/.xml)
Install in your own project environment by providing pip with the repo link. This project works well with uv.
uv pip install https://github.com/jmrfox/mascaf.gitIf you are working in a local clone of this repo:
uv syncRun code or tests inside that environment:
uv run python your_script.py
uv run pytestIf you already have a valid skeleton file, you do not need the internal CGAL setup at all.
- Prepare a mesh — Start from a closed triangle mesh.
- Generate or obtain a skeleton — Use one of the skeletonization options below.
- Load inputs — Load the mesh with
MeshManagerand the skeleton withSkeletonGraph.from_txt(). - Fit a morphology graph — Use
CableFitterwithFitOptions. - Optionally optimize the morphology basis — Use
BasisOptimizerthroughFitOptions.basis_optimizer_optionsif you want additional geometric refinement before final radius fitting. - Validate and export — Inspect results, optionally scale radii to match the mesh, and export to SWC.
MASCAF includes a few CGAL operations:
repair(mesh -> mesh)simplify(mesh -> mesh)skeletonize(mesh -> .polylines.txt)
The native C++ project lives in cpp/, and the Python interface for it lives in mascaf.cgal.
The Python module can help orchestrate the build, but it does not remove the need for a native toolchain. Before using internal CGAL, the user must install:
- a C++ compiler toolchain (e.g., Visual Studio Build Tools on Windows)
- CMake
- vcpkg
- the CGAL/Eigen dependencies resolved through the
cpp/vcpkg.jsonmanifest
- Install Visual Studio or Build Tools for Visual Studio with C++ support.
- Install CMake and make sure
cmakeis available onPATH. - Install vcpkg and set the
VCPKG_ROOTenvironment variable. - Open the terminal where your compiler environment is available (e.g., Visual Studio Developer Command Prompt).
- From the repo root, configure the native project:
cmake -S cpp -B cpp/build -DCMAKE_TOOLCHAIN_FILE="%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake"- Build the executables:
cmake --build cpp/build --config Release- Confirm that executables such as
mesh_repair.exe,mesh_simplify.exe, andmesh_skeletonize.exeexist in the build output.
- Install a C++17-capable compiler toolchain.
- Install CMake.
- Install vcpkg and export
VCPKG_ROOT. - Open a terminal where your compiler environment is available.
- From the repo root, configure the native project:
cmake -S cpp -B cpp/build -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"- Build the executables:
cmake --build cpp/build --config Release- Confirm that
mesh_repair,mesh_simplify, andmesh_skeletonizeare present in the build output.
The current skeletonization executable accepts:
mesh_skeletonize <input.obj> <output.polylines.txt> [w_H] [w_M]
where:
w_His the quality/speed tradeoff (quality_speed_tradeoff)w_Mis the medially-centered speed tradeoff (medially_centered_speed_tradeoff)
Example:
mesh_skeletonize neuron.obj neuron.polylines.txt 0.5 5.0MASCAF includes helpers for configure/build orchestration and executable discovery:
CGALConfigCGALBuilderCGALOperator
Example:
from mascaf import CGALBuilder, CGALConfig
config = CGALConfig(build_dir="cpp/build")
builder = CGALBuilder(config)
builder.run_configure()
builder.run_build(config="Release")This still assumes the native toolchain prerequisites are already installed correctly.
from mascaf import CGALConfig, CGALOperator
config = CGALConfig(build_dir="cpp/build")
ops = CGALOperator(config=config)
ops.skeletonize(
"neuron.obj",
"neuron.polylines.txt",
quality_speed_tradeoff=0.5,
medially_centered_speed_tradeoff=5.0,
)MASCAF also includes a placeholder Python method for suggesting these parameters automatically in the future, but it does not yet perform real tuning.
The CGAL distribution includes a demo application (CGAL Lab) that can run triangulated surface mesh skeletonization interactively.
Typical steps:
- Download CGAL Lab from the CGAL website: CGAL Lab 6.1 Download.
- Open CGAL Lab and load your mesh.
- Run Operations → Triangulated Surface Mesh Skeletonization → Mean Curvature Skeleton (Advanced).
- Export the result as a polylines text file.
You can use any external method if it produces a MASCAF-compatible polylines file:
N x1 y1 z1 x2 y2 z2 ... xN yN zN
That is, one line per branch, starting with the number of points on that line.
| Piece | Role |
|---|---|
MeshManager |
Load and hold a trimesh.Trimesh mesh. |
SkeletonGraph |
Load and store skeleton geometry from polylines text or GraphML. |
FitOptions |
Controls cable fitting, radius estimation, and optional basis optimization. |
CableFitter |
Main workflow object for converting mesh + skeleton into a MorphologyGraph. |
BasisOptimizerOptions |
Configuration for morphology-basis optimization before final radius fitting. |
BasisOptimizer |
Geometric optimization of the morphology basis against the mesh. |
MorphologyGraph |
Graph-based morphology representation with radii and SWC export support. |
Validation |
Compare mesh and morphology geometry metrics. |
CGALConfig, CGALBuilder, CGALOperator |
Optional CGAL-backed native preprocessing and build orchestration. |
- Resampling and basis construction —
CableFitterconverts the skeleton into a morphology basis whose segment lengths satisfyFitOptions.max_edge_length. - Optional basis optimization — If
FitOptions.basis_optimizer_optionsis set, MASCAF runsBasisOptimizeron the intermediate morphology basis before radius fitting. - Cross-section and radius estimation — MASCAF estimates local radii from mesh geometry using the configured
radius_strategy. - Output morphology graph — The result is a
MorphologyGraphthat preserves graph connectivity, including cycles.
Supported radius strategies include:
equivalent_areaequivalent_perimetersection_mediansection_circle_fitnearest_surface
However, equivalent_area is recommended for most use cases.
At branch points, multi_tangent_reduction is applied to the set of per-edge radii, e.g. median(r_1, r_2, ..., r_n) = r_branch.
SWC is inherently a tree format, but MASCAF can handle morphologies with topological cycles. MASCAF keeps arbitrary graph topology in memory and only enforces a tree when writing SWC, by duplicating nodes where necessary and optionally recording cycle-closure information in the output.
By default, MASCAF will duplicate nodes to break cycles when writing SWC files. This ensures that the output SWC file is a valid tree structure while preserving the original graph topology in memory. For each broken cycle, a directive is added to the SWC file header specifying reconnection: # CYCLE_BREAK <node_id> <parent_id>.
from mascaf import (
BasisOptimizerOptions,
CableFitter,
FitOptions,
MeshManager,
SkeletonGraph,
)
mesh_mgr = MeshManager(mesh_path="neuron.obj")
skeleton = SkeletonGraph.from_txt("neuron.polylines.txt")
fit_options = FitOptions(
max_edge_length=1.0,
radius_strategy="equivalent_area",
basis_optimizer_options=BasisOptimizerOptions(
do_snapping=True,
do_forcing=True,
max_iterations=50,
smoothing_weight=0.5,
),
)
fitter = CableFitter(fit_options)
morphology = fitter.fit(mesh_mgr, skeleton)
morphology.scale_radii_to_match_mesh(mesh_mgr, metric="surface_area")
morphology.to_swc_file("neuron.swc")Minimal example if you already have a skeleton and do not want basis optimization:
from mascaf import CableFitter, FitOptions, MeshManager, SkeletonGraph
mesh_mgr = MeshManager(mesh_path="shape.obj")
skel = SkeletonGraph.from_txt("shape.polylines.txt")
fitter = CableFitter(FitOptions(max_edge_length=0.5))
morph = fitter.fit(mesh_mgr, skel)
morph.to_swc_file("shape.swc")-
Executable not found
- Build the native
cpp/project first. - Make sure you are looking in the same build configuration that you compiled, such as
ReleaseorDebug.
- Build the native
-
VCPKG_ROOTis not set- Set the environment variable so CMake or
CGALBuildercan locate the vcpkg toolchain file.
- Set the environment variable so CMake or
-
CMake cannot find CGAL or Eigen
- Confirm that you configured with the vcpkg toolchain file and that the
cpp/vcpkg.jsondependencies were resolved.
- Confirm that you configured with the vcpkg toolchain file and that the
-
The project built in
Debug, but MASCAF cannot find the binary- Point
CGALConfig(build_dir=...)orCGALConfig(executable_dir=...)at the actual output location.
- Point
-
Skeletonization fails on the mesh
- The current CGAL skeletonization path expects a closed triangle mesh.
-
You already have a valid skeleton file
- Skip internal CGAL entirely and load the skeleton directly with
SkeletonGraph.from_txt().
- Skip internal CGAL entirely and load the skeleton directly with
mascaf/cable_fitting.py— current fitting workflow,CableFitter, andFitOptionsmascaf/basis_optimizer.py— morphology-basis optimizationmascaf/morphology_graph.py— SWC export and geometry utilitiesmascaf/cgal.py— optional internal CGAL integrationtests/— runnable examples and regression coverage