Laboratório 1#

Introdução ao processamento digital de imagens, resoluções, metadados e busca por imagens no GEE

Objetivos:

  1. Familiarizar-se com o Google Earth Engine através do Google Colab

  2. Manipulação de Dados Geoespaciais provindos de sensores remotos (satélites)

  3. Visualização desses Dados Geoespaciais

Ferramentas:

[ ]:
# OBS: Em Python, usamos o snake_case para nomes de variáveis, seguindo a convenção.
# Porém, em alguns métodos nativos do GEE você pode encontrar o PascalCase ou o camelCase.
# Não se assuste, é normal. Apenas siga o padrão do método que você está usando.

Instalação#

Precisamos importar as bibliotecas necessárias para trabalharmos nesta sessão do Colab.

[ ]:
import ee
import geemap.geemap as geemap

Após importar, o earth-engine-api exige que façamos a autenticação com uma conta Google. Para isso, basta executar o código abaixo e seguir as instruções.

[ ]:
# Ref: https://developers.google.com/earth-engine/apidocs/ee-authenticate
# Para inicializar a sessão para execução insira o id do projeto em ee.Initialize().
ee.Authenticate()
ee.Initialize(project='id_projeto')

A cada vez que você autenticar, um token diferente será gerado. Portanto, não se preocupe em guardar o token gerado.

Não conseguiu instalar? 😩#

Felizmente, o Colab já vem com várias bibliotecas instaladas, mas caso exista algum problema, podemos instalar as bibliotecas necessárias através do comando pip install, por exemplo:

[ ]:
# %pip install geemap

Para checar se deu tudo certo agora, você pode rodar:

[ ]:
# import sys

# print(
#     f"You have geemap version {geemap.__version__} running on Python {sys.version} and your operating system is {sys.platform}."
# )

