Skip to content

Reproject

aef_bng.reproject

UTM → BNG reprojection via rasterio.warp.

Handles reprojection of individual tiles and merging of overlapping tiles at UTM zone boundaries using a first-valid strategy.

reproject_tile_to_bng(src_data, src_transform, src_crs, dst_transform, dst_shape, resampling='nearest')

Reproject a single tile from UTM to a BNG chunk grid.

Parameters:

Name Type Description Default
src_data ndarray

Source array of shape (64, H, W) int8.

required
src_transform Affine

Affine transform of the source tile.

required
src_crs str

CRS string of the source tile (e.g. "EPSG:32630").

required
dst_transform Affine

Affine transform of the destination BNG chunk.

required
dst_shape tuple[int, int]

Pixel dimensions (rows, cols) of the destination.

required
resampling str

Resampling method name.

'nearest'

Returns:

Type Description
ndarray

Reprojected array of shape (64, *dst_shape) int8 with -128 for nodata.

Source code in aef_bng/reproject.py
def reproject_tile_to_bng(
    src_data: np.ndarray,
    src_transform: Affine,
    src_crs: str,
    dst_transform: Affine,
    dst_shape: tuple[int, int],
    resampling: str = "nearest",
) -> np.ndarray:
    """Reproject a single tile from UTM to a BNG chunk grid.

    Args:
        src_data: Source array of shape (64, H, W) int8.
        src_transform: Affine transform of the source tile.
        src_crs: CRS string of the source tile (e.g. "EPSG:32630").
        dst_transform: Affine transform of the destination BNG chunk.
        dst_shape: Pixel dimensions (rows, cols) of the destination.
        resampling: Resampling method name.

    Returns:
        Reprojected array of shape (64, *dst_shape) int8 with -128 for nodata.
    """
    dst = np.full((AEF_NUM_BANDS, *dst_shape), AEF_NODATA, dtype=np.int8)

    resampling_method = RESAMPLING_METHODS.get(resampling, Resampling.nearest)

    reproject(
        source=src_data,
        destination=dst,
        src_transform=src_transform,
        src_crs=src_crs,
        dst_transform=dst_transform,
        dst_crs=BNG_CRS,
        resampling=resampling_method,
        src_nodata=AEF_NODATA,
        dst_nodata=AEF_NODATA,
    )

    return dst

merge_tiles(arrays)

Merge multiple reprojected tile arrays using first-valid strategy.

At UTM zone boundaries, the same ground area appears in tiles from adjacent UTM zones. After reprojection to BNG, both tiles yield nearly identical values (same satellite data, different projection path). Nearest-neighbour resampling from different UTM zones may produce values differing by at most 1 int8 unit due to grid alignment differences, which is within noise for quantised embeddings.

The first-valid strategy takes the first non-nodata value for each pixel. Callers must sort the input tiles deterministically (e.g. by path) so the merge result is reproducible across runs.

A pixel is considered nodata only when ALL 64 bands equal -128. Embeddings are atomic vectors, partial band replacement would be incorrect.

Parameters:

Name Type Description Default
arrays list[ndarray]

List of (64, H, W) int8 arrays, all same shape. Must be in deterministic order.

required

Returns:

Type Description
ndarray

Merged array of shape (64, H, W) int8.

Source code in aef_bng/reproject.py
def merge_tiles(arrays: list[np.ndarray]) -> np.ndarray:
    """Merge multiple reprojected tile arrays using first-valid strategy.

    At UTM zone boundaries, the same ground area appears in tiles from adjacent UTM zones.
    After reprojection to BNG, both tiles yield     nearly identical values (same satellite data,
    different projection path). Nearest-neighbour resampling from different UTM zones may produce
    values differing by at most 1 int8 unit due to grid alignment differences, which is within noise
    for quantised embeddings.

    The first-valid strategy takes the first non-nodata value for each pixel. Callers must sort the
    input tiles deterministically (e.g. by path) so the merge result is reproducible across runs.

    A pixel is considered nodata only when ALL 64 bands equal -128. Embeddings are atomic vectors,
    partial band replacement would be incorrect.

    Args:
        arrays: List of (64, H, W) int8 arrays, all same shape.
            Must be in deterministic order.

    Returns:
        Merged array of shape (64, H, W) int8.
    """
    if not arrays:
        raise ValueError("No arrays to merge")

    if len(arrays) == 1:
        return arrays[0]

    merged = arrays[0].copy()

    for arr in arrays[1:]:
        merge_tile_into(merged, arr)

    return merged

merge_tile_into(merged, arr)

First-valid merge of one reprojected tile into merged, in place.

Incremental counterpart to :func:merge_tiles (same strategy and caveats) - callers merge each tile as it arrives so only the accumulator and one tile are ever held in memory, instead of every reprojected tile at once.

Parameters:

Name Type Description Default
merged ndarray

Accumulator array of shape (64, H, W) int8; modified in place.

required
arr ndarray

Reprojected tile of the same shape to fill nodata pixels from.

required
Source code in aef_bng/reproject.py
def merge_tile_into(merged: np.ndarray, arr: np.ndarray) -> None:
    """First-valid merge of one reprojected tile into ``merged``, in place.

    Incremental counterpart to :func:`merge_tiles` (same strategy and caveats) -
    callers merge each tile as it arrives so only the accumulator and one tile
    are ever held in memory, instead of every reprojected tile at once.

    Args:
        merged: Accumulator array of shape (64, H, W) int8; modified in place.
        arr: Reprojected tile of the same shape to fill nodata pixels from.
    """
    nodata_mask = np.all(merged == AEF_NODATA, axis=0)
    if nodata_mask.any():
        merged[:, nodata_mask] = arr[:, nodata_mask]