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.

Window utils

Pixel ↔ geographic coordinate math

UNEP
IMEO
MARS

Module: georeader/window_utils.py (1471 LOC, the second-densest in diagrams) Role: the math underneath everything else in the package. Windows, bounds, transforms, rounding, padding, polygon reprojection. Reading these utilities once is the cheapest way to understand why the higher-level read.py API is shaped the way it is.


1. What this module owns

Three responsibilities in one file:

  1. Windows ↔ bounds. Convert between pixel-space rectangles (rasterio.windows.Window) and geographic rectangles (minx, miny, maxx, maxy) — both directions, both signs of CRS mismatch.

  2. Padding & rounding. Grow / shrink / round windows so they align with pixel boundaries, fit a fixed CNN input size, or include all partial pixels intersected by a query.

  3. Polygon ↔ pixel. Reproject Shapely geometries between CRSs and convert their vertices to pixel-coordinate paths (used by rasterize and vectorize downstream).

A single design decision sits underneath all of it: PIXEL_PRECISION = 3 decimal places. Coordinate transforms drift through reprojection round-trips, so the module deliberately tolerates ≤ 0.001-pixel error before deciding “this is or isn’t an integer offset.”


2. Window anatomy

A rasterio.windows.Window is a rectangle in pixel space — not geographic space. The constructor argument order is the source of most bugs that touch this module.

┌─────────────────────────────────────────────────────────────────────────┐
│                    RASTERIO WINDOW ANATOMY                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│    Full Raster (0,0 at top-left)                                        │
│    ↓                                                                     │
│    ┌────────────────────────────────────────────────────────┐           │
│    │ (0,0)                                        cols →    │           │
│    │     ┌────────────────────┐                             │           │
│    │     │← col_off →│        │                             │           │
│    │     │    (row_off, col_off) ← Window origin            │           │
│    │     │           ·─────────────────┐                    │           │
│  r │     │           │    WINDOW       │                    │           │
│  o │     │           │                 │ height             │           │
│  w │     │           │    width        │                    │           │
│  s │     │           └─────────────────┘                    │           │
│    │     │                                                  │           │
│  ↓ │     │                                                  │           │
│    └────────────────────────────────────────────────────────┘           │
│                                                                          │
│    Window = rasterio.windows.Window(col_off, row_off, width, height)    │
│                                     ───────  ───────  ─────  ──────     │
│                                     column   row      cols   rows       │
│                                     offset   offset                     │
└─────────────────────────────────────────────────────────────────────────┘

NOTE: Window constructor order is (col_off, row_off) but most geospatial operations use (row, col) or (y, x) order. Be careful!

That note is in the source verbatim and it earns its caps. Three places this trips people up:

The module commits to numpy order for axis-tuple arguments because that’s what users coming from np.pad expect. The Window constructor order is fixed by rasterio.


3. Window ↔ bounds — the core conversion

┌─────────────────────────────────────────────────────────────────────────┐
│              WINDOW ↔ BOUNDS TRANSFORMATION                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│    WINDOW (pixels)              AFFINE TRANSFORM           BOUNDS       │
│    ┌─────────────┐                    ║                ┌─────────────┐  │
│    │ col_off=100 │                    ║                │ minx=-122.5 │  │
│    │ row_off=200 │   ──────────────►  ║  ──────────►   │ miny=37.0   │  │
│    │ width=256   │   window_bounds()  ║                │ maxx=-122.0 │  │
│    │ height=256  │                    ║                │ maxy=37.5   │  │
│    └─────────────┘                    ║                └─────────────┘  │
│                                       ║                                  │
│                                       ║   Affine(a, b, c,               │
│                      ◄────────────────║          d, e, f)               │
│                      bounds_to_windows()                                │
│                                       ║                                  │
│    Affine Transform encodes:          ║                                  │
│    • Pixel resolution (a, e)          ║                                  │
│    • Origin coordinates (c, f)        ║                                  │
│    • Rotation/shear (b, d)            ║                                  │
└─────────────────────────────────────────────────────────────────────────┘

Two named functions handle the round-trip:

The asymmetry — one direction is geometry math, the other is CRS-aware — is why this module is bigger than it might first seem.


