fix: filtrage brume de sable via ratio bleu/rouge + seuils AFAI/MCI rehaussés
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -234,7 +234,7 @@ class IngestionService
|
|||||||
return ['type' => 'MultiPolygon', 'coordinates' => $polygons];
|
return ['type' => 'MultiPolygon', 'coordinates' => $polygons];
|
||||||
}
|
}
|
||||||
|
|
||||||
private const MIN_POLYGON_AREA_M2 = 1_000_000; // 1 km² — filtre résidus AFAI
|
private const MIN_POLYGON_AREA_M2 = 3_000_000; // 3 km² — filtre bruit résiduel (aérosols, sun glint isolés)
|
||||||
|
|
||||||
private function updateSpatialMeta(string $id): void
|
private function updateSpatialMeta(string $id): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -144,77 +144,91 @@ class SentinelHubClient
|
|||||||
|
|
||||||
private function getAfaiBinaryEvalscript(): string
|
private function getAfaiBinaryEvalscript(): string
|
||||||
{
|
{
|
||||||
// Longueurs d'onde Sentinel-2 (nm) — constantes capteur
|
// Sentinel-2 — AFAI (Hu 2009) avec filtrage aérosol renforcé.
|
||||||
// B04 (Red) : 664.5 | B08 (NIR) : 832.8 | B11 (SWIR1) : 1613.7
|
|
||||||
//
|
//
|
||||||
// Seuil AFAI : 0.015 (relevé de 0.005)
|
// Problème principal : brume de sable saharienne (mars–août en Caraïbe)
|
||||||
// → 0.005 génère des faux positifs massifs (sun glint, eaux côtières turbides)
|
// → lève le bleu (B02 490nm) et crée de faux AFAI positifs.
|
||||||
// → 0.015 correspond à une densité minimale réelle de Sargassum fluitans/natans
|
// Solution : ratio B02/B04 élevé = signature aérosol, pas sargasse.
|
||||||
// selon Hu (2009), Wang & Hu (2016) : AFAI > 0.010-0.020 pour détection fiable
|
//
|
||||||
|
// Seuil AFAI relevé 0.020 → 0.025 pour meilleure spécificité sans
|
||||||
|
// sacrifier les événements réels (Wang & Hu 2016 : 0.020–0.030 recommandé).
|
||||||
return <<<'EVALSCRIPT'
|
return <<<'EVALSCRIPT'
|
||||||
//VERSION=3
|
//VERSION=3
|
||||||
function setup() {
|
function setup() {
|
||||||
return {
|
return {
|
||||||
input: [{ bands: ["B03", "B04", "B08", "B11", "CLP"], units: "REFLECTANCE" }],
|
input: [{ bands: ["B02", "B03", "B04", "B08", "B11", "CLP"], units: "REFLECTANCE" }],
|
||||||
output: { bands: 1, sampleType: "UINT8" }
|
output: { bands: 1, sampleType: "UINT8" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluatePixel(sample) {
|
function evaluatePixel(sample) {
|
||||||
if (sample.CLP > 0.4) { return [255]; } // nodata : nuage
|
|
||||||
|
|
||||||
// Masque nuage
|
// Masque nuage
|
||||||
if (sample.CLP > 0.3) { return [255]; }
|
if (sample.CLP > 0.3) { return [255]; }
|
||||||
|
|
||||||
// SWIR élevé = artifact atmo, cirrus fin, aérosol, eau côtière turbide
|
// SWIR élevé = cirrus fin, aérosol, eau côtière turbide
|
||||||
if (sample.B11 > 0.03) { return [0]; }
|
if (sample.B11 > 0.03) { return [0]; }
|
||||||
|
|
||||||
// NIR élevé = sun glint ou eau peu profonde
|
// NIR élevé = sun glint ou eau peu profonde
|
||||||
if (sample.B08 > 0.06) { return [0]; }
|
if (sample.B08 > 0.06) { return [0]; }
|
||||||
|
|
||||||
// Masque eau : NDWI = (Green - NIR) / (Green + NIR) — terre si <= 0
|
// Filtre brume de sable saharienne :
|
||||||
|
// - B02 absolu > 0.08 : bleu trop élevé pour de l'eau propre ou des sargasses
|
||||||
|
// - B02/B04 > 2.5 : ratio bleu/rouge typique des aérosols continentaux
|
||||||
|
// (sargasses : ratio ≈ 0.8–1.2 | poussière sahélienne : ratio ≈ 2.5–5)
|
||||||
|
if (sample.B02 > 0.08) { return [0]; }
|
||||||
|
if (sample.B04 > 0.001 && (sample.B02 / sample.B04) > 2.5) { return [0]; }
|
||||||
|
|
||||||
|
// Masque eau : NDWI = (Green - NIR) / (Green + NIR)
|
||||||
const ndwiDenom = sample.B03 + sample.B08;
|
const ndwiDenom = sample.B03 + sample.B08;
|
||||||
if (ndwiDenom === 0) { return [0]; }
|
if (ndwiDenom === 0) { return [0]; }
|
||||||
const ndwi = (sample.B03 - sample.B08) / ndwiDenom;
|
const ndwi = (sample.B03 - sample.B08) / ndwiDenom;
|
||||||
if (ndwi <= 0.05) { return [0]; } // marge pour eau côtière peu profonde
|
if (ndwi <= 0.05) { return [0]; }
|
||||||
|
|
||||||
|
// AFAI — Hu 2009
|
||||||
const lambdaRed = 664.5;
|
const lambdaRed = 664.5;
|
||||||
const lambdaNir = 832.8;
|
const lambdaNir = 832.8;
|
||||||
const lambdaSwir = 1613.7;
|
const lambdaSwir = 1613.7;
|
||||||
const ratio = (lambdaNir - lambdaRed) / (lambdaSwir - lambdaRed);
|
const ratio = (lambdaNir - lambdaRed) / (lambdaSwir - lambdaRed);
|
||||||
|
|
||||||
const afai = sample.B08 - sample.B04 - (sample.B11 - sample.B04) * ratio;
|
const afai = sample.B08 - sample.B04 - (sample.B11 - sample.B04) * ratio;
|
||||||
|
|
||||||
// Seuil 0.020 : calibré pour Sargassum fluitans/natans en Caraïbe
|
return [afai > 0.025 ? 1 : 0];
|
||||||
// (Hu 2009 recommande post-filtrage spatial que l'on fait via PostGIS)
|
|
||||||
return [afai > 0.020 ? 1 : 0];
|
|
||||||
}
|
}
|
||||||
EVALSCRIPT;
|
EVALSCRIPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evalscript Sentinel-3 OLCI — FAI (Floating Algae Index) via MCI.
|
* Evalscript Sentinel-3 OLCI — MCI (Maximum Chlorophyll Index) adapté sargasses.
|
||||||
*
|
*
|
||||||
* OLCI bands (nm) : Oa08=665 (Red), Oa11=708.75 (RedEdge), Oa17=865 (NIR)
|
* OLCI bands (nm) : B03=442.5 (Bleu), B06=560 (Vert), B08=665 (Rouge),
|
||||||
* MCI = Oa17 − Oa08 − (Oa11 − Oa08) × (865−665)/(708.75−665)
|
* B11=708.75 (RedEdge), B17=865 (NIR)
|
||||||
* Seuil empirique retenu : MCI > 0.008
|
*
|
||||||
|
* Limite fondamentale : S3 OLCI n'a pas de bande SWIR → impossible de filtrer
|
||||||
|
* les aérosols comme S2. La bande bleue B03 (442nm) compense partiellement :
|
||||||
|
* la poussière saharienne a un ratio bleu/rouge >> 2 (sargasses : ≈ 0.7–1.0).
|
||||||
|
*
|
||||||
|
* Seuil MCI relevé 0.020 → 0.030 pour compenser l'absence de SWIR.
|
||||||
*/
|
*/
|
||||||
private function getFaiBinaryEvalscriptS3(): string
|
private function getFaiBinaryEvalscriptS3(): string
|
||||||
{
|
{
|
||||||
// Seuil MCI : 0.015 (relevé de 0.008)
|
|
||||||
// → même logique que S2 : 0.008 trop sensible pour la Caraïbe
|
|
||||||
return <<<'EVALSCRIPT'
|
return <<<'EVALSCRIPT'
|
||||||
//VERSION=3
|
//VERSION=3
|
||||||
function setup() {
|
function setup() {
|
||||||
return {
|
return {
|
||||||
input: [{ bands: ["B06", "B08", "B11", "B17"], units: "REFLECTANCE" }],
|
input: [{ bands: ["B03", "B06", "B08", "B11", "B17"], units: "REFLECTANCE" }],
|
||||||
output: { bands: 1, sampleType: "UINT8" }
|
output: { bands: 1, sampleType: "UINT8" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluatePixel(sample) {
|
function evaluatePixel(sample) {
|
||||||
// NIR élevé = sun glint
|
// NIR élevé = sun glint
|
||||||
if (sample.B17 > 0.06) { return [0]; }
|
if (sample.B17 > 0.05) { return [0]; }
|
||||||
|
|
||||||
|
// Filtre brume de sable via bande bleue B03 (442.5nm) :
|
||||||
|
// - bleu absolu > 0.08 : atmosphère trop chargée en aérosols
|
||||||
|
// - B03/B08 (bleu/rouge) > 2.0 : signature poussière saharienne
|
||||||
|
// (sargasses : ratio ≈ 0.7 | brume sahélienne : ratio > 2)
|
||||||
|
if (sample.B03 > 0.08) { return [0]; }
|
||||||
|
if (sample.B08 > 0.001 && (sample.B03 / sample.B08) > 2.0) { return [0]; }
|
||||||
|
|
||||||
// Masque eau : NDWI = (Green B06 560nm - NIR B17 865nm) / (Green + NIR)
|
// Masque eau : NDWI = (Green B06 560nm - NIR B17 865nm) / (Green + NIR)
|
||||||
const ndwiDenom = sample.B06 + sample.B17;
|
const ndwiDenom = sample.B06 + sample.B17;
|
||||||
@@ -222,14 +236,15 @@ function evaluatePixel(sample) {
|
|||||||
const ndwi = (sample.B06 - sample.B17) / ndwiDenom;
|
const ndwi = (sample.B06 - sample.B17) / ndwiDenom;
|
||||||
if (ndwi <= 0.05) { return [0]; }
|
if (ndwi <= 0.05) { return [0]; }
|
||||||
|
|
||||||
|
// MCI — baseline interpolée entre rouge (B08) et redEdge (B11)
|
||||||
const lambdaRed = 665.0;
|
const lambdaRed = 665.0;
|
||||||
const lambdaRedEdge = 708.75;
|
const lambdaRedEdge = 708.75;
|
||||||
const lambdaNir = 865.0;
|
const lambdaNir = 865.0;
|
||||||
|
|
||||||
const baseline = sample.B08 + (sample.B11 - sample.B08) * (lambdaNir - lambdaRed) / (lambdaRedEdge - lambdaRed);
|
const baseline = sample.B08 + (sample.B11 - sample.B08) * (lambdaNir - lambdaRed) / (lambdaRedEdge - lambdaRed);
|
||||||
const mci = sample.B17 - baseline;
|
const mci = sample.B17 - baseline;
|
||||||
|
|
||||||
return [mci > 0.020 ? 1 : 0];
|
// Seuil 0.030 (vs 0.020) : compense l'absence de SWIR pour meilleure spécificité
|
||||||
|
return [mci > 0.030 ? 1 : 0];
|
||||||
}
|
}
|
||||||
EVALSCRIPT;
|
EVALSCRIPT;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user