Tutorials/Post - Remote Sensing, GIS, Earth System, Geo-AI/ML

Thresholding the primitives to generate the Land Cover Maps: a UI application in Google Earth Engine

This simple UI interface is built on the Google Earth Engine. It uses the primitives as percentage layers to assemble them to derive land cover maps. Here we are using the Random Forest algorithm to generate the land cover maps.
You can use the sliders to change the threshold input for the tree node structure that goes into the Random Forest algorithm. The tree structure that is generated is similar to the ones that are generated by R or other tools. Just print decisionTree in the script to see how it looks like. Typically, the default decision tree once you load looks like this:

1) root 9999 9999 9999
2) mangroves>=70 9999 9999 8 *
3) mangroves<70 9999 9999 9999
6) canopy>=3 9999 9999 9999
12) canopy>=95 9999 9999 1 *
13) canopy<95 9999 9999 9999
26) canopy>=69 9999 9999 2 *
27) canopy<69 9999 9999 3 *
7) canopy<3 9999 9999 9999
14) water>=70 9999 9999 10 *
15) water<70 9999 9999 9999
30) snow>=75 9999 9999 9 *
31) snow<75 9999 9999 9999
62) barren>=70 9999 9999 5 *
63) barren<70 9999 9999 9999
126) rice>=45 9999 9999 4 *
127) rice<45 9999 9999 9999
254) cropland>=40 9999 9999 4 *
255) cropland<40 9999 9999 9999
510) urban>=75 9999 9999 6 *
511) urban<75 9999 9999 9999
1022) grass>=95 9999 9999 11 *
1023) grass<95 9999 9999 9999
2046) wetlands>=70 9999 9999 7 *
2047) wetlands<70 9999 9999 5 *

You may modify the script to generate the land cover for list of years. Of course, that would require the training data to generate primitives for those years. For this tutorial, I am generating the land cover for 2015 only.
Use this link or the code below.

var mekong = ee.FeatureCollection('ft:1tdSwUL7MVpOauSgRzqVTOwdfy17KDbw-1d9omPw');
var boundary = mekong.filter(ee.Filter.inList('Country', ['Myanmar (Burma)'])).geometry();

var repository = 'projects/servir-mekong/yearly_primitives_smoothed';
var scale = 30;
var exportPath = 'users/biplov/myanmar';
var primitives = ['mangrove', 'woody', 'water', 'snow', 'barren', 'rice', 'cropland', 'urban', 'grass', 'closedForest', 'openForest', 'wetlands'];
var classKeys = ee.List(['closedforest','water','grass','openForest','woody','cropland','other','urban','wetlands','mangroves','snow']);
var defaultThresholds = [70, 3, 70, 75, 70, 45, 40, 75, 95, 95, 69, 70];
//var defaultThresholds = [70, 3, 70, 75, 70, 45, 40, 75, 55, 63, 66, 70];
var year = 2015;
var runCount = 1;

var testAreaImage = ee.Image(1).clip(boundary);
var scaleforTestArea = 100;
var reducer = testAreaImage.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: boundary,
crs: 'EPSG:32646', // WGS Zone N 46
scale: scaleforTestArea,
maxPixels: 1E13
});
// km square
var area = ee.Number(reducer.get('constant')).multiply(scaleforTestArea).multiply(scaleforTestArea).divide(1000000);
// google reports 676,575 km2
//print(area); // outputs around 666,312 km2 because some islands might be missing

var startId = 'MGV';

var classStruct = {
'closedforest': {number: 1, color: '267300'},
'openforest': {number: 2, color: '98E600'},
'woody': {number: 3, color: 'FFAA00'},
'cropland' : {number: 4, color: 'FFFFBE'},
'other' : {number: 5, color: 'FFEBBE'},
'urban': {number: 6, color: 'FF5500'},
'wetlands': {number: 7, color: '539CC9'},
'mangroves': {number: 8, color: 'DF73FF'},
'snow': {number: 9, color: 'ffffff'},
'water': {number: 10, color: 'aec3d4'},
'grass': {number: 11, color: 'D3FFBE'}
};

var nodeStruct = {
'MGV': {band: 'mangroves', name:'mangrove', threshold: defaultThresholds[0], left: 'terminal', leftName: 'mangroves', right: 'WOD'},
'WOD': {band: 'canopy', name:'woody',threshold: defaultThresholds[1], left: 'CLF', right: 'SFW'},
'SFW': {band: 'water', name:'water',threshold: defaultThresholds[2], left: 'terminal', leftName: 'water', right: 'SNW'},
'SNW': {band: 'snow', name:'snow',threshold: defaultThresholds[3], left: 'terminal', leftName: 'snow', right: 'BAR'},
'BAR': {band: 'barren', name:'barren',threshold: defaultThresholds[4], left: 'terminal', leftName: 'other', right: 'RIC'},
'RIC': {band: 'rice', name:'rice',threshold: defaultThresholds[5], left: 'terminal', leftName: 'cropland', right: 'CRP'},
'CRP': {band: 'cropland', name:'cropland',threshold: defaultThresholds[6], left: 'terminal', leftName: 'cropland', right: 'URB'},
'URB': {band: 'urban', name:'urban',threshold: defaultThresholds[7], left: 'terminal', leftName: 'urban', right: 'GRS'},
'GRS': {band: 'grass', name:'grass',threshold: defaultThresholds[8], left: 'terminal', leftName: 'grass', right: 'WTL'},
'CLF': {band: 'canopy', name:'closedForest',threshold: defaultThresholds[9], left: 'terminal', leftName: 'closedforest', right: 'OPF'},
'OPF': {band: 'canopy', name:'openForest',threshold: defaultThresholds[10], left: 'terminal',leftName:'openforest', right: 'terminal',rightName:"woody"},
'WTL': {band: 'wetlands', name:'wetlands',threshold: defaultThresholds[11], left: 'terminal', leftName: 'wetlands',right: 'terminal',rightName:"other"}
};

// Function to get a list of column values from a structure
function getList(struct, column) {
return Object.keys(struct).map(function (k) {
var value = struct[k][column];
return value;
});
}

var palette = getList(classStruct,'color');

var classColorMap = {};
var classIndexMap = {};
for (var _class in classStruct) {
classColorMap[classStruct[_class].color] = _class;
}
classColorMap = ee.Dictionary(classColorMap);

for (var _class in classStruct) {
classIndexMap[classStruct[_class].number] = _class;
}
classIndexMap = ee.Dictionary(classIndexMap);

/* ----------------------------------------------------------------------------------------------

FUNCTIONS AND PROCEDURES BELOW HERE

*/

// container for the classified landclass image
var landClass;

// ************************************************
// ui objects
// containers for sliders
var sliders = {};
// containers for inspected values
var valLabels = {};
// list of all selectable years
//var availableYears = ['2015','2016','2017','2018'];
var availableYears = ['2015'];
// dropdown to select years
var yearList = ui.Select(availableYears, 'year', ''+year);
// button to refresh display according to parameters
var refreshDisplay = ui.Button({
label:'Refresh Display',
onClick: refresh,
style: {'fontSize':'11px', 'padding':'4px', 'margin':'0px'}
});
// button to export the current LandCover
var exportLC = ui.Button({
label:'Export current landCover',
onClick: exportHelper,
style: {'fontSize':'11px', 'padding':'4px', 'margin':'0px'}
});

// ************************************************
// functions

// function to classify stack image using decision tree
function classify (stackImage, decisionTree) {
var classifier = ee.Classifier.decisionTree(decisionTree);
var landClass = stackImage.classify(classifier);
return landClass;
}

function makeStackImageFromCollection (year) {

// 10 is radix param
// If the string begins with "0x", the radix is 16 (hexadecimal)
// If the string begins with "0", the radix is 8 (octal). This feature is deprecated
// If the string begins with any other value, the radix is 10 (decimal)
// reference https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt
year = parseInt(year, 10);
var start = ee.Date.fromYMD(year, 1, 1);
var end = ee.Date.fromYMD(year, 12, 31);

var mangroves = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/mangrove').filterDate(start, end).first().unmask(0).rename('mangroves');
var woody = ee.ImageCollection('projects/servir-mekong/MyanmarFRA/woody').filterDate(start, end).first().rename('woody');
var water = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/water').filterDate(start, end).first().rename('water');
var snow = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/snow').filterDate(start, end).first().rename('snow');
var barren = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/barren').filterDate(start, end).first().rename('barren');
var rice = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/rice').filterDate(start, end).first().rename('rice');
var cropland = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/cropland').filterDate(start, end).first().rename('cropland');
var urban = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/urban').filterDate(start, end).first().rename('urban');
var grass = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/grass').filterDate(start, end).first().rename('grass');
var closedForest = ee.ImageCollection('projects/servir-mekong/MyanmarFRA/smoothed_closedForest').filterDate(start, end).first().rename('closedforest');
var openForest = ee.ImageCollection('projects/servir-mekong/MyanmarFRA/smoothed_openForest').filterDate(start, end).first().rename('openforest');
var wetlands = ee.ImageCollection('projects/servir-mekong/yearly_primitives_smoothed/wetlands').filterDate(start, end).first().rename('wetlands');
var canopy = ee.ImageCollection('projects/servir-mekong/UMD/tree_canopy').filterDate(start, end).first().rename('canopy');

var collection = ee.ImageCollection(ee.List([mangroves, woody, water, snow, barren, rice, cropland, urban, grass, closedForest, openForest, wetlands, canopy]));

var stack = ee.Image(collection.iterate(function(img, prev) {
return ee.Image(prev).addBands(img);
}, ee.Image(1)));

stack = stack.select(ee.List.sequence(1, stack.bandNames().size().subtract(1)));
return stack;
}

