Extracción de cuerpos de agua con Sentinel-2 en Google Earth Engine
Extrae cuerpos de agua como polígonos a partir de Sentinel-2 y expórtalos como shapefile desde Google Earth Engine.
Introducción #
Un índice de agua nos dice qué píxeles son agua, pero muchas veces lo que necesitamos es un shapefile con el contorno de ríos, ciénagas y lagunas para llevarlo a QGIS o ArcGIS Pro. En esta lección pasaremos de la imagen al vector, limpiando antes el ruido de la máscara para que los polígonos salgan ordenados.
Con Sentinel-2 los polígonos salen mucho más detallados gracias a los 10 m de resolución, así que es la mejor opción para cuerpos de agua pequeños o canales estrechos.
Objetivos #
- Crear una máscara de agua combinando NDWI, MNDWI y AWEInsh.
- Eliminar islas y charcos pequeños con
connectedPixelCount(). - Convertir la máscara en polígonos con
reduceToVectors(). - Exportar los cuerpos de agua como shapefile a Google Drive.
Recursos #
- ee.Image.reduceToVectors()
- ee.Image.connectedPixelCount()
- Sentinel-2 Surface Reflectance (COPERNICUS/S2_SR_HARMONIZED)
Paso 1: Imagen e índices #
El script usa una geometría llamada AOI. Dibújala con las herramientas de geometría del mapa en el Code Editor y renómbrala como AOI en el panel de importaciones.
Preparamos una escena de Sentinel-2 sin nubes y calculamos NDWI, MNDWI y AWEInsh tal como en la lección Detección de agua con Sentinel-2. El falso color NIR-Red edge-Verde muestra el agua muy oscura y la vegetación en rojo.
// Máscara de nubes con la banda SCL y paso a reflectancia (0 a 1) function maskS2clouds(img) { var scl = img.select('SCL'); var mask = scl.neq(3) // sombra de nubes .and(scl.neq(8)) // nubes, probabilidad media .and(scl.neq(9)) // nubes, probabilidad alta .and(scl.neq(10)); // cirros return img.updateMask(mask).select('B.*').divide(10000); }
// Primera imagen de Sentinel-2 con poca nubosidad sobre el AOI var s2_image = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED") .filterDate("2023-01-01", "2023-12-01") .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', 10)) .filterBounds(AOI) .map(maskS2clouds) .first() .clip(AOI); print(s2_image);
// Falso color NIR-Borde rojo-Verde Map.addLayer(s2_image, {bands: "B8,B5,B3", min: 0, max: 0.4, gamma: 1}, "Sentinel-2"); Map.centerObject(AOI, 12);
// Índices para detectar agua con bandas de Sentinel-2 var NDWI = s2_image.normalizedDifference(['B3', 'B8']).rename('NDWI'); var MNDWI = s2_image.normalizedDifference(['B3', 'B11']).rename('MNDWI'); var AWEInsh = s2_image.expression('4 * (GREEN - SWIR1) - (0.25 * NIR + 2.75 * SWIR2)', { 'GREEN': s2_image.select('B3'), 'SWIR1': s2_image.select('B11'), 'SWIR2': s2_image.select('B12'), 'NIR': s2_image.select('B8') }).rename('AWEInsh'); var index = NDWI.addBands([MNDWI, AWEInsh]);
Paso 2: Máscara de agua #
Un píxel es agua si NDWI o MNDWI superan 0, o si AWEInsh supera 0,01. Combinar los tres índices ayuda a no perder orillas ni zonas con vegetación flotante.
// Máscara de agua con los tres índices var waterMask = function(img) { var water = img.select('NDWI').gt(0) .or(img.select('MNDWI').gt(0)) .or(img.select('AWEInsh').gt(0.01)); return water.rename('waterMask'); }; var masking = waterMask(index); Map.addLayer(masking, {}, 'Máscara de agua');
Paso 3: Limpiar la máscara #
La máscara cruda tiene píxeles sueltos que generarían cientos de polígonos diminutos. connectedPixelCount() cuenta cuántos píxeles conectados tiene cada grupo, y con eso rellenamos las islas pequeñas dentro del agua y eliminamos el agua aislada de pocos píxeles. El parámetro FILL_SIZE controla qué tan agresiva es la limpieza.
// Rellenar islas pequeñas: tierra rodeada de agua con menos de FILL_SIZE píxeles var removeIslands = function(img, FILL_SIZE) { var fill = img.not().selfMask().connectedPixelCount(FILL_SIZE).lt(FILL_SIZE); return img.where(fill, 1).rename('waterMask'); }; // Quitar charcos pequeños: agua aislada con menos de FILL_SIZE píxeles var removeSmallWater = function(img, FILL_SIZE) { var fill = img.connectedPixelCount(FILL_SIZE).lt(FILL_SIZE); return img.where(fill, 0).rename('waterMask'); }; var mask_clean = removeSmallWater(removeIslands(masking, 40), 10); Map.addLayer(mask_clean, {}, 'Máscara limpia');
Paso 4: Vectorizar y exportar #
reduceToVectors() agrupa los píxeles contiguos de agua en polígonos. La tabla resultante se exporta como shapefile y puedes abrirla directamente en QGIS.
// Convertir los píxeles de agua en polígonos var waterPolygons = mask_clean.selfMask().reduceToVectors({ geometry: AOI, scale: 10, geometryType: 'polygon', eightConnected: false, labelProperty: 'zone', maxPixels: 1e13 }); Map.addLayer(waterPolygons, {color: 'cyan'}, "Polígonos de agua"); // Exportar como shapefile a Google Drive Export.table.toDrive({ collection: waterPolygons, description: "water_polygons_sentinel", folder: "shapes", fileFormat: "SHP" });
Código completo #
Este es el script completo. Cópialo, pégalo en el Code Editor, dibuja tu AOI y presiona Run.
// Máscara de nubes con la banda SCL y paso a reflectancia (0 a 1) function maskS2clouds(img) { var scl = img.select('SCL'); var mask = scl.neq(3) // sombra de nubes .and(scl.neq(8)) // nubes, probabilidad media .and(scl.neq(9)) // nubes, probabilidad alta .and(scl.neq(10)); // cirros return img.updateMask(mask).select('B.*').divide(10000); } // Primera imagen de Sentinel-2 con poca nubosidad sobre el AOI var s2_image = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED") .filterDate("2023-01-01", "2023-12-01") .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', 10)) .filterBounds(AOI) .map(maskS2clouds) .first() .clip(AOI); print(s2_image); // Falso color NIR-Borde rojo-Verde Map.addLayer(s2_image, {bands: "B8,B5,B3", min: 0, max: 0.4, gamma: 1}, "Sentinel-2"); Map.centerObject(AOI, 12); // Índices para detectar agua con bandas de Sentinel-2 var NDWI = s2_image.normalizedDifference(['B3', 'B8']).rename('NDWI'); var MNDWI = s2_image.normalizedDifference(['B3', 'B11']).rename('MNDWI'); var AWEInsh = s2_image.expression('4 * (GREEN - SWIR1) - (0.25 * NIR + 2.75 * SWIR2)', { 'GREEN': s2_image.select('B3'), 'SWIR1': s2_image.select('B11'), 'SWIR2': s2_image.select('B12'), 'NIR': s2_image.select('B8') }).rename('AWEInsh'); var index = NDWI.addBands([MNDWI, AWEInsh]); // Máscara de agua con los tres índices var waterMask = function(img) { var water = img.select('NDWI').gt(0) .or(img.select('MNDWI').gt(0)) .or(img.select('AWEInsh').gt(0.01)); return water.rename('waterMask'); }; var masking = waterMask(index); Map.addLayer(masking, {}, 'Máscara de agua'); // Rellenar islas pequeñas: tierra rodeada de agua con menos de FILL_SIZE píxeles var removeIslands = function(img, FILL_SIZE) { var fill = img.not().selfMask().connectedPixelCount(FILL_SIZE).lt(FILL_SIZE); return img.where(fill, 1).rename('waterMask'); }; // Quitar charcos pequeños: agua aislada con menos de FILL_SIZE píxeles var removeSmallWater = function(img, FILL_SIZE) { var fill = img.connectedPixelCount(FILL_SIZE).lt(FILL_SIZE); return img.where(fill, 0).rename('waterMask'); }; var mask_clean = removeSmallWater(removeIslands(masking, 40), 10); Map.addLayer(mask_clean, {}, 'Máscara limpia'); // Convertir los píxeles de agua en polígonos var waterPolygons = mask_clean.selfMask().reduceToVectors({ geometry: AOI, scale: 10, geometryType: 'polygon', eightConnected: false, labelProperty: 'zone', maxPixels: 1e13 }); Map.addLayer(waterPolygons, {color: 'cyan'}, "Polígonos de agua"); // Exportar como shapefile a Google Drive Export.table.toDrive({ collection: waterPolygons, description: "water_polygons_sentinel", folder: "shapes", fileFormat: "SHP" });
Conclusiones #
Con Sentinel-2 obtienes contornos de agua precisos a 10 m. Si tu zona tiene muchas islas o vegetación flotante, sube el valor de FILL_SIZE para obtener polígonos más limpios.
Referencias #
- McFeeters, S. K. (1996). The use of the Normalized Difference Water Index (NDWI) in the delineation of open water features. International Journal of Remote Sensing, 17(7), 1425–1432.
- Gao, B.-C. (1996). NDWI—A normalized difference water index for remote sensing of vegetation liquid water from space. Remote Sensing of Environment, 58(3), 257–266.
- Xu, H. (2006). Modification of normalised difference water index (NDWI) to enhance open water features in remotely sensed imagery. International Journal of Remote Sensing, 27(14), 3025–3033.
- Feyisa, G. L., Meilby, H., Fensholt, R. y Proud, S. R. (2014). Automated Water Extraction Index: A new technique for surface water mapping using Landsat imagery. Remote Sensing of Environment, 140, 23–35.
