Skip to content

Commit

Permalink
Merge pull request #306 from azlinszkysinergise/main
Browse files Browse the repository at this point in the history
added kndvi
  • Loading branch information
chorng authored May 15, 2024
2 parents 1350003 + 97f22b2 commit 1fff703
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 22 deletions.
23 changes: 23 additions & 0 deletions contribute/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,26 @@ Replace `GH_VERSION` with the version number that is displayed next to github-pa
- then the site can be built with `bundle exec jekyll serve`
- The site should then be visible on `127.0.0.1:4000`
#### Windows
- 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/)
- 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):
- Go to [Ruby](https://www.ruby-lang.org/en/), download windows installer, eg from [here](https://www.ruby-lang.org/en/documentation/installation/#rubyinstaller)
- Run the installer file to install ruby
- Once the installer is ready, you can check Ruby by opening a terminal and typing `ruby -v` . A version number should be displayed.
- Install bundler
- In the terminal, type `gem install bundler`
- 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.
- Then type `git config --system core.longpaths true`
- 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):
- Navigate to the parent folder, right-click and select "Git GUI Here", and type `git clone https://github.com/sentinel-hub/custom-scripts/`
- Now you are ready to set up jekyll. Navigate to the main folder of the cloned repository ("custom-scripts")
- Type `bundle install` and wait for the process to finish
- 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"
- 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:
'Regenerating: 1 file(s) changed
... done in XX seconds`
If everything looks OK, you can commit, push and create a [pull request](https://github.com/sentinel-hub/custom-scripts/pulls).
64 changes: 64 additions & 0 deletions sentinel-2/kndvi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
title: kernel NDVI
parent: Sentinel-2
grand_parent: Sentinel
layout: script
permalink: /sentinel-2/kndvi/
nav_exclude: true
scripts:
- - Visualization
- script.js
- - EO Browser
- eob.js
- - Raw Values
- raw.js
examples:
- zoom: '12'
lat: '46.6200'
lng: '7.86'
datasetId: S2L2A
fromTime: '2023-09-16T00:00:00.000Z'
toTime: '2023-09-16T23:59:59.999Z'
platform:
- CDSE
- EOB
evalscripturl: https://custom-scripts.sentinel-hub.com/custom-scripts/sentinel-2/kndvi/eob.js
---


Interlaken, Switzerland (to show green vegetation, bare rock, snow and ice, and clouds together with kNDVI)

---

## Description of the Script

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).
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.
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.
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.

The script has 4 different outputs:
- Default has 4 bands (Red, Green, Blue and Transparency, for visualization in Copernicus Browser)
- Index is the value of kndvi for the purpose of generating histograms in the Browser, unless the image is cloudy - then it is null.
- 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.
- dataMask is a simple mask for valid/invalid pixels - note that cloudy pixels will also have valid values!

## Description of representative images

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.

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.

!['Sentinel-2 05 May 2023, Püspökladány, Hungary'](./img/Sentinel-2_L2A_pkladany_kndvi.jpg)

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.

!['Sentinel-2 05 May 2023, Püspökladány, Hungary'](./img/Sentinel-2_L2A_NDVI.jpg)

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.

!['Sentinel-2 05 May 2023, Püspökladány, Hungary'](.\img\Sentinel-2_L2A_True_color.jpg)

## References

- 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)
63 changes: 63 additions & 0 deletions sentinel-2/kndvi/eob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//VERSION = 3
//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

function setup() {
return {
input: ["B04", "B08", "SCL", "dataMask"],
output: [
{ id: "default", bands: 4 },
{ id: "index", bands: 1, sampleType: "FLOAT32" },
{ id: "eobrowserStats", bands: 1, sampleType: "FLOAT32" },
{ id: "dataMask", bands: 1 }
]
}
}

const kndvi_ramp = [
[-1.1, [0, 0, 0]],
[-0.1, [0.86, 0.86, 0.86]],
[0, [1, 1, 0.88]],
[-0.2, [0.75, 0.75, 0.75]],
[0.025, [1, 0.98, 0.8]],
[0.05, [0.93, 0.91, 0.71]],
[0.075, [0.87, 0.85, 0.61]],
[0.1, [0.8, 0.78, 0.51]],
[0.125, [0.74, 0.72, 0.42]],
[0.15, [0.69, 0.76, 0.38]],
[0.175, [0.64, 0.8, 0.35]],
[0.2, [0.57, 0.75, 0.32]],
[0.25, [0.5, 0.7, 0.28]],
[0.3, [0.44, 0.64, 0.25]],
[0.35, [0.38, 0.59, 0.21]],
[0.4, [0.31, 0.54, 0.18]],
[0.45, [0.25, 0.49, 0.14]],
[0.5, [0.19, 0.43, 0.11]],
[0.55, [0.13, 0.38, 0.07]],
[0.6, [0.06, 0.33, 0.04]]
]
visualizer = new ColorRampVisualizer(kndvi_ramp);
const cloud_palette = {
0: [0, 0, 0], // No Data (Missing data) - black
1: [1, 0, 0.016], // Saturated or defective pixel - red
2: [0.525, 0.525, 0.525], // Topographic casted shadows ("Dark features/Shadows" for data before 2022-01-25) - very dark grey
3: [0.467, 0.298, 0.043], // Cloud shadows - dark brown
6: [0, 0, 1], // Water (dark and bright) - blue
7: [0.506, 0.506, 0.506], // Unclassified - dark grey
8: [0.753, 0.753, 0.753], // Cloud medium probability - grey
9: [0.949, 0.949, 0.949], // Cloud high probability - white
10: [0.733, 0.773, 0.925], // Thin cirrus - very bright blue
11: [0.325, 1, 0.980], // Snow or ice - very bright pink
}

function evaluatePixel(sample) {
let kndvi = Math.tanh(Math.pow(((sample.B08 - sample.B04) / (sample.B08 + sample.B04)), 2));
let imgVals = kndvi <= 0.6 ? visualizer.process(kndvi) : [0, 0.27, 0];
let is_clouds = Object.keys(cloud_palette).includes(sample.SCL.toString())
imgVals = is_clouds ? cloud_palette[sample.SCL] : imgVals;
return {
default: imgVals.concat(sample.dataMask),
index: [is_clouds ? null : kndvi],
eobrowserStats: [is_clouds ? null : kndvi],
dataMask: [sample.dataMask],
}
}
Binary file added sentinel-2/kndvi/img/Sentinel-2_L2A_NDVI.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions sentinel-2/kndvi/raw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//VERSION = 3
//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

function setup() {
return {
input: ["B04", "B08", "SCL", "dataMask"],
output:
{
bands: 1,
sampleType: "FLOAT32"
},
}
}

const cloud_palette = {
0: [0, 0, 0], // No Data (Missing data) - black
1: [1, 0, 0.016], // Saturated or defective pixel - red
2: [0.525, 0.525, 0.525], // Topographic casted shadows ("Dark features/Shadows" for data before 2022-01-25) - very dark grey
3: [0.467, 0.298, 0.043], // Cloud shadows - dark brown
6: [0, 0, 1], // Water (dark and bright) - blue
7: [0.506, 0.506, 0.506], // Unclassified - dark grey
8: [0.753, 0.753, 0.753], // Cloud medium probability - grey
9: [0.949, 0.949, 0.949], // Cloud high probability - white
10: [0.733, 0.773, 0.925], // Thin cirrus - very bright blue
11: [0.325, 1, 0.980], // Snow or ice - very bright pink
}

function evaluatePixel(sample) {
let kndvi = Math.tanh(Math.pow(((sample.B08 - sample.B04) / (sample.B08 + sample.B04)), 2));
let is_clouds = Object.keys(cloud_palette).includes(sample.SCL.toString())
return [(is_clouds ? null : kndvi)]
}
55 changes: 55 additions & 0 deletions sentinel-2/kndvi/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//VERSION = 3
//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

function setup() {
return {
input: ["B04", "B08", "SCL", "dataMask"],
output: [
{ bands: 4 }
]
}
}

const kndvi_ramp = [
[-1.1, [0, 0, 0]],
[-0.1, [0.86, 0.86, 0.86]],
[0, [1, 1, 0.88]],
[-0.2, [0.75, 0.75, 0.75]],
[0.025, [1, 0.98, 0.8]],
[0.05, [0.93, 0.91, 0.71]],
[0.075, [0.87, 0.85, 0.61]],
[0.1, [0.8, 0.78, 0.51]],
[0.125, [0.74, 0.72, 0.42]],
[0.15, [0.69, 0.76, 0.38]],
[0.175, [0.64, 0.8, 0.35]],
[0.2, [0.57, 0.75, 0.32]],
[0.25, [0.5, 0.7, 0.28]],
[0.3, [0.44, 0.64, 0.25]],
[0.35, [0.38, 0.59, 0.21]],
[0.4, [0.31, 0.54, 0.18]],
[0.45, [0.25, 0.49, 0.14]],
[0.5, [0.19, 0.43, 0.11]],
[0.55, [0.13, 0.38, 0.07]],
[0.6, [0.06, 0.33, 0.04]]
]
visualizer = new ColorRampVisualizer(kndvi_ramp);
const cloud_palette = {
0: [0, 0, 0], // No Data (Missing data) - black
1: [1, 0, 0.016], // Saturated or defective pixel - red
2: [0.525, 0.525, 0.525], // Topographic casted shadows ("Dark features/Shadows" for data before 2022-01-25) - very dark grey
3: [0.467, 0.298, 0.043], // Cloud shadows - dark brown
6: [0, 0, 1], // Water (dark and bright) - blue
7: [0.506, 0.506, 0.506], // Unclassified - dark grey
8: [0.753, 0.753, 0.753], // Cloud medium probability - grey
9: [0.949, 0.949, 0.949], // Cloud high probability - white
10: [0.733, 0.773, 0.925], // Thin cirrus - very bright blue
11: [0.325, 1, 0.980], // Snow or ice - very bright pink
}

function evaluatePixel(sample) {
let kndvi = Math.tanh(Math.pow(((sample.B08 - sample.B04) / (sample.B08 + sample.B04)), 2));
let imgVals = kndvi <= 0.6 ? visualizer.process(kndvi) : [0, 0.27, 0];
let is_clouds = Object.keys(cloud_palette).includes(sample.SCL.toString())
imgVals = is_clouds ? cloud_palette[sample.SCL] : imgVals;
return imgVals.concat(sample.dataMask)
}
1 change: 1 addition & 0 deletions sentinel-2/sentinel-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Dedicated to supplying data for [Copernicus services](https://www.esa.int/Our_Ac
- [EVI](/sentinel-2/evi) - enhanced vegetation index
- [EVI2](/sentinel-2/evi2) - enhanced vegetation index 2
- [GNDVI](/sentinel-2/gndvi) - green normalized difference vegetation index
- [kNDVI](/sentinel-2/kndvi) - new alternative to NDVI with more complex transfer function
- [MCARI](/sentinel-2/mcari) - modified chlorophyll absorption in reflectance index
- [MSI](/sentinel-2/msi) - moisture index
- [NDMI](/sentinel-2/ndmi) - normalized difference moisture index
Expand Down
63 changes: 41 additions & 22 deletions sentinel2-quarterly-cloudless-mosaic/ndwi/script.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
//VERSION=3

function setup() {
return {
input: ["B03", "B08", "dataMask"],
output: { bands: 4 }
};
}


//ndwi with kndvi
const colorRamp1 = [
[0, 0xFFFFFF],
[1, 0x008000]
[0, 0xFFFFFF], //Black
[0.7, 0x008000] //Green (lower if you want a greener map)
];
const colorRamp2 = [
[0, 0xFFFFFF],
[1, 0x0000CC]
[0, 0xFFFFFF], //Black
[1, 0x0000CC] //Medium Blue
];

const viz1 = new ColorRampVisualizer(colorRamp1);
const viz2 = new ColorRampVisualizer(colorRamp2);
let viz1 = new ColorRampVisualizer(colorRamp1);
let viz2 = new ColorRampVisualizer(colorRamp2);

function setup() {
return {
input: ["B03", "B04", "B08","dataMask"],
output: [
{ id:"default", bands: 4 },
{ id: "index", bands: 1, sampleType: "FLOAT32" },
{ id: "eobrowserStats", bands: 1, sampleType: 'FLOAT32' },
{ id: "dataMask", bands: 1 }
]
};
}

function evaluatePixel(samples) {
let val = index(samples.B03, samples.B08);
if (val < 0) {
imgVals = viz1.process(-val)
} else {
imgVals = viz2.process(Math.sqrt(Math.sqrt(val)))
}
return imgVals.concat(samples.dataMask);
let factor = 1/2000;
let Green = factor * samples.B03;
let Red = factor * samples.B04;
let NIR = factor * samples.B08;
let val = index(Green, NIR);
let kndvi = Math.tanh(Math.pow(((NIR - Red) / (NIR + Red)), 2)); //https://doi.org/10.1126/sciadv.abc7447
let imgVals = null;
// The library for tiffs works well only if there is only one channel returned.
// So we encode the "no data" as NaN here and ignore NaNs on frontend.
const indexVal = samples.dataMask === 1 ? val : NaN;

if (val < -0) {
imgVals = [...viz1.process(kndvi), samples.dataMask];
} else {
imgVals = [...viz2.process(Math.sqrt(Math.sqrt(val))), samples.dataMask];
}
return {
default: imgVals,
index: [indexVal],
eobrowserStats:[val],
dataMask: [samples.dataMask]
};
}

0 comments on commit 1fff703

Please sign in to comment.