Los datos satelitales en general vienen en formato raster, por lo tanto aplican las mismas reglas que vimos anteriormente. Los comandos i.* se orientan explícitamente al procesamiento de datos satelitales aunque algunos puedan usarse para otros datos raster.
Para ejemplificar el flujo de trabajo para procesamiento de datos satelitales en GRASS GIS, en esta primer sesión vamos a trabajar con datos del satélite Landsat 8.
Antes de empezar y para ganar tiempo, conectamos nuestro drive e instalamos GRASS en Google Colab.
# import drive from google colabfrom google.colab import drive# mount drivedrive.mount("/content/drive")
e instalamos otras librerías de python que nos serán útiles.
!pip install pygdal!pip install landsatxplore
Datos para esta sesión
Escenas Landsat 8 (OLI)
Fechas: 14/01/2020 y 02/03/2020
Path/Row: 229/082 (Córdoba)
CRS: UTM zona 20 N (EPSG:32620)
Historia de la mision Landsat
El sistema de escáner multiespectral (MSS) a bordo del Landsats 1-5 disponía de cuatro bandas. El Thematic Mapper (TM) a bordo de Landsats 4 y 5 tenía siete bandas. El Enhanced Thematic Mapper Plus (ETM+) del Landsat 7 tiene 8 bandas y los Landsats 8 y 9 tienen 11 bandas. Fuente: https://landsat.gsfc.nasa.gov/satellites/landsat-9/landsat-9-bands/.
# import standard Python packages we needimport sysimport subprocess# ask GRASS GIS where its Python packages are to be able to run it from the notebooksys.path.append( subprocess.check_output(["grass", "--config", "python_path"], text=True).strip())
Ahora sí, estamos listos para importar los paquetes de GRASS e iniciar una sesión:
# import the GRASS GIS packages we needimport grass.script as gsimport grass.jupyter as gj# Start the GRASS GIS Sessionsession = gj.init(grassdata, project, mapset)
Corroboramos la proyección
# check the CRSprint(gs.read_command("g.proj", flags="p"))
Crear un nuevo mapset
Para diferenciar sesiones dentro del curso, vamos a crear un nuevo mapset llamado landsat8:
# Create a new mapsetgs.run_command("g.mapset", mapset="landsat8", flags="c")
# Check we are in the mapset just createdgs.run_command("g.mapset", flags="p")
Listamos los mapsets accesibles para corroborar que tenemos acceso a PERMANENT
# list all the mapsets in the search pathgs.mapsets(search_path=True)
Nota
Desde cualquier mapset en el que estemos trabajando, siempre vamos a tener acceso a PERMANENT.
Listemos los mapas vectoriales disponibles
# list vector maps in all mapsets in the search pathgs.list_grouped(type="vector")
Región de interés
Como primer paso para trabajar con datos raster,necesitamos establecer la región computacional. Para ello, vamos a extraer el radio urbano de Córdoba del vector de radios urbanos y luego lo vamos a usar para establecer los límites de nuestra region computacional.
# extract Cordoba urban area from `radios_urbanos`gs.run_command("v.extract", input="radios_urbanos", where="nombre == 'CORDOBA'", output="radio_urbano_cba")
# set the computational region to the extent of Cordoba urban areags.run_command("g.region", flags="p", vector="radio_urbano_cba")
Descargar e importar los datos Landsat 8
Entre las extensiones disponibles, hay una extensión i.landsat, que nos permite buscar, filtrar, descargar, importar y aplicar bandas de calidad a imágenes Landsat. Para la busqueda y descarga de datos, esta extensión depende de una librería de python denominada landsatxplore.
Desafortunadamente, landsatxplore fue abandonada por su autor, y es difícil de mantener. Estamos considerando utilizar otras opciones, pero es un WIP. Por este motivo, vamos a instalar la extensión igualmente porque nos sirve para la importación y enmascarado de pixeles según la calidad.
Nota
Voluntari@s para actualizar y testear los pull request de landsatxplore o actualizar i.landsat.download para que use otro paquete distinto, son mas que bienvenid@s!!
Por defecto, usando el comando anterior sin la opción l y proveyendo una carpeta de descarga, se descargarían todas las imágenes encontradas anteriormente. Sin embargo, también es posible descargar imágenes seleccionadas via su id.
Como el módulo de descarga no está andando y porque la descarga puede tomar mucho tiempo con conexiones lentas, vamos a usar imágenes previamente descargadas. Como primer paso vamos a imprimir todas las bandas presentes dentro de la carpeta landsat_data.
# print all landsat bands within landsat_data folder# (1: projection match, 0: projection does not match)gs.run_command("i.landsat.import",input=os.path.join(homedir, "gisdata", "landsat_data"), flags="p")
También podemos imprimir sólo las bandas seleccionadas con un patrón.
# print a selection of bands - might be sloooowgs.run_command("i.landsat.import",input=os.path.join(homedir, "gisdata", "landsat_data"), pattern="B(2|3|4|5|6|8)", flags="p")
Para empezar a trabajar con los datos L8, vamos entonces a importar todas las bandas, recortar a la región y reproyectar al vuelo, ya que vimos que su CRS no coincide con el del proyecto donde nos encontramos.
# import all bands, subset to region and reprojectgs.run_command("i.landsat.import", input=os.path.join(homedir, "gisdata", "landsat_data"), extent="region", flags="r")
Listamos las bandas importadas y revisamos los metadatos de alguna de ellas.
# list raster mapsgs.list_grouped(type="raster")["landsat8"]
# check metadata of some imported bandsgs.raster_info(map="LC08_L1TP_229082_20200114_20200127_01_T1_B4")
Pre-procesamiento de datos satelitales
De número digital (ND) a reflectancia y temperatura
Los datos L8 OLI vienen en 16-bits con rango de datos entre 0 y 65535.
i.landsat.toar convierte los números digitales (ND) en reflectancia TOA (y temperatura de brillo) para todos los sensores Landsat. Opcionalmente proporciona reflectancia de superficie (BOA) después de la corrección DOS.
i.atcorr proporciona un método de corrección atmosférica más complejo para gran variedad de sensores (S6).
Antes de comenzar a procesar los datos, vamos a definir la resolución de la región computacional a una banda de 30 m.
# set the region to a 30m bandgs.run_command("g.region", raster="LC08_L1TP_229082_20200114_20200127_01_T1_B4", flags="p")
Ahora sí, convertimos los ND a reflectancia superficial y temperatura usando el método Dark Object Substraction (DOS).
# convert from DN to surface reflectance and temperaturemetadata = os.path.join(homedir, "gisdata", "landsat_data", "LC08_L1TP_229082_20200114_20200127_01_T1_MTL.txt")gs.run_command("i.landsat.toar",input="LC08_L1TP_229082_20200114_20200127_01_T1_B", output="LC08_229082_20200114_SR_B", sensor="oli8", metfile=metadata, method="dos1")
Corroboremos los metadatos antes y después de la conversión ND >> RS para una banda.
# list output mapsgs.list_grouped(type="raster", pattern="*SR*")["landsat8"]
# check info before and after for one bandprint(gs.read_command("r.info", map="LC08_L1TP_229082_20200114_20200127_01_T1_B3"))
Hagamos una composición falso color 543. ¿Sobre qué bandas debieran realizar el ajuste?
Enmascarado de nubes con banda QA
Landsat 8 proporciona una banda de calidad (quality assessment, QA) con valores enteros de 16 bits que representan las combinaciones de superficie, atmósfera y condiciones del sensor que pueden afectar la utilidad general de un determinado pixel.
La extensión i.landsat.qa reclasifica la banda QA de Landsat 8 de acuerdo a la calidad del pixel.
Nota
Más información sobre la banda QA de L8 en la guía de usuario.
Vamos a utilizar i.landsat.qa para crear las reglas necesarias para identificar las nubes y sombras de nubes en las escenas L8.
# create a rule setgs.run_command("i.landsat.qa", dataset="landsat_8_c1", cloud_shadow_confidence="Medium,High", cloud_confidence="Medium,High", output=os.path.join(homedir, "Cloud_Mask_rules.txt"))
Con las reglas que creamos anteriormente, reclasificamos la banda QA para obtener un mapa ráster que podamos luego usar como máscara.
# reclass the BQA band based on the rule set createdgs.run_command("r.reclass",input="LC08_L1TP_229082_20200114_20200127_01_T1_BQA", output="LC08_229082_20200114_Cloud_Mask", rules=os.path.join(homedir, "Cloud_Mask_rules.txt"))
Para estimar la superficie cubierta por nubes y sombras de nubes, usamos el módulo r.report.
# report % of clouds and shadowsprint(gs.read_command("r.report",map="LC08_229082_20200114_Cloud_Mask", units="p", flags="e"))
Visualizamos el mapa reclasificado.
# display reclassified map over RGBrgb_map.d_rast(map="LC08_229082_20200114_Cloud_Mask")rgb_map.show()
Fusión de datos/Pansharpening
Vamos a usar la banda pancromática (15 m) para mejorar la definición de las bandas espectrales de 30 m, por medio de: i.fusion.hpf, que aplica un método de adición basado en un filtro de paso alto. Otros métodos de pansharpening están implementados en el módulo i.pansharpen.
Como primer paso, instalamos la extensión i.fusion.hpf.
# Install the reqquired addongs.run_command("g.extension", extension="i.fusion.hpf")
Luego, como vamos a “mejorar” la resolución espacial de las bandas multiespectrales a 15 m, necesitamos alinear la resolución de la región computacional a la banda PAN.
# Set the region to PAN band (15m)gs.run_command("g.region", raster="LC08_229082_20200114_SR_B8", flags="p")
… y ejecutamos la fusión.
# list bandsms_bands = gs.list_grouped(type="raster", pattern="*_SR_B[1-7]")["landsat8"]ms_bands
# Apply the fusion based on high pass filtergs.run_command("i.fusion.hpf", pan="LC08_229082_20200114_SR_B8", msx=ms_bands, suffix="hpf", center="high", modulation="max", trim="0.0", flags="lc")
Finalmente, listamos los mapas resultantes usando un patrón de búsqueda
# list the fused mapsgs.list_grouped(type="raster", pattern="*.hpf")["landsat8"]
# check info of a pansharpened bandgs.raster_info("LC08_229082_20200114_SR_B4.hpf")
y visualizamos las diferencias con gj.InteractiveMap.
# display original and fused mapshpf_map = gj.InteractiveMap(width =500, tiles="OpenStreetMap")hpf_map.add_raster("LC08_229082_20200114_SR_B4")hpf_map.add_raster("LC08_229082_20200114_SR_B4.hpf")hpf_map.add_layer_control(position ="bottomright")hpf_map.show()
Índices de agua y vegetación
En esta sección vamos a estimar los conocidos índices de vegetación y agua. No obstante, necesitamos primero, establecer la máscara de nubes y sombras de nubes obtenida anteriormente para evitar el cómputo de los índices sobre estas áreas.
# Set the cloud mask to avoid computing over cloudsgs.run_command("r.mask", raster="LC08_229082_20200114_Cloud_Mask")
Calculamos el NDVI y establecemos la paleta de colores.
# Compute NDVIndvi_formula ="LC08_229082_20200114_NDVI = (LC08_229082_20200114_SR_B5.hpf - LC08_229082_20200114_SR_B4.hpf) / (LC08_229082_20200114_SR_B5.hpf + LC08_229082_20200114_SR_B4.hpf) * 1.0"gs.mapcalc(exp=ndvi_formula)# Set the color palettegs.run_command("r.colors",map="LC08_229082_20200114_NDVI", color="ndvi")
Calculamos el NDWI y establecemos la paleta de colores.
# Compute NDWIndwi_formula ="LC08_229082_20200114_NDWI = (LC08_229082_20200114_SR_B5.hpf - LC08_229082_20200114_SR_B6.hpf) / (LC08_229082_20200114_SR_B5.hpf + LC08_229082_20200114_SR_B6.hpf) * 1.0"gs.mapcalc(exp=ndwi_formula)# Set the color palettegs.run_command("r.colors",map="LC08_229082_20200114_NDWI", color="ndwi")
Visualizamos los mapas resultantes.
ndi = gj.InteractiveMap(width=450, use_region=True)ndi.add_raster("LC08_229082_20200114_NDVI")ndi.add_raster("LC08_229082_20200114_NDWI")ndi.add_layer_control(position ="bottomright")ndi.show()
Tarea Opcional
Estimar NDVI y NDWI para la otra escena usando el módulo i.vi.
Clasificación No Supervisada
Los pasos para realizar una clasificación no supervisada en GRASS, implican:
Los semantic labels son etiquetas que podemos agregar a cualquier mapa ráster y que nos indican qué variable está representada en ese mapa. Estas etiquetas son especialmente relevantes para las imágenes de satélite, ya que nos permiten identificar a qué sensor y banda corresponde el mapa. Son útiles a la hora de trabajar con colecciones de imágenes de satélite y también a la hora de clasificar diferentes escenas.
Por ejemplo, si generamos firmas espectrales para unas clases usando un determinado conjunto de bandas, estas firmas pueden reutilizarse para clasificar otra escena siempre que las etiquetas semánticas sean las mismas.
¡Cuidado! Aunque es posible reutilizar las firmas espectrales para cualquier escena con las mismas bandas, los cambios temporales (estaciones, impacto meteorológico) limitan su aplicabilidad sólo a escenas obtenidas más o menos al mismo tiempo.
Probemos entonces, asignar semantic labels a las bandas de L8 y los índices de vegetación.
# list the bands needed for classificationbands = gs.list_grouped(type="raster", pattern="*_SR*hpf")["landsat8"]bands
Para bandas de satélites como Landsat y Sentinel, usamos el comando r.sematic.label que toma metadatos sobre los labels de i.band.library. Es posible agregar bandas de otros satélites siempre que se siga un determinado formato.
# add semantic labels to bandsgs.run_command("r.semantic.label",map=bands, semantic_label=labels, operation="add")
Para cualquier otro ráster al que deseemos agregar una etiqueta, usamos r.support que nos permite agregar distintos metadatos a nuestros datos en GRASS.
# add semantic labels to indicesfor i in ["NDVI", "NDWI"]: gs.run_command("r.support", map=f"LC08_229082_20200114_{i}", semantic_label=i)
Creamos un grupo de imágenes o stack con las bandas 1 a 7 más el NDVI y el NDWI.
# add ndvi and ndwi to the list of bandsfor i in ["NDVI", "NDWI"]: bands.append(f"LC08_229082_20200114_{i}")bands
# create an imagery group with the list of bandsgs.run_command("i.group", group="l8", subgroup="l8",input=bands)
# print elements within the group gs.run_command("i.group", group="l8", flags="l")
Obtenemos estadísticos -firmas- para las n clases de interés con una muestra de pixeles
# statistics for unsupervised classificationgs.run_command("i.cluster", group="l8", subgroup="l8", sig="l8_hpf", classes="7", separation="0.6")
Usamos las firmas espectrales para realizar la clasificación no supervisada de toda la imagen
# Maximum Likelihood unsupervised classificationgs.run_command("i.maxlik", group="l8", subgroup="l8", sig="l8_hpf", output="l8_hpf_class", rej="l8_hpf_rej")
# Mostrar el mapa clasificado con `InteractiveMap`clas = gj.InteractiveMap(width=450, use_region=True)clas.add_raster("l8_hpf_class")clas.add_layer_control(position ="bottomright")clas.show()
Información derivada adicional
Usualmente, para realizar clasificaciones, derivamos muchas variables a partir de las bandas de sensores satelitales multiespectrales. Otra información podría obtenerse con los siguientes módulos, entre otros:
---title: Datos satelitales en GRASS GISauthor: Verónica Andreodate: todayformat: html: code-tools: true code-copy: true code-fold: falseexecute: eval: false cache: false keep-ipynb: truejupyter: python3---Los datos satelitales en general vienen en formato raster, por lo tanto aplicanlas mismas reglas que vimos anteriormente. Los comandos **i.\*** se orientan explícitamente al procesamiento de datos satelitales aunque algunos puedan usarse para otros datos raster.Para ejemplificar el flujo de trabajo para procesamiento de datos satelitales en GRASS GIS, en esta primer sesión vamos a trabajar con datos del satélite Landsat 8.Antes de empezar y para ganar tiempo, conectamos nuestro drive e instalamos GRASS en Google Colab.```{python}# import drive from google colabfrom google.colab import drive# mount drivedrive.mount("/content/drive")``````{python}%%bashDEBIAN_FRONTEND=noninteractive sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable apt update apt install grass subversion grass-devapt remove libproj22```Chequeamos el path de instalación de GRASS```{python}!grass --config path```e instalamos otras librerías de python que nos serán útiles.```{python}!pip install pygdal!pip install landsatxplore```# Datos para esta sesión:::: columns:::{.column width="40%"}Escenas Landsat 8 (OLI)- Fechas: 14/01/2020 y 02/03/2020- Path/Row: 229/082 (Córdoba)- CRS: UTM zona 20 N (EPSG:32620)::::::{.column width="60%"}![](https://landsat.gsfc.nasa.gov/wp-content/uploads/2021/12/ldcm_2012_COL-300x168-1.png){fig-align="center"}:::::::<!-- Descargar las escenas [L8 14/01/2020 (979Mb)](https://drive.google.com/file/d/1ytQp-xin1FQr_hqtDJRLgK6g4eXwK-WI/view?usp=sharing) y [L8 02/03/2020 (880Mb)](https://drive.google.com/file/d/1Gg8FbhwpIQR-GyYepM4uw_9IOjEnji_N/view?usp=sharing) y moverlas a `$HOME/gisdata/landsat_data`. **No descomprimir!** --># Historia de la mision Landsat![Lanzamientos de satélites Lansat desde 1972](https://landsat.gsfc.nasa.gov/wp-content/uploads/2020-07/Landsat_timeline_20200318_title.gif)El sistema de escáner multiespectral (MSS) a bordo del Landsats 1-5 disponía de cuatro bandas. El Thematic Mapper (TM) a bordo de Landsats 4 y 5 tenía siete bandas. El Enhanced Thematic Mapper Plus (ETM+) del Landsat 7 tiene 8 bandas y los Landsats 8 y 9 tienen 11 bandas. Fuente: <https://landsat.gsfc.nasa.gov/satellites/landsat-9/landsat-9-bands/>. ![Comparación entre las bandas de todos los satélites Landsat](https://landsat.gsfc.nasa.gov/wp-content/uploads/2021/12/all_Landsat_bands.png){width="90%"}:::{.callout-note}Más detalles sobre las misiones Landsat pueden encontrarse en: <https://www.usgs.gov/landsat-missions>:::# Manos a la obra## Iniciamos GRASS GISIniciamos GRASS GIS en el proyecto *posgar2007_4_cba* y mapset *PERMANENT*```{python}import os# data directoryhomedir ="/content/drive/MyDrive/curso_grass_2023"# GRASS GIS database variablesgrassdata = os.path.join(homedir, "grassdata")project ="posgar2007_4_cba"mapset ="PERMANENT"``````{python}# import standard Python packages we needimport sysimport subprocess# ask GRASS GIS where its Python packages are to be able to run it from the notebooksys.path.append( subprocess.check_output(["grass", "--config", "python_path"], text=True).strip())```Ahora sí, estamos listos para importar los paquetes de GRASS e iniciar unasesión:```{python}# import the GRASS GIS packages we needimport grass.script as gsimport grass.jupyter as gj# Start the GRASS GIS Sessionsession = gj.init(grassdata, project, mapset)```Corroboramos la proyección```{python}# check the CRSprint(gs.read_command("g.proj", flags="p"))```## Crear un nuevo mapsetPara diferenciar sesiones dentro del curso, vamos a crear un nuevo mapset llamado *landsat8*:```{python}# Create a new mapsetgs.run_command("g.mapset", mapset="landsat8", flags="c")``````{python}# Check we are in the mapset just createdgs.run_command("g.mapset", flags="p")```Listamos los mapsets accesibles para corroborar que tenemos acceso a *PERMANENT*```{python}# list all the mapsets in the search pathgs.mapsets(search_path=True)```:::{.callout-note}Desde cualquier mapset en el que estemos trabajando, siempre vamos a teneracceso a *PERMANENT*.:::Listemos los mapas vectoriales disponibles```{python}# list vector maps in all mapsets in the search pathgs.list_grouped(type="vector")```## Región de interésComo primer paso para trabajar con datos raster,necesitamos establecer la región computacional. Para ello, vamos a extraer el radio urbano de Córdoba del vectorde radios urbanos y luego lo vamos a usar para establecer los límites de nuestraregion computacional.```{python}# extract Cordoba urban area from `radios_urbanos`gs.run_command("v.extract", input="radios_urbanos", where="nombre == 'CORDOBA'", output="radio_urbano_cba")``````{python}# set the computational region to the extent of Cordoba urban areags.run_command("g.region", flags="p", vector="radio_urbano_cba")```## Descargar e importar los datos Landsat 8Entre las extensiones disponibles, hay una extensión [i.landsat](https://grass.osgeo.org/grass-stable/manuals/addons/i.landsat.html), que nos permite buscar, filtrar, descargar, importar y aplicar bandas de calidada imágenes Landsat.Para la busqueda y descarga de datos, esta extensión depende de una librería depython denominada [landsatxplore](https://github.com/yannforget/landsatxplore).Desafortunadamente, landsatxplore fue abandonada por su autor, y es difícil de mantener. Estamos considerando utilizar otras opciones, pero es un WIP. Por estemotivo, vamos a instalar la extensión igualmente porque nos sirve para la importación y enmascarado de pixeles según la calidad.:::{.callout-note}Voluntari@s para actualizar y testear los pull request de landsatxplore o actualizar i.landsat.download para que use otro paquete distinto, son mas que bienvenid@s!!:::```{python}# install i.landsat toolsetgs.run_command("g.extension", extension="i.landsat")```La búsqueda de escenas se basa en la región computacional definida y funcionaríacomo se detalla a continuación.```{python}# search for Landsat 8 scenes# l8_credentials = os.path.join(homedir, "gisdata", "landsat")# gs.run_command("i.landsat.download", # settings=l8_credentials,# dataset="landsat_8_c1",# clouds="35",# start="2019-10-27",# end="2020-03-15",# flags="l")```Por defecto, usando el comando anterior sin la opción *l* y proveyendo una carpetade descarga, se descargarían todas las imágenes encontradas anteriormente. Sin embargo, también es posible descargar imágenes seleccionadas via su *id*.```{python}# download selected scenes# gs.run_command("i.landsat.download", # settings=l8_credentials,# id="LC82290822020062LGN00,LC82290822020014LGN00",# output=os.path.join(homedir, "gisdata", "landsat_data"))```Como el módulo de descarga no está andando y porque la descarga puede tomar mucho tiempo con conexiones lentas, vamos a usar imágenes previamente descargadas.Como primer paso vamos a imprimir todas las bandas presentes dentro de la carpeta`landsat_data`.```{python}# print all landsat bands within landsat_data folder# (1: projection match, 0: projection does not match)gs.run_command("i.landsat.import",input=os.path.join(homedir, "gisdata", "landsat_data"), flags="p")```También podemos imprimir sólo las bandas seleccionadas con un patrón.```{python}# print a selection of bands - might be sloooowgs.run_command("i.landsat.import",input=os.path.join(homedir, "gisdata", "landsat_data"), pattern="B(2|3|4|5|6|8)", flags="p")```Para empezar a trabajar con los datos L8, vamos entonces a importar todas las bandas, recortar a la región y reproyectar al vuelo, ya que vimos que su CRSno coincide con el del proyecto donde nos encontramos.```{python}# import all bands, subset to region and reprojectgs.run_command("i.landsat.import", input=os.path.join(homedir, "gisdata", "landsat_data"), extent="region", flags="r")```Listamos las bandas importadas y revisamos los metadatos de alguna de ellas.```{python}# list raster mapsgs.list_grouped(type="raster")["landsat8"]``````{python}# check metadata of some imported bandsgs.raster_info(map="LC08_L1TP_229082_20200114_20200127_01_T1_B4")```## Pre-procesamiento de datos satelitales![Workflow de pre-procesamiento de datos satelitales](../assets/img/rs_workflow.jpg){width=70% fig-align="center"}### De número digital (ND) a reflectancia y temperatura- Los datos L8 OLI vienen en 16-bits con rango de datos entre 0 y 65535.- [i.landsat.toar](https://grass.osgeo.org/grass-stable/manuals/i.landsat.toar.html)convierte los números digitales (ND) en reflectancia TOA (y temperatura de brillo) para todos los sensores Landsat. Opcionalmente proporciona reflectancia de superficie (BOA) después de la corrección DOS. - [i.atcorr](https://grass.osgeo.org/grass-stable/manuals/i.atcorr.html) proporcionaun método de corrección atmosférica más complejo para gran variedad de sensores (S6).Antes de comenzar a procesar los datos, vamos a definir la resolución de la región computacional a una banda de 30 m.```{python}# set the region to a 30m bandgs.run_command("g.region", raster="LC08_L1TP_229082_20200114_20200127_01_T1_B4", flags="p")```Ahora sí, convertimos los ND a reflectancia superficial y temperatura usando el método Dark Object Substraction (DOS).```{python}# convert from DN to surface reflectance and temperaturemetadata = os.path.join(homedir, "gisdata", "landsat_data", "LC08_L1TP_229082_20200114_20200127_01_T1_MTL.txt")gs.run_command("i.landsat.toar",input="LC08_L1TP_229082_20200114_20200127_01_T1_B", output="LC08_229082_20200114_SR_B", sensor="oli8", metfile=metadata, method="dos1")```Corroboremos los metadatos antes y después de la conversión ND >> RS para unabanda.```{python}# list output mapsgs.list_grouped(type="raster", pattern="*SR*")["landsat8"]``````{python}# check info before and after for one bandprint(gs.read_command("r.info", map="LC08_L1TP_229082_20200114_20200127_01_T1_B3"))``````{python}print(gs.read_command("r.info", map="LC08_229082_20200114_SR_B3"))``````{python}# Visualize resultsb3_map=gj.InteractiveMap(width =500, tiles="OpenStreetMap")b3_map.add_raster("LC08_L1TP_229082_20200114_20200127_01_T1_B3")b3_map.add_raster("LC08_229082_20200114_SR_B3")b3_map.add_layer_control(position ="bottomright")b3_map.show()```:::{.callout-caution title="Tarea Opcional"}Seguir los mismos pasos para la escena del 02/03/2020. ¿Qué notan de diferente?:::### Ajuste de color y composiciones RGBPara lograr una buena visualización en composiciones RGB, primero realizamos un ajuste de colores utilizando el módulo [i.colors.enhance](https://grass.osgeo.org/grass-stable/manuals/i.colors.enhance.html).```{python}# enhance the colorsgs.run_command("i.colors.enhance", red="LC08_229082_20200114_SR_B4", green="LC08_229082_20200114_SR_B3", blue="LC08_229082_20200114_SR_B2", strength="95")```Visualicemos la combinacion RGB color natural usando `gj.Map`.```{python}# display RGBrgb_map = gj.Map(width=450, use_region=True)rgb_map.d_rgb(red="LC08_229082_20200114_SR_B4", green="LC08_229082_20200114_SR_B3", blue="LC08_229082_20200114_SR_B2",)rgb_map.show()```:::{.callout-caution title="Tarea"}Hagamos una composición falso color 543. ¿Sobre qué bandas debieran realizar elajuste? :::### Enmascarado de nubes con banda QA- Landsat 8 proporciona una banda de calidad (quality assessment, QA) con valoresenteros de 16 bits que representan las combinaciones de superficie, atmósfera y condiciones del sensor que pueden afectar la utilidad general de un determinado pixel. - La extensión [i.landsat.qa](https://grass.osgeo.org/grass-stable/manuals/addons/i.landsat.qa.html)reclasifica la banda QA de Landsat 8 de acuerdo a la calidad del pixel. :::{.callout-note}Más información sobre la banda QA de L8 en la [guía de usuario](https://d9-wret.s3.us-west-2.amazonaws.com/assets/palladium/production/s3fs-public/media/files/LSDS-1619_Landsat8-9-Collection2-Level2-Science-Product-Guide-v5.pdf).:::Vamos a utilizar i.landsat.qa para crear las reglas necesarias para identificarlas nubes y sombras de nubes en las escenas L8.```{python}# create a rule setgs.run_command("i.landsat.qa", dataset="landsat_8_c1", cloud_shadow_confidence="Medium,High", cloud_confidence="Medium,High", output=os.path.join(homedir, "Cloud_Mask_rules.txt"))```Con las reglas que creamos anteriormente, reclasificamos la banda QA para obtener un mapa ráster que podamos luego usar como máscara.```{python}# reclass the BQA band based on the rule set createdgs.run_command("r.reclass",input="LC08_L1TP_229082_20200114_20200127_01_T1_BQA", output="LC08_229082_20200114_Cloud_Mask", rules=os.path.join(homedir, "Cloud_Mask_rules.txt"))```Para estimar la superficie cubierta por nubes y sombras de nubes, usamos el módulo [r.report](https://grass.osgeo.org/grass-stable/manuals/r.report.html).```{python}# report % of clouds and shadowsprint(gs.read_command("r.report",map="LC08_229082_20200114_Cloud_Mask", units="p", flags="e"))```Visualizamos el mapa reclasificado.```{python}# display reclassified map over RGBrgb_map.d_rast(map="LC08_229082_20200114_Cloud_Mask")rgb_map.show()```## Fusión de datos/PansharpeningVamos a usar la banda pancromática (15 m) para mejorar la definición de las bandas espectrales de 30 m, por medio de: [i.fusion.hpf](https://grass.osgeo.org/grass-stable/manuals/addons/i.fusion.hpf.html), que aplica un método de adición basado en un filtro de paso alto. Otros métodos de pansharpening están implementados en el módulo [i.pansharpen](https://grass.osgeo.org/grass-stable/manuals/i.pansharpen.html).Como primer paso, instalamos la extensión *i.fusion.hpf*.```{python}# Install the reqquired addongs.run_command("g.extension", extension="i.fusion.hpf")```Luego, como vamos a "mejorar" la resolución espacial de las bandas multiespectrales a 15 m, necesitamos alinear la resolución de la región computacional a la banda PAN.```{python}# Set the region to PAN band (15m)gs.run_command("g.region", raster="LC08_229082_20200114_SR_B8", flags="p")```... y ejecutamos la fusión.```{python}# list bandsms_bands = gs.list_grouped(type="raster", pattern="*_SR_B[1-7]")["landsat8"]ms_bands``````{python}# Apply the fusion based on high pass filtergs.run_command("i.fusion.hpf", pan="LC08_229082_20200114_SR_B8", msx=ms_bands, suffix="hpf", center="high", modulation="max", trim="0.0", flags="lc")```Finalmente, listamos los mapas resultantes usando un patrón de búsqueda```{python}# list the fused mapsgs.list_grouped(type="raster", pattern="*.hpf")["landsat8"]``````{python}# check info of a pansharpened bandgs.raster_info("LC08_229082_20200114_SR_B4.hpf")```y visualizamos las diferencias con `gj.InteractiveMap`.```{python}# display original and fused mapshpf_map = gj.InteractiveMap(width =500, tiles="OpenStreetMap")hpf_map.add_raster("LC08_229082_20200114_SR_B4")hpf_map.add_raster("LC08_229082_20200114_SR_B4.hpf")hpf_map.add_layer_control(position ="bottomright")hpf_map.show()```## Índices de agua y vegetaciónEn esta sección vamos a estimar los conocidos índices de vegetación y agua. Noobstante, necesitamos primero, establecer la máscara de nubes y sombras de nubesobtenida anteriormente para evitar el cómputo de los índices sobre estas áreas.```{python}# Set the cloud mask to avoid computing over cloudsgs.run_command("r.mask", raster="LC08_229082_20200114_Cloud_Mask")```Calculamos el NDVI y establecemos la paleta de colores.```{python}# Compute NDVIndvi_formula ="LC08_229082_20200114_NDVI = (LC08_229082_20200114_SR_B5.hpf - LC08_229082_20200114_SR_B4.hpf) / (LC08_229082_20200114_SR_B5.hpf + LC08_229082_20200114_SR_B4.hpf) * 1.0"gs.mapcalc(exp=ndvi_formula)# Set the color palettegs.run_command("r.colors",map="LC08_229082_20200114_NDVI", color="ndvi")```Calculamos el NDWI y establecemos la paleta de colores.```{python}# Compute NDWIndwi_formula ="LC08_229082_20200114_NDWI = (LC08_229082_20200114_SR_B5.hpf - LC08_229082_20200114_SR_B6.hpf) / (LC08_229082_20200114_SR_B5.hpf + LC08_229082_20200114_SR_B6.hpf) * 1.0"gs.mapcalc(exp=ndwi_formula)# Set the color palettegs.run_command("r.colors",map="LC08_229082_20200114_NDWI", color="ndwi")```Visualizamos los mapas resultantes.```{python}ndi = gj.InteractiveMap(width=450, use_region=True)ndi.add_raster("LC08_229082_20200114_NDVI")ndi.add_raster("LC08_229082_20200114_NDWI")ndi.add_layer_control(position ="bottomright")ndi.show()```:::{.callout-warning title="Tarea Opcional"}Estimar NDVI y NDWI para la otra escena usando el módulo [i.vi](https://grass.osgeo.org/grass-stable/manuals/i.vi.html).:::## Clasificación No SupervisadaLos pasos para realizar una clasificación no supervisada en GRASS, implican:- Asignar semantic labels a las bandas con [r.semantic.label](https://grass.osgeo.org/grass-stable/manuals/r.semantic.label.html)o [r.support](https://grass.osgeo.org/grass-stable/manuals/r.support.html)- Agrupar las bandas (i.e., hacer un stack): [i.group](https://grass.osgeo.org/grass-stable/manuals/i.group.html)- Generar firmas para *n* número de clases: [i.cluster](https://grass.osgeo.org/grass-stable/manuals/i.cluster.html)- Clasificar usando las firmas: [i.maxlik](https://grass.osgeo.org/grass-stable/manuals/i.maxlik.html)#### ¿Qué son los semantic labels?Los *semantic labels* son etiquetas que podemos agregar a cualquier mapa rástery que nos indican qué variable está representada en ese mapa.Estas etiquetas son especialmente relevantes para las imágenes de satélite, ya que nos permiten identificar a qué sensor y banda corresponde el mapa. Son útiles a la hora de trabajar con colecciones de imágenes de satélite y también a la hora de clasificar diferentes escenas. Por ejemplo, si generamos firmas espectrales para unas clases usando un determinado conjunto de bandas, estas firmas pueden reutilizarse para clasificar otra escena siempre que las etiquetas semánticas sean las mismas. ¡Cuidado! Aunque es posible reutilizar las firmas espectrales para cualquier escena con las mismas bandas, los cambios temporales (estaciones, impacto meteorológico) limitan su aplicabilidad sólo a escenas obtenidas más o menos al mismo tiempo.Probemos entonces, asignar semantic labels a las bandas de L8 y los índices devegetación.```{python}# list the bands needed for classificationbands = gs.list_grouped(type="raster", pattern="*_SR*hpf")["landsat8"]bands``````{python}# semantic labels listlabels = ["L8_1", "L8_2", "L8_3", "L8_4", "L8_5", "L8_6", "L8_7"]```Para bandas de satélites como Landsat y Sentinel, usamos el comando [r.sematic.label](https://grass.osgeo.org/grass-stable/manuals/r.semantic.label.html) que toma metadatos sobre los labels de [i.band.library](https://grass.osgeo.org/grass-stable/manuals/i.band.library.html). Es posible agregar bandas de otros satélites siempre que se siga un determinado formato.```{python}# add semantic labels to bandsgs.run_command("r.semantic.label",map=bands, semantic_label=labels, operation="add")```Para cualquier otro ráster al que deseemos agregar una etiqueta, usamos [r.support](https://grass.osgeo.org/grass-stable/manuals/r.support.html) que nos permite agregar distintos metadatos a nuestros datos en GRASS.```{python}# add semantic labels to indicesfor i in ["NDVI", "NDWI"]: gs.run_command("r.support", map=f"LC08_229082_20200114_{i}", semantic_label=i)```Imprimimos los semantic labels```{python}gs.raster_info("LC08_229082_20200114_SR_B7.hpf")["semantic_label"]```Creamos un grupo de imágenes o *stack* con las bandas 1 a 7 más el NDVI y el NDWI.```{python}# add ndvi and ndwi to the list of bandsfor i in ["NDVI", "NDWI"]: bands.append(f"LC08_229082_20200114_{i}")bands``````{python}# create an imagery group with the list of bandsgs.run_command("i.group", group="l8", subgroup="l8",input=bands)``````{python}# print elements within the group gs.run_command("i.group", group="l8", flags="l")```Obtenemos estadísticos -firmas- para las *n* clases de interés con una muestrade pixeles```{python}# statistics for unsupervised classificationgs.run_command("i.cluster", group="l8", subgroup="l8", sig="l8_hpf", classes="7", separation="0.6")```Usamos las firmas espectrales para realizar la clasificación no supervisada detoda la imagen```{python}# Maximum Likelihood unsupervised classificationgs.run_command("i.maxlik", group="l8", subgroup="l8", sig="l8_hpf", output="l8_hpf_class", rej="l8_hpf_rej")``````{python}# Mostrar el mapa clasificado con `InteractiveMap`clas = gj.InteractiveMap(width=450, use_region=True)clas.add_raster("l8_hpf_class")clas.add_layer_control(position ="bottomright")clas.show()```### Información derivada adicionalUsualmente, para realizar clasificaciones, derivamos muchas variables a partir de las bandas de sensores satelitales multiespectrales. Otra información podríaobtenerse con los siguientes módulos, entre otros:- medidas de textura: [r.texture](https://grass.osgeo.org/grass-stable/manuals/r.texture.html), - medidas de diversidad: [r.diversity](https://grass.osgeo.org/grass-stable/manuals/addons/r.diversity.html), - estadísticas locales con información de contexto: [r.neighbors](https://grass.osgeo.org/grass-stable/manuals/r.neighbors.html),- transformación tasseled cap: [i.tasscap](https://grass.osgeo.org/grass-stable/manuals/i.tasscap.html),- etc.# Info y ejercicios de clasificación en GRASS GIS- [Topic classification](http://grass.osgeo.org/grass-stable/manuals/topic_classification.html) en los manuales de GRASS GIS- [Image classification](http://grasswiki.osgeo.org/wiki/Image_classification) en la wiki- [Ejemplos de clasificación](http://training.gismentors.eu/grass-gis-irsae-winter-course-2018/units/28.html) en el curso dictado en Noruega en 2018- [Detección de cambios con Landsat](https://veroandreo.gitlab.io/post/jan2021_ilandsat_tutorial/)- [Taller GRASS para sensado remoto en FOSS4G 2022](https://github.com/veroandreo/foss4g2022_grass4rs)