Ejercicio: Datos satelitales en GRASS GIS
En este ejercicio, trabajaremos con imágenes del satélite Sentinel 2 en GRASS GIS. Vamos a recorrer algunos módulos específicos para la búsqueda, descarga e importación de datos Sentinel, abordar los diferentes pasos a seguir en función del nivel de procesamiento, enmascarar nubes y sombras de nubes, realizar segmentaciones y clasificaciones supervisadas.
Antes de empezar y para ganar tiempo, conectamos nuestro drive e instalamos GRASS en Google Colab.
Chequeamos el path de instalación de GRASS
e instalamos otras librerías de python que nos serán útiles.
Datos Sentinel 2
- Lanzamiento: Sentinel-2A en 2015, Sentinel-2B en 2017
- Tiempo de revisita: ~5 días
- Cobertura sistemática de áreas terrestres y costeras entre los 84°N y 56°S
- 13 bandas espectrales con resolución espacial de 10 m (VIS y NIR), 20 m (red-edge y SWIR) y 60 m (otras)
Niveles de procesamiento Sentinel 2
- L1C: Reflectancia a tope de atmósfera o Top of Atmosphere (TOA). Disponibles desde el lanzamiento.
- L2A: Reflectancia Superficial o Bottom of Atmosphere (BOA), i.e., los datos han sido corregidos para remover los efectos de la atmósfera. Sólo desde 2019.
Long Term Archive (LTA)
Todos los productos (1C o 2A) de más de un año son movidos fuera de línea y se requiere un tiempo de espera para ponerlos a disposición del usuario. Esto dificulta la automatización de tareas con productos de más de 12 meses de antigüedad.
Extensiones de GRASS para datos Sentinel
- i.sentinel.download: descarga productos Copernicus Sentinel de Copernicus Open Access Hub
- i.sentinel.import: importa datos Sentinel descargados de Copernicus Open Access Hub
- i.sentinel.preproc: importa y realiza corrección atmosférica y topográfica de imágenes S2
- i.sentinel.mask: crea máscaras de nubes y sombras para imágenes S2
- i.sentinel.coverage: comprueba la cobertura de área de las escenas de S1 o S2 seleccionadas
- i.sentinel.parallel.download: descarga imagenes Sentinel en paralelo
Para conectarse al Copernicus Open Access Hub a través de i.sentinel.download, se necesita ser usuario registrado.
Cada participante necesita registrarse y crear el archivo SENTINEL_SETTING.txt
en el directorio $HOME/gisdata/
con el siguiente contenido:
your_username
your_password
Manos a la obra
Iniciamos GRASS
Iniciar GRASS GIS, crear nuevo mapset y establecer región computacional
Para trabajar con los datos de Sentinel 2 en esta sesión, creamos un nuevo mapset y lo llamamos sentinel2.
Al igual que en la sesión anterior y como haremos cada vez que iniciemos un proyecto, primero definimos la región computacional. En este caso, vamos a usar el radio urbano de Córdoba que extrajimos en la sesión anterior.
Búsqueda y descarga de datos S2
Instalamos la caja de herramientas i.sentinel
que consta de varios módulos que facilitan la búsqueda, filtrado, descarga, importación y pre-procesado de datos Sentinel, especialmente Sentinel 2, desde una sesión de GRASS GIS. Ver i.sentinel y los links de cada módulo para más detalles.
Usamos el módulo i.sentinel.download
que internamente utiliza la librería sentinelsat
para buscar y filtrar escenas que contengan la región definida a partir del área urbana de la ciudad de Córdoba. Para esto necesitamos estar registrados en el Copernicus hub y tener nuestras credenciales en un archivo de texto. Para más detalles sobre la función y sus usos, visitar el manual.
# list all available S2 level 2A scenes that meet the criteria specified
s2_credentials = os.path.join(homedir, "gisdata", "sentinel")
start_date = "2020-03-01"
end_date = "2020-04-30"
list_prod = gs.read_command("i.sentinel.download",
settings=s2_credentials,
start=start_date,
end=end_date,
producttype="S2MSI2A",
clouds=30,
area_relation="Contains",
footprints="s2_footprints",
flags="l")
Si con los criterios de búsqueda se encuentra una larga lista de productos, se puede usar la opción limit
para controlar el número de escenas listadas.
Mostramos los footprints de las escenas que encontramos según los criterios de búsqueda:
Se observan 2 tipos de footprints: uno cuadrado y otro una pasada inclinada. Encontramos solo estos dos porque establecimos que nuestro bounding box (región computacional) debía estar contenido en la escena.
El próximo paso es descargar la escena o las escenas de interés. Para eso usamos nuevamente el módulo i.sentinel.download
, pero sin el flag l
de listar. Elegimos una imagen con nubes para demostrar posteriormente el uso de otro módulo del set de herramientas.
Como la descarga puede llevar bastante tiempo, nos saltaremos esta parte y utilizaremos directamente una escena ya descargada. Aún así, dejamos un ejemplo a continuación para referencia futura :)
Importar datos Sentinel 2 a GRASS GIS
1. Importar con corrección atmosférica
Si queremos importar con corrección atmosférica productos de nivel 1C, podemos usar el módulo i.sentinel.preproc. Este módulo utiliza i.atcorr internamente y necesita 2 inputs claves: un valor de aerosol optical depth (AOD) y un mapa de elevación.
Para una descripción más detallada del procedimiento para obtener los valores de AOD y mapas de elevación necesarios para la corrección atmosférica y topográfica, ver aquí.
2. Importar sin corrección atmosférica
Una vez descargada la escena deseada, se procede a importarla al mapset de trabajo. Para ello se usa el comando i.sentinel.import que permite subset, resampleo y reproyección al vuelo. Además, si en nuestra carpeta existen varios zip nos permite seleccionar, por medio de un patrón, cuáles queremos importar; y también, elegir solo las bandas de interés. Entonces, vamos a imprimir información sobre las bandas antes de importarlas.
# import only bands relevant for RGB, NDVI and NDWI
gs.run_command("i.sentinel.import",
input=s2_data,
pattern_file="*20200330T141049*", # in case we have more s2 scenes in the s2_data folder
pattern="B(02_1|03_1|04_1|08_1|8A_2|11_2|12_2)0m", # select bands and resolutions
extent="region", # subset import to region extent
flags="rcsj") # reproject, clouds, shadow, json metadata
Listamos los mapas importados:
A continuación, chequeamos los metadatos de uno de los mapas o bandas. Notar que todos los metadatos que vienen con la imagen original se agregan a la banda correspondiente en el momento de la importación. Esto es gracias al uso del flag j
en el comando i.sentinel.import
.
Veamos los semantic labels de las bandas importadas:
Balance de colores y composiciones
A continuación, aplicamos la paleta de grises a las bandas R, G y B, mejoramos el contraste y las mostramos usando la clase Map
de grass.jupyter
.
Realizar balance de colores y mostrar combinacion falso color NIR-RED-GREEN
Identificación y enmascarado de nubes
Como empezaremos a generar mapas raster nuevos, es fundamental que establezcamos la región computacional a los límites y resolución de una de nuestras bandas. También podría ser que nos interesase un área más pequeña para hacer unas pruebas iniciales. Esto es sumamente facil y evita que tengamos que cortar raster a raster físicamente.
Ahora demostramos el uso del módulo i.sentinel.mask que toma los metadatos registrados al importar las bandas para ejecutar la identificacion de nubes y sus sombras.
# identify and mask clouds and clouds shadows: i.sentinel.mask
gs.run_command("i.sentinel.mask",
blue="T20JLL_20200330T141049_B02_10m",
green="T20JLL_20200330T141049_B03_10m",
red="T20JLL_20200330T141049_B04_10m",
nir="T20JLL_20200330T141049_B08_10m",
nir8a="T20JLL_20200330T141049_B8A_20m",
swir11="T20JLL_20200330T141049_B11_20m",
swir12="T20JLL_20200330T141049_B12_20m",
cloud_mask="cloud",
shadow_mask="shadow",
scale_fac=10000,
flags="s",
overwrite=True)
Por defecto obtenemos un mapa vectorial con contornos de nubes y otro para las sombras de nubes:
Veamos el resultado agregando “capas” al mapa anterior:
Inspeccionamos el mapa de nubes descargado con la escena (a traves de los flags c
y s
) y establecemos la misma paleta de colores a los fines de comparar ambos productos: la máscara de nubes y sombras obtenida con i.sentinel.mask
y la provista por ESA junto con las imágenes.
Asignamos la misma paleta de colores y visualizamos el resultado:
Los resultados son bastante parecidos y en este último caso, no necesitariamos ejecutar un segundo comando. No obstante, tanto i.sentinel.mask
como la banda que viene con la escena, tienen opciones para ajustar el tamaño de nubes y sombras identificadas que aquí se dejaron por defecto.
Indices espectrales de vegetación y agua
Antes de proceder a calcular los índices, vamos a enmascarar las áreas identificadas como nubes y sombras. Para eso, primero pegamos los vectores en uno solo y luego lo aplicamos como máscara inversa. Para más detalles del funcionamiento de las máscaras en GRASS ver r.mask.
Luego utilizamos los módulos i.vi e i.wi (addon) para estimar NDVI y NDWI. Ver los respectivos manuales para más detalles sobre los índices disponibles.
# estimate water indices and set color palette
gs.run_command("i.wi",
green="T20JLL_20200330T141049_B03_10m",
nir="T20JLL_20200330T141049_B08_10m",
output="T20JLL_20200330T141049_NDWI_10m",
winame="ndwi_mf")
# add semantic label
gs.run_command("r.support",
map="T20JLL_20200330T141049_NDWI_10m",
semantic_label="NDWI")
# set ndwi color table
gs.run_command("r.colors",
map="T20JLL_20200330T141049_NDWI_10m",
color="ndwi")
# interactive maps
idx_map = gj.InteractiveMap(width = 400, use_region=True, tiles="OpenStreetMap")
idx_map.add_raster("T20JLL_20200330T141049_NDVI_10m", opacity=0.7)
idx_map.add_raster("T20JLL_20200330T141049_NDWI_10m", opacity=0.7)
idx_map.add_layer_control(position = "bottomright")
idx_map.show()
# ... use the layer selector in the corner to enable/disable the NDVI/NDWI layers
Mapas de GRASS como arrays de Numpy
Los mapas de GRASS pueden leerse como arrays de Numpy gracias a la funcion array
de la librería grass.script. Esto facilita muchas operaciones posteriores con librerías de Python que requieren un array como input. En este caso, demostramos su uso con un histograma.
# Import required libraries
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from grass.script import array as garray
# Read NDVI as numpy array
ndvi = garray.array(mapname="T20JLL_20200330T141049_NDVI_10m", null="nan")
ndwi = garray.array(mapname="T20JLL_20200330T141049_NDWI_10m", null="nan")
print(ndvi.shape,ndwi.shape)
Segmentación
Existen varios módulos para relizar segmentación en GRASS GIS, los mas conocidos son i.segment e i.superpixels.slic. Vamos a demostrar su uso.
Primero, instalamos la extensión i.superpixels.slic.
Luego, listamos los mapas y crear el grupo con las bandas y los índices.
Ejecutar i.superpixels.slic y convertir el resultado a vector
Ejecutar i.segment y convertir el resultado a vector
Comparamos el número de segmentos obtenidos:
Mostrar NDVI junto con las 2 salidas de la segmentación
Ejecutar cualquiera de los 2 métodos de segmentación con diferentes parámetros y comparar los resultados
Clasificación supervisada
Clasificación supervisada con Maximum Likelihood
Vamos a demostrar a continuacion el workflow para realizar una clasificacion supervisada por máxima verosimilitud a partir de unos polígonos de entrenamiento digitalizados en GRASS y que se encuentran en la carpeta s2_data
.
Importamos el vector y lo visualizamos.
# map
cba_rgb_train = gj.Map(width=300, use_region=True)
cba_rgb_train.d_rgb(red="T20JLL_20200330T141049_B04_10m",
green="T20JLL_20200330T141049_B03_10m",
blue="T20JLL_20200330T141049_B02_10m")
cba_rgb_train.d_grid(size=5000, fontsize=7, flags="b")
cba_rgb_train.d_vect(map="training")
cba_rgb_train.show()
Convertimos el mapa vectorial con los polígonos de entrenamiento a mapa raster. Para ello, utilizamos la columna que contiene los números de clase.
A continuación, utilizamos el módulo i.gensig para generar las firmas espectrales de las clases de interés a partir de los polígonos conocidos/etiquetados.
El archivo de firmas es un archivo de texto que se guarda automáticamente dentro de la carpeta grassdata/posgar2007_4_cba/sentinel2/signatures/sig/sig_sentinel/
y tiene la siguiente forma:
1
#
S2_2 S2_3 S2_4 S2_8 S2_11 S2_12 S2_8A NDVI NDWI
#class_1
34976
462.107 754.712 650.115 2652.12 2328.17 1354.23 2898.15 0.590024 -0.54825
3433.54
3439.85 13667.9
4587.72 3893.81 16344.2
-891.313 51917 -46133.6 508644
6381.57 8558.14 18795.3 -11728.6 64026.7
5455.05 4485.2 19848.3 -53771.7 37873.6 35044.9
-1286.3 52732.6 -45664.8 513310 -5531.12 -52538.1 526931
-1.76356 3.82015 -10.8837 64.4926 -9.11136 -12.7041 64.3455 0.0108229
1.38826 -0.0813683 6.12975 -29.5299 4.5855 6.97446 -29.4906 -0.00517479 0.00292289
#class_2
37706
1609.95 1825.69 1919.19 2607.13 2801.12 2369.91 2765.3 0.160587 -0.184169
234001
240075 251609
248408 260806 281060
151243 167006 169635 212669
123807 133556 145512 127378 164973
116139 123360 136440 90707.7 153127 163716
93815.4 104574 105790 153545 118673 82117.3 147388
-33.6194 -33.5942 -38.5054 -0.417204 -11.895 -16.7121 3.63643 0.0104315
33.4993 33.3665 35.3967 1.58334 9.64074 13.9172 -2.8798 -0.00928648 0.00896403
#class_3
13363
744.78 937.046 1247.71 1824.1 3187.07 2764.02 2009.06 0.191184 -0.325633
27636.3
34766.2 44710.3
42545.7 55127.4 70153.9
50592 65854 81541.4 102364
39674.6 49909.5 66588.2 79876.4 180631
38446.9 49209.5 64455.1 73554.7 120268 105226
49526.6 64512.1 80082.1 100116 81815.5 74146.1 99653.8
-3.37419 -4.24876 -5.95143 -4.94336 -5.45999 -6.25113 -5.00936 0.0010409
4.3353 5.42295 6.64764 6.64209 5.19235 6.46732 6.6178 -0.000876728 0.00103444
donde:
Line 1: version number (currently always 1)
Line 2: text label
Line 3: Space separated list of semantic labels
Line 4: text label of class
Line 5: number of points in class
Line 6: mean values per band of the class
Line 7-15: (semi)-matrix of band-band covariance
Para realizar la clasificación supervisada por máxima verosimilitud, el módulo i.maxlik toma el grupo y la firma como principales inputs:
Asignamos etiquetas a las clases y visualizamos el resultado.
Estadísticas por clase
Utilizamos el comando r.report para obtener la proporción de cada clase, incluida la de celdas sin datos debido al enmascarado de nubes y sombras de nubes.
A continuación, usamos el módulo r.univar para obtener estadísticas descriptivas por clase para la variable NDVI. Para esto, además del raster de NDVI, se pasa la clasificación. Así, el módulo calcula las estadísticas para cada clase.
Clasificación supervisada con Machine Learning
Primero tenemos que instalar la extensión r.learn.ml2 que consta de dos módulos: r.learn.train
y r.learn.predict
. Esta extensión utiliza la librería scikit-learn de Python y requiere que la misma esté instalada de antemano.
Ahora entrenamos el modelo ML usando r.learn.train, con el modelo “RandomForestClassifier”.
El modelo se ha almacenado en el archivo rf_model.gz
para su uso en el paso de predicción de la clasificación supervisada.
Ahora entonces aplicamos el modelo entrenado a todo el conjunto de datos.
Estimar estadísticas por clase al igual que hicimos para la clasificación por maxíma verosimilitud.
Comparemos los resultados visualmente y con el módulo r.coin, que nos permite tabular la ocurrencia mutua o coincidencia entre las categorías de dos mapas ráster.
Comparar los resultados de ambos tipos de clasificación supervisada a través del índice Kappa.
Post-procesamiento y validación
- r.reclass.area para eliminar pequeñas áreas, enmascarar nuevos valores y rellenar los huecos con r.neighbors o r.fillnulls
- convertir la salida en vector y ejecutar v.clean con
tool=rmarea
- r.kappa para la validación (idealmente también digitalizar una muestra de prueba)