ventuari.dev

Tutoriales / Imágenes

Cómo hacer un mosaico Landsat sin nubes en Google Earth Engine

Un mosaico sin nubes combina varias imágenes Landsat del mismo lugar, después de quitar de cada una las nubes y sus sombras. Aquí verás cómo hacerlo en Google Earth Engine con la imagen más despejada de cada mes. También, cómo elegir la escena por su path y row, usar las bandas QA_PIXEL y QA_RADSAT y la escala de la Colección 2, y exportar en la proyección de la escena. El mismo flujo en el Code Editor (JavaScript) y en Colab (Python).

Por VentuariPublicado el 12 min de lectura

Mosaico Landsat sin nubes de la isla de Margarita en falso color SWIR1, NIR y rojo: el suelo seco en rosado, la vegetación de los cerros y los manglares de La Restinga en verde, y el mar en negro.

Qué es un mosaico sin nubes

Un mosaico sin nubes es una imagen hecha con varias fotos del mismo lugar tomadas en fechas distintas. En cada una se quitan los píxeles con nubes o con sombra de nube, y para cada píxel se combina lo que queda. El resultado es una vista limpia de la zona en ese año.

Hace falta porque una sola imagen rara vez llega despejada, sobre todo en zonas tropicales. Landsat pasa por el mismo lugar cada 16 días por satélite, y casi siempre alguna parte de la escena viene cubierta.

Es lo que hace Landsat Explorer, nuestra aplicación de mosaicos de Venezuela: eliges la escena, el año y las imágenes, y genera el mosaico. Este tutorial explica cada paso y da un script que, además, elige solo las imágenes: la más despejada de cada mes.

Escenas de Landsat: path y row

Landsat organiza sus imágenes en una cuadrícula fija, el sistema WRS-2. Cada escena tiene un path (la órbita) y un row (la fila a lo largo de ella), y siempre cubre la misma zona. En Earth Engine se filtran con las propiedades WRS_PATH y WRS_ROW.

El nombre de cada imagen lo dice todo. Por ejemplo, LC08_006052_20250218:

ParteSignifica
LC08Landsat 8 (en Landsat 9, LC09; en Landsat 5, LT05; en Landsat 7, LE07)
006052Path 6, row 52
20250218Fecha de adquisición: 18 de febrero de 2025

Bandas y escala de la Colección 2

Landsat 8 y 9 numeran sus bandas de forma distinta a Landsat 4, 5 y 7. Para combinar sensores o reutilizar código, conviene renombrarlas, como hace el script:

BandaLandsat 4, 5 y 7Landsat 8 y 9
AzulSR_B1SR_B2
VerdeSR_B2SR_B3
RojoSR_B3SR_B4
Infrarrojo cercanoSR_B4SR_B5
Infrarrojo de onda corta 1SR_B5SR_B6
Infrarrojo de onda corta 2SR_B7SR_B7

La reflectancia de la Colección 2 se guarda como números enteros. Para convertirla hay que multiplicar por 0,0000275 y restar 0,2. El desplazamiento no se cancela en los índices ni en la mediana: sin él, los valores salen mal aunque el mapa se vea bien (ejemplo con el NDVI).

Cómo quitar nubes y sombras con QA_PIXEL

Hay dos filtros distintos y hacen falta los dos. CLOUD_COVER es el porcentaje de nubes de toda la escena, según sus metadatos: sirve para elegir imágenes. La banda QA_PIXEL marca cada píxel: sirve para quitar justo las nubes que quedan en las imágenes elegidas. El script descarta un píxel si cualquiera de estos bits está activo:

Bit de QA_PIXELMarcaSensores
0Relleno: fuera de la imagen, sin datosTodos
1Nube dilatada (el borde de la nube)Todos
2CirrosSolo Landsat 8 y 9; en Landsat 4, 5 y 7 no se usa
3NubeTodos
4Sombra de nubeTodos
5NieveTodos

Las sombras importan tanto como las nubes: oscurecen el suelo y la vegetación, y en una mediana con pocas imágenes pueden quedar como manchas. La nieve se quita porque el mosaico busca el suelo; en zonas donde la nieve es lo que quieres ver, quita el bit 5 de la máscara.

La segunda máscara usa la banda QA_RADSAT. En Landsat 8 y 9 marca los píxeles con alguna banda saturada (bits 0 a 6 y 8) y los que el relieve tapa al sensor (bit 11). El script descarta el píxel si tiene cualquiera de esas marcas, también en bandas que luego no usa: es la opción más prudente.

La imagen más despejada de cada mes y la mediana

La mediana toma, para cada píxel, el valor del medio de las fechas disponibles. Un resto de nube o de sombra que se escapó a la máscara es un valor extremo, y la mediana lo deja fuera; el promedio, en cambio, lo mezcla con el resto.

El script no usa todas las imágenes del año, sino la de menor CLOUD_COVER de cada mes. Así, cada mes pesa lo mismo en la mediana: un mes con muchas pasadas despejadas no domina el resultado. Por eso el filtro inicial es permisivo (70 %): en los meses lluviosos, una imagen con nubes parciales aporta lo que la máscara deja limpio. Los meses sin ninguna imagen se saltan.

Dos matices. CLOUD_COVER mide toda la escena, así que la imagen «más despejada» puede tener justo nubes sobre tu zona; la capa de observaciones del script lo muestra. Y un mosaico de todo el año mezcla la época seca con la de lluvias. Si quieres el paisaje de una sola estación, limita los meses (por ejemplo, con ee.Filter.calendarRange) o elige las imágenes a mano, como en Landsat Explorer.

Si adaptas el script a Landsat 5 y 7, ten en cuenta que el corrector de líneas de Landsat 7 falló el 31 de mayo de 2003. Desde entonces cada escena pierde cerca del 22 % de sus píxeles en franjas, según el USGS. Varias fechas en la mediana rellenan esas franjas; la capa de observaciones muestra dónde no lo consiguen.

La proyección al exportar

Cada escena Landsat viene en la proyección UTM de su zona. El mosaico, en cambio, no: según la guía de proyecciones de Earth Engine, una imagen hecha con median() a partir de una colección queda con WGS84 a 1° como proyección por defecto. Y una exportación sin crs toma el CRS de la primera banda de la imagen.

Sin hacer nada, el archivo saldría en EPSG:4326 y no en UTM. El script copia la proyección de la escena al mosaico con setDefaultProjection, así que exporta en su UTM sin escribir el código EPSG a mano: sirve para cualquier path y row.

El script

Busca las imágenes de Landsat 8 y 9 de la escena y el año, y elige la más despejada de cada mes. De cada una quita las nubes, las sombras, la nieve y los píxeles saturados; luego calcula la mediana y exporta el mosaico y una capa de observaciones. El ejemplo es la escena 002/052, la de la isla de Margarita (Nueva Esparta, Venezuela), en 2025.

Pega esto en el Code Editor y cambia el path, el row y el año por los de tu zona. Las funciones están al final del script: JavaScript permite usarlas antes de declararlas.

mosaico_landsat.js
/**
 * Automatic monthly Landsat mosaic
 * Collection 2, Level 2
 *
 * WRS-2:
 *     Path: 2
 *     Row: 52
 *
 * The script automatically selects the clearest Landsat
 * scene available for each month and creates an annual
 * cloud-free median mosaic.
 *
 * Satellites:
 *     - Landsat 8
 *     - Landsat 9
 */


/**
 * Configuration
 */
var path = 2;

var row = 52;

var year = 2025;

// Initial scene-level cloud filter.
// A relatively permissive threshold is useful because the
// best scene will later be selected independently for each month.
var max_cloud_cover = 70;

