Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

GIS & Geospatial Data (QGIS, GeoPandas, PostGIS)  Module 6 – Raster data and remote sensing

Resampling and reprojecting rasters

Choose nearest, bilinear, cubic, average or mode by what a raster holds, predict the grid a reprojection makes, and put rasters on one grid.

  • Intermediate
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: Every resampling method decides values the old grid never held · Reprojecting rasters builds a new grid, then resamples it. With a pass: 3 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Choose nearest, bilinear, cubic, average or mode resampling for the values a raster holds
  • Predict the grid of a reprojected raster, and set it explicitly with -tr, -tap and -te
  • Match rasters to one grid of CRS, resolution, origin and extent before combining them
  • Explain why pixels change size and shape on the ground when a raster is reprojected to degrees

Before you start

What you’ll learn · Before you start

What you will learn

  • Choose nearest, bilinear, cubic, average or mode resampling for the values a raster holds
  • Predict the grid of a reprojected raster, and set it explicitly with -tr, -tap and -te
  • Match rasters to one grid of CRS, resolution, origin and extent before combining them
  • Explain why pixels change size and shape on the ground when a raster is reprojected to degrees

Before you start

On this page

Move a land-cover raster of two classes half a cell with bilinear resampling, and 198 of its new cells become class 2, a town that is not there. Every resampling decides values the old grid never held, and reprojecting rasters always resamples them, because the pixels of a new CRS never land on the old ones.

Here you choose a method by what the values mean, see the grid a reprojected raster gets and why its pixels are square in degrees but not in metres, and follow one pixel through a warp (raster reprojection): Putting a raster onto a new grid, usually in another CRS: the new grid is chosen first, then each of its pixels is filled by finding its centre on the old raster and resampling there.. Then you measure each method’s error, put rasters on one grid before combining them, and compute areas on a grid of degrees.

Every resampling method decides values the old grid never held

resampling: Computing the cells of a new grid from an old one, whenever the cell size, the grid's position or the CRS changes. Each method, such as nearest, bilinear, cubic, average or mode, decides the new values in its own way. computes the cells of a new grid from an old one whenever the cells change: a coarser or finer cell size, a grid moved or turned, a new CRS. rasterio’s guide puts it plainly: “all methods involve some interpolation” (rasterio, Resampling). gdalwarp names the common methods (GDAL, gdalwarp):

  • Nearest takes the value of the old cell under the new cell’s centre: gdalwarp’s default for most formats, the fastest, and of the “worst interpolation quality”.
  • Bilinear weighs the four old cell centres round the new centre by their distance.
  • Cubic fits a smooth curve through the sixteen old centres round it.
  • Average takes the mean of all the old cells inside the new cell, leaving nodata out.
  • Mode takes the value that occurs most often among them.

Which one fits depends on what the numbers mean. A categorical raster holds codes, 1 for water and 3 for soil; a continuous one holds measurements, such as metres or NDVI. Here is a grid moved half a cell over two classes:

Old cells of water (1) and soil (3) under a new grid moved half a cell; nearest gives 1 and 3, bilinear invents 2 at the boundary.A grid moved half a cell, two waysOld cells: 1 water, 3 soil; dashed: the new grid1133113311331133New cells, nearest133133133New cells, bilinear123123123Bilinear averages 1 and 3 into 2, a classneither input holds; nearest keeps 1 and 3.

A grid moved half a cell over two classes, resampled with nearest and with bilinear

Text description of the diagram

At the top, a grid of 4 by 4 old cells: the two left columns hold code 1, water, shaded light blue, and the two right columns hold code 3, soil, shaded grey. Over them, a dashed blue grid of 3 by 3 new cells is moved half a cell to the right and half a cell down, so each new cell's centre, marked with a blue dot, sits on the corner where four old cells meet.

