2

I want to add hillshade to an image visualization in Earth Engine. I have done so by blending a semi-transparent image onto a hillshade layer. The desired appearance of terrain is added, but the image color is washed out. I can reduce transparency to increase original color fidelity, but then the terrain effect is less dramatic. How can I add hillshade to an image without dulling it? I've included code to demo my current semi-transparent image overlay method.

// Import an S2 surface reflectance image.
var s2 = ee.Image('COPERNICUS/S2_SR_HARMONIZED/20220726T185931_20220726T190831_T10TFT');

// Visualize the image as [R, G, B] = [SWIR1, NIR, Green]
var s2Vis = s2.visualize({bands: ['B11', 'B8', 'B3'], min: 0, max: 4200});
Map.setCenter(-120.9689, 47.5918, 11);
Map.addLayer(s2Vis, null, 's2Vis');

// Make a hillshade image from a DEM.
var hillshade = ee.Terrain.hillshade({
  input: ee.Image('NASA/NASADEM_HGT/001').select('elevation').multiply(1.1),
  azimuth: 150,
  elevation: 30
}).updateMask(s2.select(0).mask());

// Blend a 30% transparent copy of the S2 image over the hillshade image.
var s2VisConstHill = hillshade.blend(s2Vis.visualize({opacity: 0.70}));
Map.addLayer(s2VisConstHill, null, 's2VisConstHill');

3 Answers 3

3

In a hillshade image, flat-ish areas appear grey. All of this grey is washing out the image in areas where there is not much topography. You can make a variable transparency hillshade layer that enhances regions with lots of topography and leaves regions that are flat-ish mostly unaltered. Here is an implementation that builds on the demo.

// Import an S2 surface reflectance image.
var s2 = ee.Image('COPERNICUS/S2_SR_HARMONIZED/20220726T185931_20220726T190831_T10TFT');

// Visualize the image as [R, G, B] = [SWIR1, NIR, Green]
var s2Vis = s2.visualize({bands: ['B11', 'B8', 'B3'], min: 0, max: 4200});
Map.setCenter(-120.9689, 47.5918, 11);
Map.addLayer(s2Vis, null, 's2Vis');

// Make a hillshade image from a DEM.
var hillshade = ee.Terrain.hillshade({
  input: ee.Image('NASA/NASADEM_HGT/001').select('elevation').multiply(1.1),
  azimuth: 150,
  elevation: 30
}).updateMask(s2.select(0).mask());


// ####### Hillshade w/ constant transparency #######

// Blend a 30% transparent copy of the S2 image over the hillshade image.
var s2VisConstHill = hillshade.blend(s2Vis.visualize({opacity: 0.70}));
Map.addLayer(s2VisConstHill, null, 's2VisConstHill');


// ####### Hillshade w/ variable transparency #######

// Define value and opacity ranges for low and high illumination regions.
var loIllum = {
  valRange: [0, 170],     // values you consider low illumination
  opacRange: [-200, 170]  // value range to stretch opacity [1,0]
};

var hiIllum = {
  valRange: [210, 255],  // values you consider high illumination
  opacRange: [210, 700]  // value range to stretch opacity [0,1]
};

// Create low and high illumination hillshade images with transparency
// gradient controlled by illumination; combine them and blend on top
// of the original image.
var loIllumHill = ee.Image(1).subtract(  // need to reverse the value order.
  hillshade.clamp(loIllum.valRange[0], loIllum.valRange[1])
  .unitScale(loIllum.opacRange[0], loIllum.opacRange[1]))
  .selfMask()
  .visualize({min: 0, max: 1, palette: ['000000']});

var hiIllumHill =
  hillshade.clamp(hiIllum.valRange[0], hiIllum.valRange[1])
  .unitScale(hiIllum.opacRange[0], hiIllum.opacRange[1])
  .selfMask()
  .visualize({min: 0, max: 1, palette: ['FFFFFF']});