var collections = [
    'LANDSAT/LC08/C02/T1_L2',
    'LANDSAT/LC09/C02/T1_L2',
];

var input_bands = [
    'SR_B2',
    'SR_B3',
    'SR_B4',
    'SR_B5',
    'SR_B6',
    'SR_B7',
];

var output_bands = [
    'blue',
    'green',
    'red',
    'nir',
    'swir1',
    'swir2',
];


/**
 * Dates
 */
var start_date = ee.Date.fromYMD(year, 1, 1);

var end_date = start_date.advance(1, 'year');   // filterDate excludes the end date


/**
 * Candidate Landsat images
 */
var raw_collection = get_collection(collections, start_date, end_date);

print(
    'Available image count',
    raw_collection.size()
);

print(
    'Available images',
    raw_collection.aggregate_array(
        'LANDSAT_PRODUCT_ID'
    )
);

print(
    'Available CLOUD_COVER',
    raw_collection.aggregate_array(
        'CLOUD_COVER'
    )
);


/**
 * Automatic monthly selection
 */
var selected_raw = select_best_monthly_images(
    raw_collection
);

print(
    'Automatically selected image count',
    selected_raw.size()
);

print(
    'Automatically selected images',
    selected_raw.aggregate_array(
        'LANDSAT_PRODUCT_ID'
    )
);

print(
    'Selected months',
    selected_raw.aggregate_array('selection_month')
);

print(
    'Selected CLOUD_COVER',
    selected_raw.aggregate_array('CLOUD_COVER')
);


/**
 * Apply pixel-level QA masking
 *
 * Only the automatically selected monthly scenes are processed.
 */
var selection = selected_raw
    .map(prepare_image)
    .sort('system:time_start');


/**
 * Scene geometry and projection
 *
 * A median composite has no native projection: Earth Engine
 * gives it WGS84 at 1 degree. The UTM projection of the scene
 * is kept so the export does not fall back to EPSG:4326.
 */
var first_image = ee.Image(raw_collection.first());

var zone = first_image.geometry();

var projection = first_image
    .select('SR_B4')
    .projection();


/**
 * Annual median mosaic
 */
var mosaic = selection
    .median()
    .setDefaultProjection(projection)
    .clip(zone);


/**
 * Cloud-free observation count
 *
 * Number of valid observations contributing to each
 * pixel of the annual mosaic (0 where none is left).
 */
var observations = selection
    .select('red')
    .count()
    .unmask(0)
    .rename('observations')
    .setDefaultProjection(projection)
    .clip(zone);


/**
 * Pixels without observations
 */
var no_observations = observations
    .eq(0)
    .selfMask()
    .rename('no_observations');


/**
 * Low-observation pixels
 *
 * Areas supported by only one valid observation
 * during the year.
 */
var low_observations = observations
    .eq(1)
    .selfMask()
    .rename('low_observations');


/**
 * Visualization
 */
var vis_true_color = {
    bands: ['red', 'green', 'blue'],
    min: 0,
    max: 0.25,
};

var vis_false_color = {
    bands: ['swir1', 'nir', 'red'],
    min: [0, 0, 0],
    max: [0.35, 0.45, 0.20],
};

var vis_observations = {
    min: 1,
    max: 12,
    palette: [
        'd73027',
        'fc8d59',
        'fee08b',
        'ffffbf',
        'd9ef8b',
        '91cf60',
        '1a9850',
        '006837',
    ],
};

var vis_no_observations = {
    palette: ['ff0000'],
};

var vis_low_observations = {
    palette: ['ff9900'],
};


/**
 * Map
 */
Map.setOptions('SATELLITE');

Map.centerObject(zone, 9);

Map.addLayer(
    mosaic,
    vis_false_color,
    'False color (SWIR1, NIR, red)',
    true
);

Map.addLayer(
    mosaic,
    vis_true_color,
    'True color',
    false
);

Map.addLayer(
    observations,
    vis_observations,
    'Cloud-free observations',
    false
);

