@@ -900,3 +900,130 @@ def inverse(self, value):
900900# compatibility with earlier class names that violated convention:
901901normalize = Normalize
902902no_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