Zonal stats along line segments

This quick example shows off using rasterize(), and Xarray’s GroupBy to do zonal stats along a line.

import numpy as np
import pyproj
import shapely
import xarray as xr

ds = xr.Dataset(
    coords={
        "spatial_ref": (
            (),
            0,
            pyproj.CRS.from_epsg(3035).to_cf() | {"GeoTransform": "100000 12000 0 60000 0 -12000"},
        )
    },
)
ds["foo"] = xr.DataArray(
    np.random.default_rng(0).integers(0, 256, size=(100, 200), dtype=np.uint8),
    dims=("y", "x"),
)
ds
<xarray.Dataset> Size: 20kB
Dimensions:      (y: 100, x: 200)
Coordinates:
    spatial_ref  int64 8B 0
Dimensions without coordinates: y, x
Data variables:
    foo          (y, x) uint8 20kB 95 130 194 217 207 235 ... 155 69 155 2 28 23

Generate coordinates with RasterIndex

We’ll use assign_index() to assign lazy coordinate locations in x and y.

<xarray.Dataset> Size: 22kB
Dimensions:      (y: 100, x: 200)
Coordinates:
  * y            (y) float64 800B 5.4e+04 4.2e+04 ... -1.122e+06 -1.134e+06
  * x            (x) float64 2kB 1.06e+05 1.18e+05 ... 2.482e+06 2.494e+06
    spatial_ref  int64 8B 0
Data variables:
    foo          (y, x) uint8 20kB 95 130 194 217 207 235 ... 155 69 155 2 28 23
Indexes:
  ┌ x        RasterIndex (crs=None)
  └ y

Make a geometry

We will take a LineString and split it in to 80km segments.

For simplicity the line will extend from the top-left corner to the bottom-right corner.

import geopandas as gpd

from rasterix.rasterize import rasterize

