Laboratório 5#
Avaliação de classificação supervisionada de imagens no GEE
Objetivos:
Divisão de amostras para avaliação de classificação
Classificação supervisionada
Seleção de modelo
[ ]:
import random
import ee
import geemap
[ ]:
# 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')
Preparação dos dados#
Primeiro definimos uma região de interesse, que pode convenientemente ser a mesma área do laboratório anterior, o que facilitará bastante o desenvolvimento deste laboratório.
[ ]:
# roi: região de interesse
roi = ee.Geometry.BBox(
west=-46.88655, south=-23.6906, east=-46.611642, north=-23.590958
)
roi
Vamos realizar algumas operações idênticas ao laboratório anterior, para carregar a imagem do Sentinel-2
e preparar os dados para o laboratório.
Para começar, vamos definir algumas variáveis globais que serão utilizadas ao longo do laboratório.
[ ]:
escala = 10 # resolução espacial, em metros
limite_nuvens = 10 # limite de cobertura de nuvens, em porcentagem
dataset = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED") # dataset original
sao_paulo = ee.Geometry.Point([-46.6333, -23.5500]) # um ponto qualquer em São Paulo
data_inicio = "2019-01-01"
data_fim = "2023-01-01"
banda_infra_vermelho = "B8" # banda do infravermelho próximo (NIR)
banda_vermelho = "B4" # banda do vermelho
Agora definimos algumas funções relevantes para o pré-processamento dos dados.
[ ]:
def recorta(img: ee.Image) -> ee.Image:
"""Função para recortar parte da img na região desejada. Assume que já
existe uma variável roi definida."""
return img.clip(roi)
def remove_nuvens(img: ee.Image) -> ee.Image:
"""Função para remover nuvens de uma img. Só funciona para o Sentinel-2."""
return (
img.updateMask(img.select("MSK_CLDPRB").lt(limite_nuvens))
.updateMask(img.select("SCL").neq(3))
.updateMask(img.select("SCL").neq(7))
.updateMask(img.select("SCL").neq(8))
.updateMask(img.select("SCL").neq(9))
.updateMask(img.select("SCL").neq(10))
)
def remove_valores_invalidos(img: ee.Image) -> ee.Image:
"""Função para remoção de valores inválidos. Funciona somente para o
Sentinel-2."""
return (
img.updateMask(img.select("B2").lt(10000))
.updateMask(img.select("B3").lt(10000))
.updateMask(img.select("B4").lt(10000))
.updateMask(img.select("B8").lt(10000))
)
Agora executamos o pré-processamento dos dados, igual fizemos no laboratório anterior.
[ ]:
# Filtra o dataset para a região, datas e bandas de interesse.
dataset = (
dataset.filterBounds(sao_paulo)
.filterMetadata("CLOUDY_PIXEL_PERCENTAGE", "less_than", limite_nuvens)
.filterDate(data_inicio, data_fim)
.select(["B2", "B3", "B4", "B8", "MSK_CLDPRB", "SCL"])
)
# Recorta o dataset para a região de interesse.
dataset_clipped = dataset.map(recorta)
# Remove as nuvens do dataset.
dataset_sem_nuvens = dataset_clipped.map(remove_nuvens)
# Remove valores inválidos na coleção.
dataset_sem_nuvens = dataset_sem_nuvens.map(remove_valores_invalidos)
dataset_sem_nuvens
Até aqui apenas editamos o dataset, vamos então criar uma ee.Image
a partir dele, tal qual fizemos no laboratório anterior.
[ ]:
# Cria o mosaico a partir da coleção.
imagem = dataset_sem_nuvens.mosaic()
# Cria a composição a partir da coleção.
imagem = dataset_sem_nuvens.mean()
# Ajuste nos valores de reflectância das imagens.
imagem = imagem.multiply(0.0001)
# Extrai bandas do vermelho e do infravermelho próximo
imagem_banda_nir = imagem.select(banda_infra_vermelho)
imagem_banda_vermelho = imagem.select(banda_vermelho)
# Calcula o NDVI
imagem = imagem.addBands(
imagem_banda_nir.subtract(imagem_banda_vermelho)
.divide(imagem_banda_nir.add(imagem_banda_vermelho))
.rename("NDVI"),
["NDVI"],
)
# Ajuste nos valores de reflectância das imagens.
# imagem = imagem.multiply(0.0001) # TODO: por que tem que multiplicar 2 vezes?
# Seleciona as bandas de interesse.
imagem = imagem.select(ee.List(["B2", "B3", "B4", "B8", "NDVI"]))
imagem
Vamos utilizar os resultados do laboratório anterior para treinar e avaliar um modelo de classificação supervisionada. Para isso, vamos precisar do arquivo shapefile coletado ao final do laboratório anterior. Se você não tiver o arquivo samples.shp
no seu drive, copie-o do diretório data
do repositório do curso no GitHub.
O módulo geopandas
é necessário para ler o arquivo shapefile. Execute a célula abaixo para instalar o módulo, caso necessário.
[ ]:
# %pip install geopandas
[ ]:
# TODO: usar um link pro github, colocar caminho absoluto.
amostras_completas = geemap.shp_to_ee("../data/samples_lab4/samples_lab4.shp")
amostras_completas
[ ]:
amostras_agua = amostras_completas.filterMetadata("classe", "equals", 0)
amostras_veget_baixa = amostras_completas.filterMetadata("classe", "equals", 1)
amostras_veget_alta = amostras_completas.filterMetadata("classe", "equals", 2)
amostras_construcoes = amostras_completas.filterMetadata("classe", "equals", 3)
amostras_solo = amostras_completas.filterMetadata("classe", "equals", 4)
Utilizamos o método sampleRegions
para extrair amostras de pixels da imagem para cada polígono do shapefile.
Aqui é importante que as amostras
estejam contidas no mesmo lugar geométrico da imagem
.
[ ]:
# amostras_selecionadas = imagem.sampleRegions(
# collection=amostras_completas, properties=["classe"], scale=escala
# )
[ ]:
# amostras_selecionadas
Classificação supervisionada#
A partir do passo da coleta das amostras vamos prosseguir com algumas opções mais avançadas. Veremos alguns métodos alternativos de seleção de pixels para treinamento e teste.
Amostras com tamanho pré-definido#
Começaremos com a segunda opção de amostragem, pegando um número limitado de pixels por classe.
Para prevenir sobrecarga no GEE, vamos limitar o tamanho das amostras por classe usando a função sample()
em cada ee.FeatureCollection
do dicionário amostras_dict
.
A sample()
é uma função que automatiza a amostragem de pixels numa região, iniciando com uma semente aleatória.
[ ]:
def coleta_pixels_amostra(regiao_amostra: ee.Geometry) -> ee.FeatureCollection:
"""Realiza uma amostragem limitada de pixels dentro das regiões de uma
FeatureCollection."""
# Gera uma semente aleatória para a amostragem
semente = ee.Number(random.random()).multiply(10000).toLong()
# Amostra a imagem dentro da região dada
sample = imagem.sample(
region=ee.FeatureCollection(regiao_amostra),
dropNulls=True, # Descarta quaisquer amostras com valores nulos
seed=semente, # Usa a semente gerada para amostragem aleatória
scale=escala, # A escala na qual a amostragem deve ser feita
numPixels=200, # O número de pixels para coletar
geometries=False, # Falso para economizar tempo de processamento
)
# Recupera o número da classe da primeira feição na coleção
numero_classe = ee.FeatureCollection(regiao_amostra).first().get("classe")
# Mapeia sobre a amostra, definindo o número da classe para cada feição
return sample.map(lambda feature: feature.set({"classe": numero_classe}))
Em seguida vamos criar uma lista com as feature collections das regiões amostradas para cada classe.
[ ]:
lista_amostra_classes = ee.List(
[
amostras_agua,
amostras_veget_baixa,
amostras_veget_alta,
amostras_construcoes,
amostras_solo,
]
)
lista_amostra_classes
Agora precisamos executar a função para colher amostras nas feature collections de cada classe a partir da lista de amostras. O resultado será uma lista com uma feature collection para cada cada classe com a respectiva amostra.
[ ]:
lista_amostra_pixels = lista_amostra_classes.map(coleta_pixels_amostra)
lista_amostra_pixels
Finalmente, as Feature Collections são combinadas e achatadas usando flatten()
para formar uma única Feature Collection com todas as Features.
[ ]:
amostras_pixels = ee.FeatureCollection(lista_amostra_pixels).flatten()
amostras_pixels
Divisão das amostras em subconjuntos#
Vamos dividir as amostras em 3 conjuntos: treinamento, validação e testes.
Uma semente aleatória gera números entre 0 e 1 para cada Feature de pixel em random
. As amostras são divididas usando filtros em random
, mantendo proporções uniformes. Por exemplo, random
< 0.7 aloca 70% para treino, e 15% para teste e validação cada.
[ ]:
semente = ee.Number(random.random()).multiply(10000).toLong()
Adiciona uma propriedade a cada pixel da amostra com um número real entre 0 e 1 aleatório.
[ ]:
amostras_pixels = amostras_pixels.randomColumn("random", semente)
amostras_pixels
Vamos separar 70% dos dados para treinamento, 15% para validação e 15% para teste final.
[ ]:
treinamento = amostras_pixels.filter(ee.Filter.lt("random", 0.7))
validacao = amostras_pixels.filter(ee.Filter.gte("random", 0.7)).filter(
ee.Filter.lt("random", 0.85)
)
teste = amostras_pixels.filter(ee.Filter.gte("random", 0.85))
[ ]:
treinamento # para visualizar a amostra de treinamento
[ ]:
validacao # para visualizar a amostra de validação
[ ]:
teste # para visualizar a amostra de teste
Treinamento e avaliação dos modelos#
O passo inicial do processo é treinar os modelos desejados e avaliá-los na amostra de validação, para enfim tomar uma decisão sobre a escolha do modelo, ou reparametrização.
Treinamento#
Para treinar o modelo, é necessário criar uma instância do classificador selecionado, configurando-o com os parâmetros apropriados, e depois aplicar o método train()
.
No nosso caso, usaremos o classificador smileRandomForest()
, configurado inicialmente para usar 50 árvores de decisão. Este número de árvores é ajustável e deve ser refinado baseado no desempenho observado no conjunto de validação. Informações adicionais sobre outros parâmetros configuráveis podem ser consultadas na documentação do Google Earth Engine (GEE), e suas aplicações práticas estão detalhadas na literatura especializada.
[ ]:
# Instancia um classificador na memória com os parâmetros dados e treinando no conjunto de treinamento.
classificador_treinado = ee.Classifier.smileRandomForest(50).train(
features=treinamento, # amostra a ser usada para treinamento
classProperty="classe", # propriedade que contém o número que identifica a classe
inputProperties=ee.List(["B2", "B3", "B4", "B8", "NDVI"]), # lista de bandas
)
classificador_treinado
Validação#
O processo de validação tenta compensar os efeitos do conjunto de treinamento sobre a matriz de confusão. Aqui, o classificador treinado é usado para classificar a amostra de validação e uma matriz de erros é gerada.
A tendência é que a acurácia seja menor, caso a acurácia reduza excessivamente, pode ser efeito de um overfitting, onde o modelo tem um desempenho excelente no conjunto de treinamento, mas no de validação tem um desempenho ruim. Nesse caso, ajustes devem ser feitos nos parâmetros do modelo e conjunto de treinamento.
/vamos iniciar aplicando o classificador que foi treinado no conjunto de treinamento sobre o conjunto de validação.
[ ]:
validacao_classificada = validacao.classify(classificador_treinado)
validacao_classificada
Calcula a matriz de erros do classificador aplicado ao conjunto de validação
[ ]:
matriz_validacao = validacao_classificada.errorMatrix("classe", "classification")
matriz_validacao
Calcula a acurácia do classificador aplicado ao conjunto de validação
[ ]:
matriz_validacao.accuracy()
Teste final e classificação da imagem#
Vamos classificar o conjunto de testes usando o modelo validado, aplicando o método classify()
do GEE ao objeto da amostra de testes com o classificador treinado.
[ ]:
validacao_classificada = validacao.classify(classificador_treinado)
validacao_classificada
A matriz de erro é então calculada e podemos visualizar a acurácia dos testes.
[ ]:
matriz_validacao = validacao_classificada.errorMatrix("classe", "classification")
matriz_validacao
Aqui é feita a avaliação do modelo escolhido, estimando seu erro de predição em novos dados através da acurácia.
[ ]:
matriz_validacao.accuracy()
O resultado do método classify()
, diferentemente dos conjuntos de validação e teste, é um objeto do tipo ee.Image
com uma única banda em que os pixels armazenam o valor relativo às classes que foram atribuídas no momento do desenho das regiões das amostras em tela.
Abaixo vamos aplicar o classificador que foi treinado no conjunto de treinamento sobre o conjunto de teste.
[ ]:
teste_classificada = teste.classify(classificador_treinado)
teste_classificada
Calcula a matriz de erros da amostra de testes.
[ ]:
matriz_teste = teste_classificada.errorMatrix("classe", "classification")
matriz_teste
Calcula a acurácia da matriz de erros da amostra de testes.
[ ]:
matriz_teste.accuracy()
Classifica a imagem com o classificador treinado e com os parâmetros definidos.
[ ]:
imagem_classificada = imagem.classify(classificador_treinado)
imagem_classificada
Visualiza o mapa final com as imagens geradas
[ ]:
paleta_cores = [
"#1f77b4", # Água - um azul mais suave e claro.
"#98df8a", # Vegetação rasteira - um verde claro para diferenciar da vegetação alta.
"#2ca02c", # Vegetação alta - um verde mais vibrante e menos saturado.
"#7f7f7f", # Construção - um cinza médio que representa áreas construídas.
"#ff7f0e", # Solo exposto - um laranja mais vibrante e atraente.
]
[ ]:
my_map = geemap.Map()
my_map.centerObject(roi, 12)
my_map.addLayer(
imagem_classificada, {"min": 0, "max": 4, "palette": paleta_cores}, "Classificação"
)
my_map