4. Rounding: outer vs inner

Bounds rarely land on integer pixel boundaries. You have to choose which way to round.

┌─────────────────────────────────────────────────────────────────────────┐
│                    WINDOW ROUNDING STRATEGIES                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Exact bounds (before rounding):                                       │
│   ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐                                │
│   │           Desired area             │                                │
│   └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘                                │
│                                                                          │
│   round_outer_window():                 round_inner_window():           │
│   ┌─────────────────────────────┐      ┌─────────────────────┐         │
│   │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐  │      │  ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │         │
│   │ │                       │  │      │  │               │  │         │
│   │ │   Expands outward     │  │      │  │ Shrinks inward │  │         │
│   │ │   to include all      │  │      │  │ to only fully  │  │         │
│   │ │   partial pixels      │  │      │  │ covered pixels │  │         │
│   │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘  │      │  └ ─ ─ ─ ─ ─ ─ ─ ┘ │         │
│   └─────────────────────────────┘      └─────────────────────┘         │
│                                                                          │
│   Use outer when: You need all data that intersects the bounds          │
│   Use inner when: You need only data fully within the bounds            │
└─────────────────────────────────────────────────────────────────────────┘

Default in georeader: outer. Reasons:

round_inner_window shows up in two niche cases: when you’re cropping to a strict mask (no partial-pixel contamination), and when you’re computing the largest fully covered sub-window for tile-aligned reads.

Both functions tolerate PIXEL_PRECISION = 3 slop before rounding — the example in the docstring: 99.9997 → 100, 100.5 → unchanged. That tolerance is what lets reprojection round-trips not slowly drift the window by sub-pixel amounts each pass.


5. Padding for CNN tiles

Two related functions, both in the “make my tile a fixed size” job.

pad_window(window, pad_size=(pad_rows, pad_cols))

Symmetric expansion on all four sides. Used to add context around an inference tile.

Original window:              Padded window (pad_size=(2, 3)):
┌─────────────┐               ┌─────────────────────┐
│             │               │← 3 cols → ← 3 cols →│
│   100×50    │     ───►      │↑         ↑          │
│   window    │               │2   106×54 window    │
│             │               │↓         ↓          │
└─────────────┘               │← 3 cols → ← 3 cols →│
                              └─────────────────────┘

Output: width = 100 + 2×3 = 106
        height = 50 + 2×2 = 54
        col_off = original - 3
        row_off = original - 2

The output window may have negative offsets — that’s intentional, and you read it via read_from_window(..., boundless=True) to pad the off-edge region with fill_value_default. (Chapter 3 §6.)

pad_window_to_size(window, size=(height, width))

Re-centre to a target size. Both expansion and contraction work — passing a size smaller than the window gives you a centre crop.

Expansion (size > window):          Contraction (size < window):

┌───────────────────────┐          ┌───────────────────────┐
│     padded area       │          │                       │
│   ┌─────────────┐     │          │   ┌─────────────┐     │
│   │             │     │          │   │┌───────────┐│     │
│   │   original  │     │    ◄──   │   ││  center   ││     │
│   │    window   │     │          │   ││   crop    ││     │
│   └─────────────┘     │          │   │└───────────┘│     │
│     padded area       │          │   └─────────────┘     │
└───────────────────────┘          └───────────────────────┘

Symmetric expansion             Symmetric contraction

Odd differences favour the bottom/right edge (integer division). When you need exact symmetry, use pad_window with explicit pad_size.


6. figure_out_transform — building output grids

When mosaicking, reprojecting, or designing an output grid, you usually have some of {transform, bounds, resolution_dst} and want georeader to derive the rest.

┌────────────┬────────┬──────────────┬─────────────────────────────┐
│ transform  │ bounds │ resolution   │ Result                      │
├────────────┼────────┼──────────────┼─────────────────────────────┤
│ ✓          │ ✗      │ ✗            │ Return unchanged            │
│ ✓          │ ✗      │ ✓            │ Rescale resolution          │
│ ✓          │ ✓      │ ✗            │ Shift origin to bounds      │
│ ✓          │ ✓      │ ✓            │ Rescale + shift             │
│ ✗          │ ✓      │ ✓            │ Create new rectilinear      │
│ ✗          │ ✗      │ any          │ ERROR (need bounds)         │
│ ✗          │ ✓      │ ✗            │ ERROR (need resolution)     │
└────────────┴────────┴──────────────┴─────────────────────────────┘

