Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

05 — Patching: grid → process → stitch

Patching — grid sample → per-chip op → stitch

This notebook is the gridding & patching walkthrough: take one large Sentinel-2 scene that is too big to feed to a tile-sized model (or a memory-constrained operator), break it into overlapping chips with geopatcher’s SpatialPatcher, run a per-chip operator over each, then blend the results back into a seamless full-resolution raster.

geotoolz.patch_ops ships three thin Operator wrappers around geopatcher so the whole flow composes inside a single Sequential:

StageOperatorWhat it does
splitgz.patch_ops.GridSampler(patcher)Field → list[Patch]
applygz.patch_ops.ApplyToChips(op)list[Patch] → list[Patch] (runs op on each patch.data)
stitchgz.patch_ops.Stitch(aggregation, domain=…)list[Patch] → Field (overlap-add or majority-vote merge)

Scenario.

AOI bbox (EPSG:4326)(-120.10, 38.92, -119.93, 39.27) (Lake Tahoe main body)
Date range2024-06-01 to 2024-07-15
STACMicrosoft Planetary Computer (sentinel-2-l2a)
MGRS tile10SGJ
Per-chip op`gz.radiometry.DNToReflectance
Patcher256 × 256 chips, 64-pixel stride overlap, Hann window
StitchingSpatialOverlapAdd — overlap-add blend across windowed chips

1. Load one Sentinel-2 scene

Same plumbing as the Lake Tahoe operators notebook — STAC search → rioxarray → wrap as a GeoTensor. Here we stack Red + NIR into one 2-band carrier; that’s what the per-chip operator expects.

using S2B_MSIL2A_20240614T183919_R070_T10SGJ_20240615T003207  cloud_cover=0.01%
rn_gt: shape=(2, 3935, 1599)  dtype=uint16

2. Wrap as a Field

geopatcher’s SpatialPatcher operates on Field objects. RasterField is the thin adapter for any georeader reader — including a GeoTensor, which doubles as one. The same Field protocol generalises to XarrayField, RioXarrayField, GeoPandasField, XvecField, DaskField for non-raster sources.

field.domain.shape = (2, 3935, 1599)
field.domain.bounds = (750180.0, 4311890.0, 766170.0, 4351240.0)
field.domain.crs    = EPSG:32610

3. Build a SpatialPatcher

SpatialPatcher decomposes “how to slide a window over a Field” into four orthogonal axes:

AxisPickJob
geometrySpatialRectangular(size=(256, 256))shape & size of each chip
samplerSpatialRegularStride(step=(192, 192))where chips are placed (regular grid with 25 % overlap)
windowSpatialHann()the windowing function applied for blending
aggregationSpatialOverlapAdd()how overlapping chips combine on merge

Swap any axis independently — SpatialRegularStride for tiling, SpatialPoissonDisk for training-chip sampling, SpatialBoxcar instead of SpatialHann to see seams, SpatialHardVote for segmentation merges, etc.

split produced 140 chips of size (2, 256, 256)

Visualise the patch grid: show one true-color preview of the scene with chip anchors overlaid.

<Figure size 700x1100 with 1 Axes>

4. The full pipeline — Sequential([GridSampler, ApplyToChips, Stitch])

Per-chip operator: DN → reflectance → NDVI. Same gz.radiometry / gz.indices ops we used for the full scene in the Lake Tahoe notebook, only now they execute inside each chip. The patcher handles split + stitch.

Sequential([GridSampler(patcher={'geometry': {'class': 'SpatialRectangular', 'config': {'size': [256, 256]}}, 'sampler': {'class': 'SpatialRegularStride', 'config': {'step': [192, 192]}}, 'window': {'class': 'SpatialHann', 'config': {}}, 'aggregation': {'class': 'SpatialOverlapAdd', 'config': {'streaming': False, 'target_path': None, 'chunks': None, 'normalize_by_window': True}}}), ApplyToChips(operator={'class': 'Sequential', 'config': {'operators': [{'class': 'DNToReflectance', 'config': {'scale': 0.0001, 'offset': 0.0, 'axis': 0}}, {'class': 'NDVI', 'config': {'nir_idx': 1, 'red_idx': 0, 'axis': 0, 'eps': 1e-10}}]}}), Stitch(aggregation={'class': 'SpatialOverlapAdd', 'config': {'streaming': False, 'target_path': None, 'chunks': None, 'normalize_by_window': True}})])
stitched output: (2, 3935, 1599) float64

A note on output shape

The field’s spatial domain is (2, H, W) — two bands. SpatialOverlapAdd merges the chip outputs into that same shape, so even though each chip’s NDVI is a single-channel (256, 256) map, the aggregation broadcasts it back over the 2-band field shape. The two output channels are identical NDVI replicas — we collapse to a single map by taking channel 0.

channels identical? True
collapsed stitched shape: (3935, 1599)

5. Compare against the full-scene NDVI

Sanity check: the stitched per-chip NDVI should match the result of running NDVI on the whole scene at once. They will agree across the interior; edge effects appear at the field boundary where the patcher’s boundary="drop" policy leaves a margin of pixels uncovered by any chip, and around the seam between non-overlapping tile rows / columns.

<Figure size 1800x800 with 6 Axes>
abs diff: mean=0.0378  max=1.0000

6. Window choice matters — Hann vs Boxcar

The window controls how chip contributions are blended where they overlap. A Hann window tapers smoothly to zero at the edges, so overlap-add averages chip centres without leaving seams. A Boxcar window is uniform — fast, but visible tile boundaries appear wherever chips overlap unevenly (or, when they don’t overlap, wherever per-chip content statistics differ across the seam).

Swap one axis of the patcher, re-run the same Sequential, look at the residual.

<Figure size 1300x800 with 2 Axes>
Boxcar abs diff: mean=0.0374
Hann   abs diff: mean=0.0378

7. Recap

gz.patch_ops is three operators. The composition core does the rest:

  • GridSampler(patcher) turns a Field into a list of chips.
  • ApplyToChips(op) is a map over chips; the inner op sees a regular GeoTensor of fixed chip size.
  • Stitch(aggregation, domain=…) merges the chip outputs back to the field’s domain via overlap-add (continuous outputs) or majority-vote (segmentation labels).

The four SpatialPatcher axes (geometry / sampler / window / aggregation) are independent — pick them for the task:

TaskPick
Tiled inference of a continuous mapSpatialRectangular + SpatialRegularStride + SpatialHann + SpatialOverlapAdd (this notebook)
Training-chip dataset for a segmentation modelSpatialRectangular + SpatialPoissonDisk + SpatialBoxcar + SpatialHardVote
Per-polygon zonal statisticsSpatialPolygonIntersection + SpatialExplicit + SpatialBoxcar + SpatialMean
Hexagonal h3 cells over a vector fieldSpatialKNNGraph + SpatialExplicit + SpatialBoxcar + SpatialMean

The ML / augmentation slice of the same machinery — chip extraction, gz.augment chain, ModelOp inference, vote-stitched classification — lives in ml_patches.

For the cross-package end-to-end (STAC discovery → catalog domain → patcher → operators), see the canonical multi-repo notebook: geocatalog/docs/notebooks/end_to_end_lake_tahoe.ipynb.