Skip to content

Commit b0440de

Browse files
authored
Add GPU support for hillshade (#151)
* correct docs * add gpu support for hillshade * curvature: correct order of if else statement * hillshade: add gpu cupy support
1 parent 8b99213 commit b0440de

File tree

3 files changed

+143
-41
lines changed

3 files changed

+143
-41
lines changed

xrspatial/curvature.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,14 +120,14 @@ def curvature(agg, name='curvature'):
120120
elif has_cuda() and isinstance(agg.data, cupy.ndarray):
121121
out = _run_cupy(agg.data, cellsize)
122122

123-
# dask + numpy case
124-
elif isinstance(agg.data, da.Array):
125-
out = _run_dask_numpy(agg.data, cellsize)
126-
127123
# dask + cupy case
128124
elif has_cuda() and isinstance(agg.data, da.Array) and is_cupy_backed(agg):
129125
out = _run_dask_cupy(agg.data, cellsize)
130126

127+
# dask + numpy case
128+
elif isinstance(agg.data, da.Array):
129+
out = _run_dask_numpy(agg.data, cellsize)
130+
131131
else:
132132
raise TypeError('Unsupported Array Type: {}'.format(type(agg.data)))
133133

xrspatial/hillshade.py

100644100755
Lines changed: 107 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1+
# std lib
12
from functools import partial
3+
from math import sqrt
4+
from typing import Union
25

3-
import numpy as np
4-
5-
from xarray import DataArray
6+
# 3rd-party
7+
try:
8+
import cupy
9+
except ImportError:
10+
class cupy(object):
11+
ndarray = False
612

713
import dask.array as da
814

15+
from numba import cuda
16+
17+
import numpy as np
18+
import xarray as xr
919

10-
def _hillshade(data, azimuth=225, angle_altitude=25):
20+
# local modules
21+
from xrspatial.utils import cuda_args
22+
from xrspatial.utils import has_cuda
23+
from xrspatial.utils import is_cupy_backed
24+
25+
26+
def _run_numpy(data, azimuth=225, angle_altitude=25):
1127
azimuth = 360.0 - azimuth
1228
x, y = np.gradient(data)
1329
slope = np.pi/2. - np.arctan(np.sqrt(x*x + y*y))
@@ -18,7 +34,71 @@ def _hillshade(data, azimuth=225, angle_altitude=25):
1834
result = (shaded + 1) / 2
1935
result[(0, -1), :] = np.nan
2036
result[:, (0, -1)] = np.nan
21-
return data
37+
return result
38+
39+
40+
def _run_dask_numpy(data, azimuth, angle_altitude):
41+
_func = partial(_run_numpy, azimuth=azimuth, angle_altitude=angle_altitude)
42+
out = data.map_overlap(_func,
43+
depth=(1, 1),
44+
boundary=np.nan,
45+
meta=np.array(()))
46+
return out
47+
48+
49+
@cuda.jit
50+
def _gpu_calc(x, y, out):
51+
i, j = cuda.grid(2)
52+
if i < out.shape[0] and j < out.shape[1]:
53+
out[i, j] = sqrt(x[i, j] * x[i, j] + y[i, j] * y[i, j])
54+
55+
56+
@cuda.jit
57+
def _gpu_cos_part(cos_altituderad, cos_slope, cos_aspect, out):
58+
i, j = cuda.grid(2)
59+
if i < out.shape[0] and j < out.shape[1]:
60+
out[i, j] = cos_altituderad * cos_slope[i, j] * cos_aspect[i, j]
61+
62+
63+
def _run_cupy(data, azimuth, angle_altitude):
64+
x, y = np.gradient(data.get())
65+
x = cupy.asarray(x, dtype=x.dtype)
66+
y = cupy.asarray(y, dtype=y.dtype)
67+
68+
altituderad = angle_altitude * np.pi / 180.
69+
sin_altituderad = np.sin(altituderad)
70+
cos_altituderad = np.cos(altituderad)
71+
72+
griddim, blockdim = cuda_args(data.shape)
73+
arctan_part = cupy.empty(data.shape, dtype='f4')
74+
_gpu_calc[griddim, blockdim](x, y, arctan_part)
75+
76+
slope = np.pi / 2. - np.arctan(arctan_part)
77+
sin_slope = np.sin(slope)
78+
sin_part = sin_altituderad * sin_slope
79+
80+
azimuthrad = (360.0 - azimuth) * np.pi / 180.
81+
aspect = (azimuthrad - np.pi / 2.) - np.arctan2(-x, y)
82+
cos_aspect = np.cos(aspect)
83+
cos_slope = np.cos(slope)
84+
85+
cos_part = cupy.empty(data.shape, dtype='f4')
86+
_gpu_cos_part[griddim, blockdim](cos_altituderad, cos_slope,
87+
cos_aspect, cos_part)
88+
shaded = sin_part + cos_part
89+
out = (shaded + 1) / 2
90+
91+
out[0, :] = cupy.nan
92+
out[-1, :] = cupy.nan
93+
out[:, 0] = cupy.nan
94+
out[:, -1] = cupy.nan
95+
96+
return out
97+
98+
99+
def _run_dask_cupy(data, azimuth, angle_altitude):
100+
msg = 'Upstream bug in dask prevents cupy backed arrays'
101+
raise NotImplementedError(msg)
22102

23103

24104
def hillshade(agg, azimuth=225, angle_altitude=25, name='hillshade'):
@@ -32,14 +112,6 @@ def hillshade(agg, azimuth=225, angle_altitude=25, name='hillshade'):
32112
azimuth : int, optional (default: 315)
33113
The angle between the north vector and the perpendicular projection
34114
of the light source down onto the horizon specified in degrees.
35-
cmap : list of colors or matplotlib.colors.Colormap, optional
36-
The colormap to use. Can be either a list of colors (in any of the
37-
formats described above), or a matplotlib colormap object.
38-
Default is `["lightgray", "black"]`
39-
alpha : int, optional
40-
Value between 0 - 255 representing the alpha value of pixels which contain
41-
data (i.e. non-nan values). Regardless of this value, `NaN` values are
42-
set to fully transparent.
43115
44116
Returns
45117
-------
@@ -51,14 +123,27 @@ def hillshade(agg, azimuth=225, angle_altitude=25, name='hillshade'):
51123
- http://geoexamples.blogspot.com/2014/03/shaded-relief-images-using-gdal-python.html
52124
"""
53125

54-
if isinstance(agg.data, da.Array):
55-
_func = partial(_hillshade, azimuth=azimuth, angle_altitude=angle_altitude)
56-
out = agg.data.map_overlap(_func,
57-
depth=(1, 1),
58-
boundary=np.nan,
59-
meta=np.array(()))
126+
# numpy case
127+
if isinstance(agg.data, np.ndarray):
128+
out = _run_numpy(agg.data, azimuth, angle_altitude)
129+
130+
# cupy case
131+
elif has_cuda() and isinstance(agg.data, cupy.ndarray):
132+
out = _run_cupy(agg.data, azimuth, angle_altitude)
133+
134+
# dask + cupy case
135+
elif has_cuda() and isinstance(agg.data, da.Array) and is_cupy_backed(agg):
136+
out = _run_dask_cupy(agg.data, azimuth, angle_altitude)
137+
138+
# dask + numpy case
139+
elif isinstance(agg.data, da.Array):
140+
out = _run_dask_numpy(agg.data, azimuth, angle_altitude)
141+
60142
else:
61-
out = _hillshade(agg.data, azimuth, angle_altitude)
143+
raise TypeError('Unsupported Array Type: {}'.format(type(agg.data)))
62144

63-
return DataArray(out, name=name, dims=agg.dims,
64-
coords=agg.coords, attrs=agg.attrs)
145+
return xr.DataArray(out,
146+
name=name,
147+
coords=agg.coords,
148+
dims=agg.dims,
149+
attrs=agg.attrs)

xrspatial/tests/test_hillshade.py

100644100755
Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
import pytest
12
import xarray as xr
23
import numpy as np
34

5+
import dask.array as da
6+
47
from xrspatial import hillshade
8+
from xrspatial.utils import doesnt_have_cuda
59

6-
import dask.array as da
10+
11+
elevation = np.asarray(
12+
[[1432.6542, 1432.4764, 1432.4764, 1432.1207, 1431.9429, np.nan],
13+
[1432.6542, 1432.6542, 1432.4764, 1432.2986, 1432.1207, np.nan],
14+
[1432.832, 1432.6542, 1432.4764, 1432.2986, 1432.1207, np.nan],
15+
[1432.832, 1432.6542, 1432.4764, 1432.4764, 1432.1207, np.nan],
16+
[1432.832, 1432.6542, 1432.6542, 1432.4764, 1432.2986, np.nan],
17+
[1432.832, 1432.6542, 1432.6542, 1432.4764, 1432.2986, np.nan],
18+
[1432.832, 1432.832, 1432.6542, 1432.4764, 1432.4764, np.nan]],
19+
dtype=np.float32)
720

821

922
def _do_sparse_array(data_array):
@@ -53,26 +66,30 @@ def test_hillshade():
5366

5467

5568
def test_numpy_equals_dask():
56-
57-
# input data
58-
data = np.asarray(
59-
[[1432.6542, 1432.4764, 1432.4764, 1432.1207, 1431.9429, np.nan],
60-
[1432.6542, 1432.6542, 1432.4764, 1432.2986, 1432.1207, np.nan],
61-
[1432.832, 1432.6542, 1432.4764, 1432.2986, 1432.1207, np.nan],
62-
[1432.832, 1432.6542, 1432.4764, 1432.4764, 1432.1207, np.nan],
63-
[1432.832, 1432.6542, 1432.6542, 1432.4764, 1432.2986, np.nan],
64-
[1432.832, 1432.6542, 1432.6542, 1432.4764, 1432.2986, np.nan],
65-
[1432.832, 1432.832, 1432.6542, 1432.4764, 1432.4764, np.nan]],
66-
dtype=np.float32)
67-
6869
attrs = {'res': (10.0, 10.0)}
6970

70-
small_numpy_based_data_array = xr.DataArray(data, attrs=attrs)
71-
dask_data = da.from_array(data, chunks=(3, 3))
71+
small_numpy_based_data_array = xr.DataArray(elevation, attrs=attrs)
72+
dask_data = da.from_array(elevation, chunks=(3, 3))
7273
small_das_based_data_array = xr.DataArray(dask_data, attrs=attrs)
7374

7475
numpy_result = hillshade(small_numpy_based_data_array, name='numpy')
7576
dask_result = hillshade(small_das_based_data_array, name='dask')
7677
dask_result.data = dask_result.data.compute()
7778

7879
assert np.isclose(numpy_result, dask_result, equal_nan=True).all()
80+
81+
82+
@pytest.mark.skipif(doesnt_have_cuda(), reason="CUDA Device not Available")
83+
def test_hillshade_gpu_equals_cpu():
84+
85+
import cupy
86+
87+
small_da = xr.DataArray(elevation, attrs={'res': (10.0, 10.0)})
88+
cpu = hillshade(small_da, name='numpy_result')
89+
90+
small_da_cupy = xr.DataArray(cupy.asarray(elevation), attrs={'res': (10.0, 10.0)})
91+
gpu = hillshade(small_da_cupy, name='cupy_result')
92+
93+
assert isinstance(gpu.data, cupy.ndarray)
94+
95+
assert np.isclose(cpu, gpu, equal_nan=True).all()

0 commit comments

Comments
 (0)