Skip to main content

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.

Calculated and Output Values

The calculated and output values depend on what users specify in their evalscripts (or custom scripts). By calculated values we are referring to the values that are returned from the evaluatePixel() function or from a simple script. Output values are values returned, after the calculated values go through formatting defined by sampleType. In the evalscript, calculated and output values are controlled by:

  • In the setup() function, the requested bands and units define what values are used as input for the calculation (in simple scripts, default units are used). For example, if Sentinel–2 band B04 is requested in REFLECTANCE, the input values will be in the range 0–1. If Sentinel–2 band B04 is requested in DN (digital numbers), the input values for the calculation will be in the range 0–10000. Typical value ranges can be found in our public data documentation, chapter Units for each data collection.
  • The evaluatePixel() function defines the actual calculation (in simple scripts, the entire script is its equivalent). Browser uses double precision for all calculations and rounds only the final calculated values before they are outputted.
  • The value of the sampleType parameter in the setup() function defines the format of the output values. Possible values are AUTO, UINT8, UINT16 and FLOAT32. See our sampleType documentation for more details. When the sampleType is not specified (for example, in simple scripts), the default value AUTO will be used. sampleType.AUTO takes calculated values from the interval 0–1 and stretches them to 0– 255. If your calculated values are not in the range 0–1, make sure you either scale them to this range in the evaluatePixel() function or specify another sampleType.

Example 1: NDVI

In this example, we want to output values of the NDVI index, calculated based on Sentinel–2 data. Our evaluatePixel() function is:

function evaluatePixel(sample) {
let NDVI = (sample.B08 – sample.B04)/( sample.B08 + sample.B04)
return [NDVI]
}

The requested units in this example do not have any influence on the calculated values (first column) of the NDVI. The output values returned by Browser (columns 2, 3, 4 and 5) for different sampleTypes are:

Calculated ValuesampleType.AUTOsampleType.UINT8sampleType.UINT16sampleType.FLOAT32
-1000-1
00000
0.2564000.25
1255111

Use sampleType:"FLOAT32" to return full floating -1 to 1 values. See the example here.

If you do not need values, but a visualization, you can use sampleType:"AUTO", but make sure to either:

  • map the NDVI values to the 0–1 interval in the evaluatePixel() function, for example:
function evaluatePixel(sample) {
let NDVI = (sample.B08 – sample.B04)/( sample.B08 + sample.B04)
return [(NDVI+1)/2]
}

Example 2: Sentinel–2 band B04

In this example, we want to output raw values of Sentinel–2 band 4. Our evaluatePixel() function looks like this:

function evaluatePixel(sample) {
return [sample.B04];
}

If we request units: REFLECTANCE, the output values (columns 2, 3, 4 and 5) returned by Browser for different sampleTypes are:

Calculated ValuesampleType.AUTOsampleType.UINT8sampleType.UINT16sampleType.FLOAT32
00000
0.2564000.25
0.5128110.5
1255111
1.05255111.05

If we request units: DN, the output values returned by Sentinel Hub for different sampleTypes are:

Calculated ValuesampleType.AUTOsampleType.UINT8sampleType.UINT16sampleType.FLOAT32
00000
250025525525002500
500025525550005000
100002552551000010000
105002552551050010500

Example 3: Brightness Temperature Bands

Here we output a Sentinel–3 SLSTR band F1 with typical values between 250–320 representing brightness temperature in Kelvin. The evaluatePixel() function is:

function evaluatePixel(sample) {
return [sample.F1];
}

The output values (columns 2, 3, 4, 5) returned by Browser for different sampleTypes are:

Calculated ValuesampleType.AUTOsampleType.UINT8sampleType.UINT16sampleType.FLOAT32
250255250250250
255255255255255
275.3255255275275.3
320255255320320

Use sampleType:"FLOAT32" to return original values. If integer values are still acceptable for your application, use sampleType:"UINT16".

If you do not need values but a visualization, you can use sampleType:"AUTO", but make sure to either:

  • map the values to the 0–1 interval in the evaluatePixel() function, for example:
function evaluatePixel(sample) {
return [sample.F1 / 320];
}

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 or FLOAT32: 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];
}

Examine in the Browser

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

note

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 public data section for each data collection (for example, 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 (for example, ORBIT or TILE)
  • data collection (for example, Sentinel-2 L2A, Sentinel-1, Sentinel-5p)
  • function in the evalscript (for example, 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 using scenes.orbits
  • Passing metadata from scenes to the userdata.json file using outputMetadata.userData in updateOutputMetadata 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 using scenes.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

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 (for example, ndviThreshold) and an array of values (for example, 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 of userData. Note how to use the global variable tilesPPS: assign it a value in preProcessScenes and output it in the updateOutputMetadata function.
  • Dates of all tiles available after the filtering. These dates are listed in the tilesDates property of userData.
  • 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
}

note

You can download a Jupyter Notebook with all the examples here.