Skip to content

Getting Started

Installation

pip install contourrs
uv add contourrs

Pre-built wheels are available for Linux, macOS, and Windows on Python 3.12+.

Development setup

git clone https://github.com/taylor-geospatial/contourrs.git
cd contourrs
uv sync --extra dev
uv run maturin develop --release

Run tests:

uv run pytest tests/ -v

Pre-commit hooks:

uv run pre-commit install
uv run pre-commit run --all-files
uv run pyrefly check

Rust dependencies shared by both crates and their lint settings live in the root Cargo.toml. The checks apply selected BeautifulRust practices, including documented public APIs, explicit unsafe invariants, checked conversions, and no unexplained production unwrap() calls. Clippy covers all targets and features; warnings become errors in pre-commit and CI.

For Rust API changes, also check the default feature set, release-mode bounds, and generated documentation:

cargo test -p contourrs --no-default-features
cargo test -p contourrs --release --test raster_access
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps

First usage

Polygonize a categorical raster

import numpy as np
from contourrs import shapes

raster = np.array(
    [
        [1, 1, 2],
        [1, 2, 2],
        [3, 3, 3],
    ],
    dtype=np.uint8,
)

for geojson, value in shapes(raster, connectivity=4):
    print(f"value={value}, type={geojson['type']}")

Each geojson is a standard GeoJSON Polygon geometry dict. The value is the pixel value for that region.

Contour a continuous raster

import numpy as np
from contourrs import contours

dem = np.random.default_rng(42).random((256, 256)).astype(np.float32)

for geojson, value in contours(dem, thresholds=[0.25, 0.5, 0.75]):
    print(f"band={value}, rings={len(geojson['coordinates'])}")

Thresholds define the break values between bands. Each band covers the interval [lo, hi) between consecutive thresholds.

Arrow output (zero-copy)

For best performance and GeoParquet export, use the Arrow variants:

from contourrs import shapes_arrow, contours_arrow

table = shapes_arrow(raster, connectivity=4)
# table.schema: geometry (binary/WKB), value (float64)

import pyarrow.parquet as pq

pq.write_table(table, "output.parquet")

Excluding nodata values

If your array uses a sentinel nodata value, pass it directly:

from contourrs import shapes

polygons = shapes(raster, nodata=0, connectivity=4)

Using an Affine transform

You can pass an affine.Affine directly without converting it to a tuple:

from affine import Affine
from contourrs import shapes

transform = Affine(10.0, 0.0, 500000.0, 0.0, -10.0, 4500000.0)
polygons = shapes(raster, transform=transform)

Building the docs

Install the docs dependencies with uv sync --extra docs, then run bash scripts/build_tutorials.sh and uv run --extra docs zensical build --clean. Run make docs to preview the generated site. The build executes the quickstart, DEM, and CDL notebooks and converts the TorchGeo notebook without running inference. Cells tagged skip_ci are omitted from executed tutorials.