IN PROGRESS: Modules for image classification in Google Earth Engine with topographic and spectral variables
Zach Levitt, '20.5
Jeff Howarth, Associate Professor of Geography
Middlebury College, Vermont, USA
These module functions are provided here as a reference for the main tutorial, which outlines a workflow for performing machine learning image classification with topographic and spectral variables. This overview is organized in the same way as the module script, reflecting the major tasks that are involved in performing this type of analysis. Each function and section is linked below.
- IMPORT ASSETS
- LOAD and FILTER IMAGERY
- PRE-PROCESS IMAGERY
- DEM PROCESSING
- TOPOGRAPHIC VARIABLES
- MULTIBAND IMAGE ANALYSIS
- VISUALIZATION
Input: path (string)
Output: ee.Image
exports.loadImageAsset = function(path){
return ee.Image(path)
}
Input: extent (ee.Feature or ee.FeatureCollection), studyYear (Number)
Output: ee.ImageCollection
//Loads NAIP imagery for a given extent and study year.
//Adds an NDVI band and filters for imagery with >3 bands
exports.loadNAIP = function(extent,studyYear){
return ee.ImageCollection("USDA/NAIP/DOQQ")
.filterBounds(extent)
.map(function(image){
var imageYear = image.date().get('year')
var bandNames = image.bandNames().length()
return image.set({year:imageYear, bands:bandNames})
})
.filter(ee.Filter.gt('bands', 3))
.map(function(image){
var ndvi = image.normalizedDifference(['N', 'R']).rename('NDVI');
return image.addBands(ndvi);
});
}
Input:
Output:
//Loads Sentinel 2-A imagery for a given extent, start and end date, and max cloud percentage.
//Filters imagery for cloudy pixels
exports.loadSentinel = function(extent,startDate,endDate,cloudPercentage){
return ee.ImageCollection("COPERNICUS/S2_SR")
.filterBounds(extent)
.filterDate(startDate, endDate)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',cloudPercentage))
.map(function(image){
var qa = image.select('QA60');
// Bits 10 and 11 are clouds and cirrus, respectively.
var cloudBitMask = 1 << 10;
var cirrusBitMask = 1 << 11;
// Both flags should be set to zero, indicating clear conditions.
var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
.and(qa.bitwiseAnd(cirrusBitMask).eq(0));
return image.updateMask(mask).divide(10000);
});
}
Input:
Output:
exports.addPercentDiffSentinel = function(image,bandName){
var added = (image.select('1_'+bandName).subtract(image.select('0_'+bandName))).divide(image.select('1_'+bandName)).rename(bandName+'_diff');
return image.addBands(added);
}
Input:
Output:
exports.addBandRatioSentinel = function(image,bandName){
var added = image.normalizedDifference(['0_'+bandName, '1_'+bandName]).rename(bandName+'_diff');
return image.addBands(added);
}
Input:
Output:
// Add NDVI attribute to all NAIP images in a collection
exports.addNDVI = function(images){
return images.map(function(image){
var ndvi = image.normalizedDifference(['N', 'R']).rename('NDVI');
return image.addBands(ndvi);
});
}
Input:
Output:
//Mosaic NAIP images from a collection and reproject
exports.mosaicNAIP = function(images,year,mask,outCRS,outScale){
return images.filter(ee.Filter.calendarRange(year, year, 'year'))
.mosaic()
.set({year:year})
.mask(mask)
.reproject(outCRS,null,outScale)
.resample('bilinear')
.mask(mask)
}
Input:
Output:
//Mosaic Sentinel images from a collection using a quality attribute and reproject
exports.mosaicSentinel = function(images,mask,outCRS,outScale){
return images
.qualityMosaic('QA10')
.mask(mask)
.reproject(outCRS,null,outScale)
.resample('bilinear')
.mask(mask)
}
Input:
Output:
//Calculates a mask for a digital elevation model of an islannd
//based on a provided minimum elevation and buffer distance.
exports.calculateMask = function(dem,minElevation,bufferDistance){
return ee.Image(1)
.cumulativeCost({
source: dem.gt(minElevation).selfMask(),
maxDistance: bufferDistance,
}).lt(bufferDistance)
}
Input:
Output:
//Masks image and reprojects/resamples if crs and scale are provided
exports.processElevationData = function(image,mask,crs,scale) {
if (crs === null){
return image.mask(mask);
}
else {
return image
.reduceResolution({
reducer: ee.Reducer.mean(),
maxPixels: 1024
})
// Request the data at the scale and projection of the MODIS image.
.reproject({
crs: crs,
scale: scale
})
.mask(mask)
}
};
Input:
Output:
//Reproject DEM to desired CRS and scale, resample with bilinear method
exports.resampleDEM = function(dem,outCRS,outScale){
return dem.reproject(outCRS,null,outScale).resample('bilinear')
}
Input:
Output:
//Compute Canopy Height Model
exports.elevationDifference = function(dem,dsm){
return dsm.subtract(dem).rename('diff')
}
Input:
Output:
//Calculate slope in degrees from elevation data
exports.calculateSlopeDegrees = function(dem,mask){
return ee.Terrain.slope(dem).mask(mask)
}
Input:
Output:
//Calculate heat load index, based on Theobald et al (2015)
exports.calculateTheobaldHLI = function(dem,mask) {
var aspectRadians = ee.Terrain.aspect(dem).mask(mask).multiply(ee.Number(0.01745329252))
var slopeRadians = ee.Terrain.slope(dem).mask(mask).multiply(ee.Number(0.01745329252))
var foldedAspect = aspectRadians.subtract(ee.Number(4.3196899)).abs().multiply(ee.Number(-1)).add(ee.Number(3.141593)).abs()
var theobaldHLI = (slopeRadians.cos().multiply(ee.Number(1.582)).multiply(ee.Number(0.82886954044))).subtract(foldedAspect.cos().multiply(ee.Number(1.5)).multiply(slopeRadians.sin().multiply(ee.Number(0.55944194062)))).subtract(slopeRadians.sin().multiply(ee.Number(0.262)).multiply(ee.Number(0.55944194062))).add(foldedAspect.sin().multiply(ee.Number(0.607)).multiply(slopeRadians.sin())).add(ee.Number(-1.467));
return theobaldHLI.exp().rename('hli')
};
Input:
Output:
// Calculate a neighborhood mean using a pre-defined kernel radius
exports.calculateNeighborhoodMean = function(image, kernelRadius) {
return image.reduceNeighborhood({
reducer: ee.Reducer.mean(),
kernel: ee.Kernel.square(kernelRadius,'pixels',false),
optimization: 'boxcar',
});
}
Input:
Output:
//Calculate a neighborhood standard deviation using a pre-defined kernel radius
exports.calculateNeighborhoodStdDev = function(image, kernelRadius) {
return image.reduceNeighborhood({
reducer: ee.Reducer.stdDev(),
kernel: ee.Kernel.square(kernelRadius,'pixels',false),
optimization: 'window',
});
}
Input:
Output:
//Calculate topographic position index from a pre-defined mean image
exports.calculateTPI = function(image, meanImage) {
return image.subtract(meanImage).rename('tpi')
}
Input:
Output:
//Calculate mean topographic position index, based on Theobald et al (2015)
exports.calculateMeanTPI = function(image1,image2,image3){
return (image1.add(image2).add(image3)).divide(ee.Number(3)).rename('meanTPI')
}
Input:
Output:
exports.calculateLandforms = function(dem,slopeDegrees,theobaldHLI,meanTPI,tpi_270m){
var slopeReclass = ee.Image(1)
.where(slopeDegrees.gt(50), 5000)
.where(slopeDegrees.gt(2).and(slopeDegrees.lte(50)), 1000)
.where(slopeDegrees.lte(2), 2000)
.selfMask();
var theobaldHLIReclass = ee.Image(1)
.where(theobaldHLI.lte(0.448), 100)
.where(theobaldHLI.gt(0.448).and(theobaldHLI.lte(0.767)), 200)
.where(theobaldHLI.gt(0.767), 300)
.selfMask();
var meanTPIReclass = ee.Image(1)
.where(meanTPI.lte(-1.2), 10)
.where(meanTPI.gt(-1.2).and(meanTPI.lte(-0.75)), 20)
.where(meanTPI.gt(-0.75).and(meanTPI.lte(0)), 30)
.where(meanTPI.gt(0),40)
.selfMask();
var tpi_270mReclass = ee.Image(1)
.where(tpi_270m.lte(-5), 1)
.where(tpi_270m.gt(-5).and(tpi_270m.lte(0)), 2)
.where(tpi_270m.gt(0).and(tpi_270m.lte(30)), 3)
.where(tpi_270m.gt(30).and(tpi_270m.lte(300)), 4)
.where(tpi_270m.gt(300),5)
.selfMask();
var reclassCombined = slopeReclass.add(theobaldHLIReclass).add(meanTPIReclass).add(tpi_270mReclass).updateMask(dem);
return reclassCombined
.where(reclassCombined.eq(2344)
.or(reclassCombined.eq(1344)),11)
.where(reclassCombined.eq(1244),12)
.where(reclassCombined.eq(1144),13)
.where(reclassCombined.eq(1145)
.or(reclassCombined.eq(1245))
.or(reclassCombined.eq(1345))
.or(reclassCombined.eq(2345)),14)
.where(reclassCombined.gte(5000).and(reclassCombined.lte(6000)),15)
.where(reclassCombined.eq(1341)
.or(reclassCombined.eq(1342))
.or(reclassCombined.eq(1343)),21)
.where(reclassCombined.eq(1241)
.or(reclassCombined.eq(1242))
.or(reclassCombined.eq(1243)),22)
.where(reclassCombined.eq(1141)
.or(reclassCombined.eq(1142))
.or(reclassCombined.eq(1143)),23)
.where(reclassCombined.eq(2341)
.or(reclassCombined.eq(2342))
.or(reclassCombined.eq(2343)),24)
.where(reclassCombined.eq(1331)
.or(reclassCombined.eq(1332))
.or(reclassCombined.eq(1333))
.or(reclassCombined.eq(1334))
.or(reclassCombined.eq(1335)),31)
.where(reclassCombined.eq(1231)
.or(reclassCombined.eq(1232))
.or(reclassCombined.eq(1233))
.or(reclassCombined.eq(1234))
.or(reclassCombined.eq(1235)),32)
.where(reclassCombined.eq(1131)
.or(reclassCombined.eq(1132))
.or(reclassCombined.eq(1133))
.or(reclassCombined.eq(1134))
.or(reclassCombined.eq(1135)),33)
.where(reclassCombined.eq(2332)
.or(reclassCombined.eq(2333))
.or(reclassCombined.eq(2334))
.or(reclassCombined.eq(2335))
.or(reclassCombined.eq(2331)),34)
.where(reclassCombined.eq(1112)
.or(reclassCombined.eq(1113))
.or(reclassCombined.eq(1121))
.or(reclassCombined.eq(1122))
.or(reclassCombined.eq(1123))
.or(reclassCombined.eq(1124))
.or(reclassCombined.eq(1212))
.or(reclassCombined.eq(1213))
.or(reclassCombined.eq(1221))
.or(reclassCombined.eq(1222))
.or(reclassCombined.eq(1223))
.or(reclassCombined.eq(1224))
.or(reclassCombined.eq(1312))
.or(reclassCombined.eq(1313))
.or(reclassCombined.eq(1321))
.or(reclassCombined.eq(1322))
.or(reclassCombined.eq(1323))
.or(reclassCombined.eq(1324))
.or(reclassCombined.eq(2312))
.or(reclassCombined.eq(2313))
.or(reclassCombined.eq(2321))
.or(reclassCombined.eq(2322))
.or(reclassCombined.eq(2323))
.or(reclassCombined.eq(2324)),41)
.where(reclassCombined.eq(1211)
.or(reclassCombined.eq(1311))
.or(reclassCombined.eq(2311))
.or(reclassCombined.eq(1111)),42)
.updateMask(reclassCombined.select('constant').gt(999))
}
Input:
Output:
//Reclassify landforms layer to fit with visualization parameters.
exports.remapLandforms = function(landforms){
return landforms.remap([11,12,13,14,15,21,22,23,24,31,32,33,34,41,42],[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14])
}
Input:
Output:
//Add aligned rasters to a single image as bands
exports.toBandedImage = function(imageCollection){
return imageCollection.toBands()
}
Input:
Output:
//Add aligned rasters to a single image as bands
exports.generateSampleData = function(image,bands,collection,property){
return image.select(bands).sampleRegions({
collection: collection,
properties: [property]
})
}
Input:
Output:
exports.paintImageWithFeatures = function(features){
return ee.Image().byte().paint({
featureCollection: features,
color: 'Class',
}).rename('class')
}
Input:
Output:
exports.createStratifiedPoints = function(image,numPointsPerClass,outCRS,outScale,geometry){
return image.addBands(ee.Image.pixelLonLat())
.stratifiedSample({
numPoints: numPointsPerClass,
classBand: 'class',
projection:outCRS,
scale: outScale,
region: geometry,
}).map(function(f) {
return f.setGeometry(ee.Geometry.Point([f.get('longitude'), f.get('latitude')]))
})
}
Input:
Output:
//Spatial join of raster data to vector points
exports.spatialJoin = function(image,points){
return image.reduceRegions(points,ee.Reducer.first())
}
Input:
Output:
exports.filterPoints = function(features,filter,field){
filter.map(function(classFilter){
features = features.filter(ee.Filter.and(ee.Filter.eq('class', classFilter['class']),ee.Filter.gt(field, classFilter['max']),ee.Filter.lt(field,classFilter['min'])).not());
return classFilter;
});
return features;
}
Input:
Output:
exports.createRFDict = function(RF_matrix){
var RF_producers = RF_matrix.producersAccuracy()
var RF_consumers = RF_matrix.consumersAccuracy()
return ee.Dictionary({'Overall accuracy':ee.Number(RF_matrix.accuracy()).format('%.4f'),
'Kappa':ee.Number(RF_matrix.kappa()).format('%.4f'),
'% of class 0 accurately identified':ee.Number(RF_producers.get([0,0])).format('%.4f'),
'% of class 1 accurately identified':ee.Number(RF_producers.get([1,0])).format('%.4f'),
'% of class 2 accurately identified':ee.Number(RF_producers.get([2,0])).format('%.4f'),
'% of class 3 accurately identified':ee.Number(RF_producers.get([3,0])).format('%.4f'),
'% correct of class 0 output':ee.Number(RF_consumers.get([0,0])).format('%.4f'),
'% correct of class 1 output':ee.Number(RF_consumers.get([0,1])).format('%.4f'),
'% correct of class 2 output':ee.Number(RF_consumers.get([0,2])).format('%.4f'),
'% correct of class 3 output':ee.Number(RF_consumers.get([0,3])).format('%.4f'),
});
}