Evalscript
An evalscript (or custom script) is a piece of Javascript code that defines how the platform processes satellite data and what values the service returns. It is a required part of any process, batch process or OGC request.
The evalscript functions section contains detailed explanations of parameters and functions that can be used in evalscripts. Evalscripts can use any JavaScript function or language structure and include certain utility functions specific to the platform. Evalscripts run the Chrome V8 JavaScript engine.
Evalscripts can be used to calculate a spectral index, create visualizations, do multi-temporal analysis and visualization, use data fusion, and do statistical analysis. Other things, such as setting the resolution of output, setting the projection of output, and defining the time range of requests, are set as request parameters for the various APIs.
Transparency
Parts of the image can be made fully or partially transparent by including the fourth output channel, the alpha channel. The value 0 in the alpha channel makes a pixel fully transparent, while the maximum value in the alpha channel makes it fully opaque (not transparent). The values in between will make the pixel proportionally transparent. The maximum value in the alpha channel depends on an image bit depth as specified by sampleType:
- for
sampleType
AUTO
orFLOAT32
: values in the alpha channel should be from the interval [0, 1] - for
sampleType
UINT8
: values in the alpha channel should be from the interval [0, 255] - for
sampleType
UINT16
: values in the alpha channel should be from the interval [0, 65535]
PNG and TIFF are output file formats that support transparency, while JPEG does not.
Transparent NoData
Pixels
NoData
pixels are identified by the value 0 in the dataMask
band. In the following evalscript, if the wetness index (NDWI) is positive, the returned value will be 0, making water areas transparent and only land visible in the returned image.
//VERSION=3
function setup() {
return {
input: ['B02', 'B03', 'B04', 'B08'],
output: { bands: 4 },
};
}
function evaluatePixel(sample) {
let NDWI = (sample.B03 - sample.B08) / (sample.B03 + sample.B08);
let transparency = 0;
if (NDWI < 0) {
transparency = 1;
}
return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02, transparency];
}
This approach works when the AUTO
or FLOAT32
sample types are being requested. To achieve the same transparency, scale the output values for other sample types. See the examples below for guidance.
Transparent NoData
pixels and sampleType: UINT16
When using sampleType
UINT16
, the range of output values in an image becomes [0, 65535]. The value 65535 must be returned in the alpha channel for pixels that should not be transparent, as shown in the example below.
//VERSION=3
function setup() {
return {
input: ['B04', 'B03', 'B02', 'dataMask'],
output: { bands: 4, sampleType: 'UINT16' },
};
}
function evaluatePixel(samples, scenes) {
return [
samples.B04 * 3.5 * 65535,
samples.B03 * 3.5 * 65535,
samples.B02 * 3.5 * 65535,
samples.dataMask * 65535,
];
}
Transparent NoData
pixels and sampleType: UINT8
The same logic applies to sampleType
UINT8
, except that the range of output values in this case is [0, 255]. The same evalscript as above but for UINT8
:
//VERSION=3
function setup() {
return {
input: ['B04', 'B03', 'B02', 'dataMask'],
output: { bands: 4, sampleType: 'UINT8' },
};
}
function evaluatePixel(samples, scenes) {
return [
samples.B04 * 3.5 * 255,
samples.B03 * 3.5 * 255,
samples.B02 * 3.5 * 255,
samples.dataMask * 255,
];
}
Transparent Data Pixels
To use some other condition for turning pixels transparent, return the condition in the fourth channel and output four bands in the setup()
function. The example below shows how to return the Sentinel-2 L1C NDVI index and values larger than 0.6 as transparent. This example also leaves the NoData
pixels non-transparent and thus does not need to use the dataMask
input band.
//VERSION=3
function setup() {
return {
input: ['B02', 'B03', 'B04', 'B08'],
output: { bands: 4 },
};
}
function evaluatePixel(samples, scenes) {
var NDVI = (samples.B08 - samples.B04) / (samples.B08 + samples.B04);
return [samples.B04 * 2.5, samples.B03 * 2.5, samples.B02 * 2.5, NDVI < 0.6];
}
Data Mask
Evalscripts allow control over which parts (pixels) of the image to return. This way, parts with NoData
can be removed. The setup function allows a user to request dataMask
as an input array element and then use it in the evaluatePixel
function in the same manner as any other input band.
General notes
dataMask
has a value of 0 for NoData
pixels and 1 elsewhere.
What NoData
means:
- All pixels that lie outside of the requested polygon (if specified).
- All pixels where no source data was found.
- All pixels where there is data explicitly set to the
NoData
value.
All NoData
pixels, as defined above, have a dataMask
value of 0. All band values for these pixels are also 0, except for Landsat data collections, where band values for NoData
pixels are NaN.
NoData
pixels are treated like any other in the evalscript. Their value is applied to the evalscript like any other pixel. For example, return [sample.B04*sample.B03]
returns 0 for NoData
pixels, while return [sample.B04/sample.B03]
would return "Infinity" (if sampleType
is FLOAT32
) due to division by zero or "NaN." To treat NoData
pixels differently, they should be handled explicitly in evalscripts. See the examples below.
Example 1: Assign an arbitrary value (99) to NoData
pixels
//VERSION=3
function setup() {
return {
input: ['B02', 'B03', 'B04', 'dataMask'],
output: { bands: 3 },
};
}
function evaluatePixel(sample) {
if (sample.dataMask == 1) {
return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];
} else {
return [99, 99, 99];
}
}
Example 2: Use values in dataMask
as the transparency band
To use this example, set the output.responses.format.type
parameter of your process API request to image/png
or image/tiff
. The PNG format will automatically interpret the fourth band as transparency.
//VERSION=3
function setup() {
return {
input: ['B02', 'B03', 'B04', 'dataMask'],
output: { bands: 4 },
};
}
function evaluatePixel(sample) {
return [
2.5 * sample.B04,
2.5 * sample.B03,
2.5 * sample.B02,
sample.dataMask,
];
}
Working with Metadata in Evalscripts
Metadata provided in raster format is available as additional bands in the collection. Like any other input band, this metadata can be accessed and processed in evalscripts. Basic examples and metadata are listed in the data section for each data collection (e.g., sunAzimuthAngles).
Check Which Metadata is Available
Metadata is stored in two objects, which are called inputMetadata
and scenes
. The properties of the scenes
object can be different depending on the selection of:
- mosaicking (e.g.,
ORBIT
orTILE
) - data collection (e.g., Sentinel-2 L2A, Sentinel-1, Sentinel-5p)
- function in the evalscript (e.g.,
evaluatePixel
,preProcessScenes
,updateOutputMetadata
)
A convenient way to check which metadata is available to be requested in scenes is to write all object properties to the userdata.json file. This basic example shows how to do this with the Processing API.
Properties of Scenes Object and Mosaicking ORBIT
This example shows:
- Accessing metadata when mosaicking is
ORBIT
usingscenes.orbits
- Passing metadata from
scenes
to the userdata.json file usingoutputMetadata.userData
inupdateOutputMetadata
function
evalscript = """
//VERSION=3
function setup() {
return {
input: ["B02", "dataMask"],
mosaicking: Mosaicking.ORBIT,
output: {
id: "default",
bands: 1
}
}
}
function evaluatePixel(samples, scenes, inputMetadata, customData, outputMetadata) {
//Average value of band B02 based on the requested scenes
var sumOfValidSamplesB02 = 0
var numberOfValidSamples = 0
for (i = 0; i < samples.length; i++) {
var sample = samples[i]
if (sample.dataMask == 1){
sumOfValidSamplesB02 += sample.B02
numberOfValidSamples += 1
}
}
return [sumOfValidSamplesB02 / numberOfValidSamples]
}
function updateOutputMetadata(scenes, inputMetadata, outputMetadata) {
outputMetadata.userData = {
"inputMetadata": inputMetadata
}
outputMetadata.userData["orbits"] = scenes.orbits
}
"""
request = {
"input": {
"bounds": {
"bbox": [13.8, 45.8, 13.9, 45.9]
},
"data": [{
"type": "sentinel-2-l1c",
"dataFilter": {
"timeRange": {
"from": "2020-12-01T00:00:00Z",
"to": "2020-12-06T23:59:59Z"
}
}
}]
},
"output": {
"responses": [{
"identifier": "default",
"format": {
"type": "image/tiff"
}
},
{
"identifier": "userdata",
"format": {
"type": "application/json"
}
}
]
},
"evalscript": evalscript
}
Properties of Scenes Object and Mosaicking TILE
This example shows how to:
- Access scenes metadata when mosaicking is
TILE
usingscenes.tiles
and writing it to the userdata.json file - Calculate a maximum value of band B02 and writing it to the userdata.json file.
Note that a global variable maxValueB02
is used to assign a value to it in the evaluatePixel
function but not to write its value to metadata in the updateOutputMetadata
function. The advantage of this approach is that maxValueB02
is written to metadata only once and not for each output pixel.
evalscript = """
//VERSION=3
function setup() {
return {
input: ["B02", "dataMask"],
mosaicking: Mosaicking.TILE,
output: {
id: "default",
bands: 1
}
}
}
var maxValueB02 = 0
function evaluatePixel(samples, scenes, inputMetadata, customData, outputMetadata) {
//Average value of band B02 based on the requested tiles
var sumOfValidSamplesB02 = 0
var numberOfValidSamples = 0
for (i = 0; i < samples.length; i++) {
var sample = samples[i]
if (sample.dataMask == 1){
sumOfValidSamplesB02 += sample.B02
numberOfValidSamples += 1
if (sample.B02 > maxValueB02){
maxValueB02 = sample.B02
}
}
}
return [sumOfValidSamplesB02 / numberOfValidSamples]
}
function updateOutputMetadata(scenes, inputMetadata, outputMetadata) {
outputMetadata.userData = { "tiles": scenes.tiles }
outputMetadata.userData.maxValueB02 = maxValueB02
}
"""
request = {
"input": {
"bounds": {
"bbox": [13.8, 45.8, 13.9, 45.9]
},
"data": [{
"type": "sentinel-2-l1c",
"dataFilter": {
"timeRange": {
"from": "2020-12-01T00:00:00Z",
"to": "2020-12-06T23:59:59Z"
}
}
}]
},
"output": {
"responses": [{
"identifier": "default",
"format": {
"type": "image/tiff"
}
},
{
"identifier": "userdata",
"format": {
"type": "application/json"
}
}
]
},
"evalscript": evalscript
}
Output Metadata into userdata.json
file
This example shows how to write several pieces of information to the userdata.json
file:
- A version of the software used to process the data. This information comes from
inputMetadata
. - Dates when the data used for processing were acquired. This information comes from
scene.tiles
. - Values set by the user and used for processing, such as thresholds (e.g.,
ndviThreshold
) and an array of values (e.g.,notAllowedDates
). - Dates of all available tiles before filtering out those acquired on dates given in the notAllowedDates array. These dates are listed in the
tilesPPSDates
property ofuserData
. Note how to use the global variable tilesPPS: assign it a value inpreProcessScenes
and output it in theupdateOutputMetadata
function. - Dates of all tiles available after the filtering. These dates are listed in the
tilesDates
property ofuserData
. - Description of the processing implemented in the evalscript and links to external resources.
evalscript = """
//VERSION=3
function setup() {
return {
input: ["B08", "B04", "dataMask"],
mosaicking: Mosaicking.TILE,
output: {
id: "default",
bands: 1
}
}
}
// User's inputs
var notAllowedDates = ["2020-12-06", "2020-12-09"]
var ndviThreshold = 0.2
var tilesPPS = []
function preProcessScenes(collections) {
tilesPPS = collections.scenes.tiles
collections.scenes.tiles = collections.scenes.tiles.filter(function(tile) {
var tileDate = tile.date.split("T")[0];
return !notAllowedDates.includes(tileDate);
})
return collections
}
function evaluatePixel(samples, scenes, inputMetadata, customData, outputMetadata) {
var valid_ndvi_sum = 0
var numberOfValidSamples = 0
for (i = 0; i < samples.length; i++) {
var sample = samples[i]
if (sample.dataMask == 1){
var ndvi = (sample.B08 - sample.B04)/(sample.B08 + sample.B04)
if (ndvi <= ndviThreshold){
valid_ndvi_sum += ndvi
numberOfValidSamples += 1
}
}
}
return [valid_ndvi_sum / numberOfValidSamples]
}
function updateOutputMetadata(scenes, inputMetadata, outputMetadata) {
outputMetadata.userData = {
"inputMetadata.serviceVersion": inputMetadata.serviceVersion
}
outputMetadata.userData.description = "The evalscript calculates average ndvi " +
"in a requested time period. Data collected on notAllowedDates is excluded. " +
"ndvi values greater than ndviThreshold are excluded. " +
"More about ndvi: https://www.indexdatabase.de/db/i-single.php?id=58."
// Extract dates for all available tiles (before filtering)
var tilePPSDates = []
for (i = 0; i < tilesPPS.length; i++){
tilePPSDates.push(tilesPPS[i].date)
}
outputMetadata.userData.tilesPPSDates = tilePPSDates
// Extract dates for tiles after filtering out tiles with "notAllowedDates"
var tileDates = []
for (i = 0; i < scenes.tiles.length; i++){
tileDates.push(scenes.tiles[i].date)
}
outputMetadata.userData.tilesDates = tileDates
outputMetadata.userData.notAllowedDates = notAllowedDates
outputMetadata.userData.ndviThreshold = ndviThreshold
}
"""
request = {
"input": {
"bounds": {
"bbox": [13.8, 45.8, 13.9, 45.9]
},
"data": [{
"type": "sentinel-2-l1c",
"dataFilter": {
"timeRange": {
"from": "2020-12-01T00:00:00Z",
"to": "2020-12-15T23:59:59Z"
}
}
}]
},
"output": {
"responses": [{
"identifier": "default",
"format": {
"type": "image/tiff"
}
},
{
"identifier": "userdata",
"format": {
"type": "application/json"
}
}
]
},
"evalscript": evalscript
}
A Jupyter Notebook with all the examples may be downloaded here.
Resources
Python Notebook - Interactive Intro to Evalscripts
Explore a collection of custom scripts for Sentinel Hub.
Custom Scripts: Faster, Cheaper, Better!
Explore a blog on good scripting practices.
SampleType: what’s all the fuss about?
Explore a blog post on sampleType.
Introduction to Custom Scripts on the Planet Insights Platform
Check out the Jupyter notebooks tutorial on GitHub for using evalscripts.