Skip to content

Commit 14261f5

Browse files
committed
FIX: Fix DEM management with RPC orthorectification: handle correctly the vertical CRS (see dem notebook and EOREADER_DEM_VCRS environment variable). #53
1 parent e52b49e commit 14261f5

4 files changed

Lines changed: 56 additions & 145 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- FIX: Loosen the constraints on PlanetScope stack name as it may change, from `Analytic` to `composite`, etc. [#244](https://github.com/sertit/eoreader/issues/244)
1212
- FIX: Add a fallback in case of impossibleness of reading ICEYE `QUICKLOOK.kml` file
1313
- FIX: Manage the case of Maxar data with negative absolute calibration factor: don't compute the reflectance and leave it as is.
14+
- FIX: Fix DEM management with RPC orthorectification: handle correctly the vertical CRS (see dem notebook and `EOREADER_DEM_VCRS` environment variable). [#53](https://github.com/sertit/eoreader/issues/53)
1415

1516
## 0.22.4 (2025-07-07)
1617

docs/notebooks/dem.ipynb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"\n",
3636
"from eoreader.reader import Reader\n",
3737
"from eoreader.bands import DEM, SLOPE, HILLSHADE, VV, RED\n",
38-
"from eoreader.env_vars import DEM_PATH, SNAP_DEM_NAME\n",
38+
"from eoreader.env_vars import DEM_PATH, DEM_VCRS, SNAP_DEM_NAME\n",
3939
"from eoreader.keywords import DEM_KW, SLOPE_KW, HILLSHADE_KW\n",
4040
"from eoreader.products import SnapDems"
4141
]
@@ -421,8 +421,16 @@
421421
"\n",
422422
"<div class=\"alert alert-warning\">\n",
423423
" \n",
424-
"<strong>Warning:</strong> The orthorectification may take a while !\n",
424+
"<strong>Warning:</strong> The orthorectification may take a while!\n",
425425
" \n",
426+
"</div>\n",
427+
"\n",
428+
"<div class=\"alert alert-warning\">\n",
429+
"\n",
430+
"<strong>Warning:</strong> Orthorectification with RPCs should be done with an ellipsoid-based DEM! <br>\n",
431+
"EOReader is able to convert your put DEM if you provide the corresponding vertical CRS (passing a 'vcrs' keyword or put it in 'EOREADER_DEM_VCRS' environment variable). <br>\n",
432+
"More insights here: https://xdem.readthedocs.io/en/stable/vertical_ref.html\n",
433+
"\n",
426434
"</div>"
427435
]
428436
},
@@ -460,7 +468,9 @@
460468
"outputs": [],
461469
"source": [
462470
"# With environment variable\n",
463-
"with tempenv.TemporaryEnvironment({DEM_PATH: dem}):\n",
471+
"# COPDEM-30 is based on EGM08\n",
472+
"# (however, EOReader is able to detect COPDEM vertical CRS thanks to the filename: it's looking for 'COPDEM' or 'Copernicus' in it)\n",
473+
"with tempenv.TemporaryEnvironment({DEM_PATH: dem, DEM_VCRS: \"EGM08\"}):\n",
464474
" red = prod.load(RED)[RED]\n",
465475
"\n",
466476
"red.plot()"

eoreader/env_vars.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
DEM_PATH = "EOREADER_DEM_PATH"
2828
"""Environment variable for overriding default DEM path"""
2929

30+
DEM_VCRS = "EOREADER_DEM_VCRS"
31+
"""
32+
Environment variable for setting the vertical CRS of the DEM.
33+
Only useful to reproject your data with RPCs.
34+
Not useful if your DEM has already a vertical CRS or if its height is already taken from the ellipsoid.
35+
36+
- EOReader is able to detect the vertical CRS of the COPDEM (if COPDEM or Copernicus in its name).
37+
- :code:`xdem` has also a mechanism of auto-detection of some CRS. See their documentation for more details.
38+
"""
39+
3040
SNAP_DEM_NAME = "EOREADER_SNAP_DEM_NAME"
3141
"""
3242
Environment variable for overriding default DEM name used in SNAP.

eoreader/products/product.py

Lines changed: 32 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
from eoreader.env_vars import (
8383
CI_EOREADER_BAND_FOLDER,
8484
DEM_PATH,
85+
DEM_VCRS,
8586
LEGACY_BAND_NAME_RESOLUTION,
8687
TILE_SIZE,
8788
)
@@ -2575,7 +2576,7 @@ def _reproject(
25752576
ortho_path: AnyPathStrType,
25762577
pixel_size: float = None,
25772578
**kwargs,
2578-
) -> (np.ndarray, dict):
2579+
) -> xr.DataArray:
25792580
"""
25802581
Reproject using RPCs (cannot use another pixel size than src to ensure RPCs are valid)
25812582
@@ -2586,153 +2587,42 @@ def _reproject(
25862587
dem_path (str): DEM path
25872588
25882589
Returns:
2589-
(np.ndarray, dict): Reprojected array and its metadata
2590+
xr.DataArray: Reprojected array
25902591
"""
25912592
if pixel_size is None:
25922593
pixel_size = self.pixel_size
25932594

2594-
kw = utils._prune_keywords(["nodata"], **kwargs)
2595-
2596-
# Set RPC keywords
2597-
# See https://gdal.org/en/stable/api/gdal_alg.html#_CPPv426GDALCreateRPCTransformerV2PK13GDALRPCInfoV2idPPc
2598-
LOGGER.debug(f"Orthorectifying data with {dem_path}")
2599-
2600-
# RPC_DEM doesn't work with cloud-based DEM
2601-
# Read it to the extent of the product and save it on disk
2602-
if path.is_cloud_path(dem_path):
2603-
cached_dem_path, cached_dem_exists = self._get_out_path(
2604-
AnyPath(dem_path).name
2605-
)
2606-
if not cached_dem_exists:
2607-
LOGGER.warning(
2608-
"gdalwarp cannot process DEM stored on cloud with 'RPC_DEM' argument, "
2609-
"hence cloud-stored DEM cannot be used with non orthorectified data. "
2610-
f"(DEM: {dem_path}). "
2611-
"The DEM will be cached before the operation."
2612-
)
2613-
2614-
utils.write(
2615-
utils.read(dem_path, window=self.extent()),
2616-
cached_dem_path,
2617-
dtype=np.float32,
2618-
)
2619-
2620-
LOGGER.debug("DEM cached.")
2621-
dem_path = str(cached_dem_path)
2622-
2623-
# Set SRC crs if needed
2624-
if src_xda.rio.crs is None:
2625-
src_xda.rio.write_crs(self._get_raw_crs(), inplace=True)
2626-
2627-
kw.update(
2628-
{
2629-
"RPC_DEM": dem_path,
2630-
"RPC_DEM_MISSING_VALUE": 0,
2631-
"OSR_USE_ETMERC": "YES",
2632-
"BIGTIFF": "IF_NEEDED",
2633-
}
2634-
)
2635-
# https://gis.stackexchange.com/questions/328366/gdalwarp-orthorectification-worldview-3-not-using-rpc-projection-properly fixed
2636-
# Error threshold for transformation approximation, expressed as a number of source pixels.
2637-
# Defaults to 0.125 pixels unless the RPC_DEM transformer option is specified, in which case an exact transformer, i.e. err_threshold=0, will be used.
2638-
2639-
# Reproject with rioxarray
2640-
# Seems to handle the resolution well on the contrary to rasterio's reproject...
2641-
2595+
kw = utils._prune_keywords(["nodata", "num_threads"], **kwargs)
26422596
resampling = kw.pop("resampling", self.band_resampling)
2597+
vcrs = kw.pop("vcrs", os.getenv(DEM_VCRS))
2598+
out_xda = rasters.reproject(
2599+
src_xda,
2600+
rpcs=rpcs,
2601+
dem_path=dem_path,
2602+
pixel_size=pixel_size,
2603+
dst_crs=self.crs(),
2604+
resampling=resampling,
2605+
nodata=self._raw_nodata,
2606+
num_threads=utils.get_max_cores(),
2607+
vcrs=vcrs,
2608+
**kwargs,
2609+
).rename(f"Reprojected stack of {self.name}")
26432610

2644-
try:
2645-
out_xda = src_xda.rio.reproject(
2646-
dst_crs=self.crs(),
2647-
resolution=pixel_size,
2648-
resampling=resampling,
2649-
nodata=self._raw_nodata,
2650-
num_threads=utils.get_max_cores(),
2651-
rpcs=rpcs,
2652-
dtype=src_xda.dtype,
2653-
**kw,
2654-
)
2655-
out_xda.rename(f"Reprojected stack of {self.name}")
2656-
2657-
if "long_name" in kw:
2658-
out_xda.attrs["long_name"] = kw["long_name"]
2659-
elif kw.get("band") == PAN:
2660-
out_xda.attrs["long_name"] = "PAN"
2661-
else:
2662-
out_xda.attrs["long_name"] = self.get_bands_names()
2663-
2664-
utils.write(
2665-
out_xda,
2666-
ortho_path,
2667-
dtype=np.float32,
2668-
nodata=self._raw_nodata,
2669-
tags=kw.get("tags"),
2670-
predictor=kw.get("predictor"),
2671-
)
2672-
2673-
# Daskified reproject doesn't seem to work with RPC
2674-
# See https://github.com/opendatacube/odc-geo/issues/193
2675-
# from odc.geo import xr # noqa
2676-
# out_xda = src_xda.odc.reproject(
2677-
# how=self.crs(),
2678-
# resolution=self.pixel_size,
2679-
# resampling=kwargs.pop("resampling", self.band_resampling),
2680-
# dst_nodata=self._raw_nodata,
2681-
# num_threads=utils.get_max_cores(),
2682-
# rpcs=rpcs,
2683-
# dtype=src_xda.dtype,
2684-
# **kwargs
2685-
# )
2686-
2687-
# Legacy with rasterio directly: rioxarray is bugged with RPCs and Python 3.9
2688-
# https://github.com/corteva/rioxarray/issues/844
2689-
except ValueError:
2690-
from sertit.rasters import get_nodata_value_from_xr
2691-
from sertit.rasters_rio import write
2692-
2693-
nodata = get_nodata_value_from_xr(src_xda)
2694-
arr = src_xda.fillna(nodata) if nodata is not None else src_xda
2695-
2696-
# WARNING: may not give correct output pixel size
2697-
out_arr, dst_transform = warp.reproject(
2698-
arr.compute().data,
2699-
src_transform=None,
2700-
rpcs=rpcs,
2701-
src_crs=self._get_raw_crs(),
2702-
src_nodata=self._raw_nodata,
2703-
dst_crs=self.crs(),
2704-
dst_resolution=pixel_size,
2705-
dst_nodata=self._raw_nodata, # input data should be in integer
2706-
num_threads=utils.get_max_cores(),
2707-
resampling=resampling,
2708-
**kw,
2709-
)
2710-
# Get dims
2711-
count, height, width = out_arr.shape
2712-
2713-
# Update metadata
2714-
meta = {
2715-
"driver": "GTiff",
2716-
"dtype": src_xda.dtype,
2717-
"nodata": self._raw_nodata,
2718-
"width": width,
2719-
"height": height,
2720-
"count": count,
2721-
"crs": self.crs(),
2722-
"transform": dst_transform,
2723-
"compress": "lzw",
2724-
}
2725-
write(
2726-
out_arr,
2727-
meta,
2728-
ortho_path,
2729-
dtype=np.float32,
2730-
nodata=self._raw_nodata,
2731-
tags=kw.get("tags"),
2732-
predictor=kw.get("predictor"),
2733-
driver=utils.get_driver(kw),
2734-
)
2735-
out_xda = utils.read(ortho_path)
2611+
if "long_name" in kw:
2612+
out_xda.attrs["long_name"] = kw["long_name"]
2613+
elif kw.get("band") == PAN:
2614+
out_xda.attrs["long_name"] = "PAN"
2615+
else:
2616+
out_xda.attrs["long_name"] = self.get_bands_names()
2617+
2618+
utils.write(
2619+
out_xda,
2620+
ortho_path,
2621+
dtype=np.float32,
2622+
nodata=self._raw_nodata,
2623+
tags=kw.get("tags"),
2624+
predictor=kw.get("predictor"),
2625+
)
27362626
return out_xda
27372627

27382628
def _warp_band(

0 commit comments

Comments
 (0)