Mean annual temperature and number of hot days in a year
This week I got request from my colleagues to calculate mean annual temperature and number of hot days in a year using MODIS Daily Land Surface Temperature (LST) data from 2000 until 2020 for Indonesia and Latin America Countries. The first thing that comes to mind is to use Google Earth Engine (GEE).
For this task, I will use MOD11A1.006 Terra Land Surface Temperature and Emissivity Daily Global 1km - https://developers.google.com/earth-engine/datasets/catalog/MODIS_006_MOD11A1. From the description: The MOD11A1 V6 product provides daily land surface temperature (LST) and emissivity values in a 1200 x 1200 kilometer grid. The temperature value is derived from the MOD11_L2 swath product. Above 30 degrees latitude, some pixels may have multiple observations where the criteria for clear-sky are met. When this occurs, the pixel value is the average of all qualifying observations. Provided along with both the day-time and night-time surface temperature bands and their quality indicator layers are MODIS bands 31 and 32 and six observation layers.
From above picture, we can see MOD11A1 has many bands. For this exercise, I only use 4 bands: LST_Day_1km, QC_Day, LST_Night_1km and QC_Night.
If you would like to test, you can access link from GEE: https://code.earthengine.google.com/?scriptPath=Examples:Datasets/MODIS_006_MOD11A1
Land Surface Temperature
Ok, let start! I need to define the geographic domain and the time range.
// Define geographic domain and time range. //--- var bbox_IDN = ee.Geometry.Rectangle(94.522, 6.125, 142.149, -11.409); // Indonesia var bbox_LAC = ee.Geometry.Rectangle(-120.254, 33.348, -28.44, -58.64); // Latin America Countries Map.setCenter(0,0,3); // Period var startDate = '2020-01-01'; var endDate = '2020-12-31'; var year = new Date(startDate).getFullYear();
Add boundary layer as a guidance when overlaying LST data, and I can use US Department of State LSIB 2017 data in Earth Engine data catalog. https://developers.google.com/earth-engine/datasets/catalog/USDOS_LSIB_SIMPLE_2017?hl=en
// These are USDOS LSIB boundaries simplified somewhat for visualization. var bnd = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017");
I need a function to translate MODIS LST data to Celsius, with equation: Digital Number (DN) to Celsius = DN* 0.02 -273.15. I found an example of function written by Gennadii Donchyts at GEE Developers Group https://groups.google.com/g/google-earth-engine-developers/c/ebrSLPCjZ8c/m/TiL2gtSnDgAJ
Slightly different with code from above link, I will create two function, for LST_Day_1km and LST_Night_1km variable.
// Function to convert to Celcius for Day and Night Temperature function toCelciusDay(image){ var lst = image.select('LST_Day_1km').multiply(0.02).subtract(273.15); var overwrite = true; var result = image.addBands(lst, ['LST_Day_1km'], overwrite); return result; } function toCelciusNight(image){ var lst = image.select('LST_Night_1km').multiply(0.02).subtract(273.15); var overwrite = true; var result = image.addBands(lst, ['LST_Night_1km'], overwrite); return result; }
Next, I will add MOD11A1, select the LST and pre-process Quality Control Band
var mod11a1_day = ee.ImageCollection ('MODIS/006/MOD11A1') .select(['LST_Day_1km','QC_Day']) .map(toCelciusDay) //Select dates of interest .filterDate(ee.Date(startDate),ee.Date(endDate)); var mod11a1_night = ee.ImageCollection ('MODIS/006/MOD11A1') .select(['LST_Night_1km','QC_Night']) .map(toCelciusNight) //Select dates of interest .filterDate(ee.Date(startDate),ee.Date(endDate));
The tricky part will be considering only pixel with good quality from QC bitmask. From the catalog, we can see:
QC Bitmask
---
Bits 0-1: Mandatory QA flags
0: LST produced, good quality, not necessary to examine more detailed QA
1: LST produced, other quality, recommend examination of more detailed QA
2: LST not produced due to cloud effects
3: LST not produced primarily due to reasons other than cloud
Then I can use below script to select only the best quality images for each data. Below is the function for each Day and Night data.
var QC_Day_mask = function(image2) { return image2.updateMask(image2.select('QC_Day').bitwiseAnd(0x1).not()); }; var QC_Night_mask = function(image3) { return image3.updateMask(image3.select('QC_Night').bitwiseAnd(0x1).not()); };
As alternative way to get good quality image, I can use bitwiseExtract function from Daniel Wiell
function bitwiseExtract(value, fromBit, toBit) { if (toBit === undefined) toBit = fromBit; var maskSize = ee.Number(1).add(toBit).subtract(fromBit); var mask = ee.Number(1).leftShift(maskSize).subtract(1); return value.rightShift(fromBit).bitwiseAnd(mask); } var QC_Day_mask = function(image2) { return image2.updateMask(bitwiseExtract(image2.select('QC_Day'), 0, 1)); }; var QC_Night_mask = function(image3) { return image3.updateMask(bitwiseExtract(image3.select('QC_Night'), 0, 1)); };
Next, let’s apply above function to get all the image collection cleaned, only considering pixel with good quality. After that we can do mosaic Day and Night for further analysis.
// Removed bad pixel // Mask the less than Good images from the collection of images var LSTD_YYYY_QC = mod11a1_day.map(QC_Day_mask); var LSTN_YYYY_QC = mod11a1_night.map(QC_Night_mask); // Merge two image collection (cleaned day and night) var LST_cleaned_combine = LSTD_YYYY_QC.combine(LSTN_YYYY_QC); // Sort by date var LST_cleaned_combine_sorted = LST_cleaned_combine.sort("system:time_start"); // Calculate average daily Day and Night data var Mean_Day_LST = ee.Image((LSTD_YYYY_QC.select('LST_Day_1km')).mean()); var Mean_Night_LST = ee.Image((LSTN_YYYY_QC.select('LST_Night_1km')).mean());
Now I can calculate the annual Mean, Max and Min using below script.
// Daily cleaned LST Mean var LST_Mean_Daily = LST_cleaned_combine_sorted.map( function (image) { var img_Mean_src = image.expression( '(Day + Night)/2', { 'Day': image.select('LST_Day_1km'), 'Night': image.select('LST_Night_1km') } ); var img_Mean = img_Mean_src.rename('LST_Daily_mean_1km'); image = image.addBands([img_Mean]); var time = image.get('system:time_start'); return image.set('system:time_start', time); } ); // Daily cleaned LST Max var LST_Max_Daily = LST_cleaned_combine_sorted.map( function (image) { var img_Day = image.select('LST_Day_1km'); var img_Night = image.select('LST_Night_1km'); var img_Max = img_Day.max(img_Night).rename('LST_Daily_max_1km'); image = image.addBands([img_Max]); var time = image.get('system:time_start'); return image.set('system:time_start', time); } ); // Daily cleaned LST Min var LST_Min_Daily = LST_cleaned_combine_sorted.map( function (image) { var img_Day = image.select('LST_Day_1km'); var img_Night = image.select('LST_Night_1km'); var img_Min = img_Day.min(img_Night).rename('LST_Daily_min_1km'); image = image.addBands([img_Min]); var time = image.get('system:time_start'); return image.set('system:time_start', time); } ); // Calculate Annual Daily Mean, Max and Min var Mean_LST = ee.Image((LST_Mean_Daily.select('LST_Daily_mean_1km')).mean()); var Max_LST = ee.Image((LST_Max_Daily.select('LST_Daily_max_1km')).mean()); var Min_LST = ee.Image((LST_Min_Daily.select('LST_Daily_min_1km')).mean());
Hot Days
To get number of hot days in a year, I will use 35 degree Celsius as a threshold, so every LST exceeding the threshold, will added to new collection and later will summed to get total number in a year.
This case I try to get number of hot days using LST Day (average day time temperature) and LST Mean (average daily temperature - 24h average)
// Function to get hot days function hotdays(image){ var hot = image.gt(35); return image.addBands(hot.rename('hotdays') .set('system:time_start', image.get('system:time_start'))); } // Number of hot days, Daily LST day and mean exceeding 35degC var LST35Mean = ee.ImageCollection(LST_Mean_Daily.select('LST_Daily_mean_1km')).map(hotdays); var LST35Day = ee.ImageCollection(LSTD_YYYY_QC.select('LST_Day_1km')).map(hotdays); // Calculate the total number of hot days in a year (selected time range) var totalHotDaysLSTMean = ee.ImageCollection(LST35Mean.select('hotdays')).sum().float(); var totalHotDaysLSTDay = ee.ImageCollection(LST35Day.select('hotdays')).sum().float();
Number of Consecutive Hot Days
Although this is not part of my objective, I found a script to calculate number of consecutive hot days from Jamon Van Den Hoek at GEE Developers Group and I think this is still relevant with what I have done. So I decided to put this script too, maybe someday I need it.
// Function to calculate length of consecutive days above 35degC function consecutiveDays(this_img, cum_prev_max){ // Load cumulative # days var cum_img = ee.Image(cum_prev_max).select(0); // Load previous day's image var prev_img = ee.Image(cum_prev_max).select(1); // Load maximum # consecutive data var max_run = ee.Image(cum_prev_max).select(2); // Set masked pixels to 0 this_img = this_img.unmask(); // If this and previous day were > 35, record this consecutive day cum_img = cum_img.where(this_img.eq(1).and(prev_img.eq(1)), cum_img.add(1)); // If < 35 deg, reset counter cum_img = cum_img.where(this_img.neq(1), 0); // Last data from the time range max_run = max_run.where(cum_img.gt(max_run),cum_img); // This return value becomes cum_prev input return cum_img.addBands(this_img).addBands(max_run); } var max_value = 365; // Mean var LST35CMean = ee.ImageCollection(LST35Mean.select('hotdays').toList(max_value)); // Need to add 1 day to get # days rather than # two-date periods > 35 deg var Max_HOT_LSTMean = ee.Image(LST35CMean.iterate(consecutiveDays, ee.Image([0,0,0]))).select(2).add(1).rename('LSTMean_HOT_MaxConDay'); var Cum_HOT_LSTMean = ee.Image(LST35CMean.iterate(consecutiveDays, ee.Image([0,0,0]))).select(0).add(1).rename('LSTMean_HOT_CumConDay'); // Day var LST35CDay = ee.ImageCollection(LST35Day.select('hotdays').toList(max_value)); // Need to add 1 day to get # days rather than # two-date periods > 35 deg var Max_HOT_LSTDay = ee.Image(LST35CDay.iterate(consecutiveDays, ee.Image([0,0,0]))).select(2).add(1).rename('LSTDay_HOT_MaxConDay'); var Cum_HOT_LSTDay = ee.Image(LST35CDay.iterate(consecutiveDays, ee.Image([0,0,0]))).select(0).add(1).rename('LSTDay_HOT_CumConDay');
Symbology, Downloads and Legend
It’s time to visualize all the result and displaying into map with legend, and download it via Google Drive.
// SYMBOLOGY //--- // LST var LSTvis = { min: 10, max: 40, palette: [ '040274', '040281', '0502a3', '0502b8', '0502ce', '0502e6', '0602ff', '235cb1', '307ef3', '269db1', '30c8e2', '32d3ef', '3be285', '3ff38f', '86e26f', '3ae237', 'b5e22e', 'd6e21f', 'fff705', 'ffd611', 'ffb613', 'ff8b13', 'ff6e08', 'ff500d', 'ff0000', 'de0101', 'c21301', 'a71001', '911003' ], }; // Hot days var HotDaysvis = {min:0, max:365, palette:['grey','yellow','orange','red']}; // Consecutive hot days var ConHotDaysvis = {min:0, max:50, palette:['grey','red']}; // Boundaries var BNDvis = {fillColor: "#00000000", color: '646464', width: 0.5,}; //Add LST layer to the map Map.addLayer(Mean_Day_LST, LSTvis, 'Annual Daily LST Day', false); Map.addLayer(Mean_Night_LST, LSTvis, 'Annual Daily LST Night', false); Map.addLayer(Mean_LST, LSTvis, 'Annual Daily LST Mean', false); Map.addLayer(Max_LST, LSTvis, 'Annual Daily LST Max', false); Map.addLayer(Min_LST, LSTvis, 'Annual Daily LST Min', false); Map.addLayer(totalHotDaysLSTDay, HotDaysvis, 'Number of Hot Days from Daily LST Day', false); Map.addLayer(totalHotDaysLSTMean, HotDaysvis, 'Number of Hot Days from Daily LST Mean', false); Map.addLayer(Max_HOT_LSTMean, ConHotDaysvis, 'Maximum Consecutive Days from Daily LST Mean', false); Map.addLayer(Cum_HOT_LSTMean, ConHotDaysvis, 'Cumulative Consecutive Days from Daily LST Mean',false); Map.addLayer(Max_HOT_LSTDay, ConHotDaysvis, 'Maximum Consecutive Days from Daily LST Max', false); Map.addLayer(Cum_HOT_LSTDay, ConHotDaysvis, 'Cumulative Consecutive Days from Daily LST Max',false); Map.addLayer(bnd.style({fillColor: "#00000000", color: '646464', width: 0.5})); // Convert the geometry to a JSON string used as an argument to Export. var IDN_json = bbox_IDN.toGeoJSON(); var LAC_json = bbox_LAC.toGeoJSON(); // Export the image, specifying scale and region. // Day and Night Export.image.toDrive({ image:Mean_Day_LST, description:'IDN_LSTD_Mean_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:IDN_json }); Export.image.toDrive({ image:Mean_Day_LST, description:'LAC_LSTD_Mean_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:LAC_json }); // Mean Max Min Export.image.toDrive({ image:Mean_LST, description:'IDN_LSTMean_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:IDN_json }); Export.image.toDrive({ image:Max_LST, description:'IDN_LSTMax_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:IDN_json }); Export.image.toDrive({ image:Min_LST, description:'IDN_LSTMin_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:IDN_json }); Export.image.toDrive({ image:Mean_LST, description:'LAC_LSTMean_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:LAC_json }); Export.image.toDrive({ image:Max_LST, description:'LAC_LSTMax_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:LAC_json }); Export.image.toDrive({ image:Min_LST, description:'LAC_LSTMin_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:LAC_json }); // HotDays Export.image.toDrive({ image:totalHotDaysLSTMean, description:'IDN_Hotdays_LSTMean_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:IDN_json }); Export.image.toDrive({ image:totalHotDaysLSTDay, description:'IDN_Hotdays_LSTDay_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:IDN_json }); Export.image.toDrive({ image:totalHotDaysLSTMean, description:'LAC_Hotdays_LSTMean' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:LAC_json }); Export.image.toDrive({ image:totalHotDaysLSTDay, description:'LAC_Hotdays_LSTDay_' + year, folder:'GEE', scale:1000, maxPixels:1e13, region:LAC_json }); // LEGEND PANEL //--- // A color bar widget. Makes a horizontal color bar to display the given color palette. function ColorBar(palette) { return ui.Thumbnail({ image: ee.Image.pixelLonLat().select(0), params: { bbox: [0, 0, 1, 0.1], dimensions: '100x10', format: 'png', min: 0, max: 1, palette: palette, }, style: {stretch: 'horizontal', margin: '0px 8px'}, }); } // Returns our labeled legend, with a color bar and three labels representing // the minimum, middle, and maximum values. function makeLegend() { var labelPanel = ui.Panel( [ // Label for LST legend ui.Label('<10', {margin: '4px 8px'}), ui.Label('25',{margin: '4px 8px', textAlign: 'center', stretch: 'horizontal'}), ui.Label('>40', {margin: '4px 8px'}) // Label for Hotdays legend // ui.Label('0', {margin: '4px 8px'}), // ui.Label('180',{margin: '4px 8px', textAlign: 'center', stretch: 'horizontal'}), // ui.Label('365', {margin: '4px 8px'}) ], ui.Panel.Layout.flow('horizontal')); // LST color return ui.Panel([ColorBar(LSTvis.palette), labelPanel]); // Hotdays color // return ui.Panel([ColorBar(HotDaysvis.palette), labelPanel]); } // Styling for the legend title. var LEGEND_TITLE_STYLE = { fontSize: '20px', fontWeight: 'bold', stretch: 'horizontal', textAlign: 'center', margin: '4px', }; // Styling for the legend footnotes. var LEGEND_FOOTNOTE_STYLE = { fontSize: '10px', stretch: 'horizontal', textAlign: 'center', margin: '4px', }; // Assemble the legend panel. Map.add(ui.Panel( [ // Legend title for LST ui.Label('Global Land Surface Temperature', LEGEND_TITLE_STYLE), makeLegend(), // Legend title for Hotdays ui.Label('Number of Hotdays, LST exceeding 35°C', LEGEND_TITLE_STYLE), makeLegend(), ui.Label('(degree Celcius in 1km resolution per pixel)', LEGEND_FOOTNOTE_STYLE), ui.Label('Source: MODIS MOD11A1.006 Terra', LEGEND_FOOTNOTE_STYLE) ], ui.Panel.Layout.flow('vertical'), {width: '400px', position: 'bottom-left'})); // End of script
Full script available here: https://code.earthengine.google.com/f2109fc305e987b97503146d4c209871 (Update 22 Dec 2021)
And below is the example results.