# Pixel coordinate conventions and geometry origin Before diving into mask generation, this notebook clarifies how GridR reasons about pixel coordinates and how geometries (Shapely polygons) are aligned with a raster. Picking the wrong convention is the most common source of off-by-half errors when masking with vector data. **What you'll learn** - How GridR defines a raster's coordinate frame (`shape`, `resolution`, `origin`) - The two community conventions for pixel-center coordinates (integer vs. half-real) and the one GridR adopts - The role of `geometry_origin` and how it lets you reuse geometries built against a different convention ## Setting things up ```python import numpy as np import shapely from gridr.core.grid import grid_commons ``` ## A pixel and its coordinate frame A pixel (short for *picture element*) is the smallest individual unit of a digital image. Conceptually, it is a tiny square dot carrying a single colour value at its centre. A raster image is just a grid of such pixels. When performing a geometric transformation on a raster image it is critical to associate a coordinate frame with it. GridR defines this frame through three attributes: - **`shape`** -- the dimensions of the raster (in pixels), as `(nrows, ncols)`. - **`resolution`** -- the integer step size between adjacent samples along the same dimension. Relevant when working with resampling grids interpreted as rasters. - **`origin`** -- the floating-point coordinates of the centre of the upper-left (first) pixel. Two conventions are widely used in the community for where a pixel's *centre* lies in its coordinate frame: - **Integer pixel centre**: the centre of a pixel falls on whole integer coordinates -- e.g. the first pixel is centred at `(0, 0)`. **GridR uses this convention.** - **Half-real pixel centre**: the centre of a pixel falls on half-real coordinates -- e.g. the first pixel is centred at `(0.5, 0.5)`. Many geometry libraries use this convention. The two figures below illustrate the same `(6, 8)` raster under each convention. Compare where the pixel centres (orange dots) sit relative to the integer grid lines. ```python shape, resolution = (6, 8), (1, 1) origin_int, origin_half = (0., 0.), (0.5, 0.5) # Grid-coordinate arrays for both conventions, used by plotting helpers. cxx_int, cyy_int = grid_commons.grid_regular_coords_2d(shape, origin_int, resolution, sparse=False) cxx_half, cyy_half = grid_commons.grid_regular_coords_2d(shape, origin_half, resolution, sparse=False) ``` ![image_coordinates_convention_integer.png](core_masking_001_conventions_files/image_coordinates_convention_integer.png) ![image_coordinates_convention_half.png](core_masking_001_conventions_files/image_coordinates_convention_half.png) ## Geometries and `geometry_origin` GridR's Python API supports Shapely `Polygon`, `MultiPolygon` and lists of `Polygon` objects as geometries. We focus on a single `Polygon` here for clarity. A geometry in GridR is defined by two things: its **shape** (the polygon coordinates themselves) and its **`geometry_origin`**. The latter is the offset to apply to the polygon's vertices to align them with GridR's coordinate frame -- it lets you keep geometries written against any convention without rewriting them. > **Note** -- Shapely polygons use the `(x, y)` coordinate order. GridR's `geometry_origin`, like `shape`, `resolution` and `origin`, uses the `(y, x)` (row, column) order. Below we define a polygon in the *half-real* convention (its vertices fall on `.5` coordinates) and tell GridR about it by setting `geometry_origin = (0.5, 0.5)`. ```python geometry = shapely.geometry.Polygon([ (3.5, 2.5), (7.5, 2.5), (7.5, 6.5), (3.5, 6.5), ]) geometry_origin = (0.5, 0.5) ``` ![gridr_geometry_origin_half_convention.png](core_masking_001_conventions_files/gridr_geometry_origin_half_convention.png) The polygon's upper-left corner at `(x=3.5, y=2.5)` aligns with the centre of the pixel at `(x=3, y=2)` in GridR's integer convention -- exactly what the half-real convention claims. ### Shifting the geometry origin The next two figures show what happens if we lie about the geometry's origin: setting it to `(0., 0.)` shifts the polygon by half a pixel, and setting it to `(2.5, 0.5)` shifts it by two and a half rows on top of the half-pixel correction. ![gridr_geometry_origin_int_convention.png](core_masking_001_conventions_files/gridr_geometry_origin_int_convention.png) ![gridr_geometry_origin_half_convention_shift.png](core_masking_001_conventions_files/gridr_geometry_origin_half_convention_shift.png) ### Wrapping the full raster For reference, this is how to write a polygon that wraps the full raster under the integer convention -- the vertices are offset by `0.5` on each side to enclose every pixel centre. ```python geometry_origin = (0., 0.) geometry = shapely.geometry.Polygon([ (-0.5, -0.5), (shape[1] - 1 + 0.5, -0.5), (shape[1] - 1 + 0.5, shape[0] - 1 + 0.5), (-0.5, shape[0] - 1 + 0.5), ]) ``` ![gridr_geometry_wrap_raster.png](core_masking_001_conventions_files/gridr_geometry_wrap_raster.png) The next notebook puts these conventions to work and shows how GridR's rasterisation module turns geometries into binary rasters.