Below, two small grids of 3 by 3 show the new cells. With nearest, each new cell takes the code of one old cell: the columns read 1, 3 and 3, and only codes that exist appear. With bilinear, each new cell averages its four old cells: the left column reads 1, the right column 3, and the middle column, whose centres sit between water and soil, reads 2, shaded and with a dashed outline. The caption says that bilinear averages 1 and 3 into 2, a class neither input holds, while nearest keeps 1 and 3.

The program does the same to the field of the lesson raster that the river crosses, rows 240 to 319 and columns 100 to 199, where only water and soil occur:

scene.py: the lesson raster the programs share Python · raster/scene.py
"""The lesson's raster: two bands, red and near-infrared, of 800 by 600 pixels of 10 m in UTM zone 43N.

Each value is a reflectance times 10,000, stored as an unsigned 16-bit integer, and 0 marks a pixel without data.
The scene is made by whole-number formulas, so every program that imports it, here or in a browser, gets the
same numbers: a river, fields of crops and bare soil, a town, and a hole of 2,400 pixels with no data.
"""

WIDTH, HEIGHT = 800, 600  # columns, rows
TRANSFORM = (10.0, 0.0, 370_000.0, 0.0, -10.0, 2_046_000.0)  # a, b, c, d, e, f
CRS = "EPSG:32643"  # WGS 84 / UTM zone 43N
NODATA = 0


def noise(row, col):
    """A fixed whole number from -40 to 40 for each row and column, the same on every computer."""
    h = ((row * 73_856_093) ^ (col * 19_349_663)) & 0xFFFFFFFF
    h = ((h ^ (h >> 13)) * 1_274_126_177) & 0xFFFFFFFF
    return h % 81 - 40


def wobble(row, col):
    """Texture that changes smoothly over 16 pixels, as real ground does, plus a little noise of its own."""
    r, c, fr, fc = row // 16, col // 16, row % 16, col % 16
    top = noise(r, c) * (16 - fc) + noise(r, c + 1) * fc
    bottom = noise(r + 1, c) * (16 - fc) + noise(r + 1, c + 1) * fc
    return (top * (16 - fr) + bottom * fr) // 256 + noise(row, col) // 8


