fix: durcir les filtres AFAI — SWIR guard, NDWI marge côtière, filtre PostGIS 1km²

- 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 <noreply@anthropic.com>
This commit is contained in:
Gwadaking
2026-04-02 17:04:20 -04:00
parent edca50200e
commit 6752b6df0b
2 changed files with 49 additions and 13 deletions

View File

@@ -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

View File

@@ -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;
}