Skip to content

Rotated Raster with Non-Square Pixels

rasterGeoTIFFEPSG:32633tinybundled

A raster rotated about 25 degrees with a skew sign opposite to rotated_two_islands and pixels that are 30 m by 20 m rather than square. rotated_two_islands was 2-for-2 across two validation runs and three libraries and was still the only rotated raster in the corpus, so "handles rotation" and "handles this rotation" were indistinguishable. The non-square pixels are deliberate: a scalar resolution= cannot express them, so a harness passing one silently squares the grid.

Pixels of rotated_nonsquare_small, a 12x12 raster, with NoData in magenta
1 band, 12x12 px, float32, sentinel NoData. Rendered from the case's actual pixels, contrast-stretched for display; NoData is shown in magenta.
Property Value
Case ID rotated_nonsquare_small
Category raster
Format GeoTIFF
CRS EPSG:32633
Location Southern Italy / Sicily (synthetic, UTM 33N) — 15.00°E, 40.65°N → 15.00°E, 40.65°N
Test tier unit
Size class tiny
Storage class bundled
Redistributable yes
Loader rasterio
Status validated

Use this case

import pytest


@pytest.mark.geocase_case("rotated_nonsquare_small")
def test_rotated_nonsquare_small(geocase_case) -> None:
    data = geocase_case.load()
    assert data is not None

Use GeoCase in your tests

Install the complete set of vector, raster, and NetCDF dependencies:

pip install "geocase[all]"

View GeoCase on PyPI.

What this case checks

Confirm that a consumer honours both skew terms and a non-square pixel size at once, so that squaring the grid or dropping the rotation is a visible wrong answer rather than a one-pixel edge effect.

Risk types covered

Expected behavior

Assertion Expected
expect_loadable yes
expect_crs yes
expected_epsg 32633
expect_nodata yes
expected_band_count 1
expected_dtype float32
expected_shape [12, 12]
expected_nodata_value -9999.0
nodata_convention sentinel
expected_band_names elevation
expected_transform_signs negative_e, rotated

Known answer

Computed from the actual bytes and gated against them. Grade your own output against these.

Quantity Value
Mean over valid pixels 72.0
Mean including NoData 2.0625
NoData pixels 1
Bounds (case CRS) [499898.5716171822, 4499782.486131111, 500326.2708033332, 4500152.1425742265]
Pixel to world (row, col, x, y) [[0.0, 0.0, 500009.36843418813, 4499997.276196056], [11.0, 11.0, 500215.4739863273, 4499937.3525092825], [6.0, 6.0, 500121.7896444459, 4499964.590548725]]

Notes

Three fixtures covering two georeferencing conventions that nearly every consumer assumes without checking, and that nothing in this catalog exercised before plan 34. Like the footprint edge cases, they share a directory because their value is in the comparison between them.

Why these exist

Both failures here are silent. Neither produces an exception, a warning, or a visibly broken file — the output is a plausible raster describing the wrong piece of ground.

bottom_up_dem_small — positive e

Every other raster in the catalog is built with rasterio.transform.from_origin, which always emits a negative e term: north-up, origin at the top-left, row 0 the northernmost. Code that hardcodes that assumption passes all 32 of them.

This one has e = +30.0, origin at the bottom-left, and row 0 is the southernmost row. The array is the north-up ramp flipped vertically, so the file describes the same ground the other two do — get the flip wrong and you have a file that is internally consistent and geographically upside down, which is the exact bug the case exists to catch.

A concrete trap this fixture exposes: rasterio does not normalise bounds. BoundingBox is computed straight from the affine, so this file reports bottom = 4200360 and top = 4200000 — the larger northing in bottom. Anything computing a height as top - bottom, or handing these bounds to a windowing helper, gets a negative number or an empty read. Every other fixture in the catalog hides this, because for them the two happen to be ordered the way people expect.

pixel_is_area_dem_small / pixel_is_point_dem_small — AREA_OR_POINT

The pair is the test. Both files carry the same array and the same transform, and differ only in one metadata tag:

  • Area — the transform's coordinates name pixel corners.
  • Point — they name pixel centres.

That is a half-pixel difference in where every value sits: 15 m on a 30 m grid. It survives every gate this repository had — shape, dtype, CRS, nodata, checksum, even the rendered preview are all identical.

Area is written explicitly, though GDAL's default would produce the same behaviour by omitting the tag. A file that says nothing cannot be one half of a differential: a consumer reading tags()["AREA_OR_POINT"] on a silent file gets a KeyError, and one using .get() without a default gets None and guesses. assert_pixel_anchor defaults to "Area" for exactly this reason.

Typical checks

  • src.transform.e > 0 distinguishes the bottom-up case.
  • src.tags().get("AREA_OR_POINT", "Area") — with the default.
  • Order bounds explicitly rather than assuming bottom < top.

Widening the axis (plan 37 phase 3)

The external validation runs of 2026-08-31 found real defects in rio-tiler, titiler and rio-stac, and every one of them came from a transform convention rather than a format baseline. At that point the corpus held exactly one rotated raster and exactly one bottom-up raster — a sample size of one per axis, each of which paid on its first run. Two cases here widen it:

  • rotated_bottom_up_small carries both conventions at once: non-zero b/d skew terms and a positive e. rio-tiler's two defects have adjacent root causes and a single WarpedVRT guard addresses both, so a case carrying only one convention cannot distinguish a complete fix from a partial one. Its rows are flipped for the same reason bottom_up_dem_small's are: the file has to describe the same ground, not the same array. Flipping the transform sign alone yields a file that is internally consistent and geographically upside down.
  • rotated_steep_small rotates by roughly 40°, against rotated_two_islands' ~14°. At that angle a north-up assumption misplaces the grid's far corner by about ten cells rather than one, so a differential report shows the magnitude and direction of a consumer's error instead of something that reads as a one-pixel edge effect.

Both reuse the affine_transform_bug risk type rather than minting new terms.

rotated_two_islands and nonsquare_diagonal_sparse also carry non-default transforms — a rotated affine and anisotropic 60 m × 30 m pixels respectively. Both were backfilled with expected_transform_signs in plan 34, turning a property the corpus always had into one it declares.

Required capabilities

  • load
  • affine-transform-check
  • bounds-check
  • transform-inspection

Files

Browse this case on GitHub

Source and license

  • Source: geocase-curated
  • License: MIT

Tags

georeferencing geotiff raster rotated transform