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:
Gwadaking
2026-04-22 22:52:40 -04:00
parent 4897397df8
commit 493fa35d54
2 changed files with 42 additions and 27 deletions

View File

@@ -234,7 +234,7 @@ class IngestionService
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
{

View File

@@ -144,77 +144,91 @@ class SentinelHubClient
private function getAfaiBinaryEvalscript(): string
{
// Longueurs d'onde Sentinel-2 (nm) — constantes capteur
// B04 (Red) : 664.5 | B08 (NIR) : 832.8 | B11 (SWIR1) : 1613.7
// Sentinel-2 — AFAI (Hu 2009) avec filtrage aérosol renforcé.
//
// Seuil AFAI : 0.015 (relevé de 0.005)
// → 0.005 génère des faux positifs massifs (sun glint, eaux côtières turbides)
// → 0.015 correspond à une densité minimale réelle de Sargassum fluitans/natans
// selon Hu (2009), Wang & Hu (2016) : AFAI > 0.010-0.020 pour détection fiable
// Problème principal : brume de sable saharienne (marsaoût en Caraïbe)
// → lève le bleu (B02 490nm) et crée de faux AFAI positifs.
// Solution : ratio B02/B04 élevé = signature aérosol, pas sargasse.
//
// Seuil AFAI relevé 0.020 → 0.025 pour meilleure spécificité sans
// sacrifier les événements réels (Wang & Hu 2016 : 0.0200.030 recommandé).
return <<<'EVALSCRIPT'
//VERSION=3
function setup() {
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" }
};
}
function evaluatePixel(sample) {
if (sample.CLP > 0.4) { return [255]; } // nodata : nuage
// Masque nuage
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]; }
// NIR élevé = sun glint ou eau peu profonde
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.81.2 | poussière sahélienne : ratio ≈ 2.55)
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;
if (ndwiDenom === 0) { return [0]; }
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 lambdaNir = 832.8;
const lambdaSwir = 1613.7;
const ratio = (lambdaNir - lambdaRed) / (lambdaSwir - lambdaRed);
const afai = sample.B08 - sample.B04 - (sample.B11 - sample.B04) * ratio;
// Seuil 0.020 : calibré pour Sargassum fluitans/natans en Caraïbe
// (Hu 2009 recommande post-filtrage spatial que l'on fait via PostGIS)
return [afai > 0.020 ? 1 : 0];
return [afai > 0.025 ? 1 : 0];
}
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)
* MCI = Oa17 Oa08 (Oa11 Oa08) × (865665)/(708.75665)
* Seuil empirique retenu : MCI > 0.008
* OLCI bands (nm) : B03=442.5 (Bleu), B06=560 (Vert), B08=665 (Rouge),
* B11=708.75 (RedEdge), B17=865 (NIR)
*
* 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.71.0).
*
* Seuil MCI relevé 0.020 → 0.030 pour compenser l'absence de SWIR.
*/
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'
//VERSION=3
function setup() {
return {
input: [{ bands: ["B06", "B08", "B11", "B17"], units: "REFLECTANCE" }],
input: [{ bands: ["B03", "B06", "B08", "B11", "B17"], units: "REFLECTANCE" }],
output: { bands: 1, sampleType: "UINT8" }
};
}
function evaluatePixel(sample) {
// 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)
const ndwiDenom = sample.B06 + sample.B17;
@@ -222,14 +236,15 @@ function evaluatePixel(sample) {
const ndwi = (sample.B06 - sample.B17) / ndwiDenom;
if (ndwi <= 0.05) { return [0]; }
// MCI — baseline interpolée entre rouge (B08) et redEdge (B11)
const lambdaRed = 665.0;
const lambdaRedEdge = 708.75;
const lambdaNir = 865.0;
const baseline = sample.B08 + (sample.B11 - sample.B08) * (lambdaNir - lambdaRed) / (lambdaRedEdge - lambdaRed);
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;
}