var s2VisVariHill = s2Vis.blend(loIllumHill.blend(hiIllumHill));
Map.addLayer(s2VisVariHill, null, 's2VisVariHill');

Code Editor link

enter image description here

1

The "gee-blend" module for the Code Editor offers a series of common image blending functions (like those found in QGIS, ArcGIS Pro, Adobe Illustrator).

See: https://github.com/jessjaco/gee-blend

The blend.multiply mode is the same as the "hillshade-as-weighting-factor method" described in a different answer, but wrapped in a convenient function. Try other modes and further customize the effect by adjusting gamma and transparency as you usually would. Here is an example of blend.multiply building off the demo.

// Import an S2 surface reflectance image.
var s2 = ee.Image('COPERNICUS/S2_SR_HARMONIZED/20220726T185931_20220726T190831_T10TFT');

// Visualize the image as [R, G, B] = [SWIR1, NIR, Green]
var s2Vis = s2.visualize({bands: ['B11', 'B8', 'B3'], min: 0, max: 4200});
Map.setCenter(-120.9689, 47.5918, 11);
Map.addLayer(s2Vis, null, 's2Vis');

// Make a hillshade image from a DEM.
var hillshade = ee.Terrain.hillshade({
  // Divide the DEM by 2 to flatten it a little, otherwise the terrain effect is
  // too exagerated - play around with how much you flatten it.
  input: ee.Image('NASA/NASADEM_HGT/001').select('elevation').divide(2),
  azimuth: 150,
  elevation: 30
});

// Load the blend module.
var blend = require('users/jja/public:blend.js');

// Use the `multiply` blend function to weight the image by hillshade. 
Map.addLayer(blend.multiply(s2Vis, hillshade), {min: 0, max: 0.7, gamma: 1.3});
1

You can also use the hillshade layer as a weighting factor to make low-illumination pixels darker by dividing the hillshade layer by 255 to get it into range [0,1] and then multiply it by the RGB visualization image. When visualizing an 8-bit RGB image, the typical stretch is [0,255], but after weighting the image, the majority of the values will be reduced, so you need to adjust the max value lower. Another factor to play around with is DEM exaggeration: to make the hillshade more dramatic, multiple the DEM by e.g., 2 and to soften the hillshade, divide by 2. This method can saturate high illumination regions if the max visualization parameter is set too low. Here is an example that builds off the initial demo.

// Import an S2 surface reflectance image.
var s2 = ee.Image('COPERNICUS/S2_SR_HARMONIZED/20220726T185931_20220726T190831_T10TFT');

// Visualize the image as [R, G, B] = [SWIR1, NIR, Green]
var s2Vis = s2.visualize({bands: ['B11', 'B8', 'B3'], min: 0, max: 4200});
Map.setCenter(-120.9689, 47.5918, 11);
Map.addLayer(s2Vis, null, 's2Vis');

// Make a hillshade image from a DEM.
var hillshade = ee.Terrain.hillshade({
  // Divide the DEM by 2 to flatten it a little, otherwise the terrain effect is
  // too exagerated - play around with how much you flatten it.
  input: ee.Image('NASA/NASADEM_HGT/001').select('elevation').divide(2),
  azimuth: 150,
  elevation: 30
})
// Divide hillshade by 255 to get range [0,1] for weighting the RGB viz image.
.divide(255);

// Multiply the RGB image by the hillshade weight - low illumination pixels are
// made darker across all three bands of the RGB image.
var s2VisHill = s2Vis.multiply(hillshade);

// When visualizing the hillshade-weighted RGB image, adjust the max
// (255 for 8-bit RGB images) to a lower value because most of the image
// has been darkened, lowering the max value will brighten it up again -
// you'll maybe want to play around with the new max value.
Map.addLayer(s2VisHill, {min: 0, max: 160}, 's2VisHill');

Code Editor link

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.