ramify
Characterize binary branching shapes (e.g. rivers, floodplains, glaciers,
roots, veins...). Given a shape mask, a root point, and (optionally) branch
tips, ramify extracts a topology-aware centerline network, decomposes it into
hierarchically ordered paths, allocates every pixel of the shape to its path,
and estimates local width everywhere.
Install
pip install ramify
Development (clone, then sync with dev extras):
git clone https://github.com/avkoehl/ramify.git
cd ramify
uv sync --extra dev
Usage
import ramify
from ramify.data import load
mask, root, tips = load() # bundled toy dataset
net = ramify.extract_centerlines(mask, root, tips=tips) # centerline network of ordered paths
regions = ramify.partition_by_priority(mask, net.rasterize(by="path"))
widths = ramify.interpolate_widths(mask, net.rasterize(), regions)
net.segments # DataFrame: segment_id, path_id, strahler,
# length, weight, downstream_segment_id
regions # labeled raster: each pixel -> its path
widths # float raster: local width everywhere
Inputs are np.ndarray (with pixel_size=) or georeferenced xr.DataArray;
outputs match the input type. root and tips are (row, col) pixel coordinates.
Components
Each individual component is presented below.
Centerlines
net = ramify.extract_centerlines(mask, root, tips=tips)
Skeletonizes the mask, routes from each tip to the root (pruning everything else),
and decomposes the network into ordered paths — path_id == 1 is the mainstem.
net = ramify.extract_centerlines(mask, root)
Without tips, every skeleton endpoint becomes a tip.
Tips and root can often be derived automatically — glacier branch tips Kienholz et al., 2014, channel initiation points, or the lowest point on the boundary as the root — or simply digitized in GIS software.
Partitioning
regions = ramify.partition_by_priority(mask, net.rasterize(by="path"))
Assigns every pixel to a path: paths claim territory in priority order, each limited by the local shape radius, so wide branches claim proportionally more space at junctions.
regions = ramify.partition_by_nearest(mask, net.rasterize(by="path"))
Nearest-centerline partition — no ordering, no radius limits.
seg_regions = ramify.subdivide_regions(regions, net)
Subdivides each path's territory further: within a territory, every pixel goes to its nearest centerline segment of that same path.
Widths
Exact widths (twice the distance to the boundary) are taken at the centerline and interpolated across the shape. That interpolation runs either over the whole shape or independently within each region, which keeps junction-zone pixels from averaging between a branch and its mainstem:
w = ramify.interpolate_widths(mask, net.rasterize()) # whole shape
w = ramify.interpolate_widths(mask, net.rasterize(), regions=regions) # per region
Either call also takes method="nearest", which gives each pixel the width of its
nearest centerline pixel instead of diffusing smoothly from it — piecewise
constant, and much faster:
w = ramify.interpolate_widths(mask, net.rasterize(), method="nearest")
Open boundaries
Everything above measures local half-width as the distance from each pixel to the shape's boundary, and that half-width drives three things: which branch is the mainstem, how far each path claims territory, and the width field. By default every boundary pixel is treated as a wall. Sometimes part of the boundary is not a real wall — the shape is truncated by open water, the data extent, or another medium — and treating it as one makes the half-width collapse to zero there.
Pass open_boundary: a binary mask, on the same grid as the shape, marking the
non-wall (void) pixels. Distances are then measured only to the remaining real
walls. It is optional — omitted, every boundary is a wall (the behaviour above) —
and accepted by extract_centerlines, partition_by_priority, and interpolate_widths. Give it to
every step, so all three stages measure against the same walls:
net = ramify.extract_centerlines(mask, root, tips=tips, open_boundary=open_boundary)
regions = ramify.partition_by_priority(mask, net.rasterize(by="path"), open_boundary=open_boundary)
widths = ramify.interpolate_widths(mask, net.rasterize(), regions,
open_boundary=open_boundary)
Below, the same mask, root, and tips are reused, but the void past the outlet is marked open (shaded red), and the outlet widths no longer taper to the cut edge. Mark a region with depth rather than a thin skin along the boundary: distances are measured through the open void, so a one-pixel rind would only push the wall out by one pixel.
Only the widths are shown because on this shape the partitioning didn't change.
Release files for ramify 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ramify-1.3.0.tar.gz | 477.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ramify-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 500.7 kB
Release files / ramify-1.3.0.tar.gz
| Download URL | ramify-1.3.0.tar.gz |
|---|---|
| Size | 477.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
89386d847770edce81d4afab49ed32f1461e07d784c2c13fa078a0920bb70f74
|
|
BLAKE2b-256 checksum How to use checksums |
5370159532a8dc2201323f290fb9b2cf7d5778d97cb2720177833a6a9e95e4c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ramify-1.3.0-py3-none-any.whl
| Download URL | ramify-1.3.0-py3-none-any.whl |
|---|---|
| Size | 23.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
60dab9969c00dfc85e77a27c578ccccf5fd31f027825fd4e9f4a4668180a5250
|
|
BLAKE2b-256 checksum How to use checksums |
4ea30d6cd4f5c83dd922959cf1068d2e2bb36f6a6d4a3698b3a4f606efcc7072
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|