Skip to content

API Reference

All functions are importable from the top-level contourrs package.

shapes

def shapes(
    source: NDArray,
    mask: NDArray[np.bool_] | None = None,
    connectivity: int = 4,
    transform: Affine | tuple[float, ...] | None = None,
    nodata: int | float | None = None,
) -> list[tuple[dict, float]]

Extract polygon shapes from a raster array.

Parameters:

Parameter Type Default Description
source NDArray required 2D numpy array (uint8/16/32, int16/32, float32/64)
mask NDArray[np.bool_] None 2D boolean array. True = include pixel
connectivity int 4 Pixel neighborhood: 4 or 8
transform Affine \| tuple[float, ...] None affine.Affine or (a, b, c, d, e, f)
nodata int \| float None Exclude pixels equal to this value; np.nan excludes NaNs

Returns: list[tuple[dict, float]] — GeoJSON geometry dicts paired with pixel values. Returns an eager list; rasterio.features.shapes returns an iterator.

Connectivity applies to every unmasked raster value, including zero. With connectivity=8, diagonal neighbors share a region label, but parts that touch only at a pixel corner are emitted as separate Polygon geometries. The number of output polygons can therefore exceed the number of connected regions and differ from rasterio's eight-connected output.

When a boundary revisits a pixel corner, it is split into simple rings before the affine transform is applied. An enclosed ring becomes a hole, including when that hole touches its shell at a single corner. This preserves pixel coverage without a geometry-repair step. shapes_arrow() uses the same tracing and polygon assembly.

Example:

import numpy as np
from contourrs import shapes

raster = np.array([[1, 1, 2], [1, 2, 2], [3, 3, 3]], dtype=np.uint8)
results = shapes(raster, connectivity=4)

for geojson, value in results:
    print(f"value={value}, type={geojson['type']}")

shapes_arrow

def shapes_arrow(
    source: NDArray,
    mask: NDArray[np.bool_] | None = None,
    connectivity: int = 4,
    transform: Affine | tuple[float, ...] | None = None,
    nodata: int | float | None = None,
) -> pyarrow.Table

Extract polygon shapes as a PyArrow Table with WKB geometry.

Zero-copy from Rust via Arrow C Data Interface. The table includes GeoParquet metadata for direct Parquet export and avoids constructing Python geometry dictionaries. The historical benchmarks include 5-6x speedups over shapes() for some raster sizes; see performance for all sizes and conditions.

Parameters: Same as shapes().

Returns: pyarrow.Table with columns:

Column Type Description
geometry Binary WKB-encoded polygons
value Float64 Pixel value

Example:

from contourrs import shapes_arrow
import pyarrow.parquet as pq

table = shapes_arrow(raster, connectivity=4)
pq.write_table(table, "polygons.parquet")

contours

def contours(
    source: NDArray,
    thresholds: list[float],
    mask: NDArray[np.bool_] | None = None,
    transform: Affine | tuple[float, ...] | None = None,
    nodata: int | float | None = None,
) -> list[tuple[dict, float]]

Generate filled contour (isoband) polygons from a continuous raster.

Uses marching squares to produce polygons between consecutive threshold pairs. Returns the same format as shapes() for compatibility.

Parameters:

Parameter Type Default Description
source NDArray required 2D numpy array (uint8/16/32, int16/32, float32/64)
thresholds list[float] required Break values; bands formed from consecutive pairs (min 2)
mask NDArray[np.bool_] None 2D boolean array. True = include pixel
transform Affine \| tuple[float, ...] None affine.Affine or (a, b, c, d, e, f)
nodata int \| float None Exclude pixels equal to this value; np.nan excludes NaNs

Returns: list[tuple[dict, float]] — GeoJSON geometry dicts paired with the lower threshold of each band.

Example:

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'])}")

contours_arrow

def contours_arrow(
    source: NDArray,
    thresholds: list[float],
    mask: NDArray[np.bool_] | None = None,
    transform: Affine | tuple[float, ...] | None = None,
    nodata: int | float | None = None,
) -> pyarrow.Table

Generate filled contour polygons as a PyArrow Table with WKB geometry.

Same as contours() but returns an Arrow Table via zero-copy FFI. The table includes GeoParquet-compatible metadata.

Parameters: Same as contours().

Returns: pyarrow.Table with columns:

Column Type Description
geometry Binary WKB-encoded polygons
value Float64 Band lower threshold

Example:

from contourrs import contours_arrow
import pyarrow.parquet as pq

dem = np.random.default_rng(42).random((256, 256)).astype(np.float32)
table = contours_arrow(dem, thresholds=[0.25, 0.5, 0.75])
pq.write_table(table, "contours.parquet")

Supported dtypes

All functions require two-dimensional, C-contiguous arrays. Use np.ascontiguousarray(source) after slicing or transposing an array if needed; masks must also be C-contiguous. All functions accept the following input types, and output coordinates use f64.

Input dtype Python type Precision
uint8 np.uint8 Lossless -> f64
uint16 np.uint16 Lossless -> f64
uint32 np.uint32 Lossless -> f64
int16 np.int16 Lossless -> f64
int32 np.int32 Lossless -> f64
float32 np.float32 Promoted to f64
float64 np.float64 Native (zero-copy in contours)

Contour coordinates

Contours interpolate between samples at integer (col, row) coordinates. Without an affine transform, the contour domain spans [0, width - 1] by [0, height - 1]. Categorical polygonization instead follows pixel footprints spanning [0, width] by [0, height]. Masked and nonfinite samples are excluded from contour regions.

Mask and nodata semantics

  • mask uses rasterio-style semantics: True includes a pixel, False excludes it
  • nodata= is optional sugar for building that exclusion mask from the source array
  • If both mask and nodata are passed, the effective mask is mask & (source != nodata)
  • nodata=np.nan excludes NaN pixels

Coordinate reference systems

An affine transform changes coordinates but does not assign a coordinate reference system (CRS). The Arrow output declares an unknown CRS (null); it does not infer a CRS from the transform. Assign the known CRS before exporting georeferenced data, for example:

import geopandas as gpd

gdf = gpd.GeoDataFrame.from_arrow(table).set_crs("EPSG:32615")
gdf.to_parquet("polygons.parquet")

Choose the EPSG code that describes the input raster, not the example code above.

Affine transform

The transform parameter accepts either an affine.Affine instance or a 6-element tuple (a, b, c, d, e, f) representing the affine transformation:

x' = a * col + b * row + c
y' = d * col + e * row + f

This matches the convention used by rasterio and GDAL. When None, pixel coordinates are used directly (identity transform).