Source code for imod.prepare.reproject

import affine
import numpy as np
import xarray as xr

import imod
from imod.util.imports import MissingOptionalModule

# since rasterio is a big dependency that is sometimes hard to install
# and not always required, we made this an optional dependency
try:
    import rasterio
    import rasterio.warp
except ImportError:
    rasterio = MissingOptionalModule("rasterio")


def _reproject_dst(source, src_crs, dst_crs, src_transform):
    """
    Prepares destination transform Affine and DataArray for projection.
    """
    src_height, src_width = source.y.size, source.x.size
    bounds = rasterio.transform.array_bounds(src_height, src_width, src_transform)
    dst_transform, dst_width, dst_height = rasterio.warp.calculate_default_transform(
        src_crs, dst_crs, src_width, src_height, *bounds
    )
    # from: http://xarray.pydata.org/en/stable/generated/xarray.open_rasterio.html
    x, y = dst_transform * np.meshgrid(
        np.arange(dst_width) + 0.5, np.arange(dst_height) + 0.5
    )
    dst = xr.DataArray(
        data=np.zeros((dst_height, dst_width), source.dtype),
        coords={"y": y[:, 0], "x": x[0, :]},
        dims=("y", "x"),
    )
    return dst_transform, dst


[docs] def reproject( source, like=None, src_crs=None, dst_crs=None, method="nearest", use_src_attrs=False, src_nodata=np.nan, **reproject_kwargs, ): """ Reprojects and/or resamples a 2D xarray DataArray to a different cellsize or coordinate system. * To resample to a new cellsize in the same projection: provide only ``like``. * To only reproject: provide only ``src_crs`` and ``src_crs``. * To reproject and resample to a specific domain: provide ``src_crs``, ``src_crs``, and ``like``. Note: when only ``like`` is provided, Cartesian (projected) coordinates are a ssumed for resampling. In case of non-Cartesian coordinates, specify ``src_crs`` and ``dst_crs`` for correct resampling. Parameters ---------- source: xarray DataArray The DataArray to be resampled and/or reprojected. Must contain dimensions ``y`` and ``x``. like: xarray DataArray Example DataArray that shows what the resampled result should look like in terms of coordinates. Must contain dimensions ``y`` and ``x``. src_crs: string, dict, rasterio.crs.CRS Coordinate system of ``source``. Options: * string: e.g. ``"EPSG:4326"`` * rasterio.crs.CRS dst_crs: string, dict, rasterio.crs.CRS Coordinate system of result. Options: * string: e.g. ``"EPSG:4326"`` * rasterio.crs.CRS use_src_attrs: boolean If True: Use metadata in ``source.attrs``, as generated by ``xarray.open_rasterio()``, to do reprojection. method: string The method to use for resampling/reprojection. Defaults to "nearest". GDAL methods are available: * nearest * bilinear * cubic * cubic_spline * lanczos * average * mode * gauss * max * min * med (50th percentile) * q1 (25th percentile) * q3 (75th percentile) reproject_kwargs: dict, optional keyword arguments for ``rasterio.warp.reproject()``. Returns ------- xarray.DataArray Resampled/reprojected DataArray. Examples -------- Resample a DataArray ``a`` to a new cellsize, using an existing DataArray ``b``: >>> c = imod.rasterio.reproject(source=a, like=b) Resample a DataArray to a new cellsize of 100.0, by creating a ``like`` DataArray first: (Note that dy must be negative, as is usual for geospatial grids.) >>> dims = ("y", "x") >>> coords = {"y": np.arange(200_000.0, 100_000.0, -100.0), "x": np.arange(0.0, 100_000.0, 100.0)} >>> b = xr.DataArray(data=np.empty((200, 100)), coords=coords, dims=dims) >>> c = imod.rasterio.reproject(source=a, like=b) Reproject a DataArray from one coordinate system (WGS84, EPSG:4326) to another (UTM30N, EPSG:32630): >>> c = imod.rasterio.reproject(source=a, src_crs="EPSG:4326", dst_crs="EPSG:32630") Get the reprojected DataArray in the desired shape and coordinates by providing ``like``: >>> c = imod.rasterio.reproject(source=a, like=b, src_crs="EPSG:4326", dst_crs="EPSG:32630") Open a single band raster, and reproject to RD new coordinate system (EPSG:28992), without explicitly specifying ``src_crs``. ``src_crs`` is taken from ``a.attrs``, so the raster file has to include coordinate system metadata for this to work. >>> a = rioxarray.open_rasterio("example.tif").squeeze("band") >>> c = imod.rasterio.reproject(source=a, use_src_attrs=True, dst_crs="EPSG:28992") In case of a rotated ``source``, provide ``src_transform`` directly or ``use_src_attrs=True`` to rely on generated attributes: >>> rotated = rioxarray.open_rasterio("rotated_example.tif").squeeze("band") >>> c = imod.rasterio.reproject(source=rotated, dst_crs="EPSG:28992", reproject_kwargs={"src_transform":affine.Affine(...)}) >>> c = imod.rasterio.reproject(source=rotated, dst_crs="EPSG:28992", use_src_attrs=True) """ # Make sure the rio accessor is avaible. import rioxarray # noqa pylint: F401 if not source.dims == ("y", "x"): raise ValueError( "reproject does not support dimensions other than ``x`` and ``y`` for ``source``." ) if like is not None: if not like.dims == ("y", "x"): raise ValueError( "reproject does not support dimensions other than ``x`` and ``y`` for ``like``." ) if use_src_attrs: # only provided when reproject is necessary try: src_crs = source.attrs["crs"] except KeyError: src_crs = source.rio.crs if isinstance(src_crs, str): if "epsg:" in src_crs.lower(): # Workaround for rioxarray.open_rasterio generation proj4 strings # https://github.com/mapbox/rasterio/issues/1809 epsg_code = src_crs.lower().split("epsg:")[-1] src_crs = rasterio.crs.CRS.from_epsg(epsg_code) else: src_crs = rasterio.crs.CRS.from_string(src_crs) elif isinstance(src_crs, rasterio.crs.CRS): pass else: raise ValueError( f"Invalid src_crs: {src_crs}. Must be either str or rasterio.crs.CRS object" ) if "nodatavals" in source.attrs: src_nodata = source.attrs["nodatavals"][0] else: rio_nodata = source.rio.nodata if rio_nodata is not None: src_nodata = rio_nodata resampling_methods = {e.name: e for e in rasterio.enums.Resampling} if isinstance(method, str): try: resampling_method = resampling_methods[method] except KeyError as e: raise ValueError( "Invalid resampling method. Available methods are: {}".format( resampling_methods.keys() ) ) from e elif isinstance(method, rasterio.enums.Resampling): resampling_method = method else: raise TypeError("method must be a string or rasterio.enums.Resampling") # Givens: source, like, method. No reprojection necessary. if src_crs is None and dst_crs is None: if like is None: raise ValueError( "If crs information is not provided, ``like`` must be provided." ) if resampling_method == rasterio.enums.Resampling.nearest: # this can be handled with xarray # xarray 0.10.9 needs .compute() # see https://github.com/pydata/xarray/issues/2454 return source.compute().reindex_like(like, method="nearest") else: # if no crs is defined, assume it should remain the same # in this case use UTM30, ESPG:32630, as a dummy value for GDAL # (Any projected coordinate system should suffice, Cartesian plane == Cartesian plane) dst = xr.DataArray( data=np.zeros(like.shape, source.dtype), coords={"y": like.y, "x": like.x}, dims=("y", "x"), ) src_crs = dst_crs = rasterio.crs.CRS.from_epsg(32630) src_transform = imod.util.spatial.transform(source) dst_transform = imod.util.spatial.transform(like) elif src_crs and dst_crs: if use_src_attrs: # TODO: modify if/when xarray uses affine by default for transform try: src_transform = affine.Affine(*source.attrs["transform"][:6]) except KeyError: src_transform = source.rio.transform() elif "src_transform" in reproject_kwargs.keys(): src_transform = reproject_kwargs.pop("src_transform") else: src_transform = imod.util.spatial.transform(source) # If no like is provided, just reproject to different coordinate system if like is None: dst_transform, dst = _reproject_dst(source, src_crs, dst_crs, src_transform) else: dst_transform = imod.util.spatial.transform(like) dst = xr.DataArray( data=np.zeros(like.shape, source.dtype), coords={"y": like.y, "x": like.x}, dims=("y", "x"), ) else: raise ValueError( "At least ``like``, or crs information for source and destination must be provided." ) if not src_transform[0] > 0: raise ValueError("dx of 'source' must be positive") if not src_transform[4] < 0: raise ValueError("dy of 'source' must be negative") if not dst_transform[0] > 0: raise ValueError("dx of 'like' must be positive") if not dst_transform[4] < 0: raise ValueError("dy of 'like' must be negative") rasterio.warp.reproject( source.values, dst.values, src_transform=src_transform, dst_transform=dst_transform, src_crs=src_crs, dst_crs=dst_crs, resampling=resampling_method, src_nodata=src_nodata, **reproject_kwargs, ) dst.attrs = source.attrs dst.attrs["transform"] = dst_transform dst.attrs["res"] = (abs(dst_transform[0]), abs(dst_transform[4])) dst.attrs["crs"] = dst_crs # TODO: what should be the type of "crs" field in attrs? # Doesn't actually matter for functionality # rasterio accepts string, dict, and CRS object return dst