|
2 | 2 | unicode_literals)
|
3 | 3 |
|
4 | 4 | import six
|
| 5 | +import itertools |
5 | 6 |
|
6 | 7 | from nose.tools import assert_raises
|
7 | 8 |
|
|
10 | 11 |
|
11 | 12 | import matplotlib.colors as mcolors
|
12 | 13 | import matplotlib.cm as cm
|
| 14 | +import matplotlib.cbook as cbook |
13 | 15 | import matplotlib.pyplot as plt
|
14 | 16 | from matplotlib.testing.decorators import image_comparison, cleanup
|
15 | 17 |
|
@@ -228,32 +230,105 @@ def gray_from_float_rgba():
|
228 | 230 | assert_raises(ValueError, gray_from_float_rgba)
|
229 | 231 |
|
230 | 232 |
|
231 |
| -def test_light_source_shading_color_range(): |
232 |
| - # see also |
233 |
| - #http://matplotlib.org/examples/pylab_examples/shading_example.html |
234 |
| - |
235 |
| - from matplotlib.colors import LightSource |
236 |
| - from matplotlib.colors import Normalize |
237 |
| - |
238 |
| - refinput = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) |
239 |
| - norm = Normalize(vmin=0, vmax=50) |
240 |
| - ls = LightSource(azdeg=0, altdeg=65) |
241 |
| - testoutput = ls.shade(refinput, plt.cm.jet, norm=norm) |
242 |
| - refoutput = np.array([ |
243 |
| - [[0., 0., 0.58912656, 1.], |
244 |
| - [0., 0., 0.67825312, 1.], |
245 |
| - [0., 0., 0.76737968, 1.], |
246 |
| - [0., 0., 0.85650624, 1.]], |
247 |
| - [[0., 0., 0.9456328, 1.], |
248 |
| - [0., 0., 1., 1.], |
249 |
| - [0., 0.04901961, 1., 1.], |
250 |
| - [0., 0.12745098, 1., 1.]], |
251 |
| - [[0., 0.22156863, 1., 1.], |
252 |
| - [0., 0.3, 1., 1.], |
253 |
| - [0., 0.37843137, 1., 1.], |
254 |
| - [0., 0.45686275, 1., 1.]] |
255 |
| - ]) |
256 |
| - assert_array_almost_equal(refoutput, testoutput) |
| 233 | +@image_comparison(baseline_images=['light_source_shading_topo'], |
| 234 | + extensions=['png']) |
| 235 | +def test_light_source_topo_surface(): |
| 236 | + """Shades a DEM using different v.e.'s and blend modes.""" |
| 237 | + dem = np.load(cbook.get_sample_data('jacksboro_fault_dem.npz')) |
| 238 | + |
| 239 | + # Get the true cellsize in meters for accurate vertical exaggeration |
| 240 | + dx, dy = dem['dx'], dem['dy'] # In decimal degrees |
| 241 | + # Convert to meters... |
| 242 | + dx = 111320.0 * dx * np.cos(dem['ymin']) |
| 243 | + dy = 111320.0 * dy |
| 244 | + |
| 245 | + ls = mcolors.LightSource(315, 45) |
| 246 | + elev = dem['elevation'] |
| 247 | + cmap = cm.gist_earth |
| 248 | + |
| 249 | + fig, axes = plt.subplots(nrows=3, ncols=3) |
| 250 | + for row, mode in zip(axes, ['hsv', 'overlay', 'soft']): |
| 251 | + for ax, ve in zip(row, [0.1, 1, 10]): |
| 252 | + rgb = ls.shade(elev, cmap, vert_exag=ve, dx=dx, dy=dy, |
| 253 | + blend_mode=mode) |
| 254 | + ax.imshow(rgb) |
| 255 | + ax.set(xticks=[], yticks=[]) |
| 256 | + |
| 257 | + |
| 258 | +def test_light_source_hillshading(): |
| 259 | + """Compare the current hillshading method against one that should be |
| 260 | + mathematically equivalent. Illuminates a cone from a range of angles.""" |
| 261 | + |
| 262 | + def alternative_hillshade(azimuth, elev, z): |
| 263 | + illum = _sph2cart(*_azimuth2math(azimuth, elev)) |
| 264 | + illum = np.array(illum) |
| 265 | + |
| 266 | + dy, dx = np.gradient(-z) |
| 267 | + dy = -dy |
| 268 | + dz = np.ones_like(dy) |
| 269 | + normals = np.dstack([dx, dy, dz]) |
| 270 | + normals /= np.linalg.norm(normals, axis=2)[..., None] |
| 271 | + |
| 272 | + intensity = np.tensordot(normals, illum, axes=(2, 0)) |
| 273 | + intensity -= intensity.min() |
| 274 | + intensity /= intensity.ptp() |
| 275 | + return intensity |
| 276 | + |
| 277 | + y, x = np.mgrid[5:0:-1, :5] |
| 278 | + z = -np.hypot(x - x.mean(), y - y.mean()) |
| 279 | + |
| 280 | + for az, elev in itertools.product(range(0, 390, 30), range(0, 105, 15)): |
| 281 | + ls = mcolors.LightSource(az, elev) |
| 282 | + h1 = ls.hillshade(z) |
| 283 | + h2 = alternative_hillshade(az, elev, z) |
| 284 | + assert_array_almost_equal(h1, h2) |
| 285 | + |
| 286 | + |
| 287 | +def test_light_source_planar_hillshading(): |
| 288 | + """Ensure that the illumination intensity is correct for planar |
| 289 | + surfaces.""" |
| 290 | + |
| 291 | + def plane(azimuth, elevation, x, y): |
| 292 | + """Create a plane whose normal vector is at the given azimuth and |
| 293 | + elevation.""" |
| 294 | + theta, phi = _azimuth2math(azimuth, elevation) |
| 295 | + a, b, c = _sph2cart(theta, phi) |
| 296 | + z = -(a*x + b*y) / c |
| 297 | + return z |
| 298 | + |
| 299 | + def angled_plane(azimuth, elevation, angle, x, y): |
| 300 | + """Create a plane whose normal vector is at an angle from the given |
| 301 | + azimuth and elevation.""" |
| 302 | + elevation = elevation + angle |
| 303 | + if elevation > 90: |
| 304 | + azimuth = (azimuth + 180) % 360 |
| 305 | + elevation = (90 - elevation) % 90 |
| 306 | + return plane(azimuth, elevation, x, y) |
| 307 | + |
| 308 | + y, x = np.mgrid[5:0:-1, :5] |
| 309 | + for az, elev in itertools.product(range(0, 390, 30), range(0, 105, 15)): |
| 310 | + ls = mcolors.LightSource(az, elev) |
| 311 | + |
| 312 | + # Make a plane at a range of angles to the illumination |
| 313 | + for angle in range(0, 105, 15): |
| 314 | + z = angled_plane(az, elev, angle, x, y) |
| 315 | + h = ls.hillshade(z) |
| 316 | + assert_array_almost_equal(h, np.cos(np.radians(angle))) |
| 317 | + |
| 318 | + |
| 319 | +def _sph2cart(theta, phi): |
| 320 | + x = np.cos(theta) * np.sin(phi) |
| 321 | + y = np.sin(theta) * np.sin(phi) |
| 322 | + z = np.cos(phi) |
| 323 | + return x, y, z |
| 324 | + |
| 325 | + |
| 326 | +def _azimuth2math(azimuth, elevation): |
| 327 | + """Converts from clockwise-from-north and up-from-horizontal to |
| 328 | + mathematical conventions.""" |
| 329 | + theta = np.radians((90 - azimuth) % 360) |
| 330 | + phi = np.radians(90 - elevation) |
| 331 | + return theta, phi |
257 | 332 |
|
258 | 333 |
|
259 | 334 | if __name__ == '__main__':
|
|
0 commit comments