Skip to content

Commit 1fff703

Browse files
authored
Merge pull request #306 from azlinszkysinergise/main
added kndvi
2 parents 1350003 + 97f22b2 commit 1fff703

File tree

10 files changed

+279
-22
lines changed

10 files changed

+279
-22
lines changed

contribute/index.md

+23
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,26 @@ Replace `GH_VERSION` with the version number that is displayed next to github-pa
110110
111111
- then the site can be built with `bundle exec jekyll serve`
112112
- The site should then be visible on `127.0.0.1:4000`
113+
114+
#### Windows
115+
116+
- First of all, you will have to have Git installed on your system. In a command terminal, type `git version`. If you get a version number, you have Git installed. Otherwise, follow the instructions [here](https://gitforwindows.org/)
117+
- You will mainly want to follow [this installation guide](https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/testing-your-github-pages-site-locally-with-jekyll?platform=windows):
118+
- Go to [Ruby](https://www.ruby-lang.org/en/), download windows installer, eg from [here](https://www.ruby-lang.org/en/documentation/installation/#rubyinstaller)
119+
- Run the installer file to install ruby
120+
- Once the installer is ready, you can check Ruby by opening a terminal and typing `ruby -v` . A version number should be displayed.
121+
- Install bundler
122+
- In the terminal, type `gem install bundler`
123+
124+
- If you get an error message "Cannot create directory, filename too long", you have to enable long file paths for Git on your system. Open a command prompt, running it as an administrator.
125+
- Then type `git config --system core.longpaths true`
126+
127+
- Assuming Git is already installed on your system, now you can clone the https://github.com/sentinel-hub/custom-scripts/ repository to a folder on your computer (if you haven't already):
128+
- Navigate to the parent folder, right-click and select "Git GUI Here", and type `git clone https://github.com/sentinel-hub/custom-scripts/`
129+
- Now you are ready to set up jekyll. Navigate to the main folder of the cloned repository ("custom-scripts")
130+
- Type `bundle install` and wait for the process to finish
131+
- Now type `bundle exec jekyll serve` and wait for the local server to generate ("generating..."). You will see the message with the address of the local server, eg. "Server address : http://127.0.0.1:4000"
132+
- Copy this address to a web browser, and you will see the web version of the custom script repository, but with all of your local changes included. This will allow you to test layouts and the effects of your changes. If you make a change to a file you are displaying in Jekyll, save it and wait for the regenerating process to run. You will see the message in the Git GUI window:
133+
'Regenerating: 1 file(s) changed
134+
... done in XX seconds`
135+
If everything looks OK, you can commit, push and create a [pull request](https://github.com/sentinel-hub/custom-scripts/pulls).

sentinel-2/kndvi/README.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: kernel NDVI
3+
parent: Sentinel-2
4+
grand_parent: Sentinel
5+
layout: script
6+
permalink: /sentinel-2/kndvi/
7+
nav_exclude: true
8+
scripts:
9+
- - Visualization
10+
- script.js
11+
- - EO Browser
12+
- eob.js
13+
- - Raw Values
14+
- raw.js
15+
examples:
16+
- zoom: '12'
17+
lat: '46.6200'
18+
lng: '7.86'
19+
datasetId: S2L2A
20+
fromTime: '2023-09-16T00:00:00.000Z'
21+
toTime: '2023-09-16T23:59:59.999Z'
22+
platform:
23+
- CDSE
24+
- EOB
25+
evalscripturl: https://custom-scripts.sentinel-hub.com/custom-scripts/sentinel-2/kndvi/eob.js
26+
---
27+
28+
29+
Interlaken, Switzerland (to show green vegetation, bare rock, snow and ice, and clouds together with kNDVI)
30+
31+
---
32+
33+
## Description of the Script
34+
35+
kNDVI (Kernel NDVI) is a recently proposed vegetation index (Camps-Valls 2021) based on a nonlinear generalization of the popular Normalized Differential Vegetation Index (NDVI). kNDVI works by re-scaling the relation between the difference in Red and Near Infrared (NIR) from a simple linear difference to a more complex relationship. Here we coded the simplest definition of kNDVI with a Radial Basis Function kernel as proposed in Camps-Valls (2021).
36+
It seems based on the cited literature that kNDVI provides better correlation with field biomass or crop yield and provides higher accuracy for classification than NDVI. Note that for some machine learning applications, since this is "just" a rescaling of the spectral index, kNDVI might not perform differently than NDVI.
37+
However, due to the second-order relationship with the difference of Red and NIR, kNDVI can produce high values when the difference is negative. Such cases include non-vegetated surfaces, which may thus be incorrectly displayed. Therefore, kNDVI is highly suitable for following vegetation patterns and processes, but not at all suitable on its own for separating vegetated surfaces from water, ice or clouds.
38+
This was solved by embedding the kNDVI script into the [simple scene classification](https://www.sentinel-hub.com/faq/how-get-s2a-scene-classification-sentinel-2/) available in Sentinel-2 L2A data based on Sen2Cor outputs. The resulting script now provides shades of green, yellow or white for vegetation; and blue for water, gray for clouds, brown for cloud shadows, cyan for snow and ice and red for defective pixels. I trust that by adding this classification functionality to the script kNDVI can unfold its potential for visualizing vegetation processes.
39+
40+
The script has 4 different outputs:
41+
- Default has 4 bands (Red, Green, Blue and Transparency, for visualization in Copernicus Browser)
42+
- Index is the value of kndvi for the purpose of generating histograms in the Browser, unless the image is cloudy - then it is null.
43+
- eobrowserStats is the value of kndvi for the purpose of generating Statistics API outputs such as time series, unless the image is cloudy - then it is null.
44+
- dataMask is a simple mask for valid/invalid pixels - note that cloudy pixels will also have valid values!
45+
46+
## Description of representative images
47+
48+
kNDVI is highly sensitive to the typical range of vegetation greenness, and is less sensitive to saturation at high biomass levels. Therefore it is useful for visualizing fine-scale patterns in crops or vegetation.
49+
50+
Grasslands and crop fields near Püspökladány, Hungary, **Sentinel-2A**, 2019-05-19, [**kNDVI**](https://tinyurl.com/pladanykndvi). The image highlights the fine patterns in vegetation greenness and biomass governed by microtopography. The meandering lines across the grassland are old river channels that are somewhat lower and therefore wetter than their surroundings.
51+
52+
!['Sentinel-2 05 May 2023, Püspökladány, Hungary'](./img/Sentinel-2_L2A_pkladany_kndvi.jpg)
53+
54+
For comparison, here is a visualization of the same image with the default NDVI script available in Copernicus Browser. **Sentinel-2A**, 2019-05-19, [**NDVI**](https://link.dataspace.copernicus.eu/yv4r). This visualization is saturated for large parts of the image, not showing the patterns of the grassland.
55+
56+
!['Sentinel-2 05 May 2023, Püspökladány, Hungary'](./img/Sentinel-2_L2A_NDVI.jpg)
57+
58+
Finally, for orientation, here is a true colour image. **Sentinel-2A**, 2019-05-19, [**True Color**](https://link.dataspace.copernicus.eu/m2u2). Here you can see the various land cover categories, the haze and clouds affecting the area and the wide variety of grassland biomass.
59+
60+
!['Sentinel-2 05 May 2023, Püspökladány, Hungary'](.\img\Sentinel-2_L2A_True_color.jpg)
61+
62+
## References
63+
64+
- Camps-Valls, Gustau, et al. "A unified vegetation index for quantifying the terrestrial biosphere." Science Advances 7.9 (2021): eabc7447. [link](https://www.science.org/doi/10.1126/sciadv.abc7447)

sentinel-2/kndvi/eob.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//VERSION = 3
2+
//by András Zlinszky @azlinszky - based on https://www.sentinel-hub.com/faq/how-get-s2a-scene-classification-sentinel-2/ and https://www.science.org/doi/10.1126/sciadv.abc7447
3+
4+
function setup() {
5+
return {
6+
input: ["B04", "B08", "SCL", "dataMask"],
7+
output: [
8+
{ id: "default", bands: 4 },
9+
{ id: "index", bands: 1, sampleType: "FLOAT32" },
10+
{ id: "eobrowserStats", bands: 1, sampleType: "FLOAT32" },
11+
{ id: "dataMask", bands: 1 }
12+
]
13+
}
14+
}
15+
16+
const kndvi_ramp = [
17+
[-1.1, [0, 0, 0]],
18+
[-0.1, [0.86, 0.86, 0.86]],
19+
[0, [1, 1, 0.88]],
20+
[-0.2, [0.75, 0.75, 0.75]],
21+
[0.025, [1, 0.98, 0.8]],
22+
[0.05, [0.93, 0.91, 0.71]],
23+
[0.075, [0.87, 0.85, 0.61]],
24+
[0.1, [0.8, 0.78, 0.51]],
25+
[0.125, [0.74, 0.72, 0.42]],
26+
[0.15, [0.69, 0.76, 0.38]],
27+
[0.175, [0.64, 0.8, 0.35]],
28+
[0.2, [0.57, 0.75, 0.32]],
29+
[0.25, [0.5, 0.7, 0.28]],
30+
[0.3, [0.44, 0.64, 0.25]],
31+
[0.35, [0.38, 0.59, 0.21]],
32+
[0.4, [0.31, 0.54, 0.18]],
33+
[0.45, [0.25, 0.49, 0.14]],
34+
[0.5, [0.19, 0.43, 0.11]],
35+
[0.55, [0.13, 0.38, 0.07]],
36+
[0.6, [0.06, 0.33, 0.04]]
37+
]
38+
visualizer = new ColorRampVisualizer(kndvi_ramp);
39+
const cloud_palette = {
40+
0: [0, 0, 0], // No Data (Missing data) - black
41+
1: [1, 0, 0.016], // Saturated or defective pixel - red
42+
2: [0.525, 0.525, 0.525], // Topographic casted shadows ("Dark features/Shadows" for data before 2022-01-25) - very dark grey
43+
3: [0.467, 0.298, 0.043], // Cloud shadows - dark brown
44+
6: [0, 0, 1], // Water (dark and bright) - blue
45+
7: [0.506, 0.506, 0.506], // Unclassified - dark grey
46+
8: [0.753, 0.753, 0.753], // Cloud medium probability - grey
47+
9: [0.949, 0.949, 0.949], // Cloud high probability - white
48+
10: [0.733, 0.773, 0.925], // Thin cirrus - very bright blue
49+
11: [0.325, 1, 0.980], // Snow or ice - very bright pink
50+
}
51+
52+
function evaluatePixel(sample) {
53+
let kndvi = Math.tanh(Math.pow(((sample.B08 - sample.B04) / (sample.B08 + sample.B04)), 2));
54+
let imgVals = kndvi <= 0.6 ? visualizer.process(kndvi) : [0, 0.27, 0];
55+
let is_clouds = Object.keys(cloud_palette).includes(sample.SCL.toString())
56+
imgVals = is_clouds ? cloud_palette[sample.SCL] : imgVals;
57+
return {
58+
default: imgVals.concat(sample.dataMask),
59+
index: [is_clouds ? null : kndvi],
60+
eobrowserStats: [is_clouds ? null : kndvi],
61+
dataMask: [sample.dataMask],
62+
}
63+
}
645 KB
Loading
Loading
Loading

sentinel-2/kndvi/raw.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//VERSION = 3
2+
//by András Zlinszky @azlinszky - based on https://www.sentinel-hub.com/faq/how-get-s2a-scene-classification-sentinel-2/ and https://www.science.org/doi/10.1126/sciadv.abc7447
3+
4+
function setup() {
5+
return {
6+
input: ["B04", "B08", "SCL", "dataMask"],
7+
output:
8+
{
9+
bands: 1,
10+
sampleType: "FLOAT32"
11+
},
12+
}
13+
}
14+
15+
const cloud_palette = {
16+
0: [0, 0, 0], // No Data (Missing data) - black
17+
1: [1, 0, 0.016], // Saturated or defective pixel - red
18+
2: [0.525, 0.525, 0.525], // Topographic casted shadows ("Dark features/Shadows" for data before 2022-01-25) - very dark grey
19+
3: [0.467, 0.298, 0.043], // Cloud shadows - dark brown
20+
6: [0, 0, 1], // Water (dark and bright) - blue
21+
7: [0.506, 0.506, 0.506], // Unclassified - dark grey
22+
8: [0.753, 0.753, 0.753], // Cloud medium probability - grey
23+
9: [0.949, 0.949, 0.949], // Cloud high probability - white
24+
10: [0.733, 0.773, 0.925], // Thin cirrus - very bright blue
25+
11: [0.325, 1, 0.980], // Snow or ice - very bright pink
26+
}
27+
28+
function evaluatePixel(sample) {
29+
let kndvi = Math.tanh(Math.pow(((sample.B08 - sample.B04) / (sample.B08 + sample.B04)), 2));
30+
let is_clouds = Object.keys(cloud_palette).includes(sample.SCL.toString())
31+
return [(is_clouds ? null : kndvi)]
32+
}

sentinel-2/kndvi/script.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//VERSION = 3
2+
//by András Zlinszky @azlinszky - based on https://www.sentinel-hub.com/faq/how-get-s2a-scene-classification-sentinel-2/ and https://www.science.org/doi/10.1126/sciadv.abc7447
3+
4+
function setup() {
5+
return {
6+
input: ["B04", "B08", "SCL", "dataMask"],
7+
output: [
8+
{ bands: 4 }
9+
]
10+
}
11+
}
12+
13+
const kndvi_ramp = [
14+
[-1.1, [0, 0, 0]],
15+
[-0.1, [0.86, 0.86, 0.86]],
16+
[0, [1, 1, 0.88]],
17+
[-0.2, [0.75, 0.75, 0.75]],
18+
[0.025, [1, 0.98, 0.8]],
19+
[0.05, [0.93, 0.91, 0.71]],
20+
[0.075, [0.87, 0.85, 0.61]],
21+
[0.1, [0.8, 0.78, 0.51]],
22+
[0.125, [0.74, 0.72, 0.42]],
23+
[0.15, [0.69, 0.76, 0.38]],
24+
[0.175, [0.64, 0.8, 0.35]],
25+
[0.2, [0.57, 0.75, 0.32]],
26+
[0.25, [0.5, 0.7, 0.28]],
27+
[0.3, [0.44, 0.64, 0.25]],
28+
[0.35, [0.38, 0.59, 0.21]],
29+
[0.4, [0.31, 0.54, 0.18]],
30+
[0.45, [0.25, 0.49, 0.14]],
31+
[0.5, [0.19, 0.43, 0.11]],
32+
[0.55, [0.13, 0.38, 0.07]],
33+
[0.6, [0.06, 0.33, 0.04]]
34+
]
35+
visualizer = new ColorRampVisualizer(kndvi_ramp);
36+
const cloud_palette = {
37+
0: [0, 0, 0], // No Data (Missing data) - black
38+
1: [1, 0, 0.016], // Saturated or defective pixel - red
39+
2: [0.525, 0.525, 0.525], // Topographic casted shadows ("Dark features/Shadows" for data before 2022-01-25) - very dark grey
40+
3: [0.467, 0.298, 0.043], // Cloud shadows - dark brown
41+
6: [0, 0, 1], // Water (dark and bright) - blue
42+
7: [0.506, 0.506, 0.506], // Unclassified - dark grey
43+
8: [0.753, 0.753, 0.753], // Cloud medium probability - grey
44+
9: [0.949, 0.949, 0.949], // Cloud high probability - white
45+
10: [0.733, 0.773, 0.925], // Thin cirrus - very bright blue
46+
11: [0.325, 1, 0.980], // Snow or ice - very bright pink
47+
}
48+
49+
function evaluatePixel(sample) {
50+
let kndvi = Math.tanh(Math.pow(((sample.B08 - sample.B04) / (sample.B08 + sample.B04)), 2));
51+
let imgVals = kndvi <= 0.6 ? visualizer.process(kndvi) : [0, 0.27, 0];
52+
let is_clouds = Object.keys(cloud_palette).includes(sample.SCL.toString())
53+
imgVals = is_clouds ? cloud_palette[sample.SCL] : imgVals;
54+
return imgVals.concat(sample.dataMask)
55+
}

sentinel-2/sentinel-2.md

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Dedicated to supplying data for [Copernicus services](https://www.esa.int/Our_Ac
4747
- [EVI](/sentinel-2/evi) - enhanced vegetation index
4848
- [EVI2](/sentinel-2/evi2) - enhanced vegetation index 2
4949
- [GNDVI](/sentinel-2/gndvi) - green normalized difference vegetation index
50+
- [kNDVI](/sentinel-2/kndvi) - new alternative to NDVI with more complex transfer function
5051
- [MCARI](/sentinel-2/mcari) - modified chlorophyll absorption in reflectance index
5152
- [MSI](/sentinel-2/msi) - moisture index
5253
- [NDMI](/sentinel-2/ndmi) - normalized difference moisture index
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,50 @@
11
//VERSION=3
2-
3-
function setup() {
4-
return {
5-
input: ["B03", "B08", "dataMask"],
6-
output: { bands: 4 }
7-
};
8-
}
9-
10-
2+
//ndwi with kndvi
113
const colorRamp1 = [
12-
[0, 0xFFFFFF],
13-
[1, 0x008000]
4+
[0, 0xFFFFFF], //Black
5+
[0.7, 0x008000] //Green (lower if you want a greener map)
146
];
157
const colorRamp2 = [
16-
[0, 0xFFFFFF],
17-
[1, 0x0000CC]
8+
[0, 0xFFFFFF], //Black
9+
[1, 0x0000CC] //Medium Blue
1810
];
1911

20-
const viz1 = new ColorRampVisualizer(colorRamp1);
21-
const viz2 = new ColorRampVisualizer(colorRamp2);
12+
let viz1 = new ColorRampVisualizer(colorRamp1);
13+
let viz2 = new ColorRampVisualizer(colorRamp2);
14+
15+
function setup() {
16+
return {
17+
input: ["B03", "B04", "B08","dataMask"],
18+
output: [
19+
{ id:"default", bands: 4 },
20+
{ id: "index", bands: 1, sampleType: "FLOAT32" },
21+
{ id: "eobrowserStats", bands: 1, sampleType: 'FLOAT32' },
22+
{ id: "dataMask", bands: 1 }
23+
]
24+
};
25+
}
2226

2327
function evaluatePixel(samples) {
24-
let val = index(samples.B03, samples.B08);
25-
if (val < 0) {
26-
imgVals = viz1.process(-val)
27-
} else {
28-
imgVals = viz2.process(Math.sqrt(Math.sqrt(val)))
29-
}
30-
return imgVals.concat(samples.dataMask);
28+
let factor = 1/2000;
29+
let Green = factor * samples.B03;
30+
let Red = factor * samples.B04;
31+
let NIR = factor * samples.B08;
32+
let val = index(Green, NIR);
33+
let kndvi = Math.tanh(Math.pow(((NIR - Red) / (NIR + Red)), 2)); //https://doi.org/10.1126/sciadv.abc7447
34+
let imgVals = null;
35+
// The library for tiffs works well only if there is only one channel returned.
36+
// So we encode the "no data" as NaN here and ignore NaNs on frontend.
37+
const indexVal = samples.dataMask === 1 ? val : NaN;
38+
39+
if (val < -0) {
40+
imgVals = [...viz1.process(kndvi), samples.dataMask];
41+
} else {
42+
imgVals = [...viz2.process(Math.sqrt(Math.sqrt(val))), samples.dataMask];
3143
}
44+
return {
45+
default: imgVals,
46+
index: [indexVal],
47+
eobrowserStats:[val],
48+
dataMask: [samples.dataMask]
49+
};
50+
}

0 commit comments

Comments
 (0)