SIG EDUCA YouTube Tutoriales

Extracción de cuerpos de agua con Sentinel-2 en Google Earth Engine

básicoLección 18 de 1815 minGoogle 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 #

Recursos #

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 #

© 2026 SIG EDUCA