rm(list = ls(all=TRUE))
Este cuaderno ilustra cómo acceder a series temporales de imágenes
Sentinel-2 utilizando los paquetes rstac y
gdalcubes.
rstac ofrece funciones que simplifican el proceso de
adquisición de datos espaciales desde catálogos STAC (SpatioTemporal
Asset Catalog).
gdalcubes es un paquete de R y una librería en C++ cuyo
objetivo es facilitar, agilizar y hacer más interactivo el procesamiento
de colecciones de imágenes satelitales.
Utilizaremos el catálogo de Sentinel-2 COG disponible gratuitamente en Amazon Web Services (AWS) y su correspondiente punto de acceso STAC-API.
Adicionalmente, utilizaremos terra para la lectura de
archivos ráster y tmap junto con leaflet para
la visualización.
Lalamamos los paquetes istalados
library(rstac)
library(gdalcubes)
library(stars)
library(tmap)
library(tmaptools)
library(mapview)
library(leaflet)
library(sf)
library(ggplot2)
En este cuaderno, el área de interés corresponde al municipio de Sandoná, Nariño.
A continuación, se realiza la lectura del archivo GeoJSON cargando la
capa vectorial mediante la función st_read() ya con los
datos descargados de GeoJson Studio
sandona <- st_read("C:/Users/Acer/Desktop/GB2R/Proyecto5/datos/sandona.geojson")
## Reading layer `sandona' from data source
## `C:\Users\Acer\Desktop\GB2R\Proyecto5\datos\sandona.geojson'
## using driver `GeoJSON'
## Simple feature collection with 1 feature and 1 field
## Geometry type: POLYGON
## Dimension: XY
## Bounding box: xmin: -77.50876 ymin: 1.264058 xmax: -77.44123 ymax: 1.306711
## Geodetic CRS: WGS 84
La función stac_search() permite a los usuarios buscar
datos de Sentinel-2 a partir de un área de interés (AOI)
georreferenciada en el sistema EPSG:4326 y un periodo de tiempo
determinado. Probremos su funcionamiento a continuación:
s = stac("https://earth-search.aws.element84.com/v0")
items <- s |>
stac_search(collections = "sentinel-s2-l2a-cogs",
bbox = c(st_bbox(sandona)["xmin"],
st_bbox(sandona)["ymin"],
st_bbox(sandona)["xmax"],
st_bbox(sandona)["ymax"]),
datetime = "2025-01-01/2025-12-31",
limit = 60) |>
post_request()
items
## ###Items
## - matched feature(s): 103
## - features (60 item(s) / 43 not fetched):
## - S2C_18NTG_20251211_0_L2A
## - S2B_17NRB_20251206_0_L2A
## - S2B_18NTG_20251206_0_L2A
## - S2C_17NRB_20251201_0_L2A
## - S2C_18NTG_20251201_0_L2A
## - S2B_17NRB_20251126_0_L2A
## - S2B_18NTG_20251126_0_L2A
## - S2A_17NRB_20251123_0_L2A
## - S2A_18NTG_20251123_1_L2A
## - S2C_17NRB_20251121_0_L2A
## - ... with 50 more feature(s).
## - assets:
## AOT, B01, B02, B03, B04, B05, B06, B07, B08, B09, B11, B12, B8A, info, metadata, overview, SCL, thumbnail, visual, WVP
## - item's fields:
## assets, bbox, collection, geometry, id, links, properties, stac_extensions, stac_version, type
Imprimamos la fecha y hora de la primera y última imagen obtenidas:
range(sapply(items$features, function(x) {x$properties$datetime}))
## [1] "2025-08-13T15:43:09Z" "2025-12-11T15:42:56Z"
Podemos convertir la respuesta de STAC en una colección de imágenes
de gdalcubes utilizando la función
stac_image_collection(). Esta función recibe como entrada
una lista de elementos STAC y opcionalmente permite aplicar filtros
sobre los metadatos y las bandas.
A diferencia de la creación de colecciones desde archivos locales, esta operación es mucho más rápida debido a que todos los metadatos se encuentran disponibles previamente y no requiere abrir ningún archivo de imagen. A continuación, creamos una colección con imágenes que presentan menos del 20% de cobertura de nubes.
assets = c("B01","B02","B03","B04","B05","B06", "B07","B08","B8A","B09","B11","SCL")
s2_collection = stac_image_collection(items$features, asset_names = assets, property_filter = function(x) {x[["eo:cloud_cover"]] < 20})
Comprobemos lo que hemos obtenido:
s2_collection
## Image collection object, referencing 2 images with 12 bands
## Images:
## name left top bottom right
## 1 S2B_18NTG_20251206_0_L2A -77.69653 1.808999 0.815529 -76.70948
## 2 S2B_18NTG_20250927_0_L2A -77.69653 1.808999 0.816433 -76.70950
## datetime srs
## 1 2025-12-06T15:42:44 EPSG:32618
## 2 2025-09-27T15:42:48 EPSG:32618
##
## Bands:
## name offset scale unit nodata image_count
## 1 B01 0 1 2
## 2 B02 0 1 2
## 3 B03 0 1 2
## 4 B04 0 1 2
## 5 B05 0 1 2
## 6 B06 0 1 2
## 7 B07 0 1 2
## 8 B08 0 1 2
## 9 B09 0 1 2
## 10 B11 0 1 2
## 11 B8A 0 1 2
## 12 SCL 0 1 2
En este caso, tenemos 2 imágenes.
Una vez que disponemos de un objeto de colección de imágenes, podemos
hacer uso de gdalcubes. En particular, definimos una vista
del cubo de datos (cube view) y aplicamos las operaciones
deseadas. En el siguiente ejemplo, creamos una imagen compósita RGB de
mediana con una resolución gruesa de 100 metros a partir de un cubo de
datos mensual.
(sandona_box = st_bbox(sandona))
## xmin ymin xmax ymax
## -77.508757 1.264058 -77.441229 1.306711
(sandona_b9377 <- st_bbox(st_transform(sandona, "EPSG:9377")))
## xmin ymin xmax ymax
## 4498097 1698146 4505622 1702861
gdalcubes_options(parallel = 8)
(v = cube_view(srs="EPSG:9377", dx=10, dy=10, dt="P1M",
aggregation="median", resampling = "average",
extent=list(t0 = "2025-01-01", t1 = "2025-12-31",
left=sandona_b9377["xmin"], right=sandona_b9377["xmax"],
top=sandona_b9377["ymax"], bottom=sandona_b9377["ymin"])))
## A data cube view object
##
## Dimensions:
## low high count pixel_size
## t 2025-01-01 2025-12-31 12 P1M
## y 1698143.67743554 1702863.67743554 472 10
## x 4498094.35133148 4505624.35133148 753 10
##
## SRS: "EPSG:9377"
## Temporal aggregation method: "median"
## Spatial resampling method: "average"
Primero con color natural (i.e. RGB 432).
raster_cube(s2_collection, v) |>
select_bands(c("B02","B03","B04")) |>
reduce_time(c("median(B02)", "median(B03)", "median(B04)")) |>
plot(rgb = 3:1, zlim = c(0,2500))
Ahora con falso color (i.e. RGB 8-11-4).
raster_cube(s2_collection, v) |>
select_bands(c("B04","B11","B08")) |>
reduce_time(c("median(B04)", "median(B11)", "median(B08)")) |>
plot(rgb = 3:1, zlim = c(0,4500))
Descargamos los arhivos para usarlos y verificamos lo que se descargó:
# Ruta absoluta hacia tu carpeta "datos"
tutorialDir = "C:/Users/Acer/Desktop/GB2R/Proyecto5/datos"
# Definimos la subcarpeta específica para Sandoná dentro de datos
outputFolder = file.path(tutorialDir, "sandona_10")
if (!file.exists(outputFolder))
{
# Máscara para nubes (8, 9) y sombras (3)
s2.mask = image_mask("SCL", values = c(3, 8, 9))
# Hilos de procesamiento seguros y barra de progreso activa
gdalcubes_options(parallel = 4, progress = TRUE)
# Generamos y exportamos las imágenes
raster_cube(s2_collection, v, mask = s2.mask) |>
write_tif(outputFolder)
}
# Verificar archivos creados dentro de la carpeta
list.files(outputFolder)
## [1] "cube_23e4127a56152025-01-01.tif" "cube_23e4127a56152025-02-01.tif"
## [3] "cube_23e4127a56152025-03-01.tif" "cube_23e4127a56152025-04-01.tif"
## [5] "cube_23e4127a56152025-05-01.tif" "cube_23e4127a56152025-06-01.tif"
## [7] "cube_23e4127a56152025-07-01.tif" "cube_23e4127a56152025-08-01.tif"
## [9] "cube_23e4127a56152025-09-01.tif" "cube_23e4127a56152025-10-01.tif"
## [11] "cube_23e4127a56152025-11-01.tif" "cube_23e4127a56152025-12-01.tif"
En primer lugar, leemos un archivo GeoTIFF exportado previamente.
# First, read one GTiff file:
f <- file.path(outputFolder, "cube_23e4127a56152025-12-01.tif")
sandona_s2 <- terra::rast(f)
A continuación, calculamos el Índice de Vegetación de Diferencia Normalizada (NDVI) utilizando la banda del Rojo (B4) y del Infrarrojo Cercano (B8).
# Then, compute the NDVI index:
# 1. Define the bands
red <- sandona_s2[[4]] # Band 4
nir <- sandona_s2[[8]] # Band 8
# 2. Calculate NDVI
ndvi <- (nir - red) / (nir + red)
Comprobemos las propiedades y estadísticas del ráster NDVI resultante:
ndvi
## class : SpatRaster
## size : 472, 753, 1 (nrow, ncol, nlyr)
## resolution : 10, 10 (x, y)
## extent : 4498094, 4505624, 1698144, 1702864 (xmin, xmax, ymin, ymax)
## coord. ref. : MAGNA-SIRGAS 2018 / Origen-Nacional (EPSG:9377)
## source(s) : memory
## varname : cube_23e4127a56152025-12-01
## name : B08
## min value : -0.279392
## max value : 0.940999
Ahora es momento de la visualización. Comencemos con un mapa estático sencillo:
# Map NDVI with a continuous Red-Yellow-Green palette
tm_shape(ndvi) +
tm_raster(
col_palette = "RdYlGn",
style = "cont",
title = "NDVI Value"
) +
tm_layout(main.title = "Sentinel-2 NDVI Dec. 2025")
De acuerdo con el mapa obtenido y los rangos teóricos del NDVI (-1 a 1):
Otra visualización puede ayudar a interpretar los valores de NDVI:
library(tmap)
library(terra)
# 1. Establecer el modo interactivo de tmap
tmap_mode("view")
# 2. Construir el mapa interactivo
mapa_ndvi <- tm_shape(ndvi) +
tm_raster(
style = "cont",
palette = c("red", "yellow", "green"),
title = "NDVI Index",
alpha = 0.6
) +
tm_basemap("Esri.WorldImagery")
# 3. Mostrar el mapa explícitamente en el cuaderno
mapa_ndvi