def cover(row, col):
    """What lies under a pixel: water, town, soil, crop, or None in the hole without data."""
    if 420 <= row < 460 and 100 <= col < 160:
        return None
    if abs(row - (260 + col * col // 4000)) < 18:
        return "water"
    if 60 <= row < 200 and 520 <= col < 700:
        return "town"
    field_row, field_col = row // 80, col // 100
    if (field_row * 3 + field_col) % 5 == 0:
        return "soil"
    return "crop"


def pixel(row, col):
    """(red, nir) of one pixel, reflectance times 10,000."""
    kind = cover(row, col)
    if kind is None:
        return NODATA, NODATA
    w = wobble(row, col)
    if kind == "water":
        return 350 + w, 200 + w // 2
    if kind == "town":
        return 1_700 + w, 2_200 + w
    if kind == "soil":
        return 2_200 + w, 2_800 + w
    vigour = ((row // 80) * 7 + (col // 100) * 13) % 10  # 0 to 9, one value per field
    return 800 - vigour * 40 + w, 3_000 + vigour * 150 + (col % 100) * 3 + w


def bands():
    """The two bands as lists of rows: red, then near-infrared."""
    red, nir = [], []
    for row in range(HEIGHT):
        pairs = [pixel(row, col) for col in range(WIDTH)]
        red.append([p[0] for p in pairs])
        nir.append([p[1] for p in pairs])
    return red, nir
Two classes, resampled four ways Python · raster/classes.py
"""A land-cover raster of two classes, resampled four ways: what each method does to the classes."""
from collections import Counter

from scene import cover

CODES = {"water": 1, "town": 2, "soil": 3, "crop": 4}
# Rows 240 to 319 and columns 100 to 199 of the lesson raster: a bare field with the river through it.
grid = [[CODES[cover(r, c)] for c in range(100, 200)] for r in range(240, 320)]
rows, cols = len(grid), len(grid[0])
print("source, 10 m:", dict(sorted(Counter(v for row in grid for v in row).items())))


def nearest_shifted(g):
    """Each new cell, 5 m east and 5 m south of an old one, takes the value of the old cell its centre falls in."""
    return [[g[r + 1][c + 1] for c in range(cols - 1)] for r in range(rows - 1)]


def bilinear_shifted(g):
    """Each new cell's centre lies midway between four old centres: the mean of the four, rounded to a whole code."""
    return [[int((g[r][c] + g[r][c + 1] + g[r + 1][c] + g[r + 1][c + 1]) / 4 + 0.5) for c in range(cols - 1)] for r in range(rows - 1)]


def blocks(g, size=3):
    """The 3 x 3 blocks of old cells that make each new 30 m cell."""
    return [[[g[r + i][c + j] for i in range(size) for j in range(size)] for c in range(0, cols - size + 1, size)] for r in range(0, rows - size + 1, size)]


def count(g):
    return dict(sorted(Counter(v for row in g for v in row).items()))


print("shifted 5 m, nearest: ", count(nearest_shifted(grid)))
print("shifted 5 m, bilinear:", count(bilinear_shifted(grid)))
coarse = blocks(grid)
print("30 m cells, average:  ", count([[int(sum(b) / len(b) + 0.5) for b in row] for row in coarse]))
print("30 m cells, mode:     ", count([[Counter(b).most_common(1)[0][0] for b in row] for row in coarse]))
print("code 2 is 'town': every 2 above is a town that is not there")

Output

source, 10 m: {1: 3500, 3: 4500}
shifted 5 m, nearest:  {1: 3465, 3: 4356}
shifted 5 m, bilinear: {1: 3359, 2: 198, 3: 4264}
30 m cells, average:   {1: 364, 2: 42, 3: 452}
30 m cells, mode:      {1: 385, 3: 473}
code 2 is 'town': every 2 above is a town that is not there

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 classes.py

Bilinear made 198 cells of class 2, the lesson’s code for a town, and average at 30 m made 42; nearest and mode kept only 1 and 3. Codes must never be interpolated: for classes, use nearest when the cells keep their size and mode when they grow. For measurements, rasterio’s guide warns that nearest “may not be suitable for continuous data” and points to bilinear and cubic; when the cells grow, average gives each new cell the mean of the old cells inside it.

Checkpoint

Check: Resampling methods and what they keep

  1. Question 1 of 2 A land-cover raster must move onto a grid of the same cell size, shifted by half a cell. Which method keeps its classes?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nearest

    Codes are names, not quantities, so they must never be averaged or interpolated. Nearest keeps them when the cells keep their size, and mode when the cells grow.

  2. Question 2 of 2 Why did bilinear resampling put class 2, a town, on the lesson's riverbank?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It averaged codes 1 (water) and 3 (soil) where a new cell's centre fell between them

    Interpolation treats numbers as quantities. For a categorical raster that turns the boundary between two classes into a strip of a third one, which is why classes take nearest or mode.

Reprojecting rasters builds a new grid, then resamples it

Reprojecting rasters starts with a grid, not with values. GDAL documents the rule for the grid it suggests: walk the old raster’s edges, about 20 points along each, transform them, and take the box round them as the new extent; then pick one square pixel size, so that the diagonal from the top-left corner to the bottom-right one holds as many pixels as before (GDAL, GDALSuggestedWarpOutput). The program applies that rule to the lesson raster, 800 by 600 pixels of 10 m in UTM zone 43N, for EPSG:4326, with a module of its own for the UTM projection:

utm.py: UTM zone 43N both ways, and the size of a degree Python · raster/utm.py
"""UTM zone 43N on WGS 84, both ways, and the size of a degree on the ground: what the lesson's programs reproject with.

The transverse Mercator projection is computed with Krüger's series to the sixth order of the ellipsoid's third
flattening, as Karney (2011) gives it, which is good to well under a millimetre across the zone.
"""
import math

A, F = 6_378_137.0, 1 / 298.257223563  # WGS 84 semi-major axis (m) and flattening
K0, LON0, FALSE_EASTING = 0.9996, math.radians(75.0), 500_000.0  # zone 43N: central meridian 75° E
E = math.sqrt(F * (2 - F))  # eccentricity
N = F / (2 - F)  # third flattening
RECT = A / (1 + N) * (1 + N**2 / 4 + N**4 / 64 + N**6 / 256)  # the rectifying radius
ALPHA = [
    N / 2 - 2 * N**2 / 3 + 5 * N**3 / 16 + 41 * N**4 / 180 - 127 * N**5 / 288 + 7891 * N**6 / 37800,
    13 * N**2 / 48 - 3 * N**3 / 5 + 557 * N**4 / 1440 + 281 * N**5 / 630 - 1983433 * N**6 / 1935360,
    61 * N**3 / 240 - 103 * N**4 / 140 + 15061 * N**5 / 26880 + 167603 * N**6 / 181440,
    49561 * N**4 / 161280 - 179 * N**5 / 168 + 6601661 * N**6 / 7257600,
    34729 * N**5 / 80640 - 3418889 * N**6 / 1995840,
    212378941 * N**6 / 319334400,
]
BETA = [
    N / 2 - 2 * N**2 / 3 + 37 * N**3 / 96 - N**4 / 360 - 81 * N**5 / 512 + 96199 * N**6 / 604800,
    N**2 / 48 + N**3 / 15 - 437 * N**4 / 1440 + 46 * N**5 / 105 - 1118711 * N**6 / 3870720,
    17 * N**3 / 480 - 37 * N**4 / 840 - 209 * N**5 / 4480 + 5569 * N**6 / 90720,
    4397 * N**4 / 161280 - 11 * N**5 / 504 - 830251 * N**6 / 7257600,
    4583 * N**5 / 161280 - 108847 * N**6 / 3991680,
    20648693 * N**6 / 638668800,
]


def to_utm(lon, lat):
    """Easting and northing (m) in UTM zone 43N of a longitude and latitude in degrees."""
    phi, lam = math.radians(lat), math.radians(lon) - LON0
    t = math.sinh(math.atanh(math.sin(phi)) - E * math.atanh(E * math.sin(phi)))
    xi0, eta0 = math.atan2(t, math.cos(lam)), math.atanh(math.sin(lam) / math.sqrt(1 + t * t))
    xi = xi0 + sum(a * math.sin(2 * j * xi0) * math.cosh(2 * j * eta0) for j, a in enumerate(ALPHA, 1))
    eta = eta0 + sum(a * math.cos(2 * j * xi0) * math.sinh(2 * j * eta0) for j, a in enumerate(ALPHA, 1))
    return FALSE_EASTING + K0 * RECT * eta, K0 * RECT * xi


def from_utm(x, y):
    """Longitude and latitude in degrees of an easting and northing in UTM zone 43N."""
    xi, eta = y / (K0 * RECT), (x - FALSE_EASTING) / (K0 * RECT)
    xi0 = xi - sum(b * math.sin(2 * j * xi) * math.cosh(2 * j * eta) for j, b in enumerate(BETA, 1))
    eta0 = eta - sum(b * math.cos(2 * j * xi) * math.sinh(2 * j * eta) for j, b in enumerate(BETA, 1))
    tau0 = math.sin(xi0) / math.sqrt(math.sinh(eta0) ** 2 + math.cos(xi0) ** 2)
    tau = tau0
    for _ in range(5):  # Newton's method for the conformal latitude
        s = math.sinh(E * math.atanh(E * tau / math.sqrt(1 + tau * tau)))
        guess = tau * math.sqrt(1 + s * s) - s * math.sqrt(1 + tau * tau)
        tau += (tau0 - guess) / math.sqrt(1 + guess * guess) * (1 + (1 - E * E) * tau * tau) / ((1 - E * E) * math.sqrt(1 + tau * tau))
    return math.degrees(LON0 + math.atan2(math.sinh(eta0), math.cos(xi0))), math.degrees(math.atan(tau))


def metres_per_degree(lat):
    """(east-west, north-south) length on the ground of one degree at a latitude, on the ellipsoid."""
    phi = math.radians(lat)
    w = 1 - E * E * math.sin(phi) ** 2
    across = A * math.cos(phi) / math.sqrt(w)  # radius of the parallel
    along = A * (1 - E * E) / w**1.5  # radius of curvature of the meridian
    return math.radians(1) * across, math.radians(1) * along
The lesson raster's grid in degrees Python · raster/grid.py
"""The grid the lesson raster gets in EPSG:4326, by the rule GDAL documents for its suggested warp output."""
import math

from scene import HEIGHT, TRANSFORM, WIDTH
from utm import from_utm, metres_per_degree

a, _, c, _, e, f = TRANSFORM
west, east, north, south = c, c + a * WIDTH, f, f + e * HEIGHT

# 1. Walk round the edges, 20 steps along each, and transform the points: the box round them is the output extent.
steps = [i / 20 for i in range(20)]
ring = [(west + (east - west) * s, north) for s in steps] + [(east, north + (south - north) * s) for s in steps]
ring += [(east + (west - east) * s, south) for s in steps] + [(west, south + (north - south) * s) for s in steps]
lons, lats = zip(*(from_utm(x, y) for x, y in ring))
print(f"extent: {min(lons):.6f} to {max(lons):.6f} E, {min(lats):.6f} to {max(lats):.6f} N")

# 2. One square pixel size, so that the top-left to bottom-right diagonal keeps its 1,000 pixels.
(lon1, lat1), (lon2, lat2) = from_utm(west, north), from_utm(east, south)
size = math.hypot(lon2 - lon1, lat2 - lat1) / math.hypot(WIDTH, HEIGHT)
cols, rows = round((max(lons) - min(lons)) / size), round((max(lats) - min(lats)) / size)
print(f"pixel size: {size:.4e} degrees; grid of {cols} columns and {rows} rows (was {WIDTH} by {HEIGHT})")

# 3. The same square of degrees, measured on the ground at the raster's middle latitude.
across, along = metres_per_degree((max(lats) + min(lats)) / 2)
print(f"one pixel on the ground: {size * across:.2f} m east-west by {size * along:.2f} m north-south")

# 4. The source is a slightly turned shape inside that box: the box's corners hold no data.
footprint = abs(sum(lons[i] * lats[i - 1] - lons[i - 1] * lats[i] for i in range(len(ring)))) / 2
box = (max(lons) - min(lons)) * (max(lats) - min(lats))
print(f"the source covers {footprint / box:.2%} of the box; about {round((1 - footprint / box) * cols * rows):,} of the {cols * rows:,} new pixels get nodata")

Output

extent: 73.768549 to 73.844682 E, 18.446250 to 18.500944 N
pixel size: 9.3189e-05 degrees; grid of 817 columns and 587 rows (was 800 by 600)
one pixel on the ground: 9.84 m east-west by 10.31 m north-south
the source covers 98.64% of the box; about 6,505 of the 479,579 new pixels get nodata

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grid.py

The new grid has 817 by 587 pixels of 9.3189e-05° each, square in degrees. On the ground at 18.47° N that square is 9.84 m east to west and 10.31 m north to south, because a degree of longitude shrinks with the latitude’s cosine while a degree of latitude barely changes. The source also sits slightly turned inside the new box and covers 98.64 % of it, so about 6,505 new pixels in its corners get nodata.

Each new pixel is then filled by working backwards, from the new grid to the old one. Follow one:

Interactive · stage by stage How a warp fills one pixel of the new grid Follow pixel row 300, column 400 of the grid in degrees back to the UTM raster it is filled from, then a corner pixel that finds nothing.
How a warp fills one pixel of the new grid, step 7 of 7. Showing A pixel of the new grid (row 0, col 0), Its centre in degrees (73.768595 E, 18.500898 N), Back to UTM zone 43N (370,005.2 E, 2,046,047.7 N), Place in the old grid (col 0.52, row −4.77), Resample (nearest: red 333, water), Write the new pixel (row 0, col 0: nodata). In focus: Place in the old grid, outside.outside: nodataA pixel of the new gridrow 0, col 0Its centre in degrees73.768595 E, 18.500898 NBack to UTM zone 43N370,005.2 E, 2,046,047.7 NPlace in the old gridcol 0.52, row −4.77Resamplenearest: red 333, waterWrite the new pixelrow 0, col 0: nodata

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Start from the new grid. A warp works backwards. For every pixel of the new grid, 817 by 587 pixels in degrees, it asks where on the old raster that pixel's ground lies.
  2. The centre in degrees. The new grid's corner and pixel size, 9.3189e-05 degrees, put the centre of row 300, column 400 at 73.805871 E, 18.472941 N.
  3. Back to the old CRS. The inverse transformation takes that point into UTM zone 43N, the old raster's CRS, at 373,920.3 E and 2,042,927.5 N.
  4. A place in the old grid. The old transform turns the point into column 392.03 and row 307.25, a point inside the old pixel in row 307, column 392.
  5. Resample. Nearest takes the value of that one pixel, water with red 333. Bilinear would weigh the four old pixels round the point, and cubic sixteen.
  6. Write the value. The value goes into row 300, column 400 of the new grid. Every new pixel is filled this way, so the new grid has no gaps, which mapping the old pixels forwards would leave.
  7. A corner of the new grid. Row 0, column 0 comes back to row −4.77 of the old grid, outside it. That pixel gets nodata, like about 6,505 of the 479,579 new pixels, in the corners of the box.

Key idea

A reprojected raster gets a new grid first, square in the new CRS’s units, and every pixel of it is resampled from the old raster by working backwards.

Checkpoint

Check: The grid of a reprojected raster

  1. Question 1 of 2 A raster of 600 by 800 pixels is reprojected, and its top-left and bottom-right corners land 10,000 m apart in the new CRS. What pixel size does GDAL's documented rule suggest?

    Type a number.

    A hint

    The old diagonal holds as many pixels as the new one should.

    Show the answer: Question 1 of 2

    Answer: 10 m

    The old diagonal holds √(600² + 800²) = 1,000 pixels, and the new diagonal is 10,000 m, so each square pixel is 10,000 ÷ 1,000 = 10 m.

  2. Question 2 of 2 The lesson raster's pixels in degrees are square, 9.3189e-05°, yet 9.84 m wide and 10.31 m tall on the ground. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A degree of longitude is shorter than a degree of latitude away from the equator, roughly by the latitude's cosine

    A grid in degrees is square in degrees only. On the ground its pixels narrow east to west with the cosine of the latitude, which matters for every area computed from it.

Each method's error, measured, and how to choose Pro: opens with a Pro, Premium or Ultimate pass

The rule of thumb above has numbers behind it. Here every method is measured on a hill whose true heights are known everywhere and on the lesson’s NDVI, and the results become a table that answers your own case.

In this part:

  • Finer cells: nearest, bilinear and cubic against a known hill
  • Coarser cells: average keeps the mean, nearest keeps one pixel
  • Choosing with the table, and the defaults to watch

1 runnable example (Python) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Putting rasters on one grid before combining them Pro: opens with a Pro, Premium or Ultimate pass

Cell-by-cell algebra pairs cells by row and column, so two rasters must share one grid before they are combined. Here you measure what pairing two grids 15 m apart costs, and build one grid with target-aligned pixels.

In this part:

  • What one grid means
  • Two grids 15 m apart, measured
  • -tr, -tap and -te make the grid explicit
  • Put two rasters on one grid with target-aligned pixels
    • Results of the sample tests

1 runnable example (Python) · 1 exercise with sample tests · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Areas on a grid of degrees, with numbers Pro: opens with a Pro, Premium or Ultimate pass

A pixel of degrees covers less ground the further it lies from the equator. Here the lesson’s river is measured after reprojection three ways, and one pixel’s area is followed across India.

In this part:

  • The river measured three ways
  • One pixel of 0.0001 degrees across India
  • When to measure on a grid of degrees at all

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: the grid of a reprojection

The exercise turns GDAL’s rule for the suggested grid into code and checks it on the lesson raster; then the self-check covers the first two sections, the methods and the new grid:

Exercise · Easy · Python

Work out the grid a reprojected raster gets

Write suggested_grid(extent, top_left, bottom_right, width, height) in suggest.py, following the rule GDAL documents for the output it suggests when a raster is reprojected:

  • extent is (west, south, east, north), the box round the raster's edges after they were transformed;
  • top_left and bottom_right are where the raster's top-left and bottom-right corners land, as (x, y);
  • the pixel size is square, chosen so that the diagonal from top_left to bottom_right holds as many pixels as the diagonal of the old raster of width by height pixels;
  • the new grid has round((east - west) / size) columns and round((north - south) / size) rows.

Return (size, columns, rows). The sample tests use the lesson raster, 800 by 600 pixels in UTM zone 43N, reprojected to degrees, and a raster whose corners do not move at all.

Starter code · suggest.py

import math


def suggested_grid(extent, top_left, bottom_right, width, height):
    """(pixel size, columns, rows) of the grid GDAL's rule suggests for a reprojected raster."""
    # Replace this line: one square pixel size from the two diagonals, then the size of the grid.
    return 0.0, 0, 0
The sample tests · test_suggest.py
from suggest import suggested_grid

EXTENT = (73.76854884148034, 18.446249951458224, 73.84468186084324, 18.50094444522425)
TOP_LEFT = (73.76854884148034, 18.500466524524956)
BOTTOM_RIGHT = (73.84468186084324, 18.446726373384184)


def test_lesson_raster_in_degrees():
    """gives the lesson's grid in degrees: 817 by 587 pixels of 9.3189e-05 degrees"""
    size, cols, rows = suggested_grid(EXTENT, TOP_LEFT, BOTTOM_RIGHT, 800, 600)
    assert round(size, 9) == 9.3189e-05
    assert (cols, rows) == (817, 587)


def test_a_raster_that_does_not_move():
    """keeps the grid of a raster whose corners stay where they were"""
    size, cols, rows = suggested_grid((0, 0, 8000, 6000), (0, 6000), (8000, 0), 800, 600)
    assert (size, cols, rows) == (10.0, 800, 600)


def test_rounding_of_the_grid():
    """rounds the columns and rows to the nearest whole number"""
    size, cols, rows = suggested_grid((0, 0, 1004, 1006), (0, 1000), (1000, 0), 100, 100)
    assert (size, cols, rows) == (10.0, 100, 101)
A hint

The old diagonal has math.hypot(width, height) pixels, 1,000 for 800 by 600. The new diagonal is math.hypot(x2 - x1, y2 - y1) in the new CRS's units, so one pixel is the second divided by the first.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Practice that adapts to you

Practise this lesson

6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 Which resampling method does gdalwarp use for most output formats when no -r is given?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: Nearest

    Nearest is gdalwarp's default for most formats. That suits classes, but measurements usually need bilinear, cubic or average, given by name.

  2. Question 2 of 6 An index computed from 20 m bands must be resampled to 10 m cells to combine with 10 m bands. Which methods suit it?

    Choose every answer that is right.

    Show the answer: Question 2 of 6

    Answer:

    • Cubic
    • Bilinear

    Measurements on finer cells take bilinear or cubic. Nearest and mode keep values that exist, which is right for codes and wrong for quantities.

  3. Question 3 of 6 A 10 m land-cover map must become a 100 m map of the dominant class in each cell. Which method gives that?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: Mode

    For classes on coarser cells, mode gives the majority, and only codes that exist; the lesson's coarse cells kept only water and soil with it.

  4. Question 4 of 6 In the units of the new CRS, what shape are the pixels of the grid GDAL suggests for a reprojected raster?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: Square

    The suggested grid has one square pixel size in the new CRS's units, which is why a grid square in degrees is not square on the ground.

  5. Question 5 of 6 Why do the corners of a reprojected raster's grid often hold nodata?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: The old raster sits slightly turned inside the new box, so pixels in its corners come back outside it

    The new extent is the box round the transformed edges, and a turned or curved footprint never fills a box, so the pixels outside it get nodata.

  6. Question 6 of 6 Why does a warp fill the new grid by starting from each new pixel and looking back at the old raster?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: So that every new pixel gets exactly one value, with no gaps and no pixel written twice

    Backward mapping asks one question per new pixel, where its centre lies on the old raster, and resampling answers it, so the new grid is filled completely and once.

Interview questions

Three longer questions start from this lesson, from a class that appears on every riverbank to areas summed in degrees; the bank gives what each tests and a first step:

  1. Fresher to mid

    After a land-cover map was reprojected and saved for a web map, every riverbank shows a thin strip of "town", a class that the original map never had there. What happened, and how do you produce the map again correctly?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    You combine a 10 m NDVI raster with a 30 m elevation model of the same area, both in one UTM zone, and slope never lines up with field edges. How do you check the two grids, and how do you combine them correctly?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A colleague computes forest area for every district of India from a 10 m land-cover raster reprojected to EPSG:4326, counting forest pixels and multiplying by 100 m². Which errors do you expect, and how would you compute the areas instead?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How does each of the five common resampling methods fill a new cell?

    Nearest copies the old cell under the new centre; bilinear weighs the four nearest old centres by distance; cubic fits a smooth curve through sixteen; average takes the mean of the old cells inside, leaving nodata out; mode takes the commonest value among them.

  2. Which resampling methods suit a land-cover raster, and why?

    Nearest when the cells keep their size and mode when they grow, because class codes are names and must never be interpolated. Bilinear turned 198 cells between water and soil into class 2, a town that was not there.

  3. Which resampling methods suit continuous values such as heights or NDVI?

    Bilinear or cubic when the cells keep their size or shrink, and average when they grow, so each new cell gets the mean of the old ones inside it; nearest may not suit continuous data.

  4. How does GDAL choose the grid of a reprojected raster?

    It transforms about 20 points along each edge of the old raster and takes the box round them as the new extent, then chooses one square pixel size such that the diagonal from corner to corner holds as many pixels as before.

  5. Is a pixel that is square in degrees also square on the ground?

    No. Reprojected to EPSG:4326, the lesson raster got pixels of 9.3189e-05°, which at 18.47° N are 9.84 m from west to east and 10.31 m from south to north: degrees of longitude get shorter with cos(latitude) while degrees of latitude barely change.

  6. How is each pixel of a reprojected raster filled?

    By working backwards: the new pixel's centre is transformed into the old CRS and the old raster is resampled there. Corners of the new grid that the old raster does not reach get nodata, about 6,505 pixels in the lesson's case.

Key takeaways

  • Every resampling decides new values: nearest copies one cell, bilinear and cubic interpolate, average takes the mean and mode the commonest value.
  • Codes must never be interpolated, so classes take nearest or mode, while measurements take bilinear or cubic for finer cells and average for coarser ones.
  • Reprojecting rasters builds a new grid first, its extent from the transformed edges and one square pixel size from the diagonal, then fills each pixel by working backwards to the old grid.
  • A grid that is square in degrees is not square on the ground: the lesson raster’s 9.3189e-05° pixels are 9.84 m by 10.31 m.

Terms in this lesson

resampling
Computing the cells of a new grid from an old one, whenever the cell size, the grid's position or the CRS changes. Each method, such as nearest, bilinear, cubic, average or mode, decides the new values in its own way.
warp (raster reprojection)
Putting a raster onto a new grid, usually in another CRS: the new grid is chosen first, then each of its pixels is filled by finding its centre on the old raster and resampling there.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.