SIG EDUCA YouTube Tutoriales

Mapeo de inundaciones con Sentinel-2 en Google Earth Engine

básicoLección 16 de 1815 minGoogle Earth Engine

Mapea una inundación con composiciones de Sentinel-2 y calcula el área cubierta por agua en cada periodo.

Introducción #

Sentinel-2 pasa con más frecuencia que Landsat, así que es más probable encontrar una imagen útil justo durante una inundación. En esta lección trabajaremos sobre el valle del río Sinú, alrededor de Montería (Córdoba, Colombia). Compararemos enero de 2026, durante una inundación, con los meses siguientes, y calcularemos el área cubierta por agua en cada momento.

Objetivos #

Recursos #

Cómo funciona #

Las fechas importan. Para el periodo posterior conviene una ventana corta y cercana al evento. Con ventanas largas la imagen puede corresponder a semanas después, cuando el agua ya bajó.

Paso 1: Composiciones durante y después #

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.

Usamos la máscara de nubes con la banda SCL y una mediana por periodo, que ayuda a rellenar los huecos que dejan las nubes en época de lluvias.

// 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);
}
// Composiciones durante y después de la inundación
var s2_pre = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterDate("2026-01-01", "2026-02-01")
  .filterBounds(AOI)
  .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', 20))
  .map(maskS2clouds)
  .median()
  .clip(AOI);

var s2_post = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterDate("2026-02-01", "2026-12-01")
  .filterBounds(AOI)
  .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', 20))
  .map(maskS2clouds)
  .median()
  .clip(AOI);

var visRGB = {bands: "B4,B3,B2", min: 0, max: 0.3, gamma: 1};
Map.addLayer(s2_pre, visRGB, "Sentinel-2 durante");
Map.addLayer(s2_post, visRGB, "Sentinel-2 después");
Map.centerObject(AOI, 10);

Paso 2: Índices y máscara de agua #

Agrupamos los tres índices en una función y otra función decide qué píxeles son agua. Así el mismo código sirve para las dos fechas.

// Índices para detectar agua con bandas de Sentinel-2
var waterIndex = function(img) {
  var ndwi = img.normalizedDifference(['B3', 'B8']).rename('NDWI');
  var mndwi = img.normalizedDifference(['B3', 'B11']).rename('MNDWI');
  var awei = img.expression('4 * (GREEN - SWIR1) - (0.25 * NIR + 2.75 * SWIR2)', {
    'GREEN': img.select('B3'), 'SWIR1': img.select('B11'),
    'SWIR2': img.select('B12'), 'NIR': img.select('B8')
  }).rename('AWEInsh');
  return ndwi.addBands([mndwi, awei]);
};

// Agua si cualquiera de los tres índices es mayor que 0
var waterMask = function(img) {
  var idx = waterIndex(img);
  var water = idx.select('NDWI').gt(0)
    .or(idx.select('MNDWI').gt(0))
    .or(idx.select('AWEInsh').gt(0));
  return water.rename('waterMask');
};

var mask_pre = waterMask(s2_pre);
var mask_post = waterMask(s2_post);
Map.addLayer(mask_pre.selfMask(), {palette: ["db0202"]}, 'Agua durante');
Map.addLayer(mask_post.selfMask(), {palette: ["2dd4f1"]}, 'Agua después');

Paso 3: Calcular el área de agua #

Sumamos el área de los píxeles de agua en cada periodo. La diferencia entre ambos valores estima cuánto terreno estaba cubierto por la inundación.

// Área de agua en km² dentro del AOI
var waterArea = function(mask) {
  var area = mask.eq(1).multiply(ee.Image.pixelArea()).divide(1e6).rename('area');
  var stats = area.reduceRegion({
    reducer: ee.Reducer.sum(),
    geometry: AOI,
    scale: 10,
    bestEffort: true,
    maxPixels: 1e13
  });
  return ee.Number(stats.get('area'));
};

print("Área de agua durante la inundación (km²):", waterArea(mask_pre));
print("Área de agua después de la inundación (km²):", waterArea(mask_post));

Revisa las imágenes antes de comparar cifras. Los píxeles cubiertos por nubes quedan enmascarados y no se cuentan como agua. Si una de las fechas tiene muchas nubes, el área de agua saldrá más baja de lo real.

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

// Composiciones durante y después de la inundación
var s2_pre = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterDate("2026-01-01", "2026-02-01")
  .filterBounds(AOI)
  .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', 20))
  .map(maskS2clouds)
  .median()
  .clip(AOI);

var s2_post = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
  .filterDate("2026-02-01", "2026-12-01")
  .filterBounds(AOI)
  .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', 20))
  .map(maskS2clouds)
  .median()
  .clip(AOI);

var visRGB = {bands: "B4,B3,B2", min: 0, max: 0.3, gamma: 1};
Map.addLayer(s2_pre, visRGB, "Sentinel-2 durante");
Map.addLayer(s2_post, visRGB, "Sentinel-2 después");
Map.centerObject(AOI, 10);

// Índices para detectar agua con bandas de Sentinel-2
var waterIndex = function(img) {
  var ndwi = img.normalizedDifference(['B3', 'B8']).rename('NDWI');
  var mndwi = img.normalizedDifference(['B3', 'B11']).rename('MNDWI');
  var awei = img.expression('4 * (GREEN - SWIR1) - (0.25 * NIR + 2.75 * SWIR2)', {
    'GREEN': img.select('B3'), 'SWIR1': img.select('B11'),
    'SWIR2': img.select('B12'), 'NIR': img.select('B8')
  }).rename('AWEInsh');
  return ndwi.addBands([mndwi, awei]);
};

// Agua si cualquiera de los tres índices es mayor que 0
var waterMask = function(img) {
  var idx = waterIndex(img);
  var water = idx.select('NDWI').gt(0)
    .or(idx.select('MNDWI').gt(0))
    .or(idx.select('AWEInsh').gt(0));
  return water.rename('waterMask');
};

var mask_pre = waterMask(s2_pre);
var mask_post = waterMask(s2_post);
Map.addLayer(mask_pre.selfMask(), {palette: ["db0202"]}, 'Agua durante');
Map.addLayer(mask_post.selfMask(), {palette: ["2dd4f1"]}, 'Agua después');

// Área de agua en km² dentro del AOI
var waterArea = function(mask) {
  var area = mask.eq(1).multiply(ee.Image.pixelArea()).divide(1e6).rename('area');
  var stats = area.reduceRegion({
    reducer: ee.Reducer.sum(),
    geometry: AOI,
    scale: 10,
    bestEffort: true,
    maxPixels: 1e13
  });
  return ee.Number(stats.get('area'));
};

print("Área de agua durante la inundación (km²):", waterArea(mask_pre));
print("Área de agua después de la inundación (km²):", waterArea(mask_post));

Conclusiones #

Sentinel-2 da mapas de inundación más detallados y con más fechas disponibles. Cuando las nubes no dejan ver nada durante días, el siguiente paso natural es trabajar con radar (Sentinel-1), que atraviesa las nubes.

Referencias #

© 2026 SIG EDUCA