SIG EDUCA YouTube Tutoriales
Módulo de Google Earth Engine · Agua · público

Jones_2019R

Módulo de Google Earth Engine que aplica el método DSWE (Jones, 2019) a imágenes Landsat de Colección 2, nivel 2, y devuelve una máscara de agua.

Qué hace #

Implementa las cinco pruebas espectrales de DSWE (Dynamic Surface Water Extent), el método del USGS desarrollado por John W. Jones, con la revisión de las pruebas de agua parcial publicada en 2019. Parte del código de RivWidthCloud (Yang et al., 2020), escrito para Landsat Colección 1, y lo adapta a la reflectancia de superficie de la Colección 2: los umbrales están en reflectancia de 0 a 1 y las bandas negativas se llevan a 0 para no perder el agua oscura de las costas.

Cómo cargarlo #

El módulo es público. Pega esta línea al inicio de tu script en el Code Editor y usa sus funciones con jones. delante, por ejemplo jones.ClassifyWaterJones2019(image).

var jones = require('users/sigeduca/routines:Indexes/Jones_2019R');

Funciones #

FunciónRecibeDevuelve
ClassifyWaterJones2019(image)Imagen Landsat con las bandas Blue, Green, Red, Nir, Swir1 y Swir2, en reflectancia de 0 a 1.Imagen de una banda, waterMask: 1 donde hay agua (clases 1 y 2 de DSWE) y 0 donde no.

Primero escala y renombra. Aplica el factor de escala de la Colección 2 (× 0.0000275 − 0.2) y renombra las bandas con los nombres comunes antes de llamar a la función. Con los valores digitales sin escalar, las pruebas dan resultados incorrectos.

Solo devuelve la máscara. Internamente, Dswe() calcula las cinco clases (0 a 4), pero el módulo solo exporta la máscara de agua de confianza alta y moderada. Si necesitas las clases de agua parcial, exporta también esa función.

¿Y Jones_2019, sin R? Es la versión original, con los umbrales de la Colección 1, que guardaba la reflectancia multiplicada por 10 000. Con imágenes de la Colección 2 escaladas de 0 a 1, sus pruebas 4 y 5 se cumplen siempre y marcan más agua de la real. Para imágenes actuales usa siempre Jones_2019R.

Ejemplo con una imagen #

Dibuja una geometría llamada AOI en el mapa y ejecuta este script. Toma una imagen de Landsat 9 con menos de 10 % de nubes sobre tu zona, la pasa a reflectancia y le aplica el módulo.

// Cargar el módulo
var jones = require('users/sigeduca/routines:Indexes/Jones_2019R');

// Una imagen de Landsat 9 con poca nubosidad sobre el AOI
var img = ee.ImageCollection('LANDSAT/LC09/C02/T1_L2')
  .filterBounds(AOI)
  .filter(ee.Filter.lte('CLOUD_COVER', 10))
  .first();

// Escalar a reflectancia y usar los nombres comunes de las bandas
var bands = img.select('SR_B.').multiply(0.0000275).add(-0.2)
  .select(['SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7'],
          ['Blue', 'Green', 'Red', 'Nir', 'Swir1', 'Swir2']);

// Máscara de agua
var water = jones.ClassifyWaterJones2019(bands);

Map.centerObject(AOI, 11);
Map.addLayer(bands, {bands: ['Red', 'Green', 'Blue'], min: 0, max: 0.25}, 'Color verdadero');
Map.addLayer(water.selfMask(), {palette: ['0000ff']}, 'Agua (DSWE)');

Ejemplo con una colección #

Para aplicar el módulo a muchas imágenes usamos map(): la función recibe cada imagen de la colección y devuelve el resultado. Dos detalles importan. Primero, cada imagen debe llegar escalada, con nombres comunes y sin nubes, y de eso se encarga prepare(). Segundo, las operaciones matemáticas borran las propiedades de la imagen, así que copiamos la fecha (system:time_start) con copyProperties() para poder graficar la serie en el tiempo.

