diff --git a/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/fig/fig1.png b/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/fig/fig1.png new file mode 100644 index 00000000..baf4e925 Binary files /dev/null and b/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/fig/fig1.png differ diff --git a/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/index.md b/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/index.md new file mode 100644 index 00000000..64855914 --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/index.md @@ -0,0 +1,48 @@ +--- +title: Forest Vitality Change Index - Gain Visualization (FVCI-Gain) +parent: Analysis Ready Planetscope +grand_parent: Planet +layout: script +nav_exclude: true +scripts: + - [Visualization, script.js] +additionalQueryParams: + - - themeId + - PLANET_SANDBOX +--- + +## Evaluate and visualize + - [EO Browser](https://apps.sentinel-hub.com/eo-browser/?zoom=14&lat=44.74116&lng=-0.68441&themeId=PLANET_SANDBOX&visualizationUrl=U2FsdGVkX18GOLfPT%2FOdeWMNJprEadk9%2FIkChCyr9gwge8vm%2BhkdI6vUYWV0tSp8HlOY%2FAeyWpXjS2z2APPpQqFJfSOhtNTTIuEJDM5zvCuN3HhjP6yDpS1%2BPAlYCZlf&evalscript=Ly9WRVJTSU9OPTMKLyogCiAqIEZvcmVzdCBWaXRhbGl0eSBDaGFuZ2UgSW5kZXggLSBHYWluIFZpc3VhbGl6YXRpb24KICogVGhpcyBzY3JpcHQgdmlzdWFsaXplcyBvbmx5IHBvc2l0aXZlIGNoYW5nZXMgKGdhaW5zKSBpbiBmb3Jlc3Qgdml0YWxpdHkKICogYmV0d2VlbiB0d28gdGltZXBvaW50cyBiYXNlZCBvbiB2ZWdldGF0aW9uIGluZGljZXMuCiAqIAogKiBQYXJhbWV0ZXJzOgogKiAtIFZJX1RZUEU6IFZlZ2V0YXRpb24gaW5kZXggdHlwZSAoIk5EVkkiIG9yICJTQVZJIikKICogLSBDQ19NSU46IE1pbmltdW0gY2Fub3B5IGNvdmVyIHBlcmNlbnRhZ2UKICogLSBWSV9NSU4sIFZJX01BWDogUmFuZ2UgZm9yIGNsaXBwaW5nIHZlZ2V0YXRpb24gaW5kZXggdmFsdWVzCiAqIC0gQ0hBTkdFX1RIUkVTSE9MRDogTWluaW11bSAlIGNoYW5nZSB0byBjbGFzc2lmeSBhcyBnYWluCiAqIC0gQ0xBU1NfSU5URVJWQUw6IEludGVydmFsIGZvciBjbGFzc2lmeWluZyBjaGFuZ2UgbWFnbml0dWRlCiAqLwoKZnVuY3Rpb24gc2V0dXAoKSB7CiAgcmV0dXJuIHsKICAgIGlucHV0OiBbCiAgICAgIHsKICAgICAgICBkYXRhc291cmNlOiAiYXJwcyIsCiAgICAgICAgYmFuZHM6IFsicmVkIiwgIm5pciIsICJkYXRhTWFzayIsICJjbG91ZF9tYXNrIl0sCiAgICAgIH0sCiAgICAgIHsKICAgICAgICBkYXRhc291cmNlOiAiY2Fub3B5X2NvdmVyIiwKICAgICAgICBiYW5kczogWyJDQyIsICJkYXRhTWFzayJdLAogICAgICB9LAogICAgXSwKICAgIG91dHB1dDogWwogICAgICB7IGJhbmRzOiA0IH0sIC8vIFJHQkEgZm9yIGdhaW4KICAgIF0sCiAgICBtb3NhaWNraW5nOiAiT1JCSVQiLAogIH07Cn0KCi8vIENvbmZpZ3VyYXRpb24gcGFyYW1ldGVycwpjb25zdCBWSV9UWVBFID0gIk5EVkkiOyAvLyBDaG9vc2UgIk5EVkkiIG9yICJTQVZJIgpjb25zdCBDQ19NSU4gPSAyNTsgICAgICAvLyBNaW5pbXVtIGNhbm9weSBjb3ZlciBwZXJjZW50YWdlIHRvIGNvbnNpZGVyIHZhbGlkIGZvcmVzdApjb25zdCBWSV9NSU4gPSAwLjE1OyAgICAvLyBNaW5pbXVtIHZlZ2V0YXRpb24gaW5kZXggdmFsdWUgZm9yIGNsaXBwaW5nCmNvbnN0IFZJX01BWCA9IDAuODU7ICAgIC8vIE1heGltdW0gdmVnZXRhdGlvbiBpbmRleCB2YWx1ZSBmb3IgY2xpcHBpbmcKY29uc3QgQ0hBTkdFX1RIUkVTSE9MRCA9IDIuNTsgLy8gTWluaW11bSAlIGNoYW5nZSB0byBjbGFzc2lmeSBhcyBnYWluCmNvbnN0IENMQVNTX0lOVEVSVkFMID0gNTsgLy8gSW50ZXJ2YWwgZm9yIGNsYXNzaWZ5aW5nIGNoYW5nZSBpbnRvIHBhbGV0dGVzCmNvbnN0IElOVkFMSURfVkkgPSAtOTk5OTsgIC8vIFZhbHVlIGZvciBpbnZhbGlkIHZlZ2V0YXRpb24gaW5kZXgKY29uc3QgVklfU0NBTEUgPSA5OTsgICAgICAvLyBTY2FsZSBmYWN0b3IgZm9yIFZJIHZhbHVlcyAocmVzdWx0aW5nIGluIHJhbmdlIDEtMTAwKQpjb25zdCBTQVZJX0wgPSAwLjU7ICAgICAgIC8vIFNvaWwgYWRqdXN0bWVudCBmYWN0b3IgZm9yIFNBVkkKCi8vIENvbG9yIHBhbGV0dGUgLSBmb3Igdml0YWxpdHkgZ2FpbnMgKGZyb20gc21hbGwgdG8gc2lnbmlmaWNhbnQpCmNvbnN0IGdhaW5QYWxldHRlID0gWwogIFsxLjAsIDEuMCwgMC42XSwgIC8vIDAgLSBzbWFsbGVzdCBnYWluCiAgWzEuMCwgMS4wLCAwLjVdLAogIFsxLjAsIDEuMCwgMC40XSwKICBbMC45LCAxLjAsIDAuM10sCiAgWzAuOCwgMS4wLCAwLjJdLAogIFswLjcsIDEuMCwgMC4xXSwKICBbMC42LCAxLjAsIDAuMF0sCiAgWzAuNCwgMS4wLCAwLjBdLAogIFswLjIsIDEuMCwgMC4wXSwKICBbMC4wLCAxLjAsIDAuMF0sCiAgWzAuMCwgMC45LCAwLjBdLAogIFswLjAsIDAuOCwgMC4wXSwKICBbMC4wLCAwLjYsIDAuMF0sCiAgWzAuMCwgMC40LCAwLjBdLAogIFswLjAsIDAuMiwgMC4wXSwKICBbMC42LCAwLjAsIDAuNl0sCiAgWzAuNywgMC4wLCAwLjddLAogIFswLjgsIDAuMCwgMC44XSwKICBbMC45LCAwLjAsIDAuOV0sCiAgWzEuMCwgMC4wLCAxLjBdLCAgLy8gMTkgLSBzaWduaWZpY2FudCBnYWluCl07CgovLyBDYWxjdWxhdGUgdmVnZXRhdGlvbiBpbmRleCAoVkkpIGJhc2VkIG9uIHR5cGUKZnVuY3Rpb24gY2FsY3VsYXRlVkkobmlyLCByZWQpIHsKICBpZiAobmlyID09PSB1bmRlZmluZWQgfHwgcmVkID09PSB1bmRlZmluZWQpIHJldHVybiBJTlZBTElEX1ZJOwogIGlmIChuaXIgKyByZWQgPT09IDApIHJldHVybiBJTlZBTElEX1ZJOyAvLyBBdm9pZCBkaXZpc2lvbiBieSB6ZXJvCiAgCiAgaWYgKFZJX1RZUEUgPT09ICJORFZJIikgewogICAgcmV0dXJuIChuaXIgLSByZWQpIC8gKG5pciArIHJlZCk7CiAgfSBlbHNlIGlmIChWSV9UWVBFID09PSAiU0FWSSIpIHsKICAgIHJldHVybiAoKG5pciAtIHJlZCkgLyAobmlyICsgcmVkICsgU0FWSV9MKSkgKiAoMSArIFNBVklfTCk7CiAgfSBlbHNlIHsKICAgIHJldHVybiBJTlZBTElEX1ZJOwogIH0KfQoKLy8gQ2xpcCBWSSB0byBtaW4vbWF4IHJhbmdlIGFuZCBzY2FsZSB0byAxLTEwMCByYW5nZQpmdW5jdGlvbiBjbGlwQW5kU2NhbGVWSSh2aSkgewogIGNvbnN0IGNsaXBwZWRWSSA9IE1hdGgubWF4KFZJX01JTiwgTWF0aC5taW4odmksIFZJX01BWCkpOwogIHJldHVybiBNYXRoLnJvdW5kKCgoY2xpcHBlZFZJIC0gVklfTUlOKSAvIChWSV9NQVggLSBWSV9NSU4pKSAqIFZJX1NDQUxFKSArIDE7Cn0KCi8vIEZpbHRlciBzY2VuZXMgdG8gb25seSBpbmNsdWRlIGZpcnN0IGFuZCBsYXN0IG9mIHNwZWNpZmllZCB0aW1lIHJhbmdlCmZ1bmN0aW9uIHByZVByb2Nlc3NTY2VuZXMoY29sbGVjdGlvbnMpIHsKICAvLyBIZWxwZXIgdG8ga2VlcCBvbmx5IGZpcnN0IGFuZCBsYXN0IG9yYml0IGlmIHBvc3NpYmxlCiAgZnVuY3Rpb24gZmlsdGVyT3JiaXRzKG9yYml0cywgbGFiZWwpIHsKICAgIGlmIChBcnJheS5pc0FycmF5KG9yYml0cykgJiYgb3JiaXRzLmxlbmd0aCA%2BPSAyKSB7CiAgICAgIHJldHVybiBbb3JiaXRzWzBdLCBvcmJpdHNbb3JiaXRzLmxlbmd0aCAtIDFdXTsKICAgIH0gZWxzZSB7CiAgICAgIHRocm93IG5ldyBFcnJvcihgRlZDSSByZXF1aXJlcyBhdCBsZWFzdCAyICR7bGFiZWx9IHNjZW5lcyBmb3IgY2hhbmdlIGRldGVjdGlvbmApOwogICAgfQogIH0KCiAgY29sbGVjdGlvbnMuYXJwcy5zY2VuZXMub3JiaXRzID0gZmlsdGVyT3JiaXRzKGNvbGxlY3Rpb25zLmFycHMuc2NlbmVzLm9yYml0cywgImFycHMiKTsKICBjb2xsZWN0aW9ucy5jYW5vcHlfY292ZXIuc2NlbmVzLm9yYml0cyA9IGZpbHRlck9yYml0cyhjb2xsZWN0aW9ucy5jYW5vcHlfY292ZXIuc2NlbmVzLm9yYml0cywgImNhbm9weSBjb3ZlciIpOwoKICByZXR1cm4gY29sbGVjdGlvbnM7Cn0KCi8vIE1haW4gZnVuY3Rpb24gdG8gZXZhbHVhdGUgZWFjaCBwaXhlbApmdW5jdGlvbiBldmFsdWF0ZVBpeGVsKHNhbXBsZXMpIHsKICAvLyBDaGVjayBmb3IgdW5kZWZpbmVkIGlucHV0cwogIGlmICghc2FtcGxlcy5hcnBzIHx8ICFzYW1wbGVzLmNhbm9weV9jb3ZlciB8fCAKICAgICAgc2FtcGxlcy5hcnBzLmxlbmd0aCA8IDIgfHwgc2FtcGxlcy5jYW5vcHlfY292ZXIubGVuZ3RoIDwgMikgewogICAgcmV0dXJuIFswLCAwLCAwLCAwXTsgLy8gVHJhbnNwYXJlbnQKICB9CgogIGNvbnN0IHBzX2FyZF9UMSA9IHNhbXBsZXMuYXJwc1sxXTsKICBjb25zdCBwc19hcmRfVDIgPSBzYW1wbGVzLmFycHNbMF07CiAgY29uc3QgY2Fub3B5X1QxID0gc2FtcGxlcy5jYW5vcHlfY292ZXJbMV07CiAgY29uc3QgY2Fub3B5X1QyID0gc2FtcGxlcy5jYW5vcHlfY292ZXJbMF07CgogIC8vIENoZWNrIGRhdGEgbWFza3MKICBpZiAocHNfYXJkX1QxLmRhdGFNYXNrID09PSAwIHx8IHBzX2FyZF9UMi5kYXRhTWFzayA9PT0gMCkgewogICAgcmV0dXJuIFswLCAwLCAwLCAwXTsKICB9CgogIC8vIENoZWNrIGNsb3VkIG1hc2tzCiAgaWYgKHBzX2FyZF9UMS5jbG91ZF9tYXNrICE9PSAxIHx8IHBzX2FyZF9UMi5jbG91ZF9tYXNrICE9PSAxKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOwogIH0KCiAgLy8gQ2hlY2sgY2Fub3B5IGNvdmVyIHRocmVzaG9sZHMKICBpZiAoY2Fub3B5X1QxLkNDIDw9IENDX01JTiB8fCBjYW5vcHlfVDIuQ0MgPD0gQ0NfTUlOKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOwogIH0KCiAgLy8gQ2FsY3VsYXRlIFZJIGZvciBUMSBhbmQgVDIKICBjb25zdCB2aV9UMSA9IGNhbGN1bGF0ZVZJKHBzX2FyZF9UMS5uaXIsIHBzX2FyZF9UMS5yZWQpOwogIGNvbnN0IHZpX1QyID0gY2FsY3VsYXRlVkkocHNfYXJkX1QyLm5pciwgcHNfYXJkX1QyLnJlZCk7CiAgCiAgLy8gQ2hlY2sgZm9yIGludmFsaWQgVkkgdmFsdWVzCiAgaWYgKHZpX1QxID09PSBJTlZBTElEX1ZJIHx8IHZpX1QyID09PSBJTlZBTElEX1ZJKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOwogIH0KCiAgLy8gQ2xpcCBhbmQgc2NhbGUgVkkgdmFsdWVzCiAgY29uc3QgdmkxMDBfVDEgPSBjbGlwQW5kU2NhbGVWSSh2aV9UMSk7CiAgY29uc3QgdmkxMDBfVDIgPSBjbGlwQW5kU2NhbGVWSSh2aV9UMik7CgogIC8vIENhbGN1bGF0ZSBwZXJjZW50YWdlIGNoYW5nZSBkaXJlY3RseQogIGNvbnN0IGNoYW5nZVBlcmNlbnQgPSB2aTEwMF9UMiAtIHZpMTAwX1QxOwoKICAvLyBDbGFzc2lmeSBjaGFuZ2UgaW50byBnYWluCiAgaWYgKGNoYW5nZVBlcmNlbnQgPiBDSEFOR0VfVEhSRVNIT0xEKSB7CiAgICBjb25zdCBjbGFzc0luZGV4ID0gTWF0aC5taW4oCiAgICAgIDE5LAogICAgICBNYXRoLmZsb29yKChjaGFuZ2VQZXJjZW50IC0gQ0hBTkdFX1RIUkVTSE9MRCkgLyBDTEFTU19JTlRFUlZBTCkKICAgICk7CiAgICBjb25zdCBjb2xvciA9IGdhaW5QYWxldHRlW2NsYXNzSW5kZXhdOwogICAgcmV0dXJuIFsuLi5jb2xvciwgMV07IC8vIEZ1bGx5IG9wYXF1ZSBnYWluCiAgfSBlbHNlIHsKICAgIHJldHVybiBbMCwgMCwgMCwgMF07IC8vIEZ1bGx5IHRyYW5zcGFyZW50IGlmIG5vIHNpZ25pZmljYW50IGNoYW5nZQogIH0KfQo%3D&datasetId=3f605f75-86c4-411a-b4ae-01c896f0e54e&fromTime=2023-03-20T00%3A00%3A00.000Z&toTime=2023-07-14T23%3A59%3A59.999Z&demSource3D=%22MAPZEN%22&dataFusion=%5B%7B%22id%22%3A%22CUSTOM%22%2C%22alias%22%3A%22arps%22%2C%22additionalParameters%22%3A%7B%22collectionId%22%3A%223f605f75-86c4-411a-b4ae-01c896f0e54e%22%2C%22subType%22%3Anull%2C%22locationId%22%3A%22aws-eu-central-1%22%7D%7D%2C%7B%22id%22%3A%22CUSTOM%22%2C%22alias%22%3A%22canopy_cover%22%2C%22additionalParameters%22%3A%7B%22collectionId%22%3A%22ca501757-cf8e-43a8-b1a4-1aa59ae22425%22%2C%22subType%22%3A%22BYOC%22%2C%22locationId%22%3A%22aws-eu-central-1%22%7D%2C%22timespan%22%3A%5B%222023-03-21T00%3A00%3A00.000Z%22%2C%222023-07-21T23%3A59%3A59.999Z%22%5D%7D%5D#custom-script){:target="_blank"} + + The example data is using Planet Sandox data. This data is restricted to Sentinel Hub users with active paid plans. If you are already a Planet Customer, see [here](https://community.planet.com/sentinel-hub-81/access-new-tools-for-analyzing-your-planet-data-on-sentinel-hub-732) on how to get access. + +## General description + +The FVCI-Gain script visualizes positive changes in forest vitality between two time periods. This specialized visualization focuses exclusively on areas showing improvement in forest health, making it easier to identify regeneration, growth, or recovery patterns. The script combines Analysis Ready PlanetScope imagery with the Canopy Cover layer from the Forest Carbon Monitoring dataset to ensure analysis is focused on forest areas only. + +## Details of the script + +The FVCI-Gain processing workflow includes: +1. Filter pixels based on canopy cover percentage (>25%) for both time periods +2. Calculate vegetation index (users can choose between NDVI or SAVI) for both time periods +3. Clip and scale both indices to a standardized range +4. Calculate the difference between the time periods +5. Identify pixels with positive change exceeding the threshold (>2.5%) +6. Classify positive changes into 20 intensity levels +7. Apply a color palette transitioning from yellow through green to purple to visualize different gain intensities + +The script utilizes the first and last PlanetScope ARD acquisitions and Canopy Cover products in the specified time range. + +## Description of representative images + +The FVCI-Gain visualization uses a color palette specifically designed to highlight forest vitality improvements: +- Light yellow to yellow: Minor improvements in forest health +- Yellow-green to green: Moderate improvements +- Dark green: Significant improvements +- Purple to magenta: Major improvements/regeneration + +Areas with no significant improvement or insufficient canopy cover remain transparent. + +A visualization of FVCI-Gain between March and July 2023 over Cestas, France: + +![FVCI-Gain over Cestas](fig/fig1.png) diff --git a/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/script.js b/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/script.js new file mode 100644 index 00000000..88acd38c --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_change_index_gain/script.js @@ -0,0 +1,161 @@ +//VERSION=3 +/* + * Forest Vitality Change Index - Gain Visualization + * This script visualizes only positive changes (gains) in forest vitality + * between two timepoints based on vegetation indices. + * + * Parameters: + * - VI_TYPE: Vegetation index type ("NDVI" or "SAVI") + * - CC_MIN: Minimum canopy cover percentage + * - VI_MIN, VI_MAX: Range for clipping vegetation index values + * - CHANGE_THRESHOLD: Minimum % change to classify as gain + * - CLASS_INTERVAL: Interval for classifying change magnitude + */ + +function setup() { + return { + input: [ + { + datasource: "arps", + bands: ["red", "nir", "dataMask", "cloud_mask"], + }, + { + datasource: "canopy_cover", + bands: ["CC", "dataMask"], + }, + ], + output: [ + { bands: 4 }, // RGBA for gain + ], + mosaicking: "ORBIT", + }; +} + +// Configuration parameters +const VI_TYPE = "NDVI"; // Choose "NDVI" or "SAVI" +const CC_MIN = 25; // Minimum canopy cover percentage to consider valid forest +const VI_MIN = 0.15; // Minimum vegetation index value for clipping +const VI_MAX = 0.85; // Maximum vegetation index value for clipping +const CHANGE_THRESHOLD = 2.5; // Minimum % change to classify as gain +const CLASS_INTERVAL = 5; // Interval for classifying change into palettes +const INVALID_VI = -9999; // Value for invalid vegetation index +const VI_SCALE = 99; // Scale factor for VI values (resulting in range 1-100) +const SAVI_L = 0.5; // Soil adjustment factor for SAVI + +// Color palette - for vitality gains (from small to significant) +const gainPalette = [ + [1.0, 1.0, 0.6], // 0 - smallest gain + [1.0, 1.0, 0.5], + [1.0, 1.0, 0.4], + [0.9, 1.0, 0.3], + [0.8, 1.0, 0.2], + [0.7, 1.0, 0.1], + [0.6, 1.0, 0.0], + [0.4, 1.0, 0.0], + [0.2, 1.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.9, 0.0], + [0.0, 0.8, 0.0], + [0.0, 0.6, 0.0], + [0.0, 0.4, 0.0], + [0.0, 0.2, 0.0], + [0.6, 0.0, 0.6], + [0.7, 0.0, 0.7], + [0.8, 0.0, 0.8], + [0.9, 0.0, 0.9], + [1.0, 0.0, 1.0], // 19 - significant gain +]; + +// Calculate vegetation index (VI) based on type +function calculateVI(nir, red) { + if (nir === undefined || red === undefined) return INVALID_VI; + if (nir + red === 0) return INVALID_VI; // Avoid division by zero + + if (VI_TYPE === "NDVI") { + return (nir - red) / (nir + red); + } else if (VI_TYPE === "SAVI") { + return ((nir - red) / (nir + red + SAVI_L)) * (1 + SAVI_L); + } else { + return INVALID_VI; + } +} + +// Clip VI to min/max range and scale to 1-100 range +function clipAndScaleVI(vi) { + const clippedVI = Math.max(VI_MIN, Math.min(vi, VI_MAX)); + return Math.round(((clippedVI - VI_MIN) / (VI_MAX - VI_MIN)) * VI_SCALE) + 1; +} + +// Filter scenes to only include first and last of specified time range +function preProcessScenes(collections) { + // Helper to keep only first and last orbit if possible + function filterOrbits(orbits, label) { + if (Array.isArray(orbits) && orbits.length >= 2) { + return [orbits[0], orbits[orbits.length - 1]]; + } else { + throw new Error(`FVCI requires at least 2 ${label} scenes for change detection`); + } + } + + collections.arps.scenes.orbits = filterOrbits(collections.arps.scenes.orbits, "arps"); + collections.canopy_cover.scenes.orbits = filterOrbits(collections.canopy_cover.scenes.orbits, "canopy cover"); + + return collections; +} + +// Main function to evaluate each pixel +function evaluatePixel(samples) { + // Check for undefined inputs + if (!samples.arps || !samples.canopy_cover || + samples.arps.length < 2 || samples.canopy_cover.length < 2) { + return [0, 0, 0, 0]; // Transparent + } + + const ps_ard_T1 = samples.arps[1]; + const ps_ard_T2 = samples.arps[0]; + const canopy_T1 = samples.canopy_cover[1]; + const canopy_T2 = samples.canopy_cover[0]; + + // Check data masks + if (ps_ard_T1.dataMask === 0 || ps_ard_T2.dataMask === 0) { + return [0, 0, 0, 0]; + } + + // Check cloud masks + if (ps_ard_T1.cloud_mask !== 1 || ps_ard_T2.cloud_mask !== 1) { + return [0, 0, 0, 0]; + } + + // Check canopy cover thresholds + if (canopy_T1.CC <= CC_MIN || canopy_T2.CC <= CC_MIN) { + return [0, 0, 0, 0]; + } + + // Calculate VI for T1 and T2 + const vi_T1 = calculateVI(ps_ard_T1.nir, ps_ard_T1.red); + const vi_T2 = calculateVI(ps_ard_T2.nir, ps_ard_T2.red); + + // Check for invalid VI values + if (vi_T1 === INVALID_VI || vi_T2 === INVALID_VI) { + return [0, 0, 0, 0]; + } + + // Clip and scale VI values + const vi100_T1 = clipAndScaleVI(vi_T1); + const vi100_T2 = clipAndScaleVI(vi_T2); + + // Calculate percentage change directly + const changePercent = vi100_T2 - vi100_T1; + + // Classify change into gain + if (changePercent > CHANGE_THRESHOLD) { + const classIndex = Math.min( + 19, + Math.floor((changePercent - CHANGE_THRESHOLD) / CLASS_INTERVAL) + ); + const color = gainPalette[classIndex]; + return [...color, 1]; // Fully opaque gain + } else { + return [0, 0, 0, 0]; // Fully transparent if no significant change + } +} diff --git a/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/fig/fig1.png b/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/fig/fig1.png new file mode 100644 index 00000000..505b77aa Binary files /dev/null and b/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/fig/fig1.png differ diff --git a/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/index.md b/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/index.md new file mode 100644 index 00000000..7f859710 --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/index.md @@ -0,0 +1,48 @@ +--- +title: Forest Vitality Change Index - Loss Visualization (FVCI-Loss) +parent: Analysis Ready Planetscope +grand_parent: Planet +layout: script +nav_exclude: true +scripts: + - [Visualization, script.js] +additionalQueryParams: + - - themeId + - PLANET_SANDBOX +--- + +## Evaluate and visualize + - [EO Browser](https://apps.sentinel-hub.com/eo-browser/?zoom=14&lat=44.74116&lng=-0.68441&themeId=PLANET_SANDBOX&visualizationUrl=U2FsdGVkX1%2B56l3Yf0Zyx1n%2BLK9L0BMgduA96OfvYWxkoxjKOh%2FYuImxuyoWih6Yu7bshGWs0Vq%2BW86sg8Q3axwSGOmxs%2FtC9YdVweCAwRfDrXSPLcE4YjCVf60cmKun&evalscript=Ly9WRVJTSU9OPTMKLyogCiAqIEZvcmVzdCBWaXRhbGl0eSBDaGFuZ2UgSW5kZXggLSBMb3NzIFZpc3VhbGl6YXRpb24KICogVGhpcyBzY3JpcHQgdmlzdWFsaXplcyBvbmx5IG5lZ2F0aXZlIGNoYW5nZXMgKGxvc3NlcykgaW4gZm9yZXN0IHZpdGFsaXR5CiAqIGJldHdlZW4gdHdvIHRpbWVwb2ludHMgYmFzZWQgb24gdmVnZXRhdGlvbiBpbmRpY2VzLgogKiAKICogUGFyYW1ldGVyczoKICogLSBWSV9UWVBFOiBWZWdldGF0aW9uIGluZGV4IHR5cGUgKCJORFZJIiBvciAiU0FWSSIpCiAqIC0gQ0NfTUlOOiBNaW5pbXVtIGNhbm9weSBjb3ZlciBwZXJjZW50YWdlCiAqIC0gVklfTUlOLCBWSV9NQVg6IFJhbmdlIGZvciBjbGlwcGluZyB2ZWdldGF0aW9uIGluZGV4IHZhbHVlcwogKiAtIENIQU5HRV9USFJFU0hPTEQ6IE1pbmltdW0gJSBjaGFuZ2UgdG8gY2xhc3NpZnkgYXMgbG9zcwogKiAtIENMQVNTX0lOVEVSVkFMOiBJbnRlcnZhbCBmb3IgY2xhc3NpZnlpbmcgY2hhbmdlIG1hZ25pdHVkZQogKi8KCmZ1bmN0aW9uIHNldHVwKCkgewogIHJldHVybiB7CiAgICBpbnB1dDogWwogICAgICB7CiAgICAgICAgZGF0YXNvdXJjZTogImFycHMiLAogICAgICAgIGJhbmRzOiBbInJlZCIsICJuaXIiLCAiZGF0YU1hc2siLCAiY2xvdWRfbWFzayJdLAogICAgICB9LAogICAgICB7CiAgICAgICAgZGF0YXNvdXJjZTogImNhbm9weV9jb3ZlciIsCiAgICAgICAgYmFuZHM6IFsiQ0MiLCAiZGF0YU1hc2siXSwKICAgICAgfSwKICAgIF0sCiAgICBvdXRwdXQ6IFsKICAgICAgeyBiYW5kczogNCB9LCAvLyBSR0JBIGZvciBsb3NzCiAgICBdLAogICAgbW9zYWlja2luZzogIk9SQklUIiwKICB9Owp9CgovLyBDb25maWd1cmF0aW9uIHBhcmFtZXRlcnMKY29uc3QgVklfVFlQRSA9ICJORFZJIjsgLy8gQ2hvb3NlICJORFZJIiBvciAiU0FWSSIKY29uc3QgQ0NfTUlOID0gMjU7ICAgICAgLy8gTWluaW11bSBjYW5vcHkgY292ZXIgcGVyY2VudGFnZSB0byBjb25zaWRlciB2YWxpZCBmb3Jlc3QKY29uc3QgVklfTUlOID0gMC4xNTsgICAgLy8gTWluaW11bSB2ZWdldGF0aW9uIGluZGV4IHZhbHVlIGZvciBjbGlwcGluZwpjb25zdCBWSV9NQVggPSAwLjg1OyAgICAvLyBNYXhpbXVtIHZlZ2V0YXRpb24gaW5kZXggdmFsdWUgZm9yIGNsaXBwaW5nCmNvbnN0IENIQU5HRV9USFJFU0hPTEQgPSAyLjU7IC8vIE1pbmltdW0gJSBjaGFuZ2UgdG8gY2xhc3NpZnkgYXMgbG9zcwpjb25zdCBDTEFTU19JTlRFUlZBTCA9IDU7ICAgICAvLyBJbnRlcnZhbCBmb3IgY2xhc3NpZnlpbmcgY2hhbmdlIGludG8gcGFsZXR0ZXMKY29uc3QgSU5WQUxJRF9WSSA9IC05OTk5OyAgLy8gVmFsdWUgZm9yIGludmFsaWQgdmVnZXRhdGlvbiBpbmRleApjb25zdCBWSV9TQ0FMRSA9IDk5OyAgICAgIC8vIFNjYWxlIGZhY3RvciBmb3IgVkkgdmFsdWVzIChyZXN1bHRpbmcgaW4gcmFuZ2UgMS0xMDApCmNvbnN0IFNBVklfTCA9IDAuNTsgICAgICAgLy8gU29pbCBhZGp1c3RtZW50IGZhY3RvciBmb3IgU0FWSQoKLy8gQ29sb3IgcGFsZXR0ZSAtIGZvciB2aXRhbGl0eSBsb3NzZXMgKGZyb20gc21hbGwgdG8gc2V2ZXJlKQpjb25zdCBsb3NzUGFsZXR0ZSA9IFsKICBbMC42LCAwLjgsIDAuNF0sICAvLyAwIC0gc21hbGxlc3QgbG9zcwogIFswLjcsIDAuOSwgMC40XSwKICBbMC44LCAxLjAsIDAuNF0sCiAgWzAuOSwgMS4wLCAwLjNdLAogIFsxLjAsIDEuMCwgMC4yXSwKICBbMS4wLCAwLjksIDAuMV0sCiAgWzEuMCwgMC44LCAwLjBdLAogIFsxLjAsIDAuNiwgMC4wXSwKICBbMS4wLCAwLjQsIDAuMF0sCiAgWzEuMCwgMC4yLCAwLjBdLAogIFsxLjAsIDAuMCwgMC4wXSwKICBbMC44LCAwLjAsIDAuMF0sCiAgWzAuNiwgMC4wLCAwLjBdLAogIFswLjQsIDAuMCwgMC4wXSwKICBbMC4yLCAwLjAsIDAuMF0sCiAgWzAuMCwgMC4wLCAwLjRdLAogIFswLjAsIDAuMCwgMC42XSwKICBbMC4wLCAwLjAsIDAuOF0sCiAgWzAuMCwgMC4wLCAxLjBdLAogIFswLjIsIDAuMiwgMS4wXSwgIC8vIDE5IC0gc2V2ZXJlIGxvc3MKXTsKCi8vIENhbGN1bGF0ZSB2ZWdldGF0aW9uIGluZGV4IChWSSkgYmFzZWQgb24gdHlwZQpmdW5jdGlvbiBjYWxjdWxhdGVWSShuaXIsIHJlZCkgewogIGlmIChuaXIgPT09IHVuZGVmaW5lZCB8fCByZWQgPT09IHVuZGVmaW5lZCkgcmV0dXJuIElOVkFMSURfVkk7CiAgaWYgKG5pciArIHJlZCA9PT0gMCkgcmV0dXJuIElOVkFMSURfVkk7IC8vIEF2b2lkIGRpdmlzaW9uIGJ5IHplcm8KICAKICBpZiAoVklfVFlQRSA9PT0gIk5EVkkiKSB7CiAgICByZXR1cm4gKG5pciAtIHJlZCkgLyAobmlyICsgcmVkKTsKICB9IGVsc2UgaWYgKFZJX1RZUEUgPT09ICJTQVZJIikgewogICAgcmV0dXJuICgobmlyIC0gcmVkKSAvIChuaXIgKyByZWQgKyBTQVZJX0wpKSAqICgxICsgU0FWSV9MKTsKICB9IGVsc2UgewogICAgcmV0dXJuIElOVkFMSURfVkk7CiAgfQp9CgovLyBDbGlwIFZJIHRvIG1pbi9tYXggcmFuZ2UgYW5kIHNjYWxlIHRvIDEtMTAwIHJhbmdlCmZ1bmN0aW9uIGNsaXBBbmRTY2FsZVZJKHZpKSB7CiAgY29uc3QgY2xpcHBlZFZJID0gTWF0aC5tYXgoVklfTUlOLCBNYXRoLm1pbih2aSwgVklfTUFYKSk7CiAgcmV0dXJuIE1hdGgucm91bmQoKChjbGlwcGVkVkkgLSBWSV9NSU4pIC8gKFZJX01BWCAtIFZJX01JTikpICogVklfU0NBTEUpICsgMTsKfQoKLy8gRmlsdGVyIHNjZW5lcyB0byBvbmx5IGluY2x1ZGUgZmlyc3QgYW5kIGxhc3Qgb2Ygc3BlY2lmaWVkIHRpbWUgcmFuZ2UKZnVuY3Rpb24gcHJlUHJvY2Vzc1NjZW5lcyhjb2xsZWN0aW9ucykgewogIC8vIEhlbHBlciB0byBrZWVwIG9ubHkgZmlyc3QgYW5kIGxhc3Qgb3JiaXQgaWYgcG9zc2libGUKICBmdW5jdGlvbiBmaWx0ZXJPcmJpdHMob3JiaXRzLCBsYWJlbCkgewogICAgaWYgKEFycmF5LmlzQXJyYXkob3JiaXRzKSAmJiBvcmJpdHMubGVuZ3RoID49IDIpIHsKICAgICAgcmV0dXJuIFtvcmJpdHNbMF0sIG9yYml0c1tvcmJpdHMubGVuZ3RoIC0gMV1dOwogICAgfSBlbHNlIHsKICAgICAgdGhyb3cgbmV3IEVycm9yKGBGVkNJIHJlcXVpcmVzIGF0IGxlYXN0IDIgJHtsYWJlbH0gc2NlbmVzIGZvciBjaGFuZ2UgZGV0ZWN0aW9uYCk7CiAgICB9CiAgfQoKICBjb2xsZWN0aW9ucy5hcnBzLnNjZW5lcy5vcmJpdHMgPSBmaWx0ZXJPcmJpdHMoY29sbGVjdGlvbnMuYXJwcy5zY2VuZXMub3JiaXRzLCAiYXJwcyIpOwogIGNvbGxlY3Rpb25zLmNhbm9weV9jb3Zlci5zY2VuZXMub3JiaXRzID0gZmlsdGVyT3JiaXRzKGNvbGxlY3Rpb25zLmNhbm9weV9jb3Zlci5zY2VuZXMub3JiaXRzLCAiY2Fub3B5IGNvdmVyIik7CgogIHJldHVybiBjb2xsZWN0aW9uczsKfQoKLy8gTWFpbiBmdW5jdGlvbiB0byBldmFsdWF0ZSBlYWNoIHBpeGVsCmZ1bmN0aW9uIGV2YWx1YXRlUGl4ZWwoc2FtcGxlcykgewogIC8vIENoZWNrIGZvciB1bmRlZmluZWQgaW5wdXRzCiAgaWYgKCFzYW1wbGVzLmFycHMgfHwgIXNhbXBsZXMuY2Fub3B5X2NvdmVyIHx8IAogICAgICBzYW1wbGVzLmFycHMubGVuZ3RoIDwgMiB8fCBzYW1wbGVzLmNhbm9weV9jb3Zlci5sZW5ndGggPCAyKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOyAvLyBUcmFuc3BhcmVudAogIH0KCiAgY29uc3QgcHNfYXJkX1QxID0gc2FtcGxlcy5hcnBzWzFdOwogIGNvbnN0IHBzX2FyZF9UMiA9IHNhbXBsZXMuYXJwc1swXTsKICBjb25zdCBjYW5vcHlfVDEgPSBzYW1wbGVzLmNhbm9weV9jb3ZlclsxXTsKICBjb25zdCBjYW5vcHlfVDIgPSBzYW1wbGVzLmNhbm9weV9jb3ZlclswXTsKCiAgLy8gQ2hlY2sgZGF0YSBtYXNrcwogIGlmIChwc19hcmRfVDEuZGF0YU1hc2sgPT09IDAgfHwgcHNfYXJkX1QyLmRhdGFNYXNrID09PSAwKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOwogIH0KCiAgLy8gQ2hlY2sgY2xvdWQgbWFza3MKICBpZiAocHNfYXJkX1QxLmNsb3VkX21hc2sgIT09IDEgfHwgcHNfYXJkX1QyLmNsb3VkX21hc2sgIT09IDEpIHsKICAgIHJldHVybiBbMCwgMCwgMCwgMF07CiAgfQoKICAvLyBDaGVjayBjYW5vcHkgY292ZXIgdGhyZXNob2xkcwogIGlmIChjYW5vcHlfVDEuQ0MgPD0gQ0NfTUlOIHx8IGNhbm9weV9UMi5DQyA8PSBDQ19NSU4pIHsKICAgIHJldHVybiBbMCwgMCwgMCwgMF07CiAgfQoKICAvLyBDYWxjdWxhdGUgVkkgZm9yIFQxIGFuZCBUMgogIGNvbnN0IHZpX1QxID0gY2FsY3VsYXRlVkkocHNfYXJkX1QxLm5pciwgcHNfYXJkX1QxLnJlZCk7CiAgY29uc3QgdmlfVDIgPSBjYWxjdWxhdGVWSShwc19hcmRfVDIubmlyLCBwc19hcmRfVDIucmVkKTsKICAKICAvLyBDaGVjayBmb3IgaW52YWxpZCBWSSB2YWx1ZXMKICBpZiAodmlfVDEgPT09IElOVkFMSURfVkkgfHwgdmlfVDIgPT09IElOVkFMSURfVkkpIHsKICAgIHJldHVybiBbMCwgMCwgMCwgMF07CiAgfQoKICAvLyBDbGlwIGFuZCBzY2FsZSBWSSB2YWx1ZXMKICBjb25zdCB2aTEwMF9UMSA9IGNsaXBBbmRTY2FsZVZJKHZpX1QxKTsKICBjb25zdCB2aTEwMF9UMiA9IGNsaXBBbmRTY2FsZVZJKHZpX1QyKTsKCiAgLy8gQ2FsY3VsYXRlIHBlcmNlbnRhZ2UgY2hhbmdlIGRpcmVjdGx5CiAgY29uc3QgY2hhbmdlUGVyY2VudCA9IHZpMTAwX1QyIC0gdmkxMDBfVDE7CgogIC8vIENsYXNzaWZ5IGNoYW5nZSBpbnRvIGxvc3MKICBpZiAoY2hhbmdlUGVyY2VudCA8IC1DSEFOR0VfVEhSRVNIT0xEKSB7CiAgICBjb25zdCBjbGFzc0luZGV4ID0gTWF0aC5taW4oCiAgICAgIDE5LAogICAgICBNYXRoLmZsb29yKCgtY2hhbmdlUGVyY2VudCAtIENIQU5HRV9USFJFU0hPTEQpIC8gQ0xBU1NfSU5URVJWQUwpCiAgICApOwogICAgY29uc3QgY29sb3IgPSBsb3NzUGFsZXR0ZVtjbGFzc0luZGV4XTsKICAgIHJldHVybiBbLi4uY29sb3IsIDFdOyAvLyBGdWxseSBvcGFxdWUgbG9zcwogIH0gZWxzZSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOyAvLyBGdWxseSB0cmFuc3BhcmVudCBpZiBubyBzaWduaWZpY2FudCBjaGFuZ2UKICB9Cn0K&datasetId=3f605f75-86c4-411a-b4ae-01c896f0e54e&fromTime=2023-03-20T00%3A00%3A00.000Z&toTime=2023-07-14T23%3A59%3A59.999Z&demSource3D=%22MAPZEN%22&dataFusion=%5B%7B%22id%22%3A%22CUSTOM%22%2C%22alias%22%3A%22arps%22%2C%22additionalParameters%22%3A%7B%22collectionId%22%3A%223f605f75-86c4-411a-b4ae-01c896f0e54e%22%2C%22subType%22%3Anull%2C%22locationId%22%3A%22aws-eu-central-1%22%7D%7D%2C%7B%22id%22%3A%22CUSTOM%22%2C%22alias%22%3A%22canopy_cover%22%2C%22additionalParameters%22%3A%7B%22collectionId%22%3A%22ca501757-cf8e-43a8-b1a4-1aa59ae22425%22%2C%22subType%22%3A%22BYOC%22%2C%22locationId%22%3A%22aws-eu-central-1%22%7D%2C%22timespan%22%3A%5B%222023-03-21T00%3A00%3A00.000Z%22%2C%222023-07-21T23%3A59%3A59.999Z%22%5D%7D%5D#custom-script){:target="_blank"} + + The example data is using Planet Sandox data. This data is restricted to Sentinel Hub users with active paid plans. If you are already a Planet Customer, see [here](https://community.planet.com/sentinel-hub-81/access-new-tools-for-analyzing-your-planet-data-on-sentinel-hub-732) on how to get access. + +## General description + +The FVCI-Loss script visualizes negative changes in forest vitality between two time periods. This specialized visualization focuses exclusively on areas showing decline in forest health, making it easier to identify disturbances, stress, or degradation. The script combines Analysis Ready PlanetScope imagery with the Canopy Cover layer from the Forest Carbon Monitoring dataset to ensure analysis is focused on forest areas only. + +## Details of the script + +The FVCI-Loss processing workflow includes: +1. Filter pixels based on canopy cover percentage (>25%) for both time periods +2. Calculate vegetation index (users can choose between NDVI or SAVI) for both time periods +3. Clip and scale both indices to a standardized range +4. Calculate the difference between the time periods +5. Identify pixels with negative change exceeding the threshold (>2.5%) +6. Classify negative changes into 20 intensity levels +7. Apply a color palette transitioning from light green through orange and red to blue to visualize different loss intensities + +The script utilizes the first and last PlanetScope ARD acquisitions and Canopy Cover products in the specified time range. + +## Description of representative images + +The FVCI-Loss visualization uses a color palette specifically designed to highlight forest vitality decline: +- Light green: Minor decline in forest health +- Yellow to orange: Moderate decline +- Red to dark red: Significant decline +- Dark blue to light blue: Severe decline/potential deforestation + +Areas with no significant decline or insufficient canopy cover remain transparent, allowing this output to be overlaid on other imagery. + +A visualization of FVCI-Loss between March and July 2023 over Cestas, France: + +![FVCI-Loss over Cestas](fig/fig1.png) diff --git a/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/script.js b/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/script.js new file mode 100644 index 00000000..dd411f2e --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_change_index_loss/script.js @@ -0,0 +1,161 @@ +//VERSION=3 +/* + * Forest Vitality Change Index - Loss Visualization + * This script visualizes only negative changes (losses) in forest vitality + * between two timepoints based on vegetation indices. + * + * Parameters: + * - VI_TYPE: Vegetation index type ("NDVI" or "SAVI") + * - CC_MIN: Minimum canopy cover percentage + * - VI_MIN, VI_MAX: Range for clipping vegetation index values + * - CHANGE_THRESHOLD: Minimum % change to classify as loss + * - CLASS_INTERVAL: Interval for classifying change magnitude + */ + +function setup() { + return { + input: [ + { + datasource: "arps", + bands: ["red", "nir", "dataMask", "cloud_mask"], + }, + { + datasource: "canopy_cover", + bands: ["CC", "dataMask"], + }, + ], + output: [ + { bands: 4 }, // RGBA for loss + ], + mosaicking: "ORBIT", + }; +} + +// Configuration parameters +const VI_TYPE = "NDVI"; // Choose "NDVI" or "SAVI" +const CC_MIN = 25; // Minimum canopy cover percentage to consider valid forest +const VI_MIN = 0.15; // Minimum vegetation index value for clipping +const VI_MAX = 0.85; // Maximum vegetation index value for clipping +const CHANGE_THRESHOLD = 2.5; // Minimum % change to classify as loss +const CLASS_INTERVAL = 5; // Interval for classifying change into palettes +const INVALID_VI = -9999; // Value for invalid vegetation index +const VI_SCALE = 99; // Scale factor for VI values (resulting in range 1-100) +const SAVI_L = 0.5; // Soil adjustment factor for SAVI + +// Color palette - for vitality losses (from small to severe) +const lossPalette = [ + [0.6, 0.8, 0.4], // 0 - smallest loss + [0.7, 0.9, 0.4], + [0.8, 1.0, 0.4], + [0.9, 1.0, 0.3], + [1.0, 1.0, 0.2], + [1.0, 0.9, 0.1], + [1.0, 0.8, 0.0], + [1.0, 0.6, 0.0], + [1.0, 0.4, 0.0], + [1.0, 0.2, 0.0], + [1.0, 0.0, 0.0], + [0.8, 0.0, 0.0], + [0.6, 0.0, 0.0], + [0.4, 0.0, 0.0], + [0.2, 0.0, 0.0], + [0.0, 0.0, 0.4], + [0.0, 0.0, 0.6], + [0.0, 0.0, 0.8], + [0.0, 0.0, 1.0], + [0.2, 0.2, 1.0], // 19 - severe loss +]; + +// Calculate vegetation index (VI) based on type +function calculateVI(nir, red) { + if (nir === undefined || red === undefined) return INVALID_VI; + if (nir + red === 0) return INVALID_VI; // Avoid division by zero + + if (VI_TYPE === "NDVI") { + return (nir - red) / (nir + red); + } else if (VI_TYPE === "SAVI") { + return ((nir - red) / (nir + red + SAVI_L)) * (1 + SAVI_L); + } else { + return INVALID_VI; + } +} + +// Clip VI to min/max range and scale to 1-100 range +function clipAndScaleVI(vi) { + const clippedVI = Math.max(VI_MIN, Math.min(vi, VI_MAX)); + return Math.round(((clippedVI - VI_MIN) / (VI_MAX - VI_MIN)) * VI_SCALE) + 1; +} + +// Filter scenes to only include first and last of specified time range +function preProcessScenes(collections) { + // Helper to keep only first and last orbit if possible + function filterOrbits(orbits, label) { + if (Array.isArray(orbits) && orbits.length >= 2) { + return [orbits[0], orbits[orbits.length - 1]]; + } else { + throw new Error(`FVCI requires at least 2 ${label} scenes for change detection`); + } + } + + collections.arps.scenes.orbits = filterOrbits(collections.arps.scenes.orbits, "arps"); + collections.canopy_cover.scenes.orbits = filterOrbits(collections.canopy_cover.scenes.orbits, "canopy cover"); + + return collections; +} + +// Main function to evaluate each pixel +function evaluatePixel(samples) { + // Check for undefined inputs + if (!samples.arps || !samples.canopy_cover || + samples.arps.length < 2 || samples.canopy_cover.length < 2) { + return [0, 0, 0, 0]; // Transparent + } + + const ps_ard_T1 = samples.arps[1]; + const ps_ard_T2 = samples.arps[0]; + const canopy_T1 = samples.canopy_cover[1]; + const canopy_T2 = samples.canopy_cover[0]; + + // Check data masks + if (ps_ard_T1.dataMask === 0 || ps_ard_T2.dataMask === 0) { + return [0, 0, 0, 0]; + } + + // Check cloud masks + if (ps_ard_T1.cloud_mask !== 1 || ps_ard_T2.cloud_mask !== 1) { + return [0, 0, 0, 0]; + } + + // Check canopy cover thresholds + if (canopy_T1.CC <= CC_MIN || canopy_T2.CC <= CC_MIN) { + return [0, 0, 0, 0]; + } + + // Calculate VI for T1 and T2 + const vi_T1 = calculateVI(ps_ard_T1.nir, ps_ard_T1.red); + const vi_T2 = calculateVI(ps_ard_T2.nir, ps_ard_T2.red); + + // Check for invalid VI values + if (vi_T1 === INVALID_VI || vi_T2 === INVALID_VI) { + return [0, 0, 0, 0]; + } + + // Clip and scale VI values + const vi100_T1 = clipAndScaleVI(vi_T1); + const vi100_T2 = clipAndScaleVI(vi_T2); + + // Calculate percentage change directly + const changePercent = vi100_T2 - vi100_T1; + + // Classify change into loss + if (changePercent < -CHANGE_THRESHOLD) { + const classIndex = Math.min( + 19, + Math.floor((-changePercent - CHANGE_THRESHOLD) / CLASS_INTERVAL) + ); + const color = lossPalette[classIndex]; + return [...color, 1]; // Fully opaque loss + } else { + return [0, 0, 0, 0]; // Fully transparent if no significant change + } +} diff --git a/planet/analysis_ready_planetscope/forest_vitality_index/fig/fig1.png b/planet/analysis_ready_planetscope/forest_vitality_index/fig/fig1.png new file mode 100644 index 00000000..d6b34b0f Binary files /dev/null and b/planet/analysis_ready_planetscope/forest_vitality_index/fig/fig1.png differ diff --git a/planet/analysis_ready_planetscope/forest_vitality_index/index.md b/planet/analysis_ready_planetscope/forest_vitality_index/index.md new file mode 100644 index 00000000..7b582604 --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_index/index.md @@ -0,0 +1,44 @@ +--- +title: Forest Vitality Index (FVI) +parent: Analysis Ready Planetscope +grand_parent: Planet +layout: script +nav_exclude: true +scripts: + - [Visualization, script.js] +additionalQueryParams: + - - themeId + - PLANET_SANDBOX +--- + +## Evaluate and visualize + - [EO Browser](https://apps.sentinel-hub.com/eo-browser/?zoom=14&lat=44.74116&lng=-0.68435&themeId=PLANET_SANDBOX&visualizationUrl=U2FsdGVkX18OEMNjIWAv1%2F1WcOtr7OnBlmA10HUvNTr8n8WFDN5ihKBpFzUxpfO8WQzLLfDpVDZe3Bt%2BB0RqaxvyuKtCrJ2i5SUMl9a7M%2FTwTAJf3SCQoN8p9QIYPz%2F8&evalscript=Ly9WRVJTSU9OPTMKLyogCiAqIEZvcmVzdCBWaXRhbGl0eSBJbmRleCAoRlZJKSBFdmFsdWF0aW9uIFNjcmlwdAogKiBUaGlzIHNjcmlwdCBjYWxjdWxhdGVzIEZWSSBiYXNlZCBvbiB2ZWdldGF0aW9uIGluZGljZXMgKE5EVkkgb3IgU0FWSSkKICogYW5kIGNsYXNzaWZpZXMgcGl4ZWxzIGludG8gMTUgY2xhc3NlcyByZXByZXNlbnRpbmcgZGlmZmVyZW50IHZpdGFsaXR5IGxldmVscy4KICovCgpmdW5jdGlvbiBzZXR1cCgpIHsKICByZXR1cm4gewogICAgaW5wdXQ6IFsicmVkIiwgIm5pciIsICJkYXRhTWFzayIsICJjbG91ZF9tYXNrIl0sCiAgICBvdXRwdXQ6IHsgYmFuZHM6IDQgfSwgLy8gUkdCQQogIH07Cn0KCi8vIENvbmZpZ3VyYXRpb24gcGFyYW1ldGVycwpjb25zdCBWSV9UWVBFID0gIk5EVkkiOyAvLyBDaG9vc2UgIk5EVkkiIG9yICJTQVZJIgpjb25zdCBWSV9NSU4gPSAwLjE1OyAvLyBNaW5pbXVtIFZJIHZhbHVlIGZvciBjbGlwcGluZwpjb25zdCBWSV9NQVggPSAwLjg1OyAvLyBNYXhpbXVtIFZJIHZhbHVlIGZvciBjbGlwcGluZwpjb25zdCBWSV9USFJFU0hPTEQgPSAwLjU7IC8vIFRocmVzaG9sZCBmb3IgdmVnZXRhdGlvbiBtYXNraW5nCmNvbnN0IE5VTV9DTEFTU0VTID0gMTU7ICAgIC8vIE51bWJlciBvZiBGVkkgY2xhc3Nlcwpjb25zdCBJTlZBTElEX1ZJID0gLTk5OTk7ICAvLyBWYWx1ZSBmb3IgaW52YWxpZCB2ZWdldGF0aW9uIGluZGV4CmNvbnN0IFNBVklfTCA9IDAuNTsgICAgICAgLy8gU29pbCBhZGp1c3RtZW50IGZhY3RvciBmb3IgU0FWSQpjb25zdCBWSV9TQ0FMRSA9IDk5OyAgICAgIC8vIFNjYWxlIGZhY3RvciBmb3IgVkkgdmFsdWVzIChyZXN1bHRpbmcgaW4gcmFuZ2UgMS0xMDApCgovLyBDb2xvciBwYWxldHRlIC0gdml0YWxpdHkgY2xhc3NlcyBmcm9tIGxvd2VzdCAoMSkgdG8gaGlnaGVzdCAoMTUpCmNvbnN0IGZ2aVBhbGV0dGUgPSBbCiAgWzAuMCwgMC4wLCAwLjBdLCAgIC8vIDAg4oCTIGJsYWNrIChubyBkYXRhKQogIFswLjUzLCAxLjAsIDEuMF0sICAvLyAxIOKAkyBjeWFuIChleHRyZW1lbHkgbG93IHZpdGFsaXR5KQogIFswLjY4LCAxLjAsIDEuMF0sICAvLyAyIOKAkyBsaWdodCBjeWFuCiAgWzAuMCwgMC4zMywgMS4wXSwgIC8vIDMg4oCTIGJsdWUKICBbMC4zMywgMC4wLCAwLjBdLCAgLy8gNCDigJMgZGFyayByZWQKICBbMC42NywgMC4wLCAwLjBdLCAgLy8gNSDigJMgcmVkCiAgWzEuMCwgMC41LCAwLjBdLCAgIC8vIDYg4oCTIG9yYW5nZQogIFsxLjAsIDAuNzUsIDAuMF0sICAvLyA3IOKAkyBvcmFuZ2UteWVsbG93CiAgWzEuMCwgMS4wLCAwLjBdLCAgIC8vIDgg4oCTIHllbGxvdwogIFswLjgsIDEuMCwgMC4wXSwgICAvLyA5IOKAkyB5ZWxsb3ctZ3JlZW4KICBbMC41MywgMS4wLCAwLjBdLCAgLy8gMTAg4oCTIGxpbWUgZ3JlZW4KICBbMC4wLCAxLjAsIDAuMF0sICAgLy8gMTEg4oCTIGdyZWVuCiAgWzAuMCwgMC44LCAwLjBdLCAgIC8vIDEyIOKAkyBkYXJrIGdyZWVuCiAgWzAuMCwgMC42LCAwLjBdLCAgIC8vIDEzIOKAkyBkZWVwZXIgZ3JlZW4KICBbMC4wLCAwLjQsIDAuMF0sICAgLy8gMTQg4oCTIGZvcmVzdCBncmVlbgogIFswLjAsIDAuMjUsIDAuMF0sICAvLyAxNSDigJMgdmVyeSBkYXJrIGdyZWVuIChoaWdoZXN0IHZpdGFsaXR5KQpdOwoKLy8gQ2FsY3VsYXRlIHZlZ2V0YXRpb24gaW5kZXggKFZJKSBiYXNlZCBvbiB0eXBlCmZ1bmN0aW9uIGNhbGN1bGF0ZVZJKG5pciwgcmVkLCB0eXBlKSB7CiAgaWYgKG5pciA9PT0gdW5kZWZpbmVkIHx8IHJlZCA9PT0gdW5kZWZpbmVkKSByZXR1cm4gSU5WQUxJRF9WSTsKICBpZiAobmlyICsgcmVkID09PSAwKSByZXR1cm4gSU5WQUxJRF9WSTsgLy8gQXZvaWQgZGl2aXNpb24gYnkgemVybwoKICBpZiAodHlwZSA9PT0gIk5EVkkiKSB7CiAgICByZXR1cm4gKG5pciAtIHJlZCkgLyAobmlyICsgcmVkKTsKICB9IGVsc2UgaWYgKHR5cGUgPT09ICJTQVZJIikgewogICAgcmV0dXJuICgobmlyIC0gcmVkKSAvIChuaXIgKyByZWQgKyBTQVZJX0wpKSAqICgxICsgU0FWSV9MKTsKICB9IGVsc2UgewogICAgcmV0dXJuIElOVkFMSURfVkk7CiAgfQp9CgovLyBDbGlwIFZJIHRvIG1pbi9tYXggcmFuZ2UgYW5kIHNjYWxlIHRvIDEtMTAwIHJhbmdlCmZ1bmN0aW9uIGNsaXBBbmRTY2FsZVZJKHZpKSB7CiAgY29uc3QgY2xpcHBlZFZJID0gTWF0aC5tYXgoVklfTUlOLCBNYXRoLm1pbih2aSwgVklfTUFYKSk7CiAgcmV0dXJuIE1hdGgucm91bmQoKChjbGlwcGVkVkkgLSBWSV9NSU4pIC8gKFZJX01BWCAtIFZJX01JTikpICogVklfU0NBTEUpICsgMTsKfQoKLy8gTWFpbiBmdW5jdGlvbiB0byBldmFsdWF0ZSBlYWNoIHBpeGVsCmZ1bmN0aW9uIGV2YWx1YXRlUGl4ZWwoc2FtcGxlKSB7CiAgLy8gQ2hlY2sgZm9yIHVuZGVmaW5lZCBpbnB1dHMKICBpZiAoc2FtcGxlLnJlZCA9PT0gdW5kZWZpbmVkIHx8IHNhbXBsZS5uaXIgPT09IHVuZGVmaW5lZCkgewogICAgcmV0dXJuIFswLCAwLCAwLCAwXTsgLy8gVHJhbnNwYXJlbnQKICB9CgogIC8vIENoZWNrIGNsb3VkIG1hc2sgYW5kIGRhdGEgbWFzawogIGlmICgKICAgIHNhbXBsZS5kYXRhTWFzayA9PT0gMCB8fCAvLyBObyBkYXRhCiAgICBzYW1wbGUuY2xvdWRfbWFzayAhPT0gMSAvLyBDbG91ZHkgb3IgaW52YWxpZCBjb25kaXRpb24KICApIHsKICAgIHJldHVybiBbMCwgMCwgMCwgMF07IC8vIFRyYW5zcGFyZW50CiAgfQoKICAvLyBDYWxjdWxhdGUgdmVnZXRhdGlvbiBpbmRleAogIGNvbnN0IHZpID0gY2FsY3VsYXRlVkkoc2FtcGxlLm5pciwgc2FtcGxlLnJlZCwgVklfVFlQRSk7CgogIC8vIEhhbmRsZSBpbnZhbGlkIHZlZ2V0YXRpb24gaW5kZXgKICBpZiAodmkgPT09IElOVkFMSURfVkkpIHsKICAgIHJldHVybiBbMCwgMCwgMCwgMF07IC8vIFRyYW5zcGFyZW50CiAgfQoKICAvLyBDaGVjayBpZiBWSSBpcyBiZWxvdyB0aGUgdGhyZXNob2xkCiAgaWYgKHZpIDwgVklfVEhSRVNIT0xEKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOyAvLyBUcmFuc3BhcmVudAogIH0KCiAgLy8gQ2xpcCBhbmQgc2NhbGUgVkkKICBjb25zdCB2aTEwMCA9IGNsaXBBbmRTY2FsZVZJKHZpKTsKCiAgLy8gUmVjbGFzc2lmeSBWSTEwMCB0byBGVkkgY2xhc3NlcwogIGNvbnN0IGZ2aUNsYXNzID0gTWF0aC5taW4oCiAgICBOVU1fQ0xBU1NFUywKICAgIE1hdGguY2VpbCgodmkxMDAgLyAoVklfU0NBTEUgKyAxKSkgKiBOVU1fQ0xBU1NFUykKICApOwoKICBjb25zdCBjb2xvciA9IGZ2aVBhbGV0dGVbZnZpQ2xhc3NdOwogIHJldHVybiBbLi4uY29sb3IsIDFdOyAvLyBGdWxseSBvcGFxdWUKfQo%3D&datasetId=3f605f75-86c4-411a-b4ae-01c896f0e54e&fromTime=2023-04-18T00%3A00%3A00.000Z&toTime=2023-04-18T23%3A59%3A59.999Z&demSource3D=%22MAPZEN%22#custom-script){:target="_blank"} + + The example data is using Planet Sandox data. This data is restricted to Sentinel Hub users with active paid plans. If you are already a Planet Customer, see [here](https://community.planet.com/sentinel-hub-81/access-new-tools-for-analyzing-your-planet-data-on-sentinel-hub-732) on how to get access. + +## General description + +This script calculates the Forest Vitality Index (FVI) based on vegetation indices (NDVI or SAVI) and classifies forest areas into 15 distinct classes representing different vitality levels. The FVI provides a standardized measure of forest health that can be used for monitoring and management. + +## Details of the script + +The Forest Vitality Index is calculated as follows: +1. Calculate vegetation index (NDVI or SAVI) from red and NIR bands +2. Clip the vegetation index to a predefined range (0.15-0.85) +3. Scale the clipped vegetation index to a 1-100 range +4. Classify the scaled values into 15 forest vitality classes +5. Apply a color palette to visualize the classes + +The script includes cloud masking and data validation checks to ensure only valid pixels are processed. This version of the script only uses vegetation index thresholds to mask vegetated pixels. Please check the script [Forest Vitality Index with Canopy Cover Filter (FVI-CC)](../forest_vitality_index_canopy_cover/index.md) for a forest mask based on the Canopy Cover layer from the Forest Carbon Monitoring dataset. + +## Description of representative images + +The FVI visualizes forest vitality with a color palette ranging from red/orange (low vitality) to green (high vitality): +- Classes 1-3: Cyan to blue - Very low vitality +- Classes 4-7: Red to orange - Low vitality +- Classes 8-10: Yellow to lime - Medium vitality +- Classes 11-15: Green to dark green - High vitality + +A visualization of FVI over Cestas, France (April 2023): + +![FVI classes over Cestas](fig/fig1.png) diff --git a/planet/analysis_ready_planetscope/forest_vitality_index/script.js b/planet/analysis_ready_planetscope/forest_vitality_index/script.js new file mode 100644 index 00000000..293363f9 --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_index/script.js @@ -0,0 +1,109 @@ +//VERSION=3 +/* + * Forest Vitality Index (FVI) Evaluation Script + * This script calculates FVI based on vegetation indices (NDVI or SAVI) + * and classifies pixels into 15 classes representing different vitality levels. + * + * Parameters: + * - VI_TYPE: "NDVI" or "SAVI" (Normalized Difference Vegetation Index or Soil Adjusted VI) + * - VI_MIN, VI_MAX: Range for clipping vegetation index values + * - VI_THRESHOLD: Minimum VI value to be considered vegetation + */ + +function setup() { + return { + input: ["red", "nir", "dataMask", "cloud_mask"], + output: { bands: 4 }, // RGBA + }; +} + +// Configuration parameters +const VI_TYPE = "NDVI"; // Choose "NDVI" or "SAVI" +const VI_MIN = 0.15; // Minimum VI value for clipping +const VI_MAX = 0.85; // Maximum VI value for clipping +const VI_THRESHOLD = 0.5; // Threshold for vegetation masking +const NUM_CLASSES = 15; // Number of FVI classes +const INVALID_VI = -9999; // Value for invalid vegetation index +const SAVI_L = 0.5; // Soil adjustment factor for SAVI +const VI_SCALE = 99; // Scale factor for VI values (resulting in range 1-100) + +// Color palette - vitality classes from lowest (1) to highest (15) +const fviPalette = [ + [0.0, 0.0, 0.0], // 0 – black (no data) + [0.53, 1.0, 1.0], // 1 – cyan (extremely low vitality) + [0.68, 1.0, 1.0], // 2 – light cyan + [0.0, 0.33, 1.0], // 3 – blue + [0.33, 0.0, 0.0], // 4 – dark red + [0.67, 0.0, 0.0], // 5 – red + [1.0, 0.5, 0.0], // 6 – orange + [1.0, 0.75, 0.0], // 7 – orange-yellow + [1.0, 1.0, 0.0], // 8 – yellow + [0.8, 1.0, 0.0], // 9 – yellow-green + [0.53, 1.0, 0.0], // 10 – lime green + [0.0, 1.0, 0.0], // 11 – green + [0.0, 0.8, 0.0], // 12 – dark green + [0.0, 0.6, 0.0], // 13 – deeper green + [0.0, 0.4, 0.0], // 14 – forest green + [0.0, 0.25, 0.0], // 15 – very dark green (highest vitality) +]; + +// Calculate vegetation index (VI) based on type +function calculateVI(nir, red, type) { + if (nir === undefined || red === undefined) return INVALID_VI; + if (nir + red === 0) return INVALID_VI; // Avoid division by zero + + if (type === "NDVI") { + return (nir - red) / (nir + red); + } else if (type === "SAVI") { + return ((nir - red) / (nir + red + SAVI_L)) * (1 + SAVI_L); + } else { + return INVALID_VI; + } +} + +// Clip VI to min/max range and scale to 1-100 range +function clipAndScaleVI(vi) { + const clippedVI = Math.max(VI_MIN, Math.min(vi, VI_MAX)); + return Math.round(((clippedVI - VI_MIN) / (VI_MAX - VI_MIN)) * VI_SCALE) + 1; +} + +// Main function to evaluate each pixel +function evaluatePixel(sample) { + // Check for undefined inputs + if (sample.red === undefined || sample.nir === undefined) { + return [0, 0, 0, 0]; // Transparent + } + + // Check cloud mask and data mask + if ( + sample.dataMask === 0 || // No data + sample.cloud_mask !== 1 // Cloudy or invalid condition + ) { + return [0, 0, 0, 0]; // Transparent + } + + // Calculate vegetation index + const vi = calculateVI(sample.nir, sample.red, VI_TYPE); + + // Handle invalid vegetation index + if (vi === INVALID_VI) { + return [0, 0, 0, 0]; // Transparent + } + + // Check if VI is below the threshold + if (vi < VI_THRESHOLD) { + return [0, 0, 0, 0]; // Transparent + } + + // Clip and scale VI + const vi100 = clipAndScaleVI(vi); + + // Reclassify VI100 to FVI classes + const fviClass = Math.min( + NUM_CLASSES, + Math.ceil((vi100 / (VI_SCALE + 1)) * NUM_CLASSES) + ); + + const color = fviPalette[fviClass]; + return [...color, 1]; // Fully opaque +} diff --git a/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/fig/fig1.png b/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/fig/fig1.png new file mode 100644 index 00000000..232a83e4 Binary files /dev/null and b/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/fig/fig1.png differ diff --git a/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/index.md b/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/index.md new file mode 100644 index 00000000..7e87f7d7 --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/index.md @@ -0,0 +1,45 @@ +--- +title: Forest Vitality Index with Canopy Cover Filter (FVI-CC) +parent: Analysis Ready Planetscope +grand_parent: Planet +layout: script +nav_exclude: true +scripts: + - [Visualization, script.js] +additionalQueryParams: + - - themeId + - PLANET_SANDBOX +--- + +## Evaluate and visualize + - [EO Browser](https://apps.sentinel-hub.com/eo-browser/?zoom=14&lat=44.74116&lng=-0.68435&themeId=PLANET_SANDBOX&visualizationUrl=U2FsdGVkX18sUeNI%2FMf6Q%2BSgM5Q0Do4f8fUeu3ckT3yN%2Bq8J1FinuNyQEXIiSXwBX%2Bpx7IGVGvppEfEcSHTxMX%2BxEyWmkNP8tnv9%2BukDaXNQFcTw%2BE7AiX1Z3jvEjoPQ&evalscript=Ly9WRVJTSU9OPTMKLyogCiAqIEZvcmVzdCBWaXRhbGl0eSBJbmRleCB3aXRoIENhbm9weSBDb3ZlciBGaWx0ZXIgKEZWSS1DQykgRXZhbHVhdGlvbiBTY3JpcHQKICogVGhpcyBzY3JpcHQgY2FsY3VsYXRlcyBGVkkgYmFzZWQgb24gdmVnZXRhdGlvbiBpbmRpY2VzIChORFZJIG9yIFNBVkkpCiAqIGFuZCBmaWx0ZXJzIHJlc3VsdHMgYmFzZWQgb24gYSBjYW5vcHkgY292ZXIgdGhyZXNob2xkLgogKiBJdCBjbGFzc2lmaWVzIHBpeGVscyBpbnRvIDE1IGNsYXNzZXMgcmVwcmVzZW50aW5nIGRpZmZlcmVudCB2aXRhbGl0eSBsZXZlbHMuCiAqIAogKiBQYXJhbWV0ZXJzOgogKiAtIFZJX1RZUEU6ICJORFZJIiBvciAiU0FWSSIgKE5vcm1hbGl6ZWQgRGlmZmVyZW5jZSBWZWdldGF0aW9uIEluZGV4IG9yIFNvaWwgQWRqdXN0ZWQgVkkpCiAqIC0gQ0NfTUlOOiBNaW5pbXVtIGNhbm9weSBjb3ZlciBwZXJjZW50YWdlIHRvIGNvbnNpZGVyIHZhbGlkIGZvcmVzdCBkYXRhCiAqIC0gVklfTUlOLCBWSV9NQVg6IFJhbmdlIGZvciBjbGlwcGluZyB2ZWdldGF0aW9uIGluZGV4IHZhbHVlcwogKi8KCmZ1bmN0aW9uIHNldHVwKCkgewogIHJldHVybiB7CiAgICBpbnB1dDogWwogICAgICB7CiAgICAgICAgZGF0YXNvdXJjZTogImFycHMiLAogICAgICAgIGJhbmRzOiBbInJlZCIsICJuaXIiLCAiZGF0YU1hc2siLCAiY2xvdWRfbWFzayJdLAogICAgICB9LAogICAgICB7CiAgICAgICAgZGF0YXNvdXJjZTogImNhbm9weV9jb3ZlciIsCiAgICAgICAgYmFuZHM6IFsiQ0MiLCAiZGF0YU1hc2siXSwKICAgICAgfSwKICAgIF0sCiAgICBvdXRwdXQ6IHsgYmFuZHM6IDQgfSwKICB9Owp9CgovLyBDb25maWd1cmF0aW9uIHBhcmFtZXRlcnMKY29uc3QgVklfVFlQRSA9ICJORFZJIjsgLy8gQ2hvb3NlICJORFZJIiBvciAiU0FWSSIKY29uc3QgQ0NfTUlOID0gMjU7IC8vIE1pbmltdW0gY2Fub3B5IGNvdmVyIGZvciB2YWxpZCBkYXRhCmNvbnN0IFZJX01JTiA9IDAuMTU7IC8vIE1pbmltdW0gVkkgdmFsdWUgZm9yIGNsaXBwaW5nCmNvbnN0IFZJX01BWCA9IDAuODU7IC8vIE1heGltdW0gVkkgdmFsdWUgZm9yIGNsaXBwaW5nCmNvbnN0IE5VTV9DTEFTU0VTID0gMTU7ICAgIC8vIE51bWJlciBvZiBGVkkgY2xhc3Nlcwpjb25zdCBTQVZJX0wgPSAwLjU7ICAgICAgIC8vIFNvaWwgYWRqdXN0bWVudCBmYWN0b3IgZm9yIFNBVkkKY29uc3QgSU5WQUxJRF9WSSA9IC05OTk5OyAgLy8gVmFsdWUgZm9yIGludmFsaWQgdmVnZXRhdGlvbiBpbmRleApjb25zdCBWSV9TQ0FMRSA9IDk5OyAgICAgIC8vIFNjYWxlIGZhY3RvciBmb3IgVkkgdmFsdWVzIChyZXN1bHRpbmcgaW4gcmFuZ2UgMS0xMDApCgovLyBDb2xvciBwYWxldHRlIC0gdml0YWxpdHkgY2xhc3NlcyBmcm9tIGxvd2VzdCAoMSkgdG8gaGlnaGVzdCAoMTUpCmNvbnN0IGZ2aVBhbGV0dGUgPSBbCiAgWzAuMCwgMC4wLCAwLjBdLCAgIC8vIDAg4oCTIGJsYWNrIChubyBkYXRhKQogIFswLjUzLCAxLjAsIDEuMF0sICAvLyAxIOKAkyBjeWFuIChleHRyZW1lbHkgbG93IHZpdGFsaXR5KQogIFswLjY4LCAxLjAsIDEuMF0sICAvLyAyIOKAkyBsaWdodCBjeWFuCiAgWzAuMCwgMC4zMywgMS4wXSwgIC8vIDMg4oCTIGJsdWUKICBbMC4zMywgMC4wLCAwLjBdLCAgLy8gNCDigJMgZGFyayByZWQKICBbMC42NywgMC4wLCAwLjBdLCAgLy8gNSDigJMgcmVkCiAgWzEuMCwgMC41LCAwLjBdLCAgIC8vIDYg4oCTIG9yYW5nZQogIFsxLjAsIDAuNzUsIDAuMF0sICAvLyA3IOKAkyBvcmFuZ2UteWVsbG93CiAgWzEuMCwgMS4wLCAwLjBdLCAgIC8vIDgg4oCTIHllbGxvdwogIFswLjgsIDEuMCwgMC4wXSwgICAvLyA5IOKAkyB5ZWxsb3ctZ3JlZW4KICBbMC41MywgMS4wLCAwLjBdLCAgLy8gMTAg4oCTIGxpbWUgZ3JlZW4KICBbMC4wLCAxLjAsIDAuMF0sICAgLy8gMTEg4oCTIGdyZWVuCiAgWzAuMCwgMC44LCAwLjBdLCAgIC8vIDEyIOKAkyBkYXJrIGdyZWVuCiAgWzAuMCwgMC42LCAwLjBdLCAgIC8vIDEzIOKAkyBkZWVwZXIgZ3JlZW4KICBbMC4wLCAwLjQsIDAuMF0sICAgLy8gMTQg4oCTIGZvcmVzdCBncmVlbgogIFswLjAsIDAuMjUsIDAuMF0sICAvLyAxNSDigJMgdmVyeSBkYXJrIGdyZWVuIChoaWdoZXN0IHZpdGFsaXR5KQpdOwoKLy8gQ2FsY3VsYXRlIHZlZ2V0YXRpb24gaW5kZXggKFZJKSBiYXNlZCBvbiB0eXBlCmZ1bmN0aW9uIGNhbGN1bGF0ZVZJKG5pciwgcmVkLCB0eXBlKSB7CiAgaWYgKG5pciA9PT0gdW5kZWZpbmVkIHx8IHJlZCA9PT0gdW5kZWZpbmVkKSByZXR1cm4gSU5WQUxJRF9WSTsKICBpZiAobmlyICsgcmVkID09PSAwKSByZXR1cm4gSU5WQUxJRF9WSTsgLy8gQXZvaWQgZGl2aXNpb24gYnkgemVybwoKICBpZiAodHlwZSA9PT0gIk5EVkkiKSB7CiAgICByZXR1cm4gKG5pciAtIHJlZCkgLyAobmlyICsgcmVkKTsKICB9IGVsc2UgaWYgKHR5cGUgPT09ICJTQVZJIikgewogICAgcmV0dXJuICgobmlyIC0gcmVkKSAvIChuaXIgKyByZWQgKyBTQVZJX0wpKSAqICgxICsgU0FWSV9MKTsKICB9IGVsc2UgewogICAgcmV0dXJuIElOVkFMSURfVkk7CiAgfQp9CgovLyBDbGlwIFZJIHRvIG1pbi9tYXggcmFuZ2UgYW5kIHNjYWxlIHRvIDEtMTAwIHJhbmdlCmZ1bmN0aW9uIGNsaXBBbmRTY2FsZVZJKHZpKSB7CiAgY29uc3QgY2xpcHBlZFZJID0gTWF0aC5tYXgoVklfTUlOLCBNYXRoLm1pbih2aSwgVklfTUFYKSk7CiAgcmV0dXJuIE1hdGgucm91bmQoKChjbGlwcGVkVkkgLSBWSV9NSU4pIC8gKFZJX01BWCAtIFZJX01JTikpICogVklfU0NBTEUpICsgMTsKfQoKLy8gTWFpbiBmdW5jdGlvbiB0byBldmFsdWF0ZSBlYWNoIHBpeGVsCmZ1bmN0aW9uIGV2YWx1YXRlUGl4ZWwoc2FtcGxlcykgewogIC8vIENoZWNrIGZvciB1bmRlZmluZWQgaW5wdXRzCiAgaWYgKCFzYW1wbGVzLmFycHMgfHwgIXNhbXBsZXMuY2Fub3B5X2NvdmVyKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOyAvLyBUcmFuc3BhcmVudAogIH0KICAKICBjb25zdCBwc19hcmQgPSBzYW1wbGVzLmFycHNbMF07CiAgY29uc3QgY2Fub3B5ID0gc2FtcGxlcy5jYW5vcHlfY292ZXJbMF07CgogIC8vIENoZWNrIGNhbm9weSBjb3ZlciBhbmQgZGF0YSBtYXNrcwogIGlmICgKICAgICFjYW5vcHkgfHwgCiAgICAhcHNfYXJkIHx8IAogICAgY2Fub3B5LkNDIDw9IENDX01JTiB8fCAKICAgIHBzX2FyZC5kYXRhTWFzayA9PT0gMCB8fCAKICAgIHBzX2FyZC5jbG91ZF9tYXNrICE9PSAxCiAgKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOyAvLyBUcmFuc3BhcmVudAogIH0KCiAgLy8gQ2FsY3VsYXRlIHZlZ2V0YXRpb24gaW5kZXgKICBjb25zdCB2aSA9IGNhbGN1bGF0ZVZJKHBzX2FyZC5uaXIsIHBzX2FyZC5yZWQsIFZJX1RZUEUpOwoKICAvLyBIYW5kbGUgaW52YWxpZCB2ZWdldGF0aW9uIGluZGV4CiAgaWYgKHZpID09PSBJTlZBTElEX1ZJKSB7CiAgICByZXR1cm4gWzAsIDAsIDAsIDBdOyAvLyBUcmFuc3BhcmVudAogIH0KCiAgLy8gQ2xpcCBhbmQgc2NhbGUgVkkKICBjb25zdCB2aTEwMCA9IGNsaXBBbmRTY2FsZVZJKHZpKTsKCiAgLy8gUmVjbGFzc2lmeSBWSTEwMCB0byBGVkkgY2xhc3NlcwogIGNvbnN0IGZ2aUNsYXNzID0gTWF0aC5taW4oCiAgICBOVU1fQ0xBU1NFUywKICAgIE1hdGguY2VpbCgodmkxMDAgLyAoVklfU0NBTEUgKyAxKSkgKiBOVU1fQ0xBU1NFUykKICApOwoKICBjb25zdCBjb2xvciA9IGZ2aVBhbGV0dGVbZnZpQ2xhc3NdOwogIHJldHVybiBbLi4uY29sb3IsIDFdOyAvLyBGdWxseSBvcGFxdWUKfQo%3D&datasetId=3f605f75-86c4-411a-b4ae-01c896f0e54e&fromTime=2023-04-18T00%3A00%3A00.000Z&toTime=2023-04-18T23%3A59%3A59.999Z&demSource3D=%22MAPZEN%22&dataFusion=%5B%7B%22id%22%3A%22CUSTOM%22%2C%22alias%22%3A%22arps%22%2C%22additionalParameters%22%3A%7B%22collectionId%22%3A%223f605f75-86c4-411a-b4ae-01c896f0e54e%22%2C%22subType%22%3Anull%2C%22locationId%22%3A%22aws-eu-central-1%22%7D%7D%2C%7B%22id%22%3A%22CUSTOM%22%2C%22alias%22%3A%22canopy_cover%22%2C%22additionalParameters%22%3A%7B%22collectionId%22%3A%22ca501757-cf8e-43a8-b1a4-1aa59ae22425%22%2C%22subType%22%3A%22BYOC%22%2C%22locationId%22%3A%22aws-eu-central-1%22%7D%2C%22timespan%22%3A%5B%222023-03-21T00%3A00%3A00.000Z%22%2C%222023-03-21T23%3A59%3A59.999Z%22%5D%7D%5D#custom-script){:target="_blank"} + + The example data is using Planet Sandox data. This data is restricted to Sentinel Hub users with active paid plans. If you are already a Planet Customer, see [here](https://community.planet.com/sentinel-hub-81/access-new-tools-for-analyzing-your-planet-data-on-sentinel-hub-732) on how to get access. + +## General description + +This script enhances the [Forest Vitality Index (FVI)](../forest_vitality_index/index.md) by incorporating a canopy cover filter. It combines Analysis Ready PlanetScope imagery with the Canopy Cover layer from the Forest Carbon Monitoring dataset to produce a more accurate forest vitality assessment that focuses exclusively on areas with significant tree coverage. By filtering with a minimum canopy cover threshold (default 25%), the script removes non-forest or sparse vegetation areas from the analysis. + +## Details of the script + +The FVI-CC processing workflow includes: +1. Filter pixels based on canopy cover percentage (>25%) +2. Calculate vegetation index (users can choose between NDVI or SAVI) from red and NIR bands for remaining pixels +3. Clip the vegetation index to a predefined range (0.15-0.85) +4. Scale the clipped vegetation index to a 1-100 range +5. Classify the scaled values into 15 forest vitality classes +6. Apply a color palette to visualize the classes + +The script includes cloud masking and data validation checks to ensure only valid forest pixels are processed. + +## Description of representative images + +The FVI-CC visualizes forest vitality with a color palette ranging from red/orange (low vitality) to green (high vitality), while masking non-forest areas: +- Classes 1-3: Cyan to blue - Very low vitality +- Classes 4-7: Red to orange - Low vitality +- Classes 8-10: Yellow to lime - Medium vitality +- Classes 11-15: Green to dark green - High vitality + +A visualization of FVI with Canopy Cover Filter (FVI-CC) over Cestas, France (April 2023): + +![FVI-CC classes over Cestas](fig/fig1.png) diff --git a/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/script.js b/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/script.js new file mode 100644 index 00000000..3242cda8 --- /dev/null +++ b/planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/script.js @@ -0,0 +1,120 @@ +//VERSION=3 +/* + * Forest Vitality Index with Canopy Cover Filter (FVI-CC) Evaluation Script + * This script calculates FVI based on vegetation indices (NDVI or SAVI) + * and filters results based on a canopy cover threshold. + * It classifies pixels into 15 classes representing different vitality levels. + * + * Parameters: + * - VI_TYPE: "NDVI" or "SAVI" (Normalized Difference Vegetation Index or Soil Adjusted VI) + * - CC_MIN: Minimum canopy cover percentage to consider valid forest data + * - VI_MIN, VI_MAX: Range for clipping vegetation index values + */ + +function setup() { + return { + input: [ + { + datasource: "arps", + bands: ["red", "nir", "dataMask", "cloud_mask"], + }, + { + datasource: "canopy_cover", + bands: ["CC", "dataMask"], + }, + ], + output: { bands: 4 }, + }; +} + +// Configuration parameters +const VI_TYPE = "NDVI"; // Choose "NDVI" or "SAVI" +const CC_MIN = 25; // Minimum canopy cover for valid data +const VI_MIN = 0.15; // Minimum VI value for clipping +const VI_MAX = 0.85; // Maximum VI value for clipping +const NUM_CLASSES = 15; // Number of FVI classes +const SAVI_L = 0.5; // Soil adjustment factor for SAVI +const INVALID_VI = -9999; // Value for invalid vegetation index +const VI_SCALE = 99; // Scale factor for VI values (resulting in range 1-100) + +// Color palette - vitality classes from lowest (1) to highest (15) +const fviPalette = [ + [0.0, 0.0, 0.0], // 0 – black (no data) + [0.53, 1.0, 1.0], // 1 – cyan (extremely low vitality) + [0.68, 1.0, 1.0], // 2 – light cyan + [0.0, 0.33, 1.0], // 3 – blue + [0.33, 0.0, 0.0], // 4 – dark red + [0.67, 0.0, 0.0], // 5 – red + [1.0, 0.5, 0.0], // 6 – orange + [1.0, 0.75, 0.0], // 7 – orange-yellow + [1.0, 1.0, 0.0], // 8 – yellow + [0.8, 1.0, 0.0], // 9 – yellow-green + [0.53, 1.0, 0.0], // 10 – lime green + [0.0, 1.0, 0.0], // 11 – green + [0.0, 0.8, 0.0], // 12 – dark green + [0.0, 0.6, 0.0], // 13 – deeper green + [0.0, 0.4, 0.0], // 14 – forest green + [0.0, 0.25, 0.0], // 15 – very dark green (highest vitality) +]; + +// Calculate vegetation index (VI) based on type +function calculateVI(nir, red, type) { + if (nir === undefined || red === undefined) return INVALID_VI; + if (nir + red === 0) return INVALID_VI; // Avoid division by zero + + if (type === "NDVI") { + return (nir - red) / (nir + red); + } else if (type === "SAVI") { + return ((nir - red) / (nir + red + SAVI_L)) * (1 + SAVI_L); + } else { + return INVALID_VI; + } +} + +// Clip VI to min/max range and scale to 1-100 range +function clipAndScaleVI(vi) { + const clippedVI = Math.max(VI_MIN, Math.min(vi, VI_MAX)); + return Math.round(((clippedVI - VI_MIN) / (VI_MAX - VI_MIN)) * VI_SCALE) + 1; +} + +// Main function to evaluate each pixel +function evaluatePixel(samples) { + // Check for undefined inputs + if (!samples.arps || !samples.canopy_cover) { + return [0, 0, 0, 0]; // Transparent + } + + const ps_ard = samples.arps[0]; + const canopy = samples.canopy_cover[0]; + + // Check canopy cover and data masks + if ( + !canopy || + !ps_ard || + canopy.CC <= CC_MIN || + ps_ard.dataMask === 0 || + ps_ard.cloud_mask !== 1 + ) { + return [0, 0, 0, 0]; // Transparent + } + + // Calculate vegetation index + const vi = calculateVI(ps_ard.nir, ps_ard.red, VI_TYPE); + + // Handle invalid vegetation index + if (vi === INVALID_VI) { + return [0, 0, 0, 0]; // Transparent + } + + // Clip and scale VI + const vi100 = clipAndScaleVI(vi); + + // Reclassify VI100 to FVI classes + const fviClass = Math.min( + NUM_CLASSES, + Math.ceil((vi100 / (VI_SCALE + 1)) * NUM_CLASSES) + ); + + const color = fviPalette[fviClass]; + return [...color, 1]; // Fully opaque +} diff --git a/planet/analysis_ready_planetscope/index.md b/planet/analysis_ready_planetscope/index.md index ca214d1d..6e62d344 100644 --- a/planet/analysis_ready_planetscope/index.md +++ b/planet/analysis_ready_planetscope/index.md @@ -15,3 +15,7 @@ Analysis-Ready PlanetScope combines the monitoring benefits of daily 3 m spatial - [NDVI]({% link planet/analysis_ready_planetscope/ndvi/index.md %}) - [Cloud Classification]({% link planet/analysis_ready_planetscope/cloud_mask_classification/index.md %}) - [Cloud Masked True Color]({% link planet/analysis_ready_planetscope/true_color_cloud_masked/index.md %}) + - [Forest Vitality Index (forest mask: VI threshold)]({% link planet/analysis_ready_planetscope/forest_vitality_index/index.md %}) + - [Forest Vitality Index (forest mask: Canopy Cover layer)]({% link planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/index.md %}) + - [Forest Vitality Change Index - Gain]({% link planet/analysis_ready_planetscope/forest_vitality_change_index_gain/index.md %}) + - [Forest Vitality Change Index - Loss]({% link planet/analysis_ready_planetscope/forest_vitality_change_index_loss/index.md %}) diff --git a/planet/skysat/index.md b/planet/skysat/index.md index 20d6cf96..5f8d0c65 100644 --- a/planet/skysat/index.md +++ b/planet/skysat/index.md @@ -1,7 +1,7 @@ --- layout: default title: SkySat -nav_order: 2 +nav_order: 3 parent: Planet --- diff --git a/planetary-variables/forest-carbon-monitoring/index.md b/planetary-variables/forest-carbon-monitoring/index.md index 0d04556d..f12d3791 100644 --- a/planetary-variables/forest-carbon-monitoring/index.md +++ b/planetary-variables/forest-carbon-monitoring/index.md @@ -12,3 +12,6 @@ The Forest Carbon Monitoring product consists of a bundle of data resources: can - [Aboveground Carbon Density]({% link planetary-variables/forest-carbon-monitoring/aboveground-carbon-density/index.md %}) - [Canopy Height]({% link planetary-variables/forest-carbon-monitoring/canopy-height/index.md %}) - [Canopy Cover]({% link planetary-variables/forest-carbon-monitoring/canopy-cover/index.md %}) +- [Forest Vitality Index (uses Analysis Ready PlanetScope)]({% link planet/analysis_ready_planetscope/forest_vitality_index_canopy_cover/index.md %}) +- [Forest Vitality Change Index - Gain (uses Analysis Ready PlanetScope)]({% link planet/analysis_ready_planetscope/forest_vitality_change_index_gain/index.md %}) +- [Forest Vitality Change Index - Loss (uses Analysis Ready PlanetScope)]({% link planet/analysis_ready_planetscope/forest_vitality_change_index_loss/index.md %})