fix: corriger les faux positifs AFAI — seuils, masque gdal, filtre bruit
Problème : le threshold AFAI 0.005 (S2) / MCI 0.008 (S3) trop bas classifie la majorité des pixels océan comme sargasse → gros rectangle orange sur des zones sans sargasses. Corrections : - S2 AFAI threshold 0.005 → 0.015 (littérature : Hu 2009, Wang & Hu 2016) - S3 MCI threshold 0.008 → 0.015 - Ajout protection sun glint : si NIR > 0.08 → pixel rejeté (S2 + S3) - Division NDWI sécurisée (dénominateur = 0 → return 0) - gdal_polygonize avec -mask (gdal_calc.py) : seuls les pixels DN=1 sont vectorisés, le reste n'existe pas dans le GeoJSON - Filtre MIN_RING_POINTS=8 : supprime les polygones < ~2px (bruit) - PROCESSING_VERSION 1.0.0 → 1.1.0 pour traçabilité ⚠️ Les observations en base avec processing_version='1.0.0' sont faussées — les supprimer avant de relancer l'ingestion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user