Agora, se sua dúvida for com relação ao cadastro e geração de token no GEE, tente seguir os passos da documentação oficial (https://developers.google.com/earth-engine/apidocs/ee-authenticate) ou peça ajuda aos monitores.

Parte 1: Introdução#

Selecionando área de estudo#

Vamos criar um mapa de exemplo. Escolhemos a Escola Politécnica da USP, em São Paulo, SP.

Ao criar o mapa, tente se familiarizar com os controles de zoom e de posição oferecidos pela interface do geemap.

[ ]:
lat, lon = -23.5546721, -46.7318389

# OBS: Aqui a latitude vem primeiro.
my_map = geemap.Map(center=(lat, lon), zoom=14)
my_map

Em seguida, vamos criar um ponto com as coordenadas de latitude e longitude.

[ ]:
# OBS: Aqui a longitude é quem vem primeiro, cuidado para não inverter.
poli_usp_point = ee.Geometry.Point(coords=[lon, lat], proj="EPSG:4326")
poli_usp_point

Adicionando uma coleção de imagens#

Existem inúmeros coleções de imagens disponíveis no GEE. Todos eles podem ser encontrados a partir da documentação oficial: https://developers.google.com/earth-engine/datasets

Vamos começar trabalhando com a coleção de imagens Landsat 7

[ ]:
# Ref: https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LE07_C02_T1#description
alias = "LANDSAT/LE07/C02/T1"

# Load the image collection.
dataset = ee.ImageCollection(alias)

print("Você carregou uma ImageCollection com sucesso: ", dataset.args["id"])

Nós acabamos de importar um conjunto de imagens, porém não necessariamente vamos analisar todas elas, pois isso pode ser muito custoso computacionalmente. Vamos então filtrar as imagens para selecionar somente as informações que nos interessam.

[ ]:
# Filtra por datas
## Qualquer imagem que não tenha sido capturada entre `start` e `opt_end` será removida.
## A data `start` é inclusiva, enquanto `opt_end` é exclusiva.
dataset = dataset.filterDate(start="1999-01-01", opt_end="2002-12-31")
dataset
[ ]:
# Filtra por geometria
## neste caso, vamos utilizar o ponto que criamos anteriormente.
## Qualquer imagem que não contenha o ponto será removida.
dataset = dataset.filterBounds(geometry=poli_usp_point)
dataset
[ ]:
# OBS: Caso queira entender melhor o que cada método faz, você pode usar o help do Python. Exemplo:

# help(dataset.filterBounds)
[ ]:
# Seleciona somente as bandas que queremos
bandas = ["B3", "B2", "B1"]
true_color321 = dataset.select(selectors=bandas)

# Define parâmetros de visualização para as imagens
true_color321_vis_params = {
    "min": 0,
    "max": 255,
    "gamma": 1.4,
}
[ ]:
# Temos várias imagens na coleção, vamos escolher apenas uma delas.
## no caso, vamos selecionar a primeira imagem.
# help(true_color321.first)

image = true_color321.first()
image

Agora vamos visualizar a coleção de imagens filtrada no mapa que criamos anteriormente.

[ ]:
my_map.addLayer(
    ee_object=image,
    vis_params=true_color321_vis_params,
    name="True Color (321)",
    shown=True,
    opacity=0.6,
)

Para visualizar o mapa, podemos simplesmente chamar a variável que contém o mapa.

[ ]:
my_map

Recortando uma parte de uma imagem#

Vamos selecionar uma área de estudos para recortar a imagem. Podemos definir um retângulo como geometria de recorte.

Porém, note que existem diversas outras geometrias disponíveis: https://developers.google.com/earth-engine/apidocs/ee-geometry

[ ]:
# primeiro cria um retângulo a partir das coordenadas das arestas
bbox = ee.Geometry.BBox(west=-46.81, south=-23.4, east=-46.26, north=-23.8)
[ ]:
# agora recorta a imagem usando o retângulo
clipped_image = image.clip(clip_geometry=bbox)
[ ]:
my_map.addLayer(
    ee_object=clipped_image,
    vis_params=true_color321_vis_params,
    name="Clipped (321)",
    shown=True,
    opacity=1,
)
[ ]:
my_map

Adicionou a mesma imagem várias vezes e não sabe como retirá-las? 😩#

Podemos acessar uma tupla com todas as imagens (layers) adicionadas ao mapa, veja como:

[ ]:
# my_map.layers

Assim podemos remover uma imagem a partir de seu nome, basta utilizar o método remove_layer:

[ ]:
# help(my_map.remove_layer)
[ ]:
# my_map.remove_layer(rm_layer="True Color (321) Clipped")

Calculando o valor médio dos pixels de uma banda#

Vamos aplicar uma função para calcular a média dos valores de uma banda em uma imagem. Para tanto, vamos utilizar uma estratégia de data reduction através do método reduceRegion.

[ ]:
help(clipped_image.reduceRegion)

O parâmetro reducer deve ser um algoritmo que a ser utilizado para calcular o valor desejado, no caso a média

O parâmetro maxPixels define o limite máximo de pixels a serem processados. Isso é fundamental para evitar sobrecarregar a capacidade de processamento do ambiente de execução.

[ ]:
# Vamos calcular o valor médio do pixel para cada banda dentro da região retangular selecionada.
valor_medio_pixel_rgb = clipped_image.reduceRegion(
    reducer=ee.Reducer.mean(), maxPixels=1e9
)

print(valor_medio_pixel_rgb.getInfo())

Parte 2: Visualização de propriedades das imagens#

Certo, você superou a primeira parte do laboratório, agora vamos ver como visualizar as propriedades de imagens.

Para começar, vamos importar uma outra coleção de imagens, ainda do LANDSAT 7, mas agora de um layer diferente

[ ]:
# Ref: https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LE07_C02_T1_L2
landsat_le07_c02_t1_l2 = ee.ImageCollection("LANDSAT/LE07/C02/T1_L2")
landsat_le07_c02_t1_l2

Também já vamos recortar a imagem para a área de estudo que definimos anteriormente.

[ ]:
landsat_le07_c02_t1_l2 = landsat_le07_c02_t1_l2.filterBounds(geometry=bbox)
landsat_le07_c02_t1_l2

Vamos gerar e visualizar um dicionário com as propriedades das imagens da coleção.

[ ]:
# Gerar um dicionário de metadados da coleção e armazená-lo em uma variável
dict_landsat = landsat_le07_c02_t1_l2.toDictionary()
dict_landsat

Vamos imprimir o nome de todas as propriedades do dicionário, selecionando somente a primeira imagem da coleção.

Vamos imprimir os metadados da primeira imagem da coleção.

[ ]:
landsat_le07_c02_t1_l2.first().toDictionary()

Vale notar que, além dos metadados do ImageCollection, também temos os metadados de cada imagem individualmente.

Agora a parte mais interessante, vamos calcular o percentual de nuvens da primeira imagem da coleção.

[ ]:
# Display the cloud cover percentage of the first image in the collection.
cloud_cover_percentage = (
    landsat_le07_c02_t1_l2.first().get("CLOUD_COVER").getInfo() / 100
)  # varies from 0 to 1

print(
    f"Cloud cover of the first image in the collection: {cloud_cover_percentage:.2%}",
)

Trabalhando com metadados#

Seleção de imagens em uma coleção utilizando informações dos Metadados#

Agora vamos utilizar os meta dados para selecionar imagens de uma coleção, o que pode ser particularmente interessante quando queremos trabalhar com imagens sem nuvens.

Vamos começar filtrando as imagens que possuem menos de 40% de cobertura de nuvens.

[ ]:
# Select only the images with cloud cover less than the desired value.
limit = 40
image_col_no_clouds = landsat_le07_c02_t1_l2.filterMetadata(
    name="CLOUD_COVER", operator="less_than", value=limit
)

Podemos fazer uma conta rápida de quantos imagens restaram na coleção.

[ ]:
print(
    f"Número total de imagens na coleção com cobertura por nuvens "
    + f"menor que o desejado: {image_col_no_clouds.size().getInfo()} imagens",
)

Vamos filtrar para um período de datas específico.

[ ]:
# Select images within a specified date range.
image_col_2019 = image_col_no_clouds.filterDate(
    start="2019-01-01", opt_end="2020-01-01"
)

Vamos contar quantas imagens restaram na coleção.

[ ]:
# Print collection details for the selected date range to the console.
print(f"Coleção de imagens no ano de 2019: {image_col_2019.size().getInfo()} imagens")

Caso você prefira, pode converter a coleção de imagens, que é um objeto da classe ee.ImageCollection, para uma lista de imagens, sendo cada uma um objeto da classe ee.Image.

[ ]:
# Transform the collection of images for the selected date range into a list.
lst_image_col_2019 = image_col_2019.toList(image_col_2019.size())
lst_image_col_2019

Manipulando datas nas imagens#

Uma outra forma de selecionarmos a primeira imagem da coleção é acessando o primeiro elemento da lista de imagens. Vale lembrar que em Python os índices começam em 0, então vamos pegar a image de índice 0.

[ ]:
# Extract the first image from the list.
primeira_imagem = ee.Image(lst_image_col_2019.get(0))
primeira_imagem

Podemos utilizar o método date() para acessar a data de aquisição da primeira imagem.

[ ]:
primeira_imagem.date()

Vamos fazer o mesmo para a segunda imagem da coleção.

[ ]:
# Extract the second image from the list.
segunda_imagem = ee.Image(lst_image_col_2019.get(1))
segunda_imagem

Porém, dessa vez vamos utilizar a chave «DATE_ACQUIRED» para acessar a data de aquisição da imagem diretamente do dicionário de metadados. Isso é possível pois a chave «DATE_ACQUIRED» é uma das propriedades da imagem. Veja:

[ ]:
# Using the image property directly, print the acquisition date of the second image to the console.
print(
    "Data de aquisição da segunda imagem: ",
    segunda_imagem.get("DATE_ACQUIRED").getInfo(),
)

Bandas, projeção cartográfica e resolução espacial#

Uma lista com os nomes de todas as bandas de uma imagem pode ser obtida com o uso do método bandNames()

[ ]:
image_col_2019.first().bandNames()

A projeção cartográfica das bandas de uma imagem devem ser retornadas com o uso do método projection()

[ ]:
projecao_b1 = image_col_2019.first().select("SR_B1").projection()
projecao_b1

Podemos consultar a resolução da banda 1 através do seguinte método:

[ ]:
print("Scale in meters:", ee.Projection.nominalScale(projecao_b1).getInfo())

Custom functions#

O Google Colaboratory (na verdade, o jupyter) permite que você crie funções personalizadas em diferentes células e as utilize em outras células do seu notebook.

Abaixo vamos definir duas funções de exemplo, atente-se à documentação das funções.

[ ]:
def devolve_layer_com_data(imagem):
    """Adicione uma banda à imagem com o valor numérico da data como constante
    nos pixels. A banda é chamada de 'data_numerica'.

    Parameters
    ----------
    imagem : ee.Image
        Uma imagem do GEE.

    Returns
    -------
    ee.Image
        Uma imagem do GEE com uma nova banda chamada 'data_numerica'.
    """
    return imagem.addBands(
        ee.Image(imagem.get("system:time_start")).rename("data_numerica")
    )


def devolve_data(imagem):
    """Extrai a data da imagem. A data é retornada como uma string.

    Parameters
    ----------
    imagem : ee.Image
        Uma imagem do GEE.

    Returns
    -------
    string
        Uma string representando a data da imagem.
    """
    return ee.Image(imagem).date()

Agora podemos aplicar as funções que definimos anteriormente em qualquer imagem da coleção.

Vamos utilizar o método map() para aplicar a função get_date() em todas as imagens da coleção.

[ ]:
datas = image_col_2019.map(devolve_layer_com_data)

# Coleção de imagens com o valor numérico das datas em uma banda adicionada.
datas

O código acima retorna uma ImageCollection, pois o argumento passado para o método map() é também uma ImageCollection. Porém, se aplicarmos o map() sobre uma lista, obteremos uma lista como resultado.

[ ]:
lst_datas = lst_image_col_2019.map(devolve_data)

# Lista com as datas retiradas das imagens na lista de imagens.
lst_datas

Parte 3: Indo além#

Extraindo valores com o reduce#

Essa função será apresentada no próximo laboratório, mas caso queira já entender seu funcionamento

[ ]:
# Extract the maximum date from the period.
maior_data = lst_datas.reduce(ee.Reducer.max())
maior_data

O valor da data é um inteiro que representa o número de milissegundos desde a meia-noite de 1º de janeiro de 1970. Podemos converter esse valor para uma data legível utilizando o método ee.Date() e seu submétodo format().

[ ]:
ee.Date(maior_data).format("YYYY-MM-dd").getInfo()

Se antes calculamos a data máxima, podemos facilmente calcular a data mínima também

[ ]:
menor_data = lst_datas.reduce(ee.Reducer.min())
ee.Date(menor_data).format("YYYY-MM-dd").getInfo()

Também podemo contar o número de imagens na coleção

[ ]:
contagem_images = datas.size().getInfo()
contagem_images

Operações aritméticas no GEE#

Podemos utilizar o método diference() para calcular a diferença entre duas datas. Neste caso, vamos calcular a diferença entre a data máxima e a data mínima do conjunto de imagens.

[ ]:
# Calculate the total number of days between the first and last acquisitions in the period.
days_between = ee.Date(maior_data).difference(ee.Date(menor_data), "day")
days_between

O número acima representa o número de dias entre a primeira e a última aquisição de imagem da coleção

Agora vamos calcular o a média de dias entre as aquisições de imagens da coleção. Para tanto, vamos dividir o número de dias entre a primeira e a última aquisição pelo número de imagens na coleção. Utilizaremos o método divide() para isso.

[ ]:
average_days_between = days_between.divide(contagem_images - 1)
average_days_between

# q: por que subtrair 1?
# a: porque a diferença entre a primeira e a última imagem é igual ao número de imagens menos 1.

Tipos armazenados nas bandas e resolução radiométrica#

O método bandTypes() retorna um dicionário que contém o tipo de dado de cada banda de uma imagem.

No nosso caso, vamos selecionar somente a primeira imagem da coleção, por isso o uso do método first() antes da chamada de bandTypes().

[ ]:
image_col_2019.filterBounds(bbox).first().bandTypes()

O dicionário impresso no console mostra em max o maior número que pode ser registrado em um pixel e, de forma similar, em min está o valor mínimo.

Além disso, ao lado do nome da banda, o tipo de dado que cada pixel da imagem armazena é determinado, por exemplo um tipo inteiro de 16 bits.

Através dessas informações, pode-se determinar a resolução radiométrica da banda

Sugestões de exercícios#

Com intuito de ir ainda mais além, sugere-se a realização dos seguintes treinos:

  • Adicionar outra coleção de imagens do Landsat (Landsat 8)

  • Importar coleção de imagens do Sentinel-2

Atividade#

Para finalizar, vamos fazer um exercício de fixação. Responda às perguntas estabelecidas abaixo, tome cuidado para não alterar o nome das variáveis daqui pra frente, principalmente a variável MY_FINAL_RESULT

[ ]:
p1 = "1 - Qual o seu nome?"
r1 = str("")  # preencha com uma string, exemplo: str("Meu nome")

p2 = "2 - Qual o seu número USP?"
r2 = int()  # preencha com um número inteiro, ex: int(12345678)

p3 = "3 - Qual é o valor médio de pixel na banda 2 da primeira imagem da coleção Landsat?"
r3 = float()  # preencha com um número real

p4 = f"4 - Quantas imagens restaram na coleção após filtrar aquelas com menos de 40% de cobertura de nuvens?"
r4 = int()  # preencha com um número inteiro

p5 = "5 - Qual é a resolução espacial da banda 1 da primeira imagem da coleção landsat_le07_c02_t1_l2?"
r5 = float()  # preencha com um número real

p6 = "6 - Qual é a diferença em dias entre a data de aquisição da primeira e da última imagem na coleção de 2019?"
r6 = int()  # preencha com um número inteiro

p7 = "7 - Qual a data de aquisição da 3ª imagem da coleção landsat_le07_c02_t1_l2?"

r7 = str("")  # preencha com uma string formato AAAA-MM-DD

p8 = "8- Qual a resolução radiométrica da banda 1 da 1ª imagem da coleção Landsat?"
r8 = float()  # preencha com um número real
[ ]:
MY_FINAL_RESULT = {
    "p1": r1,
    "p2": r2,
    "p3": r3,
    "p4": r4,
    "p5": r5,
    "p6": r6,
    "p7": r7,
    "p8": r8,
}

MY_FINAL_RESULT

Antes de ir, verifique se está tudo certo com a sua resolução:#

[ ]:
class ValidaResposta:
    """Classe que vai receber o dicionario MY_FINAL_RESULT e validar se as
    respostas têm o formato correto.
    """

    def __init__(self, dicionario_respostas):
        self.dicionario_respostas = dicionario_respostas
        self.perguntas_types = {
            "p1": str,
            "p2": int,
            "p3": float,
            "p4": int,
            "p5": float,
            "p6": int,
            "p7": str,
            "p8": float,
        }

    def valida_respostas(self):
        """Valida se as respostas estão no formato correto e não estão vazias.

        Returns
        -------
        bool
            True se todas as respostas estão no formato correto e não estão vazias.
            False se alguma resposta não está no formato correto ou está vazia.
        """
        for pergunta, resposta in self.dicionario_respostas.items():
            expected_type = self.perguntas_types.get(pergunta)
            if (
                expected_type is None
                or not isinstance(resposta, expected_type)
                or (
                    isinstance(resposta, str)
                    and resposta in ["Meu Nome", "", "Minha resposta"]
                )
            ):
                print(
                    f"A resposta para a {pergunta} não está no formato correto ou está vazia."
                )
                return False

        return True


validador = ValidaResposta(MY_FINAL_RESULT)
if validador.valida_respostas():
    print(">>> Todas as respostas estão no formato correto, parabéns.\n")
else:
    raise ValueError("Alguma resposta não está no formato correto ou está vazia.")

Quer levar esse notebook com você? 📚#

[ ]:
# !pip install nbconvert
[ ]:
!jupyter nbconvert --to html "lab1.ipynb" --template classic