SIG EDUCA YouTube Tutoriales

Extracción de cuerpos de agua con Landsat 9 en Google Earth Engine

básicoLección 17 de 1815 minGoogle Earth Engine

Extrae ríos, ciénagas y lagunas como polígonos a partir de Landsat 9 y expórtalos como shapefile.

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.

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 Landsat 9 con máscara de nubes y factor de escala, y calculamos NDWI, MNDWI y AWEInsh tal como en la lección Detección de agua con Landsat 9.

// 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);
}
// Primera imagen de Landsat 9 con poca nubosidad sobre el AOI
var ls9_image = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")
  .filterDate('2023-01-01', '2023-12-31')
  .filter(ee.Filter.lt("CLOUD_COVER_LAND", 10))
  .filterBounds(AOI)
  .map(mask_clouds)
  .map(scaler)
  .first()
  .clip(AOI);
print(ls9_image);
// Falso color NIR-Rojo-Verde, donde el agua se ve oscura y la vegetación roja
Map.addLayer(ls9_image, {bands: "SR_B5,SR_B4,SR_B3", min: 0.01, max: 0.41, gamma: 1}, "Landsat 9");
Map.centerObject(AOI, 12);
// Índices para detectar agua
var NDWI = ls9_image.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI');
var MNDWI = ls9_image.normalizedDifference(['SR_B3', 'SR_B6']).rename('MNDWI');
var AWEInsh = ls9_image.expression('4 * (GREEN - SWIR1) - (0.25 * NIR + 2.75 * SWIR2)', {
  'GREEN': ls9_image.select('SR_B3'),
  'SWIR1': ls9_image.select('SR_B6'),
  'SWIR2': ls9_image.select('SR_B7'),
  'NIR': ls9_image.select('SR_B5')
}).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, 30), 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: 30,
  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_landsat",
  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.

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

// Primera imagen de Landsat 9 con poca nubosidad sobre el AOI
var ls9_image = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")
  .filterDate('2023-01-01', '2023-12-31')
  .filter(ee.Filter.lt("CLOUD_COVER_LAND", 10))
  .filterBounds(AOI)
  .map(mask_clouds)
  .map(scaler)
  .first()
  .clip(AOI);
print(ls9_image);

// Falso color NIR-Rojo-Verde, donde el agua se ve oscura y la vegetación roja
Map.addLayer(ls9_image, {bands: "SR_B5,SR_B4,SR_B3", min: 0.01, max: 0.41, gamma: 1}, "Landsat 9");
Map.centerObject(AOI, 12);

// Índices para detectar agua
var NDWI = ls9_image.normalizedDifference(['SR_B3', 'SR_B5']).rename('NDWI');
var MNDWI = ls9_image.normalizedDifference(['SR_B3', 'SR_B6']).rename('MNDWI');
var AWEInsh = ls9_image.expression('4 * (GREEN - SWIR1) - (0.25 * NIR + 2.75 * SWIR2)', {
  'GREEN': ls9_image.select('SR_B3'),
  'SWIR1': ls9_image.select('SR_B6'),
  'SWIR2': ls9_image.select('SR_B7'),
  'NIR': ls9_image.select('SR_B5')
}).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, 30), 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: 30,
  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_landsat",
  folder: "shapes",
  fileFormat: "SHP"
});

Conclusiones #

Con este flujo pasas de una imagen satelital a un shapefile de cuerpos de agua listo para medir, editar o cruzar con otras capas. Ajusta los umbrales de los índices y el tamaño de limpieza según tu zona, porque no es lo mismo una ciénaga costera que un embalse de montaña.

Referencias #

© 2026 SIG EDUCA