-
Notifications
You must be signed in to change notification settings - Fork 298
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #306 from azlinszkysinergise/main
added kndvi
- Loading branch information
Showing
10 changed files
with
279 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
} | ||
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
}; | ||
} |