Skip to content

Commit d0f046b

Browse files
author
Jeff Whitaker
committed
added 'lightsource' class to colors module, shading_example.py
to illustrate it's usage to create shaded relief maps with imshow. svn path=/trunk/matplotlib/; revision=6975
1 parent ad4a190 commit d0f046b

File tree

3 files changed

+159
-0
lines changed

3 files changed

+159
-0
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2009-03-14 Added 'lightsource' class to colors module for
2+
creating shaded relief maps. shading_example.py
3+
added to illustrate usage. - JSW
4+
15
2009-03-11 Ensure wx version >= 2.8; thanks to Sandro Tosi and
26
Chris Barker. - EF
37

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import numpy as np
2+
import matplotlib.pyplot as plt
3+
from matplotlib.colors import lightsource
4+
5+
# example showing how to make shaded relief plots
6+
# like mathematica
7+
# (http://reference.wolfram.com/mathematica/ref/ReliefPlot.html )
8+
# or Generic Mapping Tools
9+
# (http://gmt.soest.hawaii.edu/gmt/doc/gmt/html/GMT_Docs/node145.html)
10+
11+
# test data
12+
X,Y=np.mgrid[-5:5:0.1,-5:5:0.1]
13+
Z=X+np.sin(X**2+Y**2)
14+
# creat light source object.
15+
ls = lightsource(azdeg=270,altdeg=20)
16+
# shade data, creating an rgb array.
17+
rgb = ls.shade(Z,plt.cm.copper)
18+
# plot un-shaded and shaded images.
19+
plt.figure(figsize=(12,5))
20+
plt.subplot(121)
21+
plt.imshow(Z,cmap=plt.cm.copper)
22+
plt.title('imshow')
23+
plt.xticks([]); plt.yticks([])
24+
plt.subplot(122)
25+
plt.imshow(rgb)
26+
plt.title('imshow with shading')
27+
plt.xticks([]); plt.yticks([])
28+
plt.show()

lib/matplotlib/colors.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,3 +900,130 @@ def inverse(self, value):
900900
# compatibility with earlier class names that violated convention:
901901
normalize = Normalize
902902
no_norm = NoNorm
903+
904+
def rgb_to_hsv(arr):
905+
"""
906+
convert rgb values in a numpy array to hsv values
907+
input and output arrays should have shape (M,N,3)
908+
"""
909+
out = np.empty_like(arr)
910+
arr_max = arr.max(-1)
911+
delta = arr.ptp(-1)
912+
s = delta / arr_max
913+
s[delta==0] = 0
914+
# red is max
915+
idx = (arr[:,:,0] == arr_max)
916+
out[idx, 0] = (arr[idx, 1] - arr[idx, 2]) / delta[idx]
917+
# green is max
918+
idx = (arr[:,:,1] == arr_max)
919+
out[idx, 0] = 2. + (arr[idx, 2] - arr[idx, 0] ) / delta[idx]
920+
# blue is max
921+
idx = (arr[:,:,2] == arr_max)
922+
out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1] ) / delta[idx]
923+
out[:,:,0] = (out[:,:,0]/6.0) % 1.0
924+
out[:,:,1] = s
925+
out[:,:,2] = arr_max
926+
return out
927+
928+
def hsv_to_rgb(hsv):
929+
"""
930+
convert hsv values in a numpy array to rgb values
931+
both input and output arrays have shape (M,N,3)
932+
"""
933+
h = hsv[:,:,0]; s = hsv[:,:,1]; v = hsv[:,:,2]
934+
r = np.empty_like(h); g = np.empty_like(h); b = np.empty_like(h)
935+
i = (h*6.0).astype(np.int)
936+
f = (h*6.0) - i
937+
p = v*(1.0 - s)
938+
q = v*(1.0 - s*f)
939+
t = v*(1.0 - s*(1.0-f))
940+
idx = i%6 == 0
941+
r[idx] = v[idx]; g[idx] = t[idx]; b[idx] = p[idx]
942+
idx = i == 1
943+
r[idx] = q[idx]; g[idx] = v[idx]; b[idx] = p[idx]
944+
idx = i == 2
945+
r[idx] = p[idx]; g[idx] = v[idx]; b[idx] = t[idx]
946+
idx = i == 3
947+
r[idx] = p[idx]; g[idx] = q[idx]; b[idx] = v[idx]
948+
idx = i == 4
949+
r[idx] = t[idx]; g[idx] = p[idx]; b[idx] = v[idx]
950+
idx = i == 5
951+
r[idx] = v[idx]; g[idx] = p[idx]; b[idx] = q[idx]
952+
idx = s == 0
953+
r[idx] = v[idx]; g[idx] = v[idx]; b[idx] = v[idx]
954+
return np.array((r,g,b)).T
955+
956+
class lightsource(object):
957+
"""
958+
Create a light source coming from the specified azimuth and elevation.
959+
Angles are in degrees, with the azimuth measured
960+
clockwise from south and elevation up from the zero plane of the surface.
961+
The :meth:`shade` is used to produce rgb values for a shaded relief image
962+
given a data array.
963+
"""
964+
def __init__(self,azdeg=315,altdeg=45,\
965+
hsv_min_val=0,hsv_max_val=1,hsv_min_sat=1,hsv_max_sat=0):
966+
"""
967+
Specify the azimuth (measured clockwise from south) and altitude
968+
(measured up from the plane of the surface) of the light source
969+
in degrees.
970+
971+
The color of the resulting image will be darkened
972+
by moving the (s,v) values (in hsv colorspace) toward
973+
(hsv_min_sat, hsv_min_val) in the shaded regions, or
974+
lightened by sliding (s,v) toward
975+
(hsv_max_sat hsv_max_val) in regions that are illuminated.
976+
The default extremes are chose so that completely shaded points
977+
are nearly black (s = 1, v = 0) and completely illuminated points
978+
are nearly white (s = 0, v = 1).
979+
"""
980+
self.azdeg = azdeg
981+
self.altdeg = altdeg
982+
self.hsv_min_val = hsv_min_val
983+
self.hsv_max_val = hsv_max_val
984+
self.hsv_min_sat = hsv_min_sat
985+
self.hsv_max_sat = hsv_max_sat
986+
987+
def shade(self,data,cmap):
988+
"""
989+
Take the input data array, convert to HSV values in the
990+
given colormap, then adjust those color values
991+
to given the impression of a shaded relief map with a
992+
specified light source.
993+
RGBA values are returned, which can then be used to
994+
plot the shaded image with imshow.
995+
"""
996+
# imagine an artificial sun placed at infinity in
997+
# some azimuth and elevation position illuminating our surface. The parts of
998+
# the surface that slope toward the sun should brighten while those sides
999+
# facing away should become darker.
1000+
# convert alt, az to radians
1001+
az = self.azdeg*np.pi/180.0
1002+
alt = self.altdeg*np.pi/180.0
1003+
# gradient in x and y directions
1004+
dx, dy = np.gradient(data)
1005+
slope = 0.5*np.pi - np.arctan(np.hypot(dx, dy))
1006+
aspect = np.arctan2(dx, dy)
1007+
intensity = np.sin(alt)*np.sin(slope) + np.cos(alt)*np.cos(slope)*np.cos(-az -\
1008+
aspect - 0.5*np.pi)
1009+
# rescale to interval -1,1
1010+
# +1 means maximum sun exposure and -1 means complete shade.
1011+
intensity = (intensity - intensity.min())/(intensity.max() - intensity.min())
1012+
intensity = 2.*intensity - 1.
1013+
# convert to rgb, then rgb to hsv
1014+
rgb = cmap((data-data.min())/(data.max()-data.min()))
1015+
hsv = rgb_to_hsv(rgb[:,:,0:3])
1016+
# modify hsv values to simulate illumination.
1017+
hsv[:,:,1] = np.where(np.logical_and(np.abs(hsv[:,:,1])>1.e-10,intensity>0),\
1018+
(1.-intensity)*hsv[:,:,1]+intensity*self.hsv_max_sat, hsv[:,:,1])
1019+
hsv[:,:,2] = np.where(intensity > 0, (1.-intensity)*hsv[:,:,1] +\
1020+
intensity*self.hsv_max_val, hsv[:,:,2])
1021+
hsv[:,:,1] = np.where(np.logical_and(np.abs(hsv[:,:,1])>1.e-10,intensity<0),\
1022+
(1.+intensity)*hsv[:,:,1]-intensity*self.hsv_min_sat, hsv[:,:,1])
1023+
hsv[:,:,2] = np.where(intensity < 0, (1.+intensity)*hsv[:,:,1] -\
1024+
intensity*self.hsv_min_val, hsv[:,:,2])
1025+
hsv[:,:,1:] = np.where(hsv[:,:,1:]<0.,0,hsv[:,:,1:])
1026+
hsv[:,:,1:] = np.where(hsv[:,:,1:]>1.,1,hsv[:,:,1:])
1027+
# convert modified hsv back to rgb.
1028+
rgb[:,:,0:3] = hsv_to_rgb(hsv)
1029+
return rgb

0 commit comments

Comments
 (0)