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:
@@ -18,7 +18,7 @@ class IngestionService
|
|||||||
private const SARGASSUM_DN = 1;
|
private const SARGASSUM_DN = 1;
|
||||||
private const SIMPLIFY_TOLERANCE = 0.001; // degrés (~100m à l'équateur)
|
private const SIMPLIFY_TOLERANCE = 0.001; // degrés (~100m à l'équateur)
|
||||||
private const MIN_RING_POINTS = 8; // filtre polygones < ~2 pixels (bruit)
|
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(
|
public function __construct(
|
||||||
private SentinelHubClient $sentinelHub,
|
private SentinelHubClient $sentinelHub,
|
||||||
@@ -218,17 +218,46 @@ class IngestionService
|
|||||||
return ['type' => 'MultiPolygon', 'coordinates' => $polygons];
|
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
|
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(
|
$this->em->getConnection()->executeStatement(
|
||||||
'UPDATE sargassum_observation
|
'UPDATE sargassum_observation
|
||||||
SET
|
SET
|
||||||
geometry = ST_SimplifyPreserveTopology(geometry, :tolerance),
|
geometry = ST_SimplifyPreserveTopology(geometry, :tolerance),
|
||||||
bbox = ST_Envelope(geometry),
|
bbox = ST_Envelope(geometry),
|
||||||
coverage_area = ROUND(CAST(ST_Area(geometry::geography) / 1000000 AS numeric), 2)
|
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]
|
['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
|
private function cleanup(string $dir): void
|
||||||
|
|||||||
@@ -158,14 +158,20 @@ function setup() {
|
|||||||
function evaluatePixel(sample) {
|
function evaluatePixel(sample) {
|
||||||
if (sample.CLP > 0.4) { return [255]; } // nodata : nuage
|
if (sample.CLP > 0.4) { return [255]; } // nodata : nuage
|
||||||
|
|
||||||
// Protection sun glint : NIR élevé → reflet solaire ou eau turbide
|
// Masque nuage
|
||||||
if (sample.B08 > 0.08) { return [0]; }
|
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
|
// Masque eau : NDWI = (Green - NIR) / (Green + NIR) — terre si <= 0
|
||||||
const ndwiDenom = sample.B03 + sample.B08;
|
const ndwiDenom = sample.B03 + sample.B08;
|
||||||
if (ndwiDenom === 0) { return [0]; }
|
if (ndwiDenom === 0) { return [0]; }
|
||||||
const ndwi = (sample.B03 - sample.B08) / ndwiDenom;
|
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 lambdaRed = 664.5;
|
||||||
const lambdaNir = 832.8;
|
const lambdaNir = 832.8;
|
||||||
@@ -174,7 +180,9 @@ function evaluatePixel(sample) {
|
|||||||
|
|
||||||
const afai = sample.B08 - sample.B04 - (sample.B11 - sample.B04) * ratio;
|
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;
|
EVALSCRIPT;
|
||||||
}
|
}
|
||||||
@@ -200,24 +208,23 @@ function setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function evaluatePixel(sample) {
|
function evaluatePixel(sample) {
|
||||||
// Protection sun glint / eau turbide
|
// NIR élevé = sun glint
|
||||||
if (sample.B17 > 0.08) { return [0]; }
|
if (sample.B17 > 0.06) { return [0]; }
|
||||||
|
|
||||||
// Masque eau : NDWI = (Green B06 560nm - NIR B17 865nm) / (Green + NIR)
|
// Masque eau : NDWI = (Green B06 560nm - NIR B17 865nm) / (Green + NIR)
|
||||||
const ndwiDenom = sample.B06 + sample.B17;
|
const ndwiDenom = sample.B06 + sample.B17;
|
||||||
if (ndwiDenom === 0) { return [0]; }
|
if (ndwiDenom === 0) { return [0]; }
|
||||||
const ndwi = (sample.B06 - sample.B17) / ndwiDenom;
|
const ndwi = (sample.B06 - sample.B17) / ndwiDenom;
|
||||||
if (ndwi <= 0) { return [0]; }
|
if (ndwi <= 0.05) { return [0]; }
|
||||||
|
|
||||||
const lambdaRed = 665.0;
|
const lambdaRed = 665.0;
|
||||||
const lambdaRedEdge = 708.75;
|
const lambdaRedEdge = 708.75;
|
||||||
const lambdaNir = 865.0;
|
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 baseline = sample.B08 + (sample.B11 - sample.B08) * (lambdaNir - lambdaRed) / (lambdaRedEdge - lambdaRed);
|
||||||
const mci = sample.B17 - baseline;
|
const mci = sample.B17 - baseline;
|
||||||
|
|
||||||
return [mci > 0.015 ? 1 : 0];
|
return [mci > 0.020 ? 1 : 0];
|
||||||
}
|
}
|
||||||
EVALSCRIPT;
|
EVALSCRIPT;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user