Skip to content

Commit 5916335

Browse files
authored
Merge pull request #355 from azlinszkysinergise/planet_scripts
new kndvi script for planetscope
2 parents 8e9a40e + 2a54bfe commit 5916335

File tree

17 files changed

+804
-26
lines changed

17 files changed

+804
-26
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
title: Agricultural growth stage
3+
parent: PlanetScope
4+
grand_parent: Planet
5+
layout: script
6+
permalink: /planet/planetscope/agriculture_growth_stage
7+
nav_exclude: true
8+
scripts:
9+
- [PlanetScope data, script.js]
10+
- [Analysis-Ready PlanetScope data, analysis_ready_planetscope.js]
11+
12+
examples:
13+
- zoom: '14'
14+
lat: '47.71371'
15+
lng: '23.09911'
16+
datasetId: ccb1f8f0-e5bf-4c31-afe5-d8803bcbde2a
17+
fromTime: '2023-05-01T00:00:00.000Z'
18+
toTime: '2023-08-30T23:59:59.999Z'
19+
platform:
20+
- EOB
21+
evalscripturl: https://custom-scripts.sentinel-hub.com/sentinel-2/agriculture_growth_stage/script.js
22+
additionalQueryParams:
23+
- - mosaickingOrder
24+
- mostRecent
25+
---
26+
27+
## Author of the script
28+
[@HarelDan](https://github.com/hareldunn/GIS_Repo/blob/master/Multi-Temporal%20NDVI%20for%20Sentinel%20Hub%20Custom%20Scripts){:target="_blank"}
29+
30+
Adapted to [PlanetScope](https://docs.planet.com/data/imagery/planetscope/) and [Analysis-Ready PlanetScope](https://docs.planet.com/data/imagery/arps/) by András Zlinszky and Github Copilot.
31+
32+
## General description of the script
33+
Agricultural growth stage is a script visualizing the multi-temporal NDVI trends in Sentinel-2 imagery. It takes the current image as baseline and calculates the average NDVI for the previous 2 months.
34+
The script requires multi-temporal processing, so the parameter TEMPORAL=true should be added to the request.
35+
A simple stretching is applied to NDVI between 0.1 and 0.7 by default, then the mean NDVI from the first, second and third month is assigned to the Red, Green and Blue channels of the image respectively, creating a composite image. What you see on the composite is
36+
- how dense and/or vigorous the vegetation is, represented by the brightness of the color from black (no vegetation at all) to white (dense green vegetation all year), with various shades of color in between
37+
- when the vegetation peak happens and how distinct it is. Vegetation with a single very distict peak will be one of the primary colors (Red, Green, Blue) while vegetation with a longer, more even growth season will be yellow (between Red and Green) or cyan (between Green and Blue). Purple color may indicate two vegetation peaks, one in the first month and another in the last, with a dry period or grassland mowing in between.
38+
39+
## How to use
40+
41+
- In Planet Insights Platform Browser, open the calendar panel dropdown (with the dropdown button on the right)
42+
- Select the time interval view (the calendar icon with arrows on the top right). You will now see two dates, labeled "from" and "until".
43+
- Select these dates to cover an interval of three months for the regular script.
44+
- Select your evalscript of choice from this script website and copy it from the code window above
45+
- In Planet Insights Browser, click the `Custom Script` in the list of layers and the `Custom Script` tab to open the evalscript code window
46+
- Select the full text inside the window (eg. with the Ctrl+A hotkey) and paste the evalscript code from the clipboard
47+
- Wait until the data loads - this may take some time for large areas.
48+
49+
## Description of representative images
50+
51+
The Agricultural growth stage script applied to the agricultural fields of the Nile Delta, just north of Cairo, Egypt.
52+
53+
![The Agricultural growth stage script applied to agricultural fields of the Nile Delta just north of Cairo.](fig/nile_delta_arps.jpg)
54+
55+
## References
56+
Based on:
57+
[source 1](https://twitter.com/sentinel_hub/status/922813457145221121){:target="_blank"},
58+
[source 2](https://twitter.com/sentinel_hub/status/1020755996359225344){:target="_blank"}
59+
60+
61+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//VERSION=3 (auto-converted from 1)
2+
/*
3+
Source: @HarelDan - https://github.com/hareldunn/GIS_Repo/blob/master/Multi-Temporal%20NDVI%20for%20Sentinel%20Hub%20Custom%20Scripts
4+
Visualizing NDVI multi-temporal trends in Sentinel-2 imagery.
5+
will take the current image as baseline and calculate average NDVI for the previous 2 months
6+
Based on:
7+
https://twitter.com/sentinel_hub/status/922813457145221121
8+
https://twitter.com/sentinel_hub/status/1020755996359225344
9+
Script requires multi-temporal processing so parameter TEMPORAL=true should be added to the request Set the time interval to at least three months.
10+
Adapted to PlanetScope and Analysis-ready PlanetScope by @azlinszky.bsky.social
11+
This version is for Analysis-ready PlanetScope data, where cloudy pixels are already filtered out.
12+
*/
13+
14+
function setup() {
15+
return {
16+
input: [
17+
{
18+
bands: ["red", "nir"],
19+
},
20+
],
21+
output: { bands: 3 },
22+
mosaicking: "ORBIT",
23+
};
24+
}
25+
26+
function calcNDVI(sample) {
27+
var denom = sample.red + sample.nir;
28+
return denom != 0 ? (sample.nir - sample.red) / denom : 0.0;
29+
}
30+
function stretch(val, min, max) {
31+
return (val - min) / (max - min);
32+
}
33+
34+
function evaluatePixel(samples, scenes) {
35+
if (!scenes || scenes.length === 0) {
36+
return [0, 0.3, 0.3]; // just a color to indicate something is wrong
37+
}
38+
var avg1 = 0;
39+
var count1 = 0;
40+
var avg2 = 0;
41+
var count2 = 0;
42+
var avg3 = 0;
43+
var count3 = 0;
44+
var endMonth = scenes[0].date.getMonth();
45+
46+
for (var i = 0; i < samples.length; i++) {
47+
var ndvi = calcNDVI(samples[i]);
48+
if (scenes[i].date.getMonth() == endMonth) {
49+
avg3 = avg3 + ndvi;
50+
count3++;
51+
} else if (scenes[i].date.getMonth() == endMonth - 1) {
52+
avg2 = avg2 + ndvi;
53+
count2++;
54+
} else {
55+
avg1 = avg1 + ndvi;
56+
count1++;
57+
}
58+
}
59+
avg1 = avg1 / count1;
60+
avg2 = avg2 / count2;
61+
avg3 = avg3 / count3;
62+
avg1 = stretch(avg1, 0.1, 0.7);
63+
avg2 = stretch(avg2, 0.1, 0.7);
64+
avg3 = stretch(avg3, 0.1, 0.7);
65+
66+
return [avg1, avg2, avg3];
67+
}
68+
69+
function preProcessScenes(collections) {
70+
collections.scenes.orbits = collections.scenes.orbits.filter(function (
71+
orbit
72+
) {
73+
var orbitDateFrom = new Date(orbit.dateFrom);
74+
return (
75+
orbitDateFrom.getTime() >=
76+
collections.to.getTime() - 3 * 31 * 24 * 3600 * 1000
77+
);
78+
});
79+
return collections;
80+
}
Loading
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//VERSION=3 (auto-converted from 1)
2+
/*
3+
Source: @HarelDan - https://github.com/hareldunn/GIS_Repo/blob/master/Multi-Temporal%20NDVI%20for%20Sentinel%20Hub%20Custom%20Scripts
4+
Visualizing NDVI multi-temporal trends in Sentinel-2 imagery.
5+
will take the current image as baseline and calculate average NDVI for the previous 2 months
6+
Based on:
7+
https://twitter.com/sentinel_hub/status/922813457145221121
8+
https://twitter.com/sentinel_hub/status/1020755996359225344
9+
Script requires multi-temporal processing so parameter TEMPORAL=true should be added to the request
10+
Adapted to PlanetScope and Analysis-ready PlanetScope by @azlinszky.bsky.social
11+
This version is for PlanetScope data, checking cloudiness with the "clear" band.
12+
*/
13+
14+
function setup() {
15+
return {
16+
input: [
17+
{
18+
bands: ["red", "nir", "clear"],
19+
},
20+
],
21+
output: { bands: 3 },
22+
mosaicking: "ORBIT",
23+
};
24+
}
25+
26+
function calcNDVI(sample) {
27+
var denom = sample.red + sample.nir;
28+
return denom != 0 ? (sample.nir - sample.red) / denom : 0.0;
29+
}
30+
function stretch(val, min, max) {
31+
return (val - min) / (max - min);
32+
}
33+
34+
function evaluatePixel(samples, scenes) {
35+
if (!scenes || scenes.length === 0) {
36+
return [0, 0.3, 0.3]; // just a color to indicate something is wrong
37+
}
38+
var avg1 = 0;
39+
var count1 = 0;
40+
var avg2 = 0;
41+
var count2 = 0;
42+
var avg3 = 0;
43+
var count3 = 0;
44+
var endMonth = scenes[0].date.getMonth();
45+
46+
for (var i = 0; i < samples.length; i++) {
47+
// Only use clear pixels
48+
if (!samples[i].clear) continue;
49+
var ndvi = calcNDVI(samples[i]);
50+
if (scenes[i].date.getMonth() == endMonth) {
51+
avg3 = avg3 + ndvi;
52+
count3++;
53+
} else if (scenes[i].date.getMonth() == endMonth - 1) {
54+
avg2 = avg2 + ndvi;
55+
count2++;
56+
} else {
57+
avg1 = avg1 + ndvi;
58+
count1++;
59+
}
60+
}
61+
avg1 = count1 > 0 ? avg1 / count1 : 0;
62+
avg2 = count2 > 0 ? avg2 / count2 : 0;
63+
avg3 = count3 > 0 ? avg3 / count3 : 0;
64+
avg1 = stretch(avg1, 0.1, 0.7);
65+
avg2 = stretch(avg2, 0.1, 0.7);
66+
avg3 = stretch(avg3, 0.1, 0.7);
67+
68+
return [avg1, avg2, avg3];
69+
}
70+
71+
function preProcessScenes(collections) {
72+
collections.scenes.orbits = collections.scenes.orbits.filter(function (
73+
orbit
74+
) {
75+
var orbitDateFrom = new Date(orbit.dateFrom);
76+
return (
77+
orbitDateFrom.getTime() >=
78+
collections.to.getTime() - 3 * 31 * 24 * 3600 * 1000
79+
);
80+
});
81+
return collections;
82+
}
Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
//VERSION=3
22
//Cloudless Mosaic with PlanetScope
3+
//adapted by András Zlinszky and Google Gemini to handle first quartile or median mosaicking
34

45
function setup() {
56
return {
67
input: ["red", "green", "blue", "dataMask", "clear"],
78
output: { bands: 4 },
8-
mosaicking: "ORBIT",
9+
mosaicking: "ORBIT", // 'ORBIT' or 'TILE' or 'NONE' - determines initial mosaicking behavior
10+
// Define a custom parameter for the mosaicking method
11+
processing: {
12+
// 'mosaickingMethod' is the name of your custom parameter.
13+
// Users can set this to 'median' or 'q1'.
14+
mosaickingMethod: {
15+
defaultValue: "q1", // Default to first quartile
16+
validValues: ["median", "q1"],
17+
},
18+
},
919
};
1020
}
1121

@@ -26,52 +36,106 @@ function preProcessScenes(collections) {
2636
function getLastObservation(arr) {
2737
for (let i = arr.length - 1; i >= 0; i--) {
2838
if (arr[i] !== 0) {
29-
// optional check if you are sure all invalid observations are filtered out
3039
return arr[i];
3140
}
3241
}
3342
return 0;
3443
}
3544

3645
function getMedian(sortedValues) {
37-
var index = Math.floor(sortedValues.length / 2);
38-
return sortedValues[index];
46+
const n = sortedValues.length;
47+
if (n === 0) {
48+
return undefined;
49+
}
50+
51+
const mid = Math.floor(n / 2);
52+
53+
if (n % 2 === 1) {
54+
return sortedValues[mid];
55+
} else {
56+
return (sortedValues[mid - 1] + sortedValues[mid]) / 2;
57+
}
58+
}
59+
60+
/**
61+
* Calculates the first quartile (Q1) of a sorted array of numbers.
62+
*
63+
* @param {number[]} sortedValues An array of numbers sorted in ascending order.
64+
* @returns {number} The first quartile (Q1) of the distribution, or undefined if array is empty.
65+
*/
66+
function getFirstQuartile(sortedValues) {
67+
const n = sortedValues.length;
68+
if (n === 0) {
69+
return undefined;
70+
}
71+
72+
const lowerHalfEndIndex = Math.floor(n / 2);
73+
const lowerHalf = sortedValues.slice(0, lowerHalfEndIndex);
74+
75+
return getMedian(lowerHalf);
3976
}
4077

41-
function evaluatePixel(samples, scenes) {
78+
// *** CRITICAL CHANGE HERE: ADD 'properties' AS THE THIRD ARGUMENT ***
79+
function evaluatePixel(samples, scenes, properties) {
4280
var reds = [];
4381
var greens = [];
44-
var blues = []; //empty arrays for reds greens and blues
45-
var a = 0; //incrementer
82+
var blues = [];
4683

84+
// Collect clear samples
4785
for (var i = 0; i < samples.length; i++) {
48-
//for each sample
49-
var sample = samples[i]; //get current sample
50-
var clear = sample.dataMask && sample.clear; //0 for clouds OR datamask, 1 for neither
86+
var sample = samples[i];
87+
var clear = sample.dataMask && sample.clear;
5188

5289
if (clear === 1) {
53-
//if not clouds nor datamask
54-
reds[a] = sample.red; //assign values for that sample to the channel arrays
55-
blues[a] = sample.blue;
56-
greens[a] = sample.green;
57-
a = a + 1; //increment a to represent that at this specific pixel, a value was detected
90+
reds.push(sample.red);
91+
blues.push(sample.blue);
92+
greens.push(sample.green);
5893
}
5994
}
6095

6196
var rValue;
6297
var gValue;
6398
var bValue;
99+
var transparency;
100+
101+
if (reds.length > 0) {
102+
// IMPORTANT: Sort the arrays by value before calculating statistics.
103+
reds.sort((a, b) => a - b);
104+
greens.sort((a, b) => a - b);
105+
blues.sort((a, b) => a - b);
106+
107+
// *** Access the method from the 'properties' object ***
108+
const method = properties.mosaickingMethod; // No 'processing' here, directly under properties
109+
110+
if (method === "median") {
111+
rValue = getMedian(reds);
112+
gValue = getMedian(greens);
113+
bValue = getMedian(blues);
114+
} else if (method === "q1") {
115+
rValue = getFirstQuartile(reds);
116+
gValue = getFirstQuartile(greens);
117+
bValue = getFirstQuartile(blues);
118+
} else {
119+
// Fallback in case of an unexpected method value (shouldn't happen with validValues)
120+
rValue = reds[0];
121+
gValue = greens[0];
122+
bValue = blues[0];
123+
}
64124

65-
if (a > 0) {
66-
rValue = getMedian(reds); // or call getLastObservation - which is less guaranteed to remove hazy images
67-
gValue = getMedian(greens);
68-
bValue = getMedian(blues);
69125
transparency = 1;
70126
} else {
71-
rValue = 1;
72-
gValue = 1;
73-
bValue = 1;
127+
// If no clear samples, default to black and fully transparent.
128+
rValue = 0;
129+
gValue = 0;
130+
bValue = 0;
74131
transparency = 0;
75132
}
76-
return [rValue / 3000, gValue / 3000, bValue / 3000, transparency];
77-
}
133+
134+
// Scale values for display (e.g., to 0-1 range).
135+
return [
136+
Math.min(1, Math.max(0, rValue / 3000)),
137+
Math.min(1, Math.max(0, gValue / 3000)),
138+
Math.min(1, Math.max(0, bValue / 3000)),
139+
transparency
140+
];
141+
}

planet/planetscope/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ The spectral bands of PlanetScope data are the following if you order a 8-band p
4747
- [True Color]({% link planet/planetscope/true_color/index.md %})
4848
- [False Color]({% link planet/planetscope/false_color/index.md %})
4949
- [Cloudless Mosaic]({% link planet/planetscope/cloudless_mosaic/index.md %})
50+
- [Agriculture Growth Stage]({% link planet/planetscope/agriculture_growth_stage/README.md %})
51+
- [Green City Index]({% link planet/planetscope/green_city/index.md %})
52+
- [Kernel NDVI]({% link planet/planetscope/kndvi/README.md %})
5053
- [NDVI]({% link planet/planetscope/ndvi/index.md %})
5154
- [Maximum NDVI]({% link planet/planetscope/max_ndvi/index.md %})
5255
- [NDVI Difference]({% link planet/planetscope/ndvi_difference/index.md %})
5356
- [NDWI]({% link planet/planetscope/ndwi/index.md %})
5457
- [NDCI - Normalized Difference Chlorophyll Index]({% link planet/planetscope/ndci/index.md %})
5558
- [NDRE - Normalized Difference Red Edge Index]({% link planet/planetscope/ndre/index.md %})
56-
- [Green City]({% link planet/planetscope/green_city/index.md %})
59+
- [Penguin Locator]({% link planet/planetscope/penguin_locator/index.md %})
5760
- [UDM2 Cloud/Snow Classification]({% link planet/planetscope/cloud_classification/index.md %})

0 commit comments

Comments
 (0)