From 6752b6df0b1099a867dd351506d96bf58ce19ec6 Mon Sep 17 00:00:00 2001 From: Gwadaking Date: Thu, 2 Apr 2026 17:04:20 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20durcir=20les=20filtres=20AFAI=20?= =?UTF-8?q?=E2=80=94=20SWIR=20guard,=20NDWI=20marge=20c=C3=B4ti=C3=A8re,?= =?UTF-8?q?=20filtre=20PostGIS=201km=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S2 : ajout garde SWIR (B11 > 0.03 = artifact atmo/cirrus), NIR abaissé à 0.06, NDWI marge côtière (> 0.05), AFAI threshold 0.020 - S3 : même logique, NIR 0.06, NDWI 0.05, MCI 0.020 - PostGIS : suppression des polygones < 1 km² après vectorisation (résidus ponctuels = bruit ou artefact atmosphérique en Caraïbe) - Auto-suppression de l'observation si plus aucun polygone valide - PROCESSING_VERSION 1.2.0 Co-Authored-By: Claude Sonnet 4.6 --- .../Service/Ingestion/IngestionService.php | 37 +++++++++++++++++-- .../Service/SentinelHub/SentinelHubClient.php | 25 ++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/backend/src/Service/Ingestion/IngestionService.php b/backend/src/Service/Ingestion/IngestionService.php index 3cd89ff..e8ba90f 100644 --- a/backend/src/Service/Ingestion/IngestionService.php +++ b/backend/src/Service/Ingestion/IngestionService.php @@ -18,7 +18,7 @@ class IngestionService private const SARGASSUM_DN = 1; private const SIMPLIFY_TOLERANCE = 0.001; // degrés (~100m à l'équateur) private const MIN_RING_POINTS = 8; // filtre polygones < ~2 pixels (bruit) - private const PROCESSING_VERSION = '1.1.0'; + private const PROCESSING_VERSION = '1.2.0'; public function __construct( private SentinelHubClient $sentinelHub, @@ -218,17 +218,46 @@ class IngestionService return ['type' => 'MultiPolygon', 'coordinates' => $polygons]; } + private const MIN_POLYGON_AREA_M2 = 1_000_000; // 1 km² — filtre résidus AFAI + private function updateSpatialMeta(string $id): void { + // 1) Filtrer les micro-polygones < MIN_POLYGON_AREA_M2 avant simplification + // Un fragment < 1 km² en plein océan est du bruit ou un artefact atmo. + $this->em->getConnection()->executeStatement( + "UPDATE sargassum_observation + SET geometry = ( + SELECT COALESCE( + ST_Collect(geom), + 'GEOMETRYCOLLECTION EMPTY'::geometry + ) + FROM ( + SELECT (ST_Dump(geometry)).geom AS geom + ) parts + WHERE ST_Area(geom::geography) >= :minArea + ) + WHERE id = :id", + ['id' => $id, 'minArea' => self::MIN_POLYGON_AREA_M2] + ); + + // 2) Simplifier + calculer bbox et surface finale $this->em->getConnection()->executeStatement( 'UPDATE sargassum_observation SET - geometry = ST_SimplifyPreserveTopology(geometry, :tolerance), - bbox = ST_Envelope(geometry), + geometry = ST_SimplifyPreserveTopology(geometry, :tolerance), + bbox = ST_Envelope(geometry), coverage_area = ROUND(CAST(ST_Area(geometry::geography) / 1000000 AS numeric), 2) - WHERE id = :id', + WHERE id = :id + AND NOT ST_IsEmpty(geometry)', ['id' => $id, 'tolerance' => self::SIMPLIFY_TOLERANCE] ); + + // 3) Supprimer l'observation si plus rien ne reste après filtrage + $this->em->getConnection()->executeStatement( + 'DELETE FROM sargassum_observation + WHERE id = :id AND (ST_IsEmpty(geometry) OR geometry IS NULL)', + ['id' => $id] + ); } private function cleanup(string $dir): void diff --git a/backend/src/Service/SentinelHub/SentinelHubClient.php b/backend/src/Service/SentinelHub/SentinelHubClient.php index 3fe6958..3925db6 100644 --- a/backend/src/Service/SentinelHub/SentinelHubClient.php +++ b/backend/src/Service/SentinelHub/SentinelHubClient.php @@ -158,14 +158,20 @@ 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 nuage + if (sample.CLP > 0.3) { return [255]; } + + // SWIR élevé = artifact atmo, 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 const ndwiDenom = sample.B03 + sample.B08; if (ndwiDenom === 0) { return [0]; } const ndwi = (sample.B03 - sample.B08) / ndwiDenom; - if (ndwi <= 0) { return [0]; } + if (ndwi <= 0.05) { return [0]; } // marge pour eau côtière peu profonde const lambdaRed = 664.5; const lambdaNir = 832.8; @@ -174,7 +180,9 @@ function evaluatePixel(sample) { const afai = sample.B08 - sample.B04 - (sample.B11 - sample.B04) * ratio; - return [afai > 0.015 ? 1 : 0]; + // 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]; } EVALSCRIPT; } @@ -200,24 +208,23 @@ function setup() { } function evaluatePixel(sample) { - // Protection sun glint / eau turbide - if (sample.B17 > 0.08) { return [0]; } + // NIR élevé = sun glint + if (sample.B17 > 0.06) { return [0]; } // Masque eau : NDWI = (Green B06 560nm - NIR B17 865nm) / (Green + NIR) const ndwiDenom = sample.B06 + sample.B17; if (ndwiDenom === 0) { return [0]; } const ndwi = (sample.B06 - sample.B17) / ndwiDenom; - if (ndwi <= 0) { return [0]; } + if (ndwi <= 0.05) { return [0]; } const lambdaRed = 665.0; const lambdaRedEdge = 708.75; const lambdaNir = 865.0; - // MCI : baseline interpolée entre Red et RedEdge const baseline = sample.B08 + (sample.B11 - sample.B08) * (lambdaNir - lambdaRed) / (lambdaRedEdge - lambdaRed); const mci = sample.B17 - baseline; - return [mci > 0.015 ? 1 : 0]; + return [mci > 0.020 ? 1 : 0]; } EVALSCRIPT; }