diff --git a/backend/src/Service/Ingestion/IngestionService.php b/backend/src/Service/Ingestion/IngestionService.php index a277a74..3cd89ff 100644 --- a/backend/src/Service/Ingestion/IngestionService.php +++ b/backend/src/Service/Ingestion/IngestionService.php @@ -17,7 +17,8 @@ class IngestionService private const CLOUD_THRESHOLD = 60.0; private const SARGASSUM_DN = 1; private const SIMPLIFY_TOLERANCE = 0.001; // degrés (~100m à l'équateur) - private const PROCESSING_VERSION = '1.0.0'; + private const MIN_RING_POINTS = 8; // filtre polygones < ~2 pixels (bruit) + private const PROCESSING_VERSION = '1.1.0'; public function __construct( private SentinelHubClient $sentinelHub, @@ -133,16 +134,44 @@ class IngestionService private function polygonize(string $tifPath, string $geojsonPath): void { - $process = new Process([ + $maskPath = dirname($tifPath) . '/mask.tif'; + + // Créer un masque binaire : pixels DN=1 → valeur 1, tout le reste → nodata (0) + // Cela permet à gdal_polygonize de n'extraire QUE les pixels sargasses, + // évitant les faux positifs issus de la vectorisation de tout le tile. + $maskProcess = new Process([ + 'gdal_calc.py', + '-A', $tifPath, + '--outfile', $maskPath, + '--calc', 'A==1', + '--type', 'Byte', + '--NoDataValue', '0', + '--quiet', + ]); + $maskProcess->setTimeout(60); + $maskProcess->run(); + + $args = [ 'gdal_polygonize.py', $tifPath, '-f', 'GeoJSON', '-q', $geojsonPath, - ]); + ]; + + if ($maskProcess->isSuccessful() && file_exists($maskPath)) { + // Insérer -mask avant le fichier de sortie + array_splice($args, 1, 0, ['-mask', $maskPath]); + } + + $process = new Process($args); $process->setTimeout(120); $process->run(); + if (file_exists($maskPath)) { + unlink($maskPath); + } + if (!$process->isSuccessful()) { throw new ProcessFailedException($process); } @@ -169,10 +198,15 @@ class IngestionService $geom = $feature['geometry']; if ($geom['type'] === 'Polygon') { - $polygons[] = $geom['coordinates']; + // Ignorer les polygones trop petits (bruit : quelques pixels isolés) + if (count($geom['coordinates'][0] ?? []) >= self::MIN_RING_POINTS) { + $polygons[] = $geom['coordinates']; + } } elseif ($geom['type'] === 'MultiPolygon') { foreach ($geom['coordinates'] as $poly) { - $polygons[] = $poly; + if (count($poly[0] ?? []) >= self::MIN_RING_POINTS) { + $polygons[] = $poly; + } } } } diff --git a/backend/src/Service/SentinelHub/SentinelHubClient.php b/backend/src/Service/SentinelHub/SentinelHubClient.php index cdda715..3fe6958 100644 --- a/backend/src/Service/SentinelHub/SentinelHubClient.php +++ b/backend/src/Service/SentinelHub/SentinelHubClient.php @@ -141,6 +141,11 @@ class SentinelHubClient { // Longueurs d'onde Sentinel-2 (nm) — constantes capteur // B04 (Red) : 664.5 | B08 (NIR) : 832.8 | B11 (SWIR1) : 1613.7 + // + // 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 return <<<'EVALSCRIPT' //VERSION=3 function setup() { @@ -153,8 +158,13 @@ function setup() { function evaluatePixel(sample) { if (sample.CLP > 0.4) { return [255]; } // nodata : nuage + // Protection sun glint : NIR élevé → reflet solaire ou eau turbide + if (sample.B08 > 0.08) { return [0]; } + // Masque eau : NDWI = (Green - NIR) / (Green + NIR) — terre si <= 0 - const ndwi = (sample.B03 - sample.B08) / (sample.B03 + sample.B08); + const ndwiDenom = sample.B03 + sample.B08; + if (ndwiDenom === 0) { return [0]; } + const ndwi = (sample.B03 - sample.B08) / ndwiDenom; if (ndwi <= 0) { return [0]; } const lambdaRed = 664.5; @@ -164,7 +174,7 @@ function evaluatePixel(sample) { const afai = sample.B08 - sample.B04 - (sample.B11 - sample.B04) * ratio; - return [afai > 0.005 ? 1 : 0]; + return [afai > 0.015 ? 1 : 0]; } EVALSCRIPT; } @@ -178,6 +188,8 @@ EVALSCRIPT; */ 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() { @@ -188,8 +200,13 @@ function setup() { } function evaluatePixel(sample) { + // Protection sun glint / eau turbide + if (sample.B17 > 0.08) { return [0]; } + // Masque eau : NDWI = (Green B06 560nm - NIR B17 865nm) / (Green + NIR) - const ndwi = (sample.B06 - sample.B17) / (sample.B06 + sample.B17); + const ndwiDenom = sample.B06 + sample.B17; + if (ndwiDenom === 0) { return [0]; } + const ndwi = (sample.B06 - sample.B17) / ndwiDenom; if (ndwi <= 0) { return [0]; } const lambdaRed = 665.0; @@ -200,7 +217,7 @@ function evaluatePixel(sample) { const baseline = sample.B08 + (sample.B11 - sample.B08) * (lambdaNir - lambdaRed) / (lambdaRedEdge - lambdaRed); const mci = sample.B17 - baseline; - return [mci > 0.008 ? 1 : 0]; + return [mci > 0.015 ? 1 : 0]; } EVALSCRIPT; }