// Cargar el módulo
var jones = require('users/sigeduca/routines:Indexes/Jones_2019R');

// Prepara cada imagen: máscara de nubes, reflectancia (0 a 1) y nombres comunes
function prepare(img) {
  var qa = img.select('QA_PIXEL');
  var clear = qa.bitwiseAnd(1 << 3).eq(0)   // bit 3: nubes
    .and(qa.bitwiseAnd(1 << 4).eq(0));       // bit 4: sombras de nubes
  var bands = img.select(['SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7'],
                         ['Blue', 'Green', 'Red', 'Nir', 'Swir1', 'Swir2'])
    .multiply(0.0000275).add(-0.2)
    .updateMask(clear);
  // Las operaciones matemáticas borran las propiedades: recuperamos la fecha
  return ee.Image(bands.copyProperties(img, ['system:time_start']));
}

// Landsat 8 y 9 de dos años sobre el AOI
var collection = ee.ImageCollection('LANDSAT/LC09/C02/T1_L2')
  .merge(ee.ImageCollection('LANDSAT/LC08/C02/T1_L2'))
  .filterBounds(AOI)
  .filterDate('2023-01-01', '2025-01-01')
  .filter(ee.Filter.lte('CLOUD_COVER', 30))
  .map(prepare);
print('Imágenes', collection.size());

// Aplicar el módulo a cada imagen, conservando la fecha
var water = collection.map(function(img) {
  return ee.Image(jones.ClassifyWaterJones2019(img).copyProperties(img, ['system:time_start']));
});

// Frecuencia de agua: porcentaje de observaciones en que cada píxel fue agua
var frequency = water.mean().multiply(100).rename('frecuencia');
Map.centerObject(AOI, 11);
Map.addLayer(frequency.selfMask(), {min: 0, max: 100, palette: ['c6dbef', '6baed6', '08306b']}, 'Frecuencia de agua (%)');

// Fracción del AOI cubierta por agua en cada fecha
var chart = ui.Chart.image.series({
  imageCollection: water,
  region: AOI,
  reducer: ee.Reducer.mean(),
  scale: 30
}).setOptions({title: 'Fracción de agua en el AOI (DSWE)', vAxis: {title: 'Fracción (0 a 1)'}, pointSize: 3});
print(chart);

Código del módulo #

No necesitas copiarlo para usarlo. Si quieres modificarlo, créalo en tu propio repositorio y cambia la ruta de require().

///////////////////////////////////////////////////////////////////////////////////
// MÓDULO JONES_2019R: AGUA SUPERFICIAL CON DSWE PARA LANDSAT COLECCIÓN 2, NIVEL 2
///////////////////////////////////////////////////////////////////////////////////

// Referencia principal:
// Jones, J. W. (2019). Improved automated detection of subpixel-scale inundation—Revised
// dynamic surface water extent (DSWE) partial surface water tests. Remote Sensing, 11(4), 374.

/* Código tomado de RivWidthCloud (Yang et al., 2020):
github.com/seanyx/RivWidthCloudPaper/blob/master/RivWidthCloud_JavaScript/functions_Landsat578/functions_waterClassification_Jones2019.js

ADAPTACIÓN A LANDSAT COLECCIÓN 2:
El original usaba Landsat Colección 1 SR, guardada x 10 000, por eso los umbrales de las
pruebas 4 y 5 eran 900, 1500, 1000, 3000 y 2500. Aquí la imagen llega escalada con scaler()
(reflectancia de 0 a 1), así que esos umbrales pasan a 0.09, 0.15, 0.1, 0.3 y 0.25,
los mismos que publica el USGS para la Colección 2.
Además, las bandas negativas se llevan a 0 al inicio de Dswe(), porque normalizedDifference()
enmascara los píxeles negativos y el agua oscura de las costas quedaba sin clasificar.
*/

// Funciones de las pruebas espectrales de DSWE
var Mndwi = function(image) {
  return(image.normalizedDifference(['Green', 'Swir1']).rename('mndwi'));
};
var Mbsrv = function(image) {
  return(image.select(['Green']).add(image.select(['Red'])).rename('mbsrv'));
};
var Mbsrn = function(image) {
  return(image.select(['Nir']).add(image.select(['Swir1'])).rename('mbsrn'));
};
var Ndvi = function(image) {
  return(image.normalizedDifference(['Nir', 'Red']).rename('ndvi'));
};
var Awesh = function(image) {
  return(image
  .expression('Blue + 2.5 * Green + (-1.5) * mbsrn + (-0.25) * Swir2', {
    'Blue': image.select(['Blue']),
    'Green': image.select(['Green']),
    'mbsrn': Mbsrn(image).select(['mbsrn']),
    'Swir2': image.select(['Swir2'])
  }));
};

var Dswe = function(i) {
  // En agua oscura (mar, lagos profundos) la reflectancia escalada puede ser negativa,
  // y normalizedDifference() enmascara esos píxeles. Se usa 0 como mínimo.
  i = i.select(['Blue', 'Green', 'Red', 'Nir', 'Swir1', 'Swir2']).max(0);
  var mndwi = Mndwi(i);
  var mbsrv = Mbsrv(i);
  var mbsrn = Mbsrn(i);
  var awesh = Awesh(i);
  var swir1 = i.select(['Swir1']);
  var nir = i.select(['Nir']);
  var ndvi = Ndvi(i);
  var blue = i.select(['Blue']);
  var swir2 = i.select(['Swir2']);
  
  
  var t1 = mndwi.gt(0.124);
  var t2 = mbsrv.gt(mbsrn);
  var t3 = awesh.gt(0);
  var t4 = mndwi.gt(-0.44)
  .and(swir1.lt(0.09))   // Colección 1: 900
  .and(nir.lt(0.15))     // Colección 1: 1500
  .and(ndvi.lt(0.7));
  var t5 = mndwi.gt(-0.5)
  .and(blue.lt(0.1))     // Colección 1: 1000
  .and(swir1.lt(0.3))    // Colección 1: 3000
  .and(swir2.lt(0.1))    // Colección 1: 1000
  .and(nir.lt(0.25));    // Colección 1: 2500
  
  var t = t1.add(t2.multiply(10)).add(t3.multiply(100)).add(t4.multiply(1000)).add(t5.multiply(10000));
  
  var noWater = t.eq(0)
  .or(t.eq(1))
  .or(t.eq(10))
  .or(t.eq(100))
  .or(t.eq(1000));
  var hWater = t.eq(1111)
  .or(t.eq(10111))
  .or(t.eq(11011))
  .or(t.eq(11101))
  .or(t.eq(11110))
  .or(t.eq(11111));
  var mWater = t.eq(111)
  .or(t.eq(1011))
  .or(t.eq(1101))
  .or(t.eq(1110))
  .or(t.eq(10011))
  .or(t.eq(10101))
  .or(t.eq(10110))
  .or(t.eq(11001))
  .or(t.eq(11010))
  .or(t.eq(11100));
  var pWetland = t.eq(11000);
  var lWater = t.eq(11)
  .or(t.eq(101))
  .or(t.eq(110))
  .or(t.eq(1001))
  .or(t.eq(1010))
  .or(t.eq(1100))
  .or(t.eq(10000))
  .or(t.eq(10001))
  .or(t.eq(10010))
  .or(t.eq(10100));
  
  var iDswe = noWater.multiply(0)
  .add(hWater.multiply(1))
  .add(mWater.multiply(2))
  .add(pWetland.multiply(3))
  .add(lWater.multiply(4));
  
  return(iDswe.rename('dswe'));
};

// Máscara de agua: clases 1 (confianza alta) y 2 (confianza moderada)
exports.ClassifyWaterJones2019 = function(img) {
  var dswe = Dswe(img);
  var waterMask = dswe.eq(1).or(dswe.eq(2)).rename(['waterMask']);
  return(waterMask);
};

Lecciones que lo usan #

Referencias #

© 2026 SIG EDUCA