Useful idioms:

The “ERROR” rows are validation: you can’t conjure an origin without bounds, and you can’t size a grid without resolution.

This function is the seam between read.read_from_bounds (which calls it) and the higher-level mosaic / reprojection workflows in read.py (Chapter 5).


7. Polygon ↔ pixel — exterior coordinates

window_polygon is the geometry-aware sibling of window_bounds. Returns a Shapely Polygon rather than a 4-tuple — important when the transform has rotation/shear (b ≠ 0 or d ≠ 0), where the bounding box and the actual footprint differ.

window_surrounding=False (default):     window_surrounding=True:
Polygon includes full pixels             Polygon passes through pixel centers

┌───┬───┬───┬───┐                       ┌───┬───┬───┬───┐
│ P │ P │ P │ P │ ◄─ Polygon            │ · │ · │ · │ · │
├───┼───┼───┼───┤    edges              ├───○───○───○───┤
│ P │ P │ P │ P │    touch              │ · │ · │ · │ · │ ◄─ Polygon
├───┼───┼───┼───┤    pixel              ├───○───○───○───┤    passes
│ P │ P │ P │ P │    boundaries         │ · │ · │ · │ · │    through ○
└───┴───┴───┴───┘                       └───┴───┴───┴───┘

The window_surrounding flag picks between two valid pixel-footprint conventions:

The default matches GIS convention; the alternative is needed when interfacing with point-sampling tools (some scipy/skimage routines treat pixels as samples by default).

Companion functions:


8. The “boundless reading” mechanics — get_slice_pad

This is the function that makes read_from_window(boundless=True) actually work, and it deserves a callout because the behaviour seems magical until you see it.

Given two windows:

…it returns:

The reader then reads the slice from disk and applies np.pad (or its own fill_value_default-aware equivalent) to produce a full-size array. CNN inference at scene edges relies entirely on this — every chip comes back the requested shape, with off-edge regions filled with nodata.

Source: window_utils.py:599.


9. Tiling-and-stitching: slice_save_for_pred

The companion to pad_window for inference pipelines: when you’ve read a tile with padded context, run a CNN, and now want to write only the centre back into a global output, this function tells you which slice to extract from the prediction.

1. Read overlapping tiles with padding to avoid edge artifacts
2. Run CNN inference on padded tiles
3. Extract only the center region (removing padding) for final output
4. Stitch extracted regions together to form complete prediction

The reference is Huang et al. (2018) — the standard tile-and-stitch recipe. Used by ml4floods and similar segmentation pipelines built on georeader.

Source: window_utils.py:1256.


10. Function reference

Grouped by purpose. Every function takes / returns rasterio.Affine, rasterio.windows.Window, or shapely Polygon/MultiPolygon — no GeoTensor/Reader anywhere in this module.

Padding & rounding

Windows ↔ bounds

Transforms

Polygons

Convenience indexes

(Functions starting with _ like _is_exact_round are internal precision helpers and aren’t part of the public API.)


11. Sharp edges


12. Why this module matters for geotoolz

Three concrete things geotoolz.sampling and geotoolz.inference will lean on:

  1. pad_window + slice_save_for_pred is the entire ApplyToChips machinery. Read with context, predict, save the centre, stitch.

  2. bounds_to_windows is what a BoundingBoxSampler (TorchGeo-style) calls under the hood when chips are specified in geographic coords rather than pixel coords.

  3. figure_out_transform is the “design my output grid” function. Every reprojection-aware operator (mosaicking, ensemble averaging across scenes) needs it.

You can build the whole geotoolz.sampling module without touching anything in georeader except this file plus read.py. That’s a clean cut for the operator layer.

Next chapter: Read — the high-level reading API (read_from_bounds, read_from_polygon, read_from_center_coords, reprojection / resampling). The single densest module in the package by diagram count, and the one most users touch first.