Map.addLayer(
    low_observations,
    vis_low_observations,
    'Only one valid observation',
    false
);

Map.addLayer(
    no_observations,
    vis_no_observations,
    'Pixels without observations',
    false
);


/**
 * Export
 *
 * Surface reflectance is multiplied by 10000 and
 * exported as signed 16-bit integers.
 *
 * No CRS is hardcoded: the export takes the CRS of the first
 * band, which is the scene's UTM projection set above.
 */
Export.image.toDrive({
    image: mosaic
        .multiply(10000)
        .round()
        .toInt16(),
    description: 'landsat_monthly_mosaic_' + path + '_' + row + '_' + year,
    folder: 'earthengine',
    region: zone,
    scale: 30,
    maxPixels: 1e10,
});


/**
 * Export observation count
 *
 * Optional quality layer showing how many cloud-free
 * observations support each pixel.
 */
Export.image.toDrive({
    image: observations.toUint8(),
    description: 'landsat_observations_' + path + '_' + row + '_' + year,
    folder: 'earthengine',
    region: zone,
    scale: 30,
    maxPixels: 1e10,
});


/**
 * Prepare Landsat image
 *
 * Applies Collection 2 Level 2 surface reflectance scaling
 * and masks invalid pixels using QA_PIXEL and QA_RADSAT.
 *
 * Masked conditions:
 *     - fill
 *     - dilated cloud
 *     - cirrus
 *     - cloud
 *     - cloud shadow
 *     - snow
 *     - radiometric saturation or terrain occlusion
 *
 * @param {ee.Image} image Landsat Collection 2 Level 2 image.
 * @returns {ee.Image} Cloud-free surface reflectance image.
 */
function prepare_image(image) {

    var qa = image.select('QA_PIXEL');

    var clear_mask = qa
        .bitwiseAnd(1 << 0)
        .eq(0)
        .and(qa.bitwiseAnd(1 << 1).eq(0))
        .and(qa.bitwiseAnd(1 << 2).eq(0))
        .and(qa.bitwiseAnd(1 << 3).eq(0))
        .and(qa.bitwiseAnd(1 << 4).eq(0))
        .and(qa.bitwiseAnd(1 << 5).eq(0));

    var saturation_mask = image
        .select('QA_RADSAT')
        .eq(0);

    var reflectance = image
        .select(input_bands, output_bands)
        .multiply(0.0000275)
        .add(-0.2)
        .updateMask(clear_mask)
        .updateMask(saturation_mask);

    // copyProperties returns an ee.Element: cast it back to ee.Image
    return ee.Image(
        reflectance.copyProperties(
            image,
            [
                'system:time_start',
                'CLOUD_COVER',
                'CLOUD_COVER_LAND',
                'WRS_PATH',
                'WRS_ROW',
                'SPACECRAFT_ID',
                'LANDSAT_PRODUCT_ID',
            ]
        )
    ).set('image_id', image.id());
}


/**
 * Load Landsat collections
 *
 * Loads all candidate Landsat scenes for the selected
 * WRS-2 path/row and year.
 *
 * @param {Array<string>} collection_ids Landsat collection IDs.
 * @param {ee.Date} start_date Start date.
 * @param {ee.Date} end_date End date.
 * @returns {ee.ImageCollection} Combined raw Landsat collection.
 */
function get_collection(collection_ids, start_date, end_date) {

    var collection = ee.ImageCollection([]);

    collection_ids.forEach(function (collection_id) {

        var landsat = ee.ImageCollection(collection_id)
            .filter(ee.Filter.eq('WRS_PATH', path))
            .filter(ee.Filter.eq('WRS_ROW', row))
            .filterDate(start_date, end_date)
            .filter(
                ee.Filter.lte('CLOUD_COVER', max_cloud_cover)
            );

        collection = collection.merge(landsat);
    });

    return collection
        .sort('system:time_start');
}


