Mapeo de inundaciones con Landsat 9 en Google Earth Engine
Mapea una inundación con imágenes de Landsat 9 de antes y después, y calcula el área inundada en Google Earth Engine.
Introducción #
Cuando un río se desborda, la forma más rápida de dimensionar el evento es comparar imágenes satelitales de antes y después. Si en ambas detectamos el agua con un índice espectral, la diferencia entre las dos máscaras muestra la zona inundada, y Earth Engine calcula su área en kilómetros cuadrados en segundos.
En esta lección mapearemos una inundación con Landsat 9. El ejemplo está en Pakistán, cerca de Dera Ghazi Khan, en la provincia de Punjab. Entre junio y octubre de 2022 unas lluvias monzónicas excepcionales desbordaron el río Indo y sus afluentes, y esta fue una de las zonas afectadas.
Objetivos #
- Seleccionar una imagen de antes y otra de después de una inundación.
- Escribir los índices para detectar agua como funciones reutilizables.
- Crear una máscara binaria de agua para cada fecha.
- Calcular el área de agua en km² y comparar ambas fechas.
Recursos #
Cómo funciona #
- Elegimos dos periodos, uno antes del evento y otro después, y tomamos una imagen limpia de cada uno.
- Calculamos NDWI y MNDWI, que vimos en la lección Detección de agua con Landsat 9. Un píxel se considera agua si alguno de los dos índices es mayor que 0.
- Multiplicamos la máscara por
ee.Image.pixelArea(), que da el área de cada píxel en m², y sumamos dentro del AOI.
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: Imágenes antes 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 las funciones de factor de escala y máscara de nubes. Para el periodo posterior aceptamos más nubosidad porque en temporada de lluvias es difícil encontrar escenas despejadas. El falso color SWIR1-NIR-Rojo hace que el agua se vea casi negra.
// Factor de escala: convierte los valores digitales en reflectancia function scaler(img) { var optical = img.select('SR_B.').multiply(0.0000275).add(-0.2).toFloat(); var thermal = img.select('ST_B.*').multiply(0.00341802).add(149).toFloat(); return img.addBands(optical, null, true).addBands(thermal, null, true); } // Máscara de nubes con la banda QA_PIXEL function mask_clouds(img) { var qa = img.select('QA_PIXEL'); var cirrus = 1 << 2; // bit 2, cirros var cloud = 1 << 3; // bit 3, nubes var cloudShadow = 1 << 4; // bit 4, sombra de nubes var snow = 1 << 5; // bit 5, nieve var mask = qa.bitwiseAnd(cirrus).eq(0) .and(qa.bitwiseAnd(cloud).eq(0)) .and(qa.bitwiseAnd(cloudShadow).eq(0)) .and(qa.bitwiseAnd(snow).eq(0)); return img.updateMask(mask); }
// Imagen antes de la inundación var ls9_pre = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2") .filterDate('2022-03-01', '2022-06-01') .filter(ee.Filter.lt("CLOUD_COVER_LAND", 20)) .filterBounds(AOI) .map(mask_clouds) .map(scaler) .first() .clip(AOI); // Imagen después de la inundación var ls9_post = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2") .filterDate('2022-08-01', '2022-12-31') .filter(ee.Filter.lt("CLOUD_COVER_LAND", 50)) .filterBounds(AOI) .map(mask_clouds) .map(scaler) .first() .clip(AOI); print(ls9_pre, ls9_post); // Falso color SWIR1-NIR-Rojo, donde el agua se ve muy oscura var visFalse = {bands: "SR_B6,SR_B5,SR_B4", min: 0.003, max: 0.42, gamma: 1}; Map.addLayer(ls9_pre, visFalse, "Landsat 9 antes"); Map.addLayer(ls9_post, visFalse, "Landsat 9 después"); Map.centerObject(AOI, 10);
Paso 2: Índices para detectar agua como funciones #
Al escribir los índices como funciones podemos aplicarlos a las dos imágenes sin repetir código.
// Funciones de índices para detectar agua: se pueden aplicar a cualquier imagen var NDWI = function(img) { return img.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI'); }; var MNDWI = function(img) { return img.normalizedDifference(['SR_B3', 'SR_B6']).rename('MNDWI'); }; var index_pre = NDWI(ls9_pre).addBands(MNDWI(ls9_pre)); var index_post = NDWI(ls9_post).addBands(MNDWI(ls9_post));
Paso 3: Máscara de agua #
Convertimos los índices en una imagen binaria donde 1 es agua y 0 no lo es. Con selfMask() ocultamos los ceros para ver solo el agua. Todo lo que aparece en azul alrededor del agua de antes corresponde a la zona inundada.
// Un píxel es agua si cualquiera de los índices es mayor que 0 var waterMask = function(img) { var water = img.select('NDWI').gt(0).or(img.select('MNDWI').gt(0)); return water.rename('waterMask'); }; var mask_pre = waterMask(index_pre); var mask_post = waterMask(index_post); // Rojo para el agua antes y azul para el agua después Map.addLayer(mask_post.selfMask(), {palette: ["3274f1"]}, 'Agua después'); Map.addLayer(mask_pre.selfMask(), {palette: ["db0202"]}, 'Agua antes');
Paso 4: Calcular el área inundada #
Sumamos el área de los píxeles de agua en cada fecha. La diferencia entre ambos valores es una estimación del área que cubrió 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: 30, bestEffort: true, maxPixels: 1e13 }); return ee.Number(stats.get('area')); }; print("Área de agua antes de 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.
// Factor de escala: convierte los valores digitales en reflectancia function scaler(img) { var optical = img.select('SR_B.').multiply(0.0000275).add(-0.2).toFloat(); var thermal = img.select('ST_B.*').multiply(0.00341802).add(149).toFloat(); return img.addBands(optical, null, true).addBands(thermal, null, true); } // Máscara de nubes con la banda QA_PIXEL function mask_clouds(img) { var qa = img.select('QA_PIXEL'); var cirrus = 1 << 2; // bit 2, cirros var cloud = 1 << 3; // bit 3, nubes var cloudShadow = 1 << 4; // bit 4, sombra de nubes var snow = 1 << 5; // bit 5, nieve var mask = qa.bitwiseAnd(cirrus).eq(0) .and(qa.bitwiseAnd(cloud).eq(0)) .and(qa.bitwiseAnd(cloudShadow).eq(0)) .and(qa.bitwiseAnd(snow).eq(0)); return img.updateMask(mask); } // Imagen antes de la inundación var ls9_pre = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2") .filterDate('2022-03-01', '2022-06-01') .filter(ee.Filter.lt("CLOUD_COVER_LAND", 20)) .filterBounds(AOI) .map(mask_clouds) .map(scaler) .first() .clip(AOI); // Imagen después de la inundación var ls9_post = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2") .filterDate('2022-08-01', '2022-12-31') .filter(ee.Filter.lt("CLOUD_COVER_LAND", 50)) .filterBounds(AOI) .map(mask_clouds) .map(scaler) .first() .clip(AOI); print(ls9_pre, ls9_post); // Falso color SWIR1-NIR-Rojo, donde el agua se ve muy oscura var visFalse = {bands: "SR_B6,SR_B5,SR_B4", min: 0.003, max: 0.42, gamma: 1}; Map.addLayer(ls9_pre, visFalse, "Landsat 9 antes"); Map.addLayer(ls9_post, visFalse, "Landsat 9 después"); Map.centerObject(AOI, 10); // Funciones de índices para detectar agua: se pueden aplicar a cualquier imagen var NDWI = function(img) { return img.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI'); }; var MNDWI = function(img) { return img.normalizedDifference(['SR_B3', 'SR_B6']).rename('MNDWI'); }; var index_pre = NDWI(ls9_pre).addBands(MNDWI(ls9_pre)); var index_post = NDWI(ls9_post).addBands(MNDWI(ls9_post)); // Un píxel es agua si cualquiera de los índices es mayor que 0 var waterMask = function(img) { var water = img.select('NDWI').gt(0).or(img.select('MNDWI').gt(0)); return water.rename('waterMask'); }; var mask_pre = waterMask(index_pre); var mask_post = waterMask(index_post); // Rojo para el agua antes y azul para el agua después Map.addLayer(mask_post.selfMask(), {palette: ["3274f1"]}, 'Agua después'); Map.addLayer(mask_pre.selfMask(), {palette: ["db0202"]}, 'Agua antes'); // Á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: 30, bestEffort: true, maxPixels: 1e13 }); return ee.Number(stats.get('area')); }; print("Área de agua antes de la inundación (km²):", waterArea(mask_pre)); print("Área de agua después de la inundación (km²):", waterArea(mask_post));
Conclusiones #
Con dos imágenes y un par de índices tienes un primer mapa de la inundación y una cifra de su extensión. Es un análisis rápido, ideal para una primera evaluación del evento.
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.
