3 Mapping mangroves using Landsat
For mapping mangroves change using Landsat our approach is a bit different than for S1+S2!
The Colombian Pacific is the most rainy region worldwide, consequently obtain cloud-free optical data is very difficult for this region. To improve spectral data and filling gaps for missing years we employ LandTrendr algorithm.
There are many ways to set up LandTrendr. Two different approaches are available in our approach:
Given the lack of images we create 3-year composites from 1985-2020 that were linearly improved using temporal segmentation from LandTrendr. However, the code can be set up to create different ranges of composites for instance 2, 3, 4 year composites or
We can create annual composites after removing bad Landsat images.
This Figure 3.1 shows a simple way to linearly improve the original spectral data from Landsat but also filling gaps in periods in which few observations were available. This process guarantees a much spectro-temporal stability of the data that might helps to produce better land cover land use maps. Depends on the geographic region you can use 1, 2, or 3 years to create your composites.
3.1 Making fitted values using Landsat archive
Landsat is a popular satellite that aids to consistently track Earth’ land surface. In tropical areas clouds and lack of Landsat observations affect tracking evolution of changes. Using LandTrendr we improve Landsat itself through some steps:
3.1.1 Remove bad imagery
In some cases even after masking out clouds, some images remain with them. Those images can affect seriously the composites we finally want to create. Specially in coastal areas is very common to find problems with images and other artifacts. One way to remove them is comparing original image vs cloud masking image. If clouds remains after masking, we should select the image ID and remove it from the ImageCollection.
This process is mostly important for early years in 80 and 90’s. Given few images available and problems with Landsat cloud masking. In some cases only one with problems can affect our final composite product.
Using the 5. Remove bad imagery Landsat code in the repository you can identify which ones should not be included in the process.
After you pick those images that cannot be part of the process, you can exclude using ’ not_contains’ from the code 6. Get_Fiited_data_and_Percentiles_Landsat
= function(firstYear,lastYear, startDay, endDay, sensor, box) {
var getSRcollection = ee.ImageCollection('LANDSAT/'+ sensor + '/C01/T1_SR') //# get surface reflectance images
var srCollection .filterBounds(box) //# filter them by a bounding box
.filter(ee.Filter.calendarRange(day_start,day_end,'day_of_month'))
.filter(ee.Filter.calendarRange(month_start,month_end,'month'))
.filter(ee.Filter.calendarRange(firstYear,lastYear,'year'))
.filterMetadata('system:index', 'not_contains', 'LT04_010059_19871113')}
3.1.2 Input parameters (1)
Common parameters in LandTrendr includes the name of the task, region, years and others. In this example we use MVI as main index for segmentation and the new fitted bands will be the spectral bands from Landsat. Our fitted data outputs could be also other indices not necessarily the Landsat spectral bands. We use “Medoid” to create the composites but we can also used a targetDay. The codes that includes all next steps are 6. Get_Fitted_data_and_Percentiles_Landsat or 6. Get_Fitted_data_and_Percentiles_Landsat_annually
A general description looks like:
= ['Man']; // Name it is useful if you want to export many different regions
var featureValues = geometry; // Region to be exported
var featureCol = 'Col'; // Name 2 // it is useful if you want to export many different regions
var featureKey = 1985; // what year do you want to start the time series
var startYear = 2020; // what year do you want to end the time series
var endYear =['01-01']; // what is the beginning of date filter | month-day
var startDay = ['12-31']; // what is the end of date filter | month-day
var endDay = [['MVI', -1, true]]; // The indices to segment on and the invert coefficient
var indexList = ['B1', 'B2', 'B3', 'B4', 'B5', 'B7']; // List of images to export
var ftvList =[];// 'YRS', 'SRC', 'FIT'
var vertList = "medoid"; // how to make annual mosaic - options: "medoid", "targetDay"
var mosaicType = null ; // you can use 172 here rather than null if use "targetDay" // if running "targetDay" mosaic, what day of year should be the target
var targetDay = 'EPSG:32618'; // what should the output projection be? 'EPSG:2317' for SouthAmerica
var outProj = 'SilviaCarbon_COL'//+ featureKey + '_' + indexList[0][0]; // what is the name of the Google Drive folder that you want the outputs placed in
var gDriveFolder = [30.0, 0, 15.0, 0, -30.0, 15.0];
var affine = 30; //1 pixel var aoiBuffer
These inputs can be modified to export many different regions at the same time. For instance if you have a FeatureCollection with a field called PATH_ROW and the values names are (‘7058’,‘7059’) you can export them individually as:
= ['7058', '7059']; // Name it is useful if you want to export many different regions
var featureValues = yourFeatureCollection; // Region to be exported
var featureCol = 'PATH_ROW'; // Name 2 // Name of the field
var featureKey ...
This code has many improvements: 1) Topographic correction 2) Remove scenes borders (-2km buffer) 3) Clumping the data between 0-10000. 3) Many different vegetation indices for temporal segmentation 4) You can create composites at different intervals. In this particular case, I use 3-year composites. 5) Annual composites works pretty similar. Main difference is how you export the percentiles and std. They are exported individually if needed. For instance:
= "std"; // how to make annual mosaic - options: "medoid", "targetDay", "per20" "per80" var mosaicType
More details about this on: https://emapr.github.io/LT-GEE/landtrendr.html
3.1.3 Input parameters (2)
LandTrendr works over annual composites. In some regions, it is possible to use annual observations however in the Colombian Pacific there is too few data. The only alternative is to increase the temporal window. In this case I use 3-year composites from 1985-2020. In other words we have 12 temporal periods for tracking mangroves extent and change. This code helps to create composites every one, two or any window you want. However, if you want to change epochLen, you also have to consider changing the startYear and endYear because the epoch might not exactly match with the amount of years.
In this cases our startYear = 1985, endYear=2020 and our epochLen =3
3.1.4 Adding percentiles
Medoid is a good strategy to create our composites. However, we can add more spectral information to improve classification. Using the same code we added the Percentile 20, 80 and Standard Deviation. Before running the code, you must create an ImageCollection in you Earth Engine account. This Collection will host all these additional information.
To create an ImageCollection: You can just go to Assest Tab, New, Image Collection.
Once you create it you can modify the assetId name:
percentil.aggregate_array('composite_year')
.evaluate(function (composite_year) {
composite_year.forEach(function (year, i) {
= percentil
var image .filterMetadata('composite_year', 'equals', year)
.first();
Export.image.toAsset({
: image,
image: 'percentiles_' + year,
description: 'YOURCOLLECTION/percentiles_' + year,
assetId: outProj,
crs: 30,
scale: 1e13,
maxPixels: featureCol,
region
});
}); });
3.1.5 Exporting assets
After you remove bad images and change parameters you can export the fitted image plus the percentiles as an ImageCollection. Given this process is very demanding computationally it might takes some minutes after you are able export the data. You should see something like this:
Note: When you run the code the script is going to be freeze for some seconds, Just click Wait if GEE asked you..
3.2 Building 3-years landcover maps
Once you save the fitted image data and the percentiles we can combine them into a final ImageCollection to run the classification process for each 3-year composite. Open the code: 7. Classification_Landsat_TimeSeries
3.2.1 Reading Landsat spectral data and stratified sampling
The first step in the code is put together all fitted values and percentiles in one final ImageCollection.
= ee.Image("projects/mangrovescience/SilviaCarbon_COL/Landsat_Predictors/Cienaga-NBR-7-19852020-01011231EPOCHS3");
var ftv_values = ee.FeatureCollection("projects/mangrovescience/SilviaCarbon_COL/Landsat_Predictors/Cienaga_Percentiles");
var percentiles = ee.Image("projects/mangrovescience/SilviaCarbon_COL/S1S2_2019_2020/S1S2_class_SNIC_R1_cleaned");
var mangroves_SNIC
//#Reading our FTV composites
= ["1987", "1990", "1993", "1996", "1999",
var lista "2002", "2005", "2008", "2011", "2014","2017","2020"];
= ee.ImageCollection([]);
var predictors
for (var i = 0; i< lista.length; i++){
= lista[i];
var year = ftv_values.select('b1_ftv_'+(year), 'b2_ftv_'+(year),
var ftv 'b3_ftv_'+(year),'b4_ftv_'+(year),'b5_ftv_'+(year),
'b7_ftv_'+(year)).rename('B1', 'B2','B3', 'B4', 'B5', 'B7')
.set('composite_year', ee.Number.parse(year));
= predictors.merge(ee.ImageCollection([ftv]))}
predictors
= ee.Filter.equals({
var filter : 'composite_year',
leftField: 'composite_year'
rightField
});
//# Create the join.
= ee.Join.inner();
var simpleJoin
//# Inner join
= ee.ImageCollection(simpleJoin.apply(predictors, percentiles, filter));
var innerJoin
= innerJoin.map(function(feature) {
var collection_final ee.Image.cat(feature.get('primary'), feature.get('secondary'));
return
});
print('collection_final', collection_final);
Now we can calculate different vegetation indices, and add other ancillary information. Also we can import the basemap from which we want to collect the spectral data. The basemap that we are going to use is the map we created using S1+S2 which is a good representation of different classes for the period 2019-2020.
= require('users/murillop/mapping_mangroves:mangroves');
var mangroves = mangroves.getAncillary();
var base_predictors
= mangroves_SNIC.clip(collection_final.first().geometry()).rename('lc');
mangroves_SNIC Map.addLayer(mangroves_SNIC, {min:1, max:5, palette: ["red","green","yellow","blue", "Chartreuse"]}, 'Basemap 2019-2020');
//Calculate all indices and GLCM predictors
= mangroves.doIndices2_land(collection_final);
var spectral_predictors
//Add ancillary data
= spectral_predictors.map(function(img){
spectral_predictors img.addBands(base_predictors).updateMask(mangroves_SNIC.gt(0))});
return
//Select Landsat data in 2017-2020
= spectral_predictors.filterMetadata('composite_year', "equals", 2020).first();
var y2017_2020_pixel Map.addLayer(y2017_2020_pixel, {min:100, max:4000, bands:['B4','B5','B3']}, 'Spectral Landsat 2017-2020');
//Get training data//
= mangroves_SNIC.addBands(ee.Image.pixelLonLat())
var stratified .stratifiedSample({
: 1,
numPoints: 'lc',
classBand: 30,
scale: mangroves_SNIC.geometry(),
region:[1,2,3,4,5], // 1=manglar // 2 = forest // 3= pastos //4 water //5 Other vegetation
classValues: [200, 200, 200, 200, 200],
classPoints.map(function(f) {
})f.setGeometry(ee.Geometry.Point([f.get('longitude'), f.get('latitude')]));
return
});//print ('Points per class', stratified.reduceColumns(ee.Reducer.frequencyHistogram(),['classification']));
print ('Strat', stratified.limit(10))
= ['B2','B3', 'B4','B5', 'B7',
var new_bands 'B3_p20', 'B4_p20','B5_p20', 'B7_p20',
'B3_p80', 'B4_p80','B5_p80', 'B7_p80',
'B3_stdDev', 'B4_stdDev','B5_stdDev', 'B7_stdDev',
'NDMI','NDFI', 'TCW', 'TCG', 'TCA', 'SHADE_NDFI', 'GV_NDFI', 'SOIL_NDFI', 'NPV_NDFI',
'NDVI', 'MNDWI', 'MVI', 'CMRI', 'NBR',
'GRAY_savg', 'GRAY_var', 'GRAY_ent', 'GRAY_contrast',
'ELEVATION', 'DEM_SLOPE' , 'ASPECT', 'TEMPERATURE', 'NIGHT_LIGHTS', 'POPULATION'];
We distributed 200 per each class. See example of the distribution:
3.2.2 Apply SNIC
We use Image Segmentation SNIC algorithm for classification. For building the clusters we use MVI, it is performs well for delineating mangroves extent. You can use other indices. Currently the available indices are:
'NDMI','NDFI', 'TCW', 'TCG', 'TCA', 'SHADE_NDFI', 'GV_NDFI',
['SOIL_NDFI', 'NPV_NDFI', 'NDVI', 'MNDWI', 'MVI', 'CMRI', 'NBR']
However you can add your own indices once you save the main library (‘users/murillop/mapping_mangroves:mangroves’); in your own repository.
//Use MVI for clustering and spectralmap in 2020.
= mangroves.snic_mangroves_col(spectral_predictors.select(new_bands), 'MVI');
var snic = snic.filterMetadata('composite_year', "equals", 2020).first();
var y2017_2020_snic =y2017_2020_snic.bandNames().remove('clusters');
var predictionBandsprint (predictionBands, 'predictionBands');
= snic.select(predictionBands);
snic
= y2017_2020_snic.sampleRegions({
var sampleAll : stratified,
collection: ['lc'],
properties: 30,
scale:2,
tileScale: true,
geometries.randomColumn();
})
//print('Full sample size',sampleAll.size());
= sampleAll.randomColumn({seed: 10});
sampleAll = 0.7; // Roughly 70% training, 30% testing
var split //create the training set
= sampleAll.filter(ee.Filter.lt('random', split));//print('trainingPoints',trainingPts);
var trainingPts //Export.table.toAsset(trainingPts, 'train', 'SilviaCarbon_COL/features/train_Landsat' )
//create the test set
= sampleAll.filter(ee.Filter.gte('random', split));//print('testingPoints',testingPts);
var testingPts //Export.table.toAsset(testingPts, 'test', 'SilviaCarbon_COL/features/test_Landsat' )
//After exporting and clean the points you can call them:
//var train = ee.FeatureCollection('projects/mangrovescience/SilviaCarbon_COL/features/train_Landsat');
//Otherwise just sample using random procedure!
= ee.List(["ffffff", "FF0000","00ff00 ", "FFA500", "0000FF", "32CD32",]); // includes one color at the beggining because class starts in 1 NOT in ZERO
var paleta
= trainingPts.map(function(f) {
var features = f.get("lc");
var klass f.set({style: {color: paleta.get(klass) }});
return
});Map.addLayer(features.style({styleProperty: "style"}),{}, 'Training Points', true);
We also use 90 trees after tunning. It seems 90 provides a more stable accuracy. See next section.
///Build the RF classifer:
= ee.Classifier.smileRandomForest(90).setOutputMode('CLASSIFICATION')
var classifier_snic //90 seems a good number of trees and Tunning plot
.train({
:trainingPts,
features:'lc',
classProperty: predictionBands
inputProperties });
3.2.3 Tunning for the amount of trees
I mimimize the number of trees to optimize computation demand. We identify the number of trees using a simple tunnin available for more detail here.
// Run .explain() to see what the classifer looks like
print(classifier_snic.explain())
= y2017_2020_snic.sampleRegions({
var test : testingPts,
collection: ['lc'],
properties: 30,
scale: 2
tileScale
});
// Tune the numberOfTrees parameter.
= ee.List.sequence(10, 150, 5);
var numTreesList = numTreesList.map(function(numTrees) {
var accuracies = ee.Classifier.smileRandomForest(numTrees)
var classifier .train({
: trainingPts,
features: 'lc',
classProperty: predictionBands
inputProperties
});
// Here we are classifying a table instead of an image
// Classifiers work on both images and tables
return test.classify(classifier)
.errorMatrix('lc', 'classification')
.accuracy();
});
= ui.Chart.array.values({
var chart : ee.Array(accuracies),
array: 0,
axis: numTreesList
xLabels.setOptions({
}): 'Hyperparameter Tuning for the numberOfTrees Parameters',
title: {title: 'Validation Accuracy'},
vAxis: {title: 'Number of Tress', gridlines: {count: 15}}
hAxis
});print(chart);
3.2.4 Apply SNIC and export each land cover map
//Full SNIC across whole collection (all years)
= snic
var classifiedCollection .map(function(image) {
image.classify(classifier_snic).copyProperties(image);
return
});print (classifiedCollection, 'classifiedCollection');
Map.addLayer(classifiedCollection.first(),{min:1, max:5, palette: ["red","green","yellow","blue", "Chartreuse"]}, '1985-1987 SNIC classification');
classifiedCollection.aggregate_array('composite_year')
.evaluate(function (systemIndexes) {
systemIndexes.forEach(function (year, i) {
//print(i); // This is your 0-based index
= classifiedCollection
var image .filterMetadata('composite_year', 'equals', year)
.first();
Export.image.toAsset({
: image,
image: 'lc_' + year,
description: 'lc_' + year,
assetId: 30,
scale: 1e13,
maxPixels: classifiedCollection.first().geometry(),
region
});
}); });
3.2.5 Maps visualization
We created a gif to visualize the dynamics of mangroves in Cienaga Grande. Our approach provides a quick reference to evaluate mangroves potential loss and gains. While there is a high variability along time in this study region, further validation and cleaning the outputs is necessary.
We encourage potential users to add or remove spectral metrics and careful select training data to improve current outputs.
3.2.6 Limitations
Outcome maps for this approach will depend on the quality of your input Landsat data, basemap detail and also training data. I recommend to collect historical data from other years and limited the amount of sampled points if you use the object-based approach. For instance, in this study case I use 200 points for each class, but points could be located at the same cluster.
While the accuracy depends of many factors are methodology combines the current state-of-the-art elements to be repeatable and reproducible in any other part of the world.