/**
 * Select best image of each month
 *
 * For each month, selects the candidate scene with the
 * lowest CLOUD_COVER metadata value.
 *
 * Only months with images are visited, so months without
 * candidates are skipped without conditionals or nulls.
 *
 * Selection happens before pixel-level QA masking to keep
 * computation lightweight.
 *
 * @param {ee.ImageCollection} collection Raw Landsat collection.
 * @returns {ee.ImageCollection} Best scene from each available month.
 */
function select_best_monthly_images(collection) {

    var with_month = collection.map(function (image) {
        return image.set(
            'selection_month',
            ee.Date(image.get('system:time_start')).get('month')
        );
    });

    var months = with_month
        .aggregate_array('selection_month')
        .distinct();

    var images = months.map(function (month) {
        return with_month
            .filter(ee.Filter.eq('selection_month', month))
            .sort('CLOUD_COVER')
            .first();
    });

    return ee.ImageCollection
        .fromImages(images)
        .sort('system:time_start');
}

La consola muestra las imágenes disponibles, las elegidas y su mes, cada una con su CLOUD_COVER. En el mapa, sobre la vista satelital, se cargan el mosaico en falso color (visible) y en color real, y el número de observaciones sin nubes por píxel. Dos capas de control marcan los píxeles con una sola observación válida, en naranja, y los que no tienen ninguna, en rojo. En Tasks quedan dos exportaciones: el mosaico, con la reflectancia multiplicada por 10 000, y la capa de observaciones.

La lógica es idéntica, pero las funciones van antes de usarse, como exige Python. Los operadores lógicos van con mayúscula (.And()) porque and es palabra reservada. Las listas de la consola se traen con getInfo(), que aquí es seguro porque son unos pocos valores. Las dos exportaciones arrancan solas con .start().

mosaico_landsat.py
"""
Automatic monthly Landsat mosaic
Collection 2, Level 2

WRS-2:
    Path: 2
    Row: 52

The script automatically selects the clearest Landsat
scene available for each month and creates an annual
cloud-free median mosaic.

Satellites:
    - Landsat 8
    - Landsat 9
"""

import ee
import geemap

ee.Authenticate()
ee.Initialize(project='tu-proyecto-gcp')


# Configuration
path = 2
row = 52
year = 2025

# Initial scene-level cloud filter.
# A relatively permissive threshold is useful because the
# best scene will later be selected independently for each month.
max_cloud_cover = 70

collections = [
    'LANDSAT/LC08/C02/T1_L2',
    'LANDSAT/LC09/C02/T1_L2',
]

input_bands = ['SR_B2', 'SR_B3', 'SR_B4', 'SR_B5', 'SR_B6', 'SR_B7']
output_bands = ['blue', 'green', 'red', 'nir', 'swir1', 'swir2']


def prepare_image(image):
    """
    Applies Collection 2 Level 2 surface reflectance scaling
    and masks invalid pixels using QA_PIXEL and QA_RADSAT.

    Masked conditions: fill, dilated cloud, cirrus, cloud,
    cloud shadow, snow, radiometric saturation or terrain occlusion.
    """
    qa = image.select('QA_PIXEL')

    clear_mask = (
        qa.bitwiseAnd(1 << 0).eq(0)
        .And(qa.bitwiseAnd(1 << 1).eq(0))
        .And(qa.bitwiseAnd(1 << 2).eq(0))
        .And(qa.bitwiseAnd(1 << 3).eq(0))
        .And(qa.bitwiseAnd(1 << 4).eq(0))
        .And(qa.bitwiseAnd(1 << 5).eq(0))
    )

    saturation_mask = image.select('QA_RADSAT').eq(0)

    reflectance = (
        image.select(input_bands, output_bands)
        .multiply(0.0000275)
        .add(-0.2)
        .updateMask(clear_mask)
        .updateMask(saturation_mask)
    )

    # copyProperties returns an ee.Element: cast it back to ee.Image
    return ee.Image(
        reflectance.copyProperties(
            image,
            [
                'system:time_start',
                'CLOUD_COVER',
                'CLOUD_COVER_LAND',
                'WRS_PATH',
                'WRS_ROW',
                'SPACECRAFT_ID',
                'LANDSAT_PRODUCT_ID',
            ],
        )
    ).set('image_id', image.id())