function buildDecisionTree(nodeStruct,classStruct,id,node,DTstring){
// Extract parameters
var lnode = 2*node; // left child node number
var rnode = lnode + 1; // right child node number
var dict = nodeStruct[id]; // current dictionary
var band = dict.band; // decision band
var threshold = dict.threshold; // decision threshold
var left = dict.left; // left result (either 'terminal' or new id)
var right = dict.right; // right result (either 'terminal' or new id)
var leftName = dict.leftName; // left class name (if 'terminal')
var rightName = dict.rightName; // right class name (if 'terminal')
var leftLine = '';
var rightLine = '';
var leftNumber = 0;
var rightNumber = 0;

// Add the left condition and right condition strings to the current decision
// tree string. If either condition is non-terminal, recursively call the
// function.
if (left == 'terminal') { // left terminal condition
leftNumber = classStruct[leftName].number;
leftLine = lnode + ') ' + band + '>=' + threshold + ' 9999 9999 ' + leftNumber + ' *';
DTstring.push(leftLine);
if (right == 'terminal') { // right terminal condition
rightNumber = classStruct[rightName].number;
rightLine = rnode + ') ' + band + '<' + threshold + ' 9999 9999 ' + rightNumber + ' *';
DTstring.push(rightLine);
return DTstring;
} else { // right non-terminal condition
rightLine = rnode + ') ' + band + '<' + threshold + ' 9999 9999 9999'; DTstring.push(rightLine); return buildDecisionTree(nodeStruct,classStruct,right,rnode,DTstring); } } else { // left non-terminal condition leftLine = lnode + ') ' + band + '>=' + threshold + ' 9999 9999 9999';
DTstring.push(leftLine);
DTstring = buildDecisionTree(nodeStruct,classStruct,left,lnode,DTstring);
if (right == 'terminal') { // right terminal condition
rightNumber = classStruct[rightName].number;
rightLine = rnode + ') ' + band + '<' + threshold + ' 9999 9999 ' + rightNumber + ' *';
DTstring.push(rightLine);
return DTstring;
} else { // right non-terminal
rightLine = rnode + ') ' + band + '<' + threshold + ' 9999 9999 9999';
DTstring.push(rightLine);
return buildDecisionTree(nodeStruct,classStruct,right,rnode,DTstring);
}
}
return DTstring;
}

function updateThresholdingValues() {
for (var node in nodeStruct) {
for (var primitive in primitives) {
var x = primitives[primitive];
if (nodeStruct[node].name === x) {
nodeStruct[node].threshold = sliders[x].getValue();
}
}
}
}

// function to start the process
function process(year){
print('*********************** Run ' + runCount + ' ***********************');
Map.centerObject(boundary);
var stackImage = makeStackImageFromCollection(year).clip(boundary);
// keep it inside this funtion otherwise it will act crazy :D
var DTstring = ['1) root 9999 9999 9999'];
var decisionTree = buildDecisionTree(nodeStruct,classStruct,startId,1,DTstring).join('\n');
landClass = classify(stackImage, decisionTree);
var lcLayer = ui.Map.Layer(landClass, {palette: palette.join(','), min: 1, max: palette.length - 1}, 'Run:' + runCount, true, 1);
Map.layers().set(primitives.length, lcLayer);
calculateArea(landClass);
runCount += 1;
}

// function to add legend to the map
function addLegend(){
var colors = classColorMap.keys().getInfo();
var labels = classColorMap.values().getInfo();
var symbolBox = ui.Panel();
for (var i = 0; i < colors.length; i++){
var symbol = ui.Label({
style: {
backgroundColor: '#' + colors[i],
padding: '8px',
margin: '0 0 4px 0'
}
});
var description = ui.Label({
value: labels[i],
style: {margin: '0 0 4px 6px'}
});
symbolBox.add(ui.Panel({
widgets: [symbol, description],
layout: ui.Panel.Layout.Flow('horizontal')
}));
}
var legendWidgets = [];
var legendTitle = ui.Label('Legend',{fontWeight: 'bold'});
legendWidgets = [legendTitle, symbolBox];
var legend = ui.Panel({
widgets:legendWidgets,
layout:ui.Panel.Layout.Flow('vertical'),
style: {
'position':'bottom-right',
'padding':'8px 16px',
}
});
Map.add(legend);
}

