Tutoriales / Agua
Cómo detectar cuerpos de agua con MNDWI y Google Earth Engine
Qué es el MNDWI, en qué se diferencia del NDWI y cómo usarlo en Google Earth Engine para mapear cuerpos de agua: un compuesto sin nubes, la máscara de agua, su superficie en hectáreas y la exportación a shapefile. El mismo flujo en el Code Editor (JavaScript) y en Colab (Python).

Qué es el MNDWI y su fórmula
El agua refleja algo de luz verde pero absorbe casi todo el infrarrojo de onda corta. El suelo, la vegetación y las construcciones reflejan más infrarrojo de onda corta que verde. El MNDWI (índice de agua de diferencia normalizada modificado, Xu, 2006) aprovecha ese contraste:
En Sentinel-2 el verde es B3 (560 nm, 10 m) y el infrarrojo de onda corta es B11 (1613,7 nm, 20 m), según el catálogo de Earth Engine para Sentinel-2A. Los valores positivos suelen ser agua. Frente al NDWI clásico, que usa el infrarrojo cercano, el MNDWI confunde menos el agua con zonas urbanas y suelo húmedo.
MNDWI vs NDWI: cuál usar
Con el nombre NDWI circulan dos índices distintos. El de McFeeters (1996) combina el verde y el infrarrojo cercano (B3 y B8 en Sentinel-2) y detecta agua abierta. El de Gao (1996) combina el infrarrojo cercano y el de onda corta (B8 y B11) y mide la humedad de la vegetación, no el agua superficial.
El MNDWI parte del NDWI de McFeeters y cambia el infrarrojo cercano por el de onda corta. Así separa mejor el agua de las zonas urbanas y del suelo húmedo, que con el NDWI se confunden con agua. Para mapear cuerpos de agua con Sentinel-2 o Landsat, el MNDWI suele ser la mejor opción; el NDWI de McFeeters funciona bien en agua abierta lejos de ciudades.
Elegir el umbral
El punto de partida habitual es 0: todo lo que esté por encima se considera agua. En aguas turbias o con vegetación flotante, como ciénagas y planos inundables, el valor puede bajar, y un umbral entre −0.1 y 0 recupera esas zonas. Conviene revisar el resultado sobre el color real antes de fijarlo.
El script
Pega esto en el Code Editor y cambia las fechas por las de tu periodo. El ejemplo es el embalse de Betania, en el Huila, entre enero y marzo de 2026.
El área de análisis es un polígono dibujado en el mapa. Antes de ejecutar, usa la herramienta de dibujo del Code Editor para trazar el contorno de tu zona; el polígono aparece en Imports con el nombre geometry, que es el que usa el script. Si le das otro nombre, cámbialo también en var aoi = geometry;.
/**
* Water surface detection using Sentinel-2 MNDWI
*
* Location:
* Embalse de Betania
* Departamento: Huila
* Colombia
*
* Period:
* 2026-01-01 to 2026-03-31
*/
/**
* Configuration
*/
var fecha_inicio = '2026-01-01';
var fecha_fin = '2026-03-31';
var umbral_claro = 0.60;
var umbral_agua = 0;
/**
* Area of interest
*
* Approximate extent of Embalse de Betania.
*/
var aoi = geometry;
/**
* Collections
*/
var cloud_score_path = 'GOOGLE/CLOUD_SCORE_PLUS/V1/S2_HARMONIZED';
var sentinel_2_path = 'COPERNICUS/S2_SR_HARMONIZED';
var s2 = ee.ImageCollection(sentinel_2_path);
var cs_plus = ee.ImageCollection(cloud_score_path);
/**
* Sentinel-2 cloud-filtered collection
*/
var imagenes = s2
.filterBounds(aoi)
.filterDate(fecha_inicio, fecha_fin)
.linkCollection(cs_plus, ['cs_cdf'])
.map(function (imagen) {
var mascara_clara = imagen
.select('cs_cdf')
.gte(umbral_claro);
return imagen.updateMask(mascara_clara);
});
print('Número de imágenes Sentinel-2', imagenes.size());
/**
* Cloud-free composite
*/
var compuesto = imagenes
.median()
.clip(aoi);
/**
* MNDWI
*
* Green: B3
* SWIR1: B11
*/
var mndwi = compuesto
.normalizedDifference(['B3', 'B11'])
.rename('mndwi');
/**
* Water classification
*
* 0 = Non-water
* 1 = Water
*/
var agua = mndwi
.gt(umbral_agua)
.rename('agua');
/**
* Water surface area
*/
var hectareas = ee.Image
.pixelArea()
.divide(1e4)
.rename('area_ha')
.updateMask(agua)
.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: aoi,
scale: 20,
maxPixels: 1e13,
tileScale: 4,
})
.get('area_ha');
print('Superficie de agua (ha)', hectareas);
/**
* Water polygons
*/
var poligonos = agua
.selfMask()
.reduceToVectors({
geometry: aoi,
scale: 20,
geometryType: 'polygon',
eightConnected: false,
labelProperty: 'agua',
maxPixels: 1e13,
tileScale: 4,
});
/**
* Visualization
*/
var vis_rgb = {
bands: ['B4', 'B3', 'B2'],
min: 0,
max: 3000,
};
var vis_mndwi = {
min: -0.6,
max: 0.6,
palette: [
'#8c510a',
'#f6e8c3',
'#c7eae5',
'#35978f',
'#01665e',
],
};
var vis_agua = {
min: 1,
max: 1,
palette: ['#2166ac'],
};
/**
* Map
*/
Map.setOptions('SATELLITE');
Map.addLayer(compuesto, vis_rgb, 'Sentinel-2 RGB', true);
Map.addLayer(mndwi, vis_mndwi, 'MNDWI', false);
Map.addLayer(agua.selfMask(), vis_agua, 'Agua', true);
Map.addLayer(aoi, { color: 'ffffff' }, 'AOI', false);
print('Ventana temporal', fecha_inicio + ' a ' + fecha_fin);
/**
* Export water mask
*/
Export.image.toDrive({
image: agua.toByte(),
description: 'agua_mndwi_betania_2026',
folder: 'earthengine',
fileNamePrefix: 'agua_mndwi_betania_2026',
region: aoi,
scale: 20,
maxPixels: 1e13,
});
/**
* Export water polygons
*/
Export.table.toDrive({
collection: poligonos,
description: 'agua_poligonos_betania_2026',
folder: 'earthengine',
fileNamePrefix: 'agua_poligonos_betania_2026',
fileFormat: 'SHP',
});En la consola aparecen el número de imágenes Sentinel-2 que entraron al compuesto, la superficie de agua en hectáreas y el periodo analizado. La capa MNDWI queda oculta en el mapa; actívala para ver cómo se distribuyen los valores antes de ajustar el umbral. En Tasks aparecen dos tareas: la máscara en GeoTIFF y los polígonos en shapefile.
La lógica es idéntica. En Colab no hay herramienta de dibujo, así que el polígono se escribe como una lista de coordenadas [lon, lat] en coordenadas_aoi. La superficie y el número de imágenes se traen con getInfo(), que aquí es seguro porque son valores sueltos.
"""
Water surface detection using Sentinel-2 MNDWI
Location:
Embalse de Betania
Departamento: Huila
Colombia
Period:
2026-01-01 to 2026-03-31
"""
import ee
import geemap
ee.Authenticate()
ee.Initialize(project='tu-proyecto-gcp')
# Configuration
fecha_inicio = '2026-01-01'
fecha_fin = '2026-03-31'
umbral_claro = 0.60
umbral_agua = 0
# Area of interest: approximate extent of Embalse de Betania.
# Same polygon as "geometry" in the Code Editor, as [lon, lat] pairs.
coordenadas_aoi = [
# [lon, lat], [lon, lat], ...
]
aoi = ee.Geometry.Polygon([coordenadas_aoi])
# Collections
cloud_score_path = 'GOOGLE/CLOUD_SCORE_PLUS/V1/S2_HARMONIZED'
sentinel_2_path = 'COPERNICUS/S2_SR_HARMONIZED'
s2 = ee.ImageCollection(sentinel_2_path)
cs_plus = ee.ImageCollection(cloud_score_path)
# Sentinel-2 cloud-filtered collection
def enmascarar(imagen):
mascara_clara = imagen.select('cs_cdf').gte(umbral_claro)
return imagen.updateMask(mascara_clara)
imagenes = (
s2.filterBounds(aoi)
.filterDate(fecha_inicio, fecha_fin)
.linkCollection(cs_plus, ['cs_cdf'])
.map(enmascarar)
)
print('Número de imágenes Sentinel-2:', imagenes.size().getInfo())
# Cloud-free composite
compuesto = imagenes.median().clip(aoi)
# MNDWI (green: B3, SWIR1: B11)
mndwi = compuesto.normalizedDifference(['B3', 'B11']).rename('mndwi')
# Water classification: 0 = non-water, 1 = water
agua = mndwi.gt(umbral_agua).rename('agua')
# Water surface area
hectareas = (
ee.Image.pixelArea()
.divide(1e4)
.rename('area_ha')
.updateMask(agua)
.reduceRegion(
reducer=ee.Reducer.sum(),
geometry=aoi,
scale=20,
maxPixels=1e13,
tileScale=4,
)
.get('area_ha')
)
print('Superficie de agua (ha):', hectareas.getInfo())
# Water polygons
poligonos = agua.selfMask().reduceToVectors(
geometry=aoi,
scale=20,
geometryType='polygon',
eightConnected=False,
labelProperty='agua',
maxPixels=1e13,
tileScale=4,
)
# Visualization
vis_rgb = {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 3000}
vis_mndwi = {
'min': -0.6,
'max': 0.6,
'palette': ['#8c510a', '#f6e8c3', '#c7eae5', '#35978f', '#01665e'],
}
vis_agua = {'min': 1, 'max': 1, 'palette': ['#2166ac']}
# Map
mapa = geemap.Map()
mapa.add_basemap('SATELLITE')
mapa.centerObject(aoi, 11)
mapa.addLayer(compuesto, vis_rgb, 'Sentinel-2 RGB', True)
mapa.addLayer(mndwi, vis_mndwi, 'MNDWI', False)
mapa.addLayer(agua.selfMask(), vis_agua, 'Agua', True)
mapa.addLayer(aoi, {'color': 'ffffff'}, 'AOI', False)
mapa
print('Ventana temporal:', fecha_inicio, 'a', fecha_fin)
# Export water mask
ee.batch.Export.image.toDrive(
image=agua.toByte(),
description='agua_mndwi_betania_2026',
folder='earthengine',
fileNamePrefix='agua_mndwi_betania_2026',
region=aoi,
scale=20,
maxPixels=1e13,
).start()
# Export water polygons
ee.batch.Export.table.toDrive(
collection=poligonos,
description='agua_poligonos_betania_2026',
folder='earthengine',
fileNamePrefix='agua_poligonos_betania_2026',
fileFormat='SHP',
).start()6.089 hectáreas de agua en el embalse de Betania
Con el script de arriba, el compuesto de enero a marzo de 2026 detecta 6.089 hectáreas de agua en el embalse de Betania. El MNDWI separa con claridad el espejo de agua y el cauce del río Magdalena del suelo y la vegetación que los rodean, y también marca cuerpos de agua pequeños alrededor del embalse.

