What is the correct way to reproject a raster from a CRS to another using Python?

Viewed 865

I have a raster of Land Cover data (specifically this one /eodata/auxdata/S2GLC/2017/S2GLC_T32TMS_2017 in https://finder.creodias.eu) that uses 'epsg:32632' as CRS. I want to reproject this raster on 'epsg:21781'. This is what the raster looks like when I open it with xarray.

fn = 'data/S2GLC_T32TMS_2017/S2GLC_T32TMS_2017.tif'
da = xr.open_rasterio(fn).sel(band=1, drop=True)
da
<xarray.DataArray (y: 10980, x: 10980)>
[120560400 values with dtype=uint8]
Coordinates:
  * y        (y) float64 5.2e+06 5.2e+06 5.2e+06 ... 5.09e+06 5.09e+06 5.09e+06
  * x        (x) float64 4e+05 4e+05 4e+05 ... 5.097e+05 5.097e+05 5.098e+05
Attributes:
    transform:      (10.0, 0.0, 399960.0, 0.0, -10.0, 5200020.0)
    crs:            +init=epsg:32632
    res:            (10.0, 10.0)
    is_tiled:       0
    nodatavals:     (nan,)
    scales:         (1.0,)
    offsets:        (0.0,)
    AREA_OR_POINT:  Area
    INTERLEAVE:     BAND

My usual workflow was to transform all the point coordinates, create my destination grid and interpolate using nearest neighbors. Something that looks like this:

import numpy as np
import xarray as xr
import pyproj
from scipy.interpolate import griddata 

y = da.y.values
x = da.x.values
xx, yy = np.meshgrid(x,y)
# (n,2) point coordinates in the original CRS
src_coords = np.column_stack([xx.flatten(), yy.flatten()]) 

transformer = pyproj.transformer.Transformer.from_crs('epsg:32632', 'epsg:21781')
xx, yy = transformer.transform(src_coords[:,0], src_coords[:,1])
# (n,2) point coordinates in the destination CRS, which are not on a regular grid
dst_coords = np.column_stack([xx.flatten(), yy.flatten()]) 


# I define my destination **regular** grid coordinates
x = np.linspace(620005,719995,10)
y = np.linspace(199995,100005,10)
xx, yy = np.meshgrid(x,y)
dst_grid = np.column_stack([xx.flatten(), yy.flatten()])

# I interpolate onto the grid
reprojected_array = griddata(
    src_coords, da.values.flatten(), dst_coords, method='nearest'
).reshape(dst_shape)

Although this method is fairly transparent and (apparently) error-free, it can take very long when dealing with billions of points. Recently, I discovered rasterio's reproject function, and I was blown away by how fast it is. This is how I implemented it:

source = da.values
destination = np.zeros(dst_shape, np.int16)

res, aff = reproject(
    source,
    destination,
    src_transform=src_transform, # affine transformation from original data
    src_crs=src_crs,
    dst_transform=dst_transform, # affine transformation that corresponds to the grid defined in the other approach
    dst_crs=dst_crs,
    resampling=Resampling.nearest) # using nearest neighbors just like with scope's griddata

Naturally I wanted to compare the results expecting them to be the same, but they were not, as you can see in the figure.

enter image description here

The resolution is 10 meters so the differences are not large, but after careful comparison with precise satellite data in the 'epsg:21781' coordinates, it looks like the old approach yields better results.

So my questions are:

  • why do these results differ?
  • is one approach better than the other? Are there specific conditions where one should prefer one or the other?
1 Answers

Griddata find nearest points in Euclidean distance, on whatever map projection you give it. Thus the nearest neighbors from a pipeline like
  4326 data points --> reproject --> nearest-Euclidean griddata
  query points

depend on the "reproject". Could you try +proj=sinu +lon_0= middle lon for both data and query ?

What one really wants is a nearest-neighbor engine with great-circle distance, not Euclidean distance. The difference may be insignificant for small grids, or near the equator, but less so in Finland -- cos 61° / cos 60° is ~ 97 %.


TL;DR
Is pyproj.transformer.Transformer.from_crs('epsg:32632', 'epsg:21781') "correct" ? Don't know. I see no test suite, and a couple of issues:
warp.reproject() generates the wrong result
roundtrip test \

"Nearest neighbor" is ill-defined / sensitive halfway between data points, e.g. along the lines x or y = int + 0.5 on an int grid. This is easy to test with KDTree.

xarray makes regular (Cartesian) grids easy, but afaik does not do curvilinear (2d) grids.

Related