//update the labels once the sampling is done
function updateValues(feature){
// print(feature.properties);
for (var i=0; i<primitives.length;i++){
var value = parseFloat(feature.properties[primitives[i]]).toFixed(2);
valLabels[primitives[i]].setValue(value);
}
}

// function to export all Land Covers
function exportHelper(){
var DTstring = ['1) root 9999 9999 9999'];
var decisionTree = buildDecisionTree(nodeStruct,classStruct,startId,1,DTstring).join('\n');
var image = landClass.set('decisionTree', decisionTree, 'year', ee.Date.fromYMD(parseInt(year, 10),1,1));
Export.image.toAsset({
image: image.toInt8(),
description: 'LandCover-' + year,
region: boundary.bounds(),
scale: scale,
assetId: exportPath + '/Landcover-' + year,
maxPixels: 1e13
});
}

// function to calculate the area of the landcover classes
function calculateArea (lcMap){
var reduce = lcMap.reduceRegion({
reducer: ee.Reducer.frequencyHistogram(),
geometry: boundary,
crs: 'EPSG:32646', // WGS Zone N 46
scale: 100,
maxPixels: 1E13
});
var data = ee.Dictionary(reduce.get('classification'));
var dataKeys = data.keys();
var keys = ee.List([]);
keys = dataKeys.map(function (key) {
return classIndexMap.get(key);
});
var values = data.values();
//var keys = ee.List(['closedforest','water','grass','openForest','woody','cropland','other','urban','wetlands','mangroves','snow']);
var stats = ee.Dictionary.fromLists(keys, values);
// this is the area in hectare for a scale of 100
// converting to meter square by multiplying with scale value i.e. 100*100
// and then converting to hectare multiplying with 0.0001
// area = reducervalue * 100 * 100 * 0.0001 # in hectare
// meaning we can use the value directly as the hectare

var chart = ui.Chart.array.values(values, 0, keys)
.setChartType('ColumnChart')
.setSeriesNames(['Area (ha)'])
.setOptions({
title: 'Area by Class',
vAxis: {
title: 'Area (hectare)'
},
pointSize: 0,
lineWidth: 1,
colors: ['red']
});
print(chart);
print(stats);
}

// function to refresh display based on parameters
function refresh () {
// get layer shown state
var layers = Map.layers();
var shownStat = layers.map(function(layer){
return layer.getShown();
});
// get selected year
year = yearList.getValue();
updateThresholdingValues();
// initiate the process
process(year);
}

// function to initialize the application
function init(){
var panel = ui.Panel([], ui.Panel.Layout.Flow('vertical'),{position:'bottom-left'});
var yearLabel = ui.Label('Year',{'width':'30px','height':'18px','fontSize':'11px', 'margin':'15px 0px'});
var yearSubPanel = ui.Panel([yearLabel, yearList], ui.Panel.Layout.Flow('horizontal'));
panel.add(yearSubPanel);
var sliderLabel = ui.Label('Adjust Probability Thresholds',{'height':'18px','fontSize':'11px', 'padding':'0px', 'margin':'1px'});
panel.add(sliderLabel);
for (var i = 0; i< primitives.length;i++){
var label = ui.Label(primitives[i],{'width':'50px','height':'18px','fontSize':'11px', 'padding':'0px', 'margin':'1px'});
sliders[primitives[i]] = ui.Slider({
min:0,
max:100,
step:1,
value:defaultThresholds[i],
style:{'height':'18px','fontSize':'11px', 'padding':'0px', 'margin':'1px'}
});
var subPanel = ui.Panel([label, sliders[primitives[i]]], ui.Panel.Layout.Flow('horizontal'),{'padding':'4px'});
panel.add(subPanel);
}
panel.add(refreshDisplay);
panel.add(exportLC);
Map.add(panel);
addLegend();
}

init();
process(year);

This script was modified from the original written by my friend Nishant Khanal.

Previous

Earth from Space

Next

Creating Mosaic of Sentinel-1 using Snappy

1 Comment

  1. thomas

    Hey! it sounds like a great work but your project is not available/public, so we can’t run it !

Leave a Reply

Powered by WordPress & Theme by Anders Norén