| Superficie de agua | 6.088,57 ha |
|---|---|
| Ubicación | Embalse de Betania, Huila |
| Periodo | 1 ene – 31 mar 2026 |
Lo que la IA suele hacer mal
Si le pides este script a un asistente de IA, lo más probable es que funcione a medias. Estos son los fallos que más aparecen en el código generado, y cómo los evita el script de arriba:
- Confunde los NDWI. Hay dos índices con el mismo nombre: el de McFeeters (
B3,B8) detecta agua abierta y el de Gao (B8,B11) mide humedad de la vegetación. Mezclarlos da mapas que no significan lo que parecen. - Calcula el área contando píxeles por 100 m². Supone píxeles de 10 m aunque el cálculo se haga a 20 m y no corrige la proyección.
ee.Image.pixelArea()da el área real de cada píxel. - Fija el umbral en 0 sin revisarlo. Sirve para agua clara, pero en ciénagas turbias o con vegetación flotante deja fuera buena parte del agua. Compara siempre con el color real.
- Vectoriza sin escala ni
maxPixels.reduceToVectorssobre un área grande sin esos parámetros falla o genera millones de polígonos diminutos.
Cómo leer los valores
Los rangos son orientativos y cambian con la turbidez del agua y la época del año.
| MNDWI | Lo que suele indicar |
|---|---|
| Mayor que 0.3 | Agua abierta y clara: lagos, embalses, ríos anchos |
| 0 a 0.3 | Agua turbia, poco profunda o mezclada con vegetación |
| −0.2 a 0 | Suelo muy húmedo, humedales, bordes de cuerpos de agua |
| Menor que −0.2 | Vegetación, suelo seco y zonas construidas |
Cuando el MNDWI se equivoca: sombras, ciénagas y vectores pesados
Sombras de montañas o de nubes detectadas como agua
Las sombras también absorben el infrarrojo y pueden pasar el umbral. En zonas de relieve, descarta las pendientes fuertes con un modelo de elevación, por ejemplo ee.Terrain.slope(ee.Image('USGS/SRTMGL1_003')).lt(10) como máscara adicional.
La exportación a shapefile falla o tarda demasiado
Un área grande a 20 m produce millones de polígonos pequeños. Sube la escala de reduceToVectors a 30 o 60 m, o elimina los parches diminutos antes de vectorizar con agua.selfMask().connectedPixelCount(100).gte(10).
El agua de una ciénaga aparece fragmentada
La vegetación flotante y la turbidez bajan el MNDWI. Prueba un umbral de −0.1 o combina el resultado con la frecuencia histórica de agua del dataset JRC Global Surface Water.
Preguntas frecuentes
¿Qué es el MNDWI?
Es el índice de agua de diferencia normalizada modificado (Xu, 2006). Combina la banda verde y la de infrarrojo de onda corta para resaltar el agua superficial: los valores positivos suelen corresponder a agua.
¿Cuál es la fórmula del MNDWI en Sentinel-2?
. En Sentinel-2 se usan las bandas B3 (verde, 560 nm) y B11 (infrarrojo de onda corta, 1613,7 nm).
¿Qué diferencia hay entre el NDWI y el MNDWI?
El NDWI de McFeeters usa verde e infrarrojo cercano; el MNDWI usa verde e infrarrojo de onda corta. El MNDWI confunde menos el agua con zonas urbanas y suelo húmedo, por eso es preferible para mapear cuerpos de agua.
¿Qué umbral de MNDWI indica agua?
El punto de partida habitual es 0. En aguas turbias, ciénagas o con vegetación flotante, un umbral entre −0.1 y 0 recupera más agua. Conviene revisar el resultado sobre la imagen en color real antes de fijarlo.
Fuentes
- Modification of normalised difference water index (NDWI) to enhance open water features in remotely sensed imageryXu, H. (2006). International Journal of Remote SensingArtículo original del MNDWI.
- The use of the Normalized Difference Water Index (NDWI) in the delineation of open water featuresMcFeeters, S. K. (1996). International Journal of Remote SensingNDWI con el verde y el infrarrojo cercano, para agua abierta.
- NDWI—A normalized difference water index for remote sensing of vegetation liquid water from spaceGao, B.-C. (1996). Remote Sensing of EnvironmentNDWI con el infrarrojo cercano y el de onda corta, para la humedad de la vegetación.
- Harmonized Sentinel-2 MSI: MultiSpectral Instrument, Level-2A (SR)Google Earth Engine, catálogo de datosBandas B3 y B11 y sus longitudes de onda.
- JRC Global Surface Water Mapping Layers, v1.4Google Earth Engine, catálogo de datosFrecuencia histórica de agua para comparar y completar el resultado.
- NASA SRTM Digital Elevation 30mGoogle Earth Engine, catálogo de datosModelo de elevación para descartar sombras de montaña.
Sigue aprendiendo
Elevación
Cinco DEM gratuitos comparados con el láser GEDIVegetación · NDVI
Serie temporal de NDVI y fenología de cultivos en Google Earth EngineIncendios · dNBR
Cómo detectar áreas quemadas con dNBR y Google Earth Engine
Mira esta técnica aplicada en el mapa de inundaciones en Córdoba y Antioquia. ¿Lo necesitas hecho y verificado para tu zona? Mira el servicio Tu análisis, automatizado.
¿Necesitas mapas de agua para varios años o un proceso que los genere solo?
Escribe a contacto@ventuari.dev con el área y el periodo, y recibirás una propuesta con alcance y precio.