-
Notifications
You must be signed in to change notification settings - Fork 85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discrepancy between xarray-spatial hillshade and GDAL hillshade #748
Comments
@thuydotm same azimuth and angle correct? Can you run with GDAL from the CLI and then post the command? |
@brendancol sure, here is the command:
|
@thuydotm I think the will require looking at the GDAL slope and aspect functions as well to see how those non-trivial calcs are made:
|
Here is some results I have, hope this helps :) TL;DR:
Coherence between the core functionsLooking at (only) the numpy algorithm from def _run_numpy(data, azimuth=225, angle_altitude=25):
data = data.astype(np.float32)
azimuth = 360.0 - azimuth
x, y = np.gradient(data)
slope = np.pi/2. - np.arctan(np.sqrt(x*x + y*y))
aspect = np.arctan2(-x, y)
azimuthrad = azimuth*np.pi/180.
altituderad = angle_altitude*np.pi/180.
shaded = np.sin(altituderad) * np.sin(slope) + np.cos(altituderad) * np.cos(slope) * np.cos((azimuthrad - np.pi/2.) - aspect)
result = (shaded + 1) / 2
result[(0, -1), :] = np.nan
result[:, (0, -1)] = np.nan
return result Aspectxarray-spatial has the same aspect function than GDAL's even if they are not written exactly the same way Core function (shaded)0. shaded = np.sin(alt_rad) * np.sin(np.pi/2. - np.arctan(np.sqrt(x2_y2))) + np.cos(alt_rad) * np.cos(np.pi/2. - np.arctan(np.sqrt(x2_y2))) * np.cos((az_rad - np.pi/2.) - aspect) 2. This can be simplified, knowing that:
into: shaded = np.sin(alt_rad) * np.cos(np.arctan(np.sqrt(x2_y2))) - np.cos(alt_rad) * np.sin(np.arctan(np.sqrt(x2_y2))) * np.sin(aspect - az_rad) 3. Moreover, since shaded = np.sin(alt_rad) * 1 / sqrt(1+x2_y2) - np.cos(alt_rad) * sqrt(x2_y2) / sqrt(1+x2_y2) * np.sin(aspect - az_rad) which can be factorised into: shaded=(np.sin(alt_rad) - np.cos(alt_rad) * sqrt(x2_y2) * np.sin(aspect - az_rad)/sqrt(1+x2_y2) 4. Knowing that here shaded=(np.sin(alt_rad) - np.cos(alt_rad) * sqrt(x2_y2) * np.sin(aspect - az_rad) / sqrt(1 + x2_y2) This is what we can see in GDAL 1.7.2, here shaded=(np.sin(alt_rad) - np.cos(alt_rad) * np.sqrt(x2_y2) * np.sin(aspect - az_rad)) / np.sqrt(1 + x2_y2) 5. Newer versions of GDAL are going further into simplification, simplifying also the aspect sinus.
Gradient issueI replicated the GDAL function here: ds = rasterio.open("dem.tif")
array = ds.read(masked=True)
# Compute angles
az_rad = azimuth * DEG_2_RAD
alt_rad = (90 - zenith) * DEG_2_RAD
# Compute slope and aspect
dx, dy = np.gradient(np.where(array.mask, 0.0, array.data), *ds.res)
x2_y2 = dx**2 + dy**2
aspect = np.arctan2(dx, dy)
# Compute hillshade (GDAL algo)
hshade = (
np.sin(alt_rad) + np.cos(alt_rad) * np.sqrt(x2_y2) * np.sin(aspect - az_rad)
) / np.sqrt(1 + x2_y2)
hshade = np.where(hshade <= 0, 1.0, 254.0 * hshade + 1) I can validate it by displaying in QGIS the input DEM with hillshade and mine (they are similar): You can see the dataset's resolution inside the gradient function. If I remove it and scale the two hillshades (mine and xarray-spatial's), I get a similar output: Here you can find the used dataset: |
Here is a proposition for the updated code, but I'm using rioxarray and it has a GDAL dependency to retrieve the resolution, so I won't make any PR because I don't know how to retrieve it otherwise. from functools import partial
import rioxarray as rxr
xds = rxr.open_rasterio("dem.tif")
hillshade(xds, azimuth=315, zenith=45)
def hillshade(
xds: xr.DataArray, azimuth: float, angle_altitude: float, **kwargs
) :
def _run_numpy(data, azimuth, angle_altitude, res):
# Compute angles
az_rad = azimuth * DEG_2_RAD
alt_rad = angle_altitude * DEG_2_RAD
# Compute slope and aspect
dx, dy = np.gradient(data, *res)
x2_y2 = dx ** 2 + dy ** 2
aspect = np.arctan2(dx, dy)
# Compute hillshade (GDAL algo)
hshade = (np.sin(alt_rad) + np.cos(alt_rad) * np.sqrt(x2_y2) * np.sin(aspect - az_rad)) / np.sqrt(1 + x2_y2)
hshade = np.where(hshade <= 0, 1.0, 254.0 * hshade + 1)
hshade[(0, -1), :] = np.nan
hshade[:, (0, -1)] = np.nan
return hshade
_func = partial(_run_numpy, azimuth=azimuth, angle_altitude=angle_altitude, res=np.abs(xds.rio.resolution()))
out = xds.data.map_overlap(
out = xds.data.map_overlap(
_func,
depth=(1, 1),
boundary=np.nan,
meta=np.array(())
)
xds = xds.copy(data=out).rename(kwargs.get("name", "hillshade"))
return xds |
I'm testing xarray-spatial against QGIS and seeing that when running hillshade on the same input data, the results by xarray-spatial and GDAL/QGIS are very different. Source code for QGIS hillshade can be found at: https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/gdal/hillshade.py. This needs more research to see how the algorithm was implemented in QGIS to clearly understand the difference between the 2 libraries.
Input data:
xarray-spatial hillshade:
QGIS/GDAL hillshade
The text was updated successfully, but these errors were encountered: