import rasterio
from rasterio.transform import array_bounds, from_origin
from rasterio.warp import Resampling, reproject, transform_bounds
from rasterio.windows import Window
from rasterio.windows import transform as window_transform
gminx, gminy, gmaxx, gmaxy = BBOX
# ── grid 1: native 10 m, British National Grid — the saved GeoTIFF ──
BNG_CRS, BNG_RES = "EPSG:27700", 10.0
_bx0, _by0, _bx1, _by1 = transform_bounds("EPSG:4326", BNG_CRS, *BBOX)
bminx, bmaxy = np.floor(_bx0 / BNG_RES) * BNG_RES, np.ceil(_by1 / BNG_RES) * BNG_RES
BW = int(np.ceil((_bx1 - bminx) / BNG_RES))
BH = int(np.ceil((bmaxy - _by0) / BNG_RES))
BNG_TRANSFORM = from_origin(bminx, bmaxy, BNG_RES, BNG_RES)
# ── grid 2: ~15 m WGS84 — only for the folium overlay ──
MOSAIC_RES = 0.00015
GW, GH = int(np.ceil((gmaxx - gminx) / MOSAIC_RES)), int(np.ceil((gmaxy - gminy) / MOSAIC_RES))
G_TRANSFORM = from_origin(gminx, gmaxy, MOSAIC_RES, MOSAIC_RES)
mosaic_cache = CACHE_DIR / f"london_pred_mosaic_{YEAR}.npy"
bng_cache = CACHE_DIR / f"london_pred_bng10m_{YEAR}.npy"
GEOTIFF_PATH = CACHE_DIR / f"london_football_pitches_{YEAR}_10m.tif"
def _paste(pred, src_tr, src_crs, grid, grid_tr, dst_crs, res, ox, oy):
"""Reproject a tile prediction into a window of `grid` (nearest, later wins)."""
tl, tb, tr_, tt = array_bounds(*pred.shape, src_tr)
wl, wb, wr, wt = transform_bounds(src_crs, dst_crs, tl, tb, tr_, tt)
c0 = max(0, int(np.floor((wl - ox) / res))); c1 = min(grid.shape[1], int(np.ceil((wr - ox) / res)))
r0 = max(0, int(np.floor((oy - wt) / res))); r1 = min(grid.shape[0], int(np.ceil((oy - wb) / res)))
if c1 <= c0 or r1 <= r0:
return
win = Window(c0, r0, c1 - c0, r1 - r0)
dst = np.zeros((r1 - r0, c1 - c0), dtype=np.uint8)
reproject(pred, dst, src_transform=src_tr, src_crs=src_crs,
dst_transform=window_transform(win, grid_tr), dst_crs=dst_crs,
resampling=Resampling.nearest)
grid[r0:r1, c0:c1] = np.where(dst > 0, dst, grid[r0:r1, c0:c1])
if mosaic_cache.exists() and bng_cache.exists():
mosaic, bng = np.load(mosaic_cache), np.load(bng_cache)
else:
mosaic = np.zeros((GH, GW), dtype=np.uint8)
bng = np.zeros((BH, BW), dtype=np.uint8)
for k, tile_dir in enumerate(london_tiles):
t_da = open_tile(tile_dir)
t_crs, t_h, t_w = t_da.rio.crs, t_da.sizes["y"], t_da.sizes["x"]
t_tr = tile_transform(t_da)
t_flat = np.asarray(t_da.values).reshape(128, -1).T
t_prob = predict_proba(torch.from_numpy(((t_flat - mu) / sd).astype(np.float32)))
t_pred = t_prob.argmax(axis=1).astype(np.uint8)
t_pred[t_prob.max(axis=1) < CONF_THRESHOLD] = 0
t_pred = t_pred.reshape(t_h, t_w)
_paste(t_pred, t_tr, t_crs, bng, BNG_TRANSFORM, BNG_CRS, BNG_RES, bminx, bmaxy)
_paste(t_pred, t_tr, t_crs, mosaic, G_TRANSFORM, "EPSG:4326", MOSAIC_RES, gminx, gmaxy)
print(f"[{k + 1:2d}/{len(london_tiles)}] {tile_dir.name} mosaicked")
np.save(mosaic_cache, mosaic)
np.save(bng_cache, bng)
# ── write the georeferenced GeoTIFF (10 m, EPSG:27700, with a class colormap) ──
CLASS_RGB = {1: (0, 131, 0), 2: (42, 120, 214), 3: (235, 104, 52)} # grass / artificial / hard
with rasterio.open(
GEOTIFF_PATH, "w", driver="GTiff", height=BH, width=BW, count=1, dtype="uint8",
crs=BNG_CRS, transform=BNG_TRANSFORM, nodata=0, compress="lzw", tiled=True,
) as dst:
dst.write(bng, 1)
dst.write_colormap(1, {0: (0, 0, 0, 0), **{v: (*rgb, 255) for v, rgb in CLASS_RGB.items()}})
dst.update_tags(1, CLASSES="; ".join(f"{i + 1}={n}" for i, n in enumerate(CLASS_NAMES[1:])))
n_pitch_px = int((bng > 0).sum())
print(f"\nGeoTIFF: {GEOTIFF_PATH}")
print(f" {BH}×{BW} px @ 10 m {BNG_CRS} · {n_pitch_px:,} predicted pitch px "
f"({100 * n_pitch_px / bng.size:.2f}% of the AOI)")