# make a diagonal line across the raster
line = gpd.GeoDataFrame(
    {
        "geometry": [
            shapely.LineString(
                [
                    [ds.x[0].item(), ds.y[0].item()],
                    [ds.x[-1].item(), ds.y[-1].item()],
                ]
            )
        ]
    }
)
# 80km segments
line = line.segmentize(80_000)
coords = shapely.get_coordinates(line)
segment_coords = np.stack([coords[:-1], coords[1:]], axis=1)  # shape (n-1, 2, 2)
segments = shapely.linestrings(segment_coords)
geoms = gpd.GeoDataFrame({"geometry": segments})
geoms
geometry
0 LINESTRING (106000 54000, 176235.294 19058.824)
1 LINESTRING (176235.294 19058.824, 246470.588 -...
2 LINESTRING (246470.588 -15882.353, 316705.882 ...
3 LINESTRING (316705.882 -50823.529, 386941.176 ...
4 LINESTRING (386941.176 -85764.706, 457176.471 ...
5 LINESTRING (457176.471 -120705.882, 527411.765...
6 LINESTRING (527411.765 -155647.059, 597647.059...
7 LINESTRING (597647.059 -190588.235, 667882.353...
8 LINESTRING (667882.353 -225529.412, 738117.647...
9 LINESTRING (738117.647 -260470.588, 808352.941...
10 LINESTRING (808352.941 -295411.765, 878588.235...
11 LINESTRING (878588.235 -330352.941, 948823.529...
12 LINESTRING (948823.529 -365294.118, 1019058.82...
13 LINESTRING (1019058.824 -400235.294, 1089294.1...
14 LINESTRING (1089294.118 -435176.471, 1159529.4...
15 LINESTRING (1159529.412 -470117.647, 1229764.7...
16 LINESTRING (1229764.706 -505058.824, 1300000 -...
17 LINESTRING (1300000 -540000, 1370235.294 -5749...
18 LINESTRING (1370235.294 -574941.176, 1440470.5...
19 LINESTRING (1440470.588 -609882.353, 1510705.8...
20 LINESTRING (1510705.882 -644823.529, 1580941.1...
21 LINESTRING (1580941.176 -679764.706, 1651176.4...
22 LINESTRING (1651176.471 -714705.882, 1721411.7...
23 LINESTRING (1721411.765 -749647.059, 1791647.0...
24 LINESTRING (1791647.059 -784588.235, 1861882.3...
25 LINESTRING (1861882.353 -819529.412, 1932117.6...
26 LINESTRING (1932117.647 -854470.588, 2002352.9...
27 LINESTRING (2002352.941 -889411.765, 2072588.2...
28 LINESTRING (2072588.235 -924352.941, 2142823.5...
29 LINESTRING (2142823.529 -959294.118, 2213058.8...
30 LINESTRING (2213058.824 -994235.294, 2283294.1...
31 LINESTRING (2283294.118 -1029176.471, 2353529....
32 LINESTRING (2353529.412 -1064117.647, 2423764....
33 LINESTRING (2423764.706 -1099058.824, 2494000 ...

Rasterize the geometries

res = (
    rasterize(
        ds,
        geoms,
        all_touched=True,
        engine="rusterize",
    )
    .compute()
    .rename("segment")
)
res
<xarray.DataArray 'segment' (y: 100, x: 200)> Size: 20kB
array([[ 0,  0, 34, ..., 34, 34, 34],
       [34,  0,  0, ..., 34, 34, 34],
       [34, 34, 34, ..., 34, 34, 34],
       ...,
       [34, 34, 34, ..., 34, 34, 34],
       [34, 34, 34, ..., 33, 33, 34],
       [34, 34, 34, ..., 34, 33, 33]], shape=(100, 200), dtype=uint8)
Coordinates:
  * y            (y) float64 800B 5.4e+04 4.2e+04 ... -1.122e+06 -1.134e+06
  * x            (x) float64 2kB 1.06e+05 1.18e+05 ... 2.482e+06 2.494e+06
    spatial_ref  int64 8B 0
Indexes:
  ┌ x        RasterIndex (crs=None)
  └ y
res.plot()
<matplotlib.collections.QuadMesh at 0x7b920af0b560>
../_images/d6b8063fc78d48d9f1f651b972ce2a0f168c99bf6170bd84fdfded6deb44f798.png

Zonal stats with Xarray’s GroupBy

(faster when powered by flox!)

zonal = ds["foo"].groupby(res).max()
# drop the last segment (background/NaN pixels)
zonal = zonal.isel(segment=slice(-1))
zonal
<xarray.DataArray 'foo' (segment: 34)> Size: 34B
array([200, 219, 250, 217, 185, 241, 241, 213, 245, 237, 238, 253, 227,
       253, 253, 245, 234, 255, 242, 225, 206, 236, 229, 232, 215, 234,
       249, 212, 242, 238, 153, 178, 237, 243], dtype=uint8)
Coordinates:
  * segment      (segment) uint8 34B 0 1 2 3 4 5 6 7 ... 26 27 28 29 30 31 32 33
    spatial_ref  int64 8B 0

Make a vector data cube with Xvec

import xvec  # noqa: F401

zonal = zonal.assign_coords(segment=geoms["geometry"])
zonal = zonal.xvec.set_geom_indexes("segment", crs=3035)
zonal
<xarray.DataArray 'foo' (segment: 34)> Size: 34B
array([200, 219, 250, 217, 185, 241, 241, 213, 245, 237, 238, 253, 227,
       253, 253, 245, 234, 255, 242, 225, 206, 236, 229, 232, 215, 234,
       249, 212, 242, 238, 153, 178, 237, 243], dtype=uint8)
Coordinates:
  * segment      (segment) geometry 272B LINESTRING (106000 54000, 176235.294...
    spatial_ref  int64 8B 0
Indexes:
    segment  GeometryIndex (crs=EPSG:3035)

Plot

f, ax = zonal.xvec.plot()
f.set_size_inches((8, 2))
../_images/f33c67d4ba397cf61bf23ae26bbc9c6b11daca5aa36d2b959c9d1d78e4beb3d1.png