def get_collection(collection_ids, start_date, end_date):
    """Loads all candidate Landsat scenes for the WRS-2 path/row and year."""
    collection = ee.ImageCollection([])
    for collection_id in collection_ids:
        landsat = (
            ee.ImageCollection(collection_id)
            .filter(ee.Filter.eq('WRS_PATH', path))
            .filter(ee.Filter.eq('WRS_ROW', row))
            .filterDate(start_date, end_date)
            .filter(ee.Filter.lte('CLOUD_COVER', max_cloud_cover))
        )
        collection = collection.merge(landsat)
    return collection.sort('system:time_start')


def select_best_monthly_images(collection):
    """
    For each month, selects the candidate scene with the lowest
    CLOUD_COVER. Only months with images are visited.
    """
    with_month = collection.map(
        lambda image: image.set(
            'selection_month',
            ee.Date(image.get('system:time_start')).get('month'),
        )
    )
    months = with_month.aggregate_array('selection_month').distinct()
    images = months.map(
        lambda month: with_month
        .filter(ee.Filter.eq('selection_month', month))
        .sort('CLOUD_COVER')
        .first()
    )
    return ee.ImageCollection.fromImages(images).sort('system:time_start')


# Dates
start_date = ee.Date.fromYMD(year, 1, 1)
end_date = start_date.advance(1, 'year')   # filterDate excludes the end date


# Candidate Landsat images
raw_collection = get_collection(collections, start_date, end_date)

print('Available image count:', raw_collection.size().getInfo())
print('Available images:', raw_collection.aggregate_array('LANDSAT_PRODUCT_ID').getInfo())
print('Available CLOUD_COVER:', raw_collection.aggregate_array('CLOUD_COVER').getInfo())


# Automatic monthly selection
selected_raw = select_best_monthly_images(raw_collection)

print('Automatically selected image count:', selected_raw.size().getInfo())
print('Automatically selected images:', selected_raw.aggregate_array('LANDSAT_PRODUCT_ID').getInfo())
print('Selected months:', selected_raw.aggregate_array('selection_month').getInfo())
print('Selected CLOUD_COVER:', selected_raw.aggregate_array('CLOUD_COVER').getInfo())


# Pixel-level QA masking, only on the selected monthly scenes
selection = selected_raw.map(prepare_image).sort('system:time_start')


# Scene geometry and projection.
# A median composite has no native projection: Earth Engine gives it
# WGS84 at 1 degree. The UTM projection of the scene is kept so the
# export does not fall back to EPSG:4326.
first_image = ee.Image(raw_collection.first())
zone = first_image.geometry()
projection = first_image.select('SR_B4').projection()


# Annual median mosaic
mosaic = selection.median().setDefaultProjection(projection).clip(zone)


# Cloud-free observations per pixel (0 where none is left)
observations = (
    selection.select('red')
    .count()
    .unmask(0)
    .rename('observations')
    .setDefaultProjection(projection)
    .clip(zone)
)
no_observations = observations.eq(0).selfMask().rename('no_observations')
low_observations = observations.eq(1).selfMask().rename('low_observations')


# Export: reflectance x 10000 as signed 16-bit integers. No CRS is
# hardcoded: the export takes the CRS of the first band (the scene's UTM)
task_mosaic = ee.batch.Export.image.toDrive(
    image=mosaic.multiply(10000).round().toInt16(),
    description=f'landsat_monthly_mosaic_{path}_{row}_{year}',
    folder='earthengine',
    region=zone,
    scale=30,
    maxPixels=1e10,
)
task_mosaic.start()

