kopia lustrzana https://github.com/OpenDroneMap/WebODM
133 wiersze
5.4 KiB
Python
133 wiersze
5.4 KiB
Python
# Based on matplotlib https://github.com/matplotlib/matplotlib/blob/master/LICENSE/LICENSE
|
|
# Copyright (c)
|
|
# 2012- Matplotlib Development Team; All Rights Reserved
|
|
# Description of changes: Factored out hillshading code
|
|
|
|
|
|
import numpy as np
|
|
|
|
def _vector_magnitude(arr):
|
|
# things that don't work here:
|
|
# * np.linalg.norm
|
|
# - doesn't broadcast in numpy 1.7
|
|
# - drops the mask from ma.array
|
|
# * using keepdims - broken on ma.array until 1.11.2
|
|
# * using sum - discards mask on ma.array unless entire vector is masked
|
|
|
|
sum_sq = 0
|
|
for i in range(arr.shape[-1]):
|
|
sum_sq += np.square(arr[..., i, np.newaxis])
|
|
return np.sqrt(sum_sq)
|
|
|
|
class LightSource:
|
|
def __init__(self, azdeg=315, altdeg=45):
|
|
self.azdeg = azdeg
|
|
self.altdeg = altdeg
|
|
|
|
@property
|
|
def direction(self):
|
|
"""The unit vector direction towards the light source."""
|
|
# Azimuth is in degrees clockwise from North. Convert to radians
|
|
# counterclockwise from East (mathematical notation).
|
|
az = np.radians(90 - self.azdeg)
|
|
alt = np.radians(self.altdeg)
|
|
return np.array([
|
|
np.cos(az) * np.cos(alt),
|
|
np.sin(az) * np.cos(alt),
|
|
np.sin(alt)
|
|
])
|
|
|
|
|
|
def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.):
|
|
"""
|
|
Calculates the illumination intensity for a surface using the defined
|
|
azimuth and elevation for the light source.
|
|
This computes the normal vectors for the surface, and then passes them
|
|
on to `shade_normals`
|
|
Parameters
|
|
----------
|
|
elevation : array-like
|
|
A 2d array (or equivalent) of the height values used to generate an
|
|
illumination map
|
|
vert_exag : number, optional
|
|
The amount to exaggerate the elevation values by when calculating
|
|
illumination. This can be used either to correct for differences in
|
|
units between the x-y coordinate system and the elevation
|
|
coordinate system (e.g. decimal degrees vs. meters) or to
|
|
exaggerate or de-emphasize topographic effects.
|
|
dx : number, optional
|
|
The x-spacing (columns) of the input *elevation* grid.
|
|
dy : number, optional
|
|
The y-spacing (rows) of the input *elevation* grid.
|
|
fraction : number, optional
|
|
Increases or decreases the contrast of the hillshade. Values
|
|
greater than one will cause intermediate values to move closer to
|
|
full illumination or shadow (and clipping any values that move
|
|
beyond 0 or 1). Note that this is not visually or mathematically
|
|
the same as vertical exaggeration.
|
|
Returns
|
|
-------
|
|
intensity : ndarray
|
|
A 2d array of illumination values between 0-1, where 0 is
|
|
completely in shadow and 1 is completely illuminated.
|
|
"""
|
|
|
|
# Because most image and raster GIS data has the first row in the array
|
|
# as the "top" of the image, dy is implicitly negative. This is
|
|
# consistent to what `imshow` assumes, as well.
|
|
dy = -dy
|
|
|
|
# compute the normal vectors from the partial derivatives
|
|
e_dy, e_dx = np.gradient(vert_exag * elevation, dy, dx)
|
|
|
|
# .view is to keep subclasses
|
|
normal = np.empty(elevation.shape + (3,)).view(type(elevation))
|
|
normal[..., 0] = -e_dx
|
|
normal[..., 1] = -e_dy
|
|
normal[..., 2] = 1
|
|
normal /= _vector_magnitude(normal)
|
|
|
|
return self.shade_normals(normal, fraction)
|
|
|
|
def shade_normals(self, normals, fraction=1.):
|
|
"""
|
|
Calculates the illumination intensity for the normal vectors of a
|
|
surface using the defined azimuth and elevation for the light source.
|
|
Imagine an artificial sun placed at infinity in some azimuth and
|
|
elevation position illuminating our surface. The parts of the surface
|
|
that slope toward the sun should brighten while those sides facing away
|
|
should become darker.
|
|
Parameters
|
|
----------
|
|
fraction : number, optional
|
|
Increases or decreases the contrast of the hillshade. Values
|
|
greater than one will cause intermediate values to move closer to
|
|
full illumination or shadow (and clipping any values that move
|
|
beyond 0 or 1). Note that this is not visually or mathematically
|
|
the same as vertical exaggeration.
|
|
Returns
|
|
-------
|
|
intensity : ndarray
|
|
A 2d array of illumination values between 0-1, where 0 is
|
|
completely in shadow and 1 is completely illuminated.
|
|
"""
|
|
|
|
intensity = normals.dot(self.direction)
|
|
|
|
# Apply contrast stretch
|
|
imin, imax = intensity.min(), intensity.max()
|
|
intensity *= fraction
|
|
|
|
# Rescale to 0-1, keeping range before contrast stretch
|
|
# If constant slope, keep relative scaling (i.e. flat should be 0.5,
|
|
# fully occluded 0, etc.)
|
|
if (imax - imin) > 1e-6:
|
|
# Strictly speaking, this is incorrect. Negative values should be
|
|
# clipped to 0 because they're fully occluded. However, rescaling
|
|
# in this manner is consistent with the previous implementation and
|
|
# visually appears better than a "hard" clip.
|
|
intensity -= imin
|
|
intensity /= (imax - imin)
|
|
intensity = np.clip(intensity, 0, 1)
|
|
|
|
return intensity |