Skip to content

Commit 0afec29

Browse files
saved edits to agri_growth_stage plus change in cloudless mosaic script to also handle first quartile mosaicking
1 parent 5d5820b commit 0afec29

File tree

3 files changed

+172
-25
lines changed

3 files changed

+172
-25
lines changed

planet/planetscope/agriculture_growth_stage/analysis_ready_planetscope.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ will take the current image as baseline and calculate average NDVI for the previ
66
Based on:
77
https://twitter.com/sentinel_hub/status/922813457145221121
88
https://twitter.com/sentinel_hub/status/1020755996359225344
9-
Script requires multi-temporal processing so parameter TEMPORAL=true should be added to the request
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.
1010
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.
1112
*/
1213

1314
function setup() {
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+
}

0 commit comments

Comments
 (0)