# Optional quality layer: cloud-free observations per pixel
task_observations = ee.batch.Export.image.toDrive(
    image=observations.toUint8(),
    description=f'landsat_observations_{path}_{row}_{year}',
    folder='earthengine',
    region=zone,
    scale=30,
    maxPixels=1e10,
)
task_observations.start()


# Map
vis_true_color = {'bands': ['red', 'green', 'blue'], 'min': 0, 'max': 0.25}
vis_false_color = {'bands': ['swir1', 'nir', 'red'], 'min': [0, 0, 0], 'max': [0.35, 0.45, 0.20]}
vis_observations = {
    'min': 1,
    'max': 12,
    'palette': ['d73027', 'fc8d59', 'fee08b', 'ffffbf', 'd9ef8b', '91cf60', '1a9850', '006837'],
}

mapa = geemap.Map()
mapa.add_basemap('SATELLITE')
mapa.centerObject(zone, 9)
mapa.addLayer(mosaic, vis_false_color, 'False color (SWIR1, NIR, red)', True)
mapa.addLayer(mosaic, vis_true_color, 'True color', False)
mapa.addLayer(observations, vis_observations, 'Cloud-free observations', False)
mapa.addLayer(low_observations, {'palette': ['ff9900']}, 'Only one valid observation', False)
mapa.addLayer(no_observations, {'palette': ['ff0000']}, 'Pixels without observations', False)
mapa

Un mosaico de Margarita con 12 de las 41 imágenes de 2025

En 2025 hubo 41 imágenes de Landsat 8 y 9 de la escena 002/052 con un 70 % de nubes o menos. Todos los meses tuvieron al menos una, así que el script eligió 12: la más despejada de cada mes. El mosaico cubre toda la isla de Margarita sin huecos visibles. En falso color, el suelo seco de las tierras bajas y de la península de Macanao sale en rosado, y la vegetación de los cerros, en verde. También salen en verde los manglares de la laguna de La Restinga, en el istmo que une las dos partes de la isla.

Mosaico Landsat sin nubes de la isla de Margarita en falso color SWIR1, NIR y rojo: el suelo seco en rosado, la vegetación de los cerros y los manglares de La Restinga en verde, y el mar en negro.
Mosaico anual de 2025 de la escena 002/052, isla de Margarita (Nueva Esparta), en falso color SWIR1, NIR y rojo.
EscenaPath 2, row 52 (isla de Margarita)
Periodo1 ene – 31 dic 2025
Nubes por escenaHasta 70 % (filtro inicial)
Imágenes disponibles41, de Landsat 8 y 9
Imágenes en el mosaico12, la más despejada de cada mes

Cómo leer el mosaico en falso color

La combinación infrarrojo de onda corta 1 (SWIR1), infrarrojo cercano (NIR) y rojo es la misma de Landsat Explorer. Separa coberturas que en color real se confunden:

ColorSuele ser
Verde intensoVegetación densa y activa
Rosado o magenta, más claro cuanto más secoVegetación seca o escasa, suelo desnudo y arena
Azul oscuro o negroAgua profunda o clara
Azul claroAgua poco profunda o con sedimentos

Los tonos exactos dependen del contraste (los valores min y max de cada capa). Lo que se mantiene es el reparto: qué coberturas se separan y cuáles se parecen.

Si necesitas mosaicos de varios años o de un territorio completo, listos para tu SIG, mira el servicio de mapas satelitales.

Lo que la IA suele hacer mal

Si le pides este script a un asistente de IA, estos fallos aparecen a menudo:

  1. Copia la máscara de nubes de la Colección 1. Los códigos viejos usan los bits 3 y 5 de pixel_qa. En la Colección 2, el bit 3 de QA_PIXEL es nube y el 5 es nieve: las sombras, que están en el bit 4, se quedan en el mosaico.
  2. Escala la reflectancia sin el desplazamiento. Multiplica por 0,0000275 pero no resta 0,2. Los valores quedan desplazados y los índices calculados después salen mal.
  3. Pide el año con filterDate(año-01-01, año-12-31). La fecha final no se incluye, así que las imágenes del 31 de diciembre quedan fuera. El fin correcto es el 1 de enero del año siguiente.
  4. Exporta el mosaico sin crs y da por hecho que sale en UTM. Un mosaico hecho con median() tiene WGS84 a 1° como proyección por defecto, y la exportación toma el CRS de la primera banda: el archivo sale en EPSG:4326. Hay que indicar el crs o copiar la proyección de la escena con setDefaultProjection.
  5. Combina Landsat 5 y 7 de después de 2003 sin revisar las franjas. Con pocas fechas, las líneas sin datos de Landsat 7 aparecen en el mosaico. La capa de observaciones las muestra.

Preguntas frecuentes

¿Qué son el path y el row de Landsat?

Son las coordenadas de la cuadrícula WRS-2 con la que Landsat organiza sus escenas: el path es la órbita y el row, la fila a lo largo de ella. Cada combinación cubre siempre la misma zona, por ejemplo la 006/052 en el norte de Falcón, Venezuela.

¿Cómo se quitan las nubes en Landsat Colección 2?

Con la banda QA_PIXEL: se descartan los píxeles con los bits 1 (nube dilatada), 2 (cirros, solo en Landsat 8 y 9), 3 (nube) o 4 (sombra de nube) activos. El bit 0 marca el relleno sin datos y el 5, la nieve. Para elegir las imágenes se usa el porcentaje de nubes de la escena, CLOUD_COVER.

¿Se pueden combinar Landsat 8 y Landsat 9 en un mismo mosaico?

Sí. Usan los mismos nombres de bandas, la misma escala de reflectancia y la misma banda de calidad en la Colección 2, así que basta con unir las dos colecciones antes de calcular la mediana.

¿Por qué usar la mediana y no el promedio?

Porque la mediana ignora los valores extremos, como un resto de nube o de sombra que la máscara no detectó. El promedio los mezcla con el resto de las fechas y deja manchas en el mosaico.

Fuentes

  1. USGS Landsat 8 Level 2, Collection 2, Tier 1Google Earth Engine, catálogo de datosBandas, escala de la reflectancia y significado de cada bit de QA_PIXEL y QA_RADSAT.
  2. USGS Landsat 9 Level 2, Collection 2, Tier 1Google Earth Engine, catálogo de datosLas mismas bandas y escala que Landsat 8.
  3. USGS Landsat 5 Level 2, Collection 2, Tier 1Google Earth Engine, catálogo de datosBandas de Landsat 4, 5 y 7, y el bit 2 sin uso.
  4. Landsat 7U.S. Geological Survey (USGS)Fallo del corrector de líneas el 31 de mayo de 2003 y los datos que se pierden.
  5. ee.ImageCollection.filterDateGoogle Earth Engine, referencia de la APILa fecha final del filtro no se incluye.
  6. Landsat AlgorithmsGoogle Earth Engine, documentaciónCómo trabaja Earth Engine con las colecciones Landsat.
  7. ProjectionsGoogle Earth Engine, documentaciónProyección por defecto de un mosaico (WGS84 a 1°) y setDefaultProjection.
  8. Exporting ImagesGoogle Earth Engine, documentaciónSin crs, la exportación toma el CRS de la primera banda.

Sigue aprendiendo

Mira esta técnica aplicada en Landsat Explorer, la aplicación de mosaicos Landsat de Venezuela. ¿Lo necesitas para tu zona? Mira el servicio Aplicaciones web para explorar resultados.

¿Necesitas mosaicos de varios años o un proceso que los genere solo?

Escribe a contacto@ventuari.dev con la zona y el periodo, y recibirás una propuesta con alcance y precio.