Skip to main content

Convert messy overlapping polylines into clean topological centerline chains

Project description

topologize

Convert messy, overlapping polylines into a clean topological skeleton.

Given a set of curves — open or closed, possibly intersecting or bundled — topologize inflates them into a region, skeletonizes that region via constrained Delaunay triangulation, and returns a list of maximal non-branching polylines tracing the medial axis.

Before and after on topologize.svg

What it does

The three-stage pipeline:

  1. Inflate — buffer all input curves by inflation_radius and union the results into one or more polygons (Clipper2)
  2. Skeletonize — constrained Delaunay triangulation of the polygon interior; midpoints of internal edges form the skeleton graph
  3. Extract chains — snap nearby endpoints, then traverse the graph to extract maximal non-branching polylines

The output chains share junction points, so the result is a proper topological graph: you can traverse it, measure it, and match it to other representations.

When to use it

Use topologize when you have geometry that approximates a graph and need an actual graph — a set of polylines with shared junction points you can traverse, measure, or match to other data.

Concrete examples:

  • Vector artwork or scanned drawings converted to machine toolpaths (laser, pen plotter, CNC, printing)
  • Road, river, or network centerline extraction from polygon or buffered line data
  • GPS or sensor traces where the same route was recorded multiple times

Installation

pip install topologize

Runtime dependency: numpy only.

To build from source (requires a Rust toolchain):

git clone https://github.com/tdamsma/topologize
cd topologize
uv run maturin develop --release

Quick start

import numpy as np
from topologize import topologize

# Any collection of (N, 2) numpy arrays — open or closed, overlapping is fine
curves = [
    np.array([[0.0, 0.0], [10.0, 0.0], [5.0, 5.0]]),
    np.array([[0.1, 0.1], [9.9, 0.1], [5.1, 4.9]]),  # near-duplicate stroke
]

result = topologize(curves, inflation_radius=0.5)
result.chains          # list of (M, 2) arrays — one per non-branching segment
result.nodes           # (K, 2) array of unique junction/endpoint positions
result.chain_node_ids  # list of (start_id, end_id) per chain

inflation_radius is the main tuning parameter. Use roughly half the typical gap between nearby strokes — small enough to keep distinct paths separate, large enough to merge strokes that belong together.

For variable-width inflation, pass a list of per-vertex radius arrays:

widths_a = np.linspace(0.3, 0.1, len(curve_a))
widths_b = np.linspace(0.1, 0.5, len(curve_b))
result = topologize([curve_a, curve_b], inflation_radius=[widths_a, widths_b])
Parameter Type Description
curves list[np.ndarray] Input polylines, each (N, 2) or (N, 3) with per-vertex widths. Closed curves should repeat the first point at the end.
inflation_radius float | list Inflation radius. A single float for uniform width, or a list of per-vertex radius arrays for variable width.
feature_size float | None Scale parameter for all derived thresholds. Defaults to inflation_radius (float) or median(all widths) (list).
simplification float | None RDP tolerance on output chains (default: feature_size / 10). Set to 0 to disable.
min_tip_fraction float | None Prune terminal chains shorter than fraction × feature_size (default: 2.0). Set to 0 to disable.
junction_merge_fraction float | None Merge nearby junctions within fraction × feature_size (default: 1.5). Set to 0 to disable.
compute_widths bool If True, populate result.chain_widths with estimated contour width at each chain point.

Visualization

TopologizeResult has a built-in plot() method (requires plotly):

result.plot(curves, inflation_radius=0.5)                    # basic
result.plot(curves, inflation_radius=0.5, show_triangulation=True)  # + CDT overlay

When compute_widths=True, the plot also shows bead-width envelopes around each chain.

Batch processing

For workloads with many independent curve-sets (e.g. per-layer toolpath slicing), topologize_batch processes them in parallel via Rayon with the GIL released. Each job carries its own parameters:

from topologize import topologize_batch, TopologizeJob

jobs = [
    TopologizeJob(curves_layer_1, inflation_radius=0.5),
    TopologizeJob(curves_layer_2, inflation_radius=1.0, simplification=0.0),
    # ...
]
results = topologize_batch(jobs)
# returns list[TopologizeResult], one per job

On multi-core machines this is significantly faster than a Python loop.

Async-style processing with ThreadPoolExecutor

Single topologize releases the GIL during Rust computation, so you can also use Python's ThreadPoolExecutor for async-style processing:

from concurrent.futures import ThreadPoolExecutor
from topologize import topologize

with ThreadPoolExecutor() as pool:
    futures = [pool.submit(topologize, cs, inflation_radius=0.5) for cs in curve_sets]
    results = [f.result() for f in futures]

This is useful when jobs arrive one at a time (e.g. from a queue) rather than all at once.

Examples

All examples are # %% cell-delimited Python files — run directly or open as Jupyter notebooks.

uv sync --group dev  # install plotly, svgpathtools, etc.

# Getting started — basic usage and inflation_radius tuning
uv run python/examples/getting_started.py

# SVG centerline extraction
uv run python/examples/svg_centerline.py python/examples/data/topologize.svg --buffer 0.47

# Variable-width per-vertex inflation
uv run python/examples/variable_width_demo.py

# Parallel / batch processing benchmarks
uv run python/examples/parallel_processing.py

Algorithm

See algorithm.md for a detailed description of all three pipeline stages, the boundary preprocessing steps (RDP simplification + subdivision), the post-processing applied to output chains (projection smoothing, endpoint straightening, RDP), and the rationale for the CDT midpoint approach over alternatives (Voronoi, Python prototype).

Project structure

src/
  lib.rs             pymodule entry point (_internal)
  python.rs          Python-facing bindings
  inflate.rs         Clipper2-based polygon inflation + boundary prep
  skeleton_cdt.rs    CDT midpoint-graph skeletonizer
  graph.rs           Endpoint snapping + chain extraction

python/
  topologize/
    __init__.py      Public API (TopologizeResult, topologize, topologize_batch, inflate, triangulate)
  examples/
    getting_started.py
    svg_centerline.py
    variable_width_demo.py
    compare_methods.py
    parallel_processing.py

tests/
  test_topologize.py
  test_batch.py

Cargo.toml           Rust manifest
pyproject.toml       Python build config (maturin)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

topologize-0.0.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl (576.6 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ i686

topologize-0.0.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl (530.8 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARMv7l

topologize-0.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (503.0 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ ARM64

topologize-0.0.6-cp312-cp312-macosx_11_0_arm64.whl (454.9 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

topologize-0.0.6-cp312-cp312-macosx_10_12_x86_64.whl (475.4 kB view details)

Uploaded CPython 3.12macOS 10.12+ x86-64

File details

Details for the file topologize-0.0.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl.

File metadata

File hashes

Hashes for topologize-0.0.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl
Algorithm Hash digest
SHA256 69ba8ccc78cba41b13f942f988869d75808c0b54283425a4305761ae28fb2650
MD5 685311e10c18192cca3042ae7ba3660e
BLAKE2b-256 f6e2a5bad27eb6071554130bdc85d836b3387ed47ee533600e98fb9d9ab3d3d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for topologize-0.0.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl:

Publisher: ci.yml on tdamsma/topologize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file topologize-0.0.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl.

File metadata

File hashes

Hashes for topologize-0.0.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Algorithm Hash digest
SHA256 8b998e59a8533006da569496b4c956243a07e9ccfd98dd55c9161f927a52d7b6
MD5 790546f1d8ec10a8b4ce24b98b08d17b
BLAKE2b-256 7f02e8558474f4977c4a47c10ba3d1146159dccb3a7ea55b16a94419f581b927

See more details on using hashes here.

Provenance

The following attestation bundles were made for topologize-0.0.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl:

Publisher: ci.yml on tdamsma/topologize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file topologize-0.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for topologize-0.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 95c22f65690b67143b67666f21a11a61b99971ba54752ff7016a09fed93909ee
MD5 4960d026870b72530020dc38326bf4d1
BLAKE2b-256 bc4e2bc9c1c4a2160b5ab27ebe29f237298d6fcc14cddab4498d6b4b4c81e365

See more details on using hashes here.

Provenance

The following attestation bundles were made for topologize-0.0.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: ci.yml on tdamsma/topologize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file topologize-0.0.6-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for topologize-0.0.6-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a8c4b350d5218aeb5783e4e37d4c8df64ae35976943d6f343def9bb1f363dec3
MD5 6c983932c34c23ba6ac9efa6eb5e2910
BLAKE2b-256 04eabfb79250a7226339640e45d6c42ee2eda271cc01ddcce188bb19596a00b3

See more details on using hashes here.

Provenance

The following attestation bundles were made for topologize-0.0.6-cp312-cp312-macosx_11_0_arm64.whl:

Publisher: ci.yml on tdamsma/topologize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file topologize-0.0.6-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for topologize-0.0.6-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 fdfc1955fb3f17bb508fd1d3b040f207ca2c0463a60e9df2b35c48557e768a82
MD5 c81c30de820e40e1f21fb1cb39b7016e
BLAKE2b-256 3d2d29d2fd79ad2a9b16fd3f3fa99f33ea02fc2db95faa24252609a735523281

See more details on using hashes here.

Provenance

The following attestation bundles were made for topologize-0.0.6-cp312-cp312-macosx_10_12_x86_64.whl:

Publisher: ci.yml on tdamsma/topologize

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page