Laboratório 3#
Cálculo do NDVI e classificação não-supervisionada de imagens no GEE
Objetivos:
Cálculo do NDVI no GEE
Classificação não supervisionada de imagens com k-means no GEE
Cálculo do número ideal de clusters
Introdução#
[ ]:
import time
import ee
import geemap.geemap as geemap
import matplotlib.pyplot as plt
[ ]:
# 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')
[ ]:
%matplotlib inline
Desenvolvimento#
Importando a coleção#
Vamos utilizar imagens que contenham valores de reflectância em seus pixels (ao invés de valores de DN). Precisaremos de uma única imagem para executarmos nossa classificação.
[ ]:
lat, lon = -23.5546721, -46.7318389
poli_usp_point = ee.Geometry.Point(lon, lat)
[ ]:
dataset = (
ee.ImageCollection("LANDSAT/LC09/C02/T1_L2")
.filterDate("2010-01-01", "2024-02-01")
.filterBounds(poli_usp_point)
)
Vamos fazer uma pouco usual porém importante, que é aplicar um scaling factor na coleção de imagens para
[ ]:
# Ref.: https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LC09_C02_T1_L2#colab-python
# Applies scaling factors.
def apply_scale_factors(image):
optical_bands = image.select("SR_B.").multiply(0.0000275).add(-0.2)
thermal_bands = image.select("ST_B.*").multiply(0.00341802).add(149.0)
return image.addBands(optical_bands, None, True).addBands(thermal_bands, None, True)
dataset = dataset.map(apply_scale_factors)
Ordena as imagens de acordo com a cobertura de nuvens, depois pega a primeira imagem da lista e a armazena na variável image
. Esta deve ser a imagem com menor cobertura de nuvens.
[ ]:
dataset = dataset.sort("CLOUD_COVER", True)
imagem = dataset.first()
Apenas para confirmação, vamos plotar o histograma das bandas principais da imagem e verificar que realmente estamos trabalhando com valores de reflectância entre 0 e 1.
[ ]:
histograma = imagem.reduceRegion(
reducer=ee.Reducer.histogram(maxBuckets=2**9, minBucketWidth=0.001), maxPixels=1e9
).getInfo()
fig, ax = plt.subplots(figsize=(9, 5))
for i, banda in enumerate(["SR_B4", "SR_B3", "SR_B2"]):
frequencias = histograma[banda]["histogram"]
bins = histograma[banda]["bucketMeans"]
ax.bar(bins, frequencias, width=bins[1] - bins[0], alpha=0.3)
ax.plot(bins, frequencias, linewidth=2, label=banda)
ax.set_title(f"Histograma da reflectância em diferentes bandas")
ax.set_xlabel("Reflectância")
ax.set_ylabel("Frequência")
ax.set_xlim([0, 1])
plt.grid(True, alpha=0.3)
plt.legend(loc="upper right")
plt.show()
Utiliza as 3 bandas principais (vermelho, verde e azul) para visualizar a imagem e visualiza o mapa
[ ]:
imagem_vis = {
"min": 0.0,
"max": 0.3,
}
my_map = geemap.Map(center=[lat, lon], zoom=10)
my_map.add_layer(imagem.select(["SR_B4", "SR_B3", "SR_B2"]), {}, "Landsat9")
my_map
Dessa vez seremos mais ousados… Você deverá selecionar um polígono no mapa do geemap
e extrair as coordenadas do polígono para utilizarmos como região de interesse (ROI) para a extração da imagem.
Escolha uma região que seja coberta predominantemente por vegetação. Regiões muito grandes podem deixar o processamento bastante lento, então tente escolher uma região de tamanho moderado.
[ ]:
polygon = my_map.draw_last_feature
polygon.getInfo()
Corta a imagem para apenas o polígono selecionado e visualiza o mapa
[ ]:
clipped = imagem.clip(polygon)
my_map.addLayer(clipped, imagem_vis, "Landsat9")
my_map
Cálculo do NDVI#
Vamos realizar algumas operações aritméticas para calcular o NDVI da imagem.
Primeiramente deve-se extrair as bandas do NIR e do vermelho e colocá-las em variáveis separadas.
[ ]:
# Veja na documentação o significado de cada banda
imagem_banda_nir = clipped.select("SR_B5") # Near Infrared (NIR)
imagem_banda_vermelho = clipped.select("SR_B4") # Red
Adicionar uma banda NDVI à imagem. A fórmula do NDVI primeiro faz a diferença entre os valores da banda NIR e da banda do vermelho visível, depois o valor resultante é dividido pela soma das mesmas duas bandas.
[ ]:
clipped = clipped.addBands(
imagem_banda_nir.subtract(imagem_banda_vermelho)
.divide(imagem_banda_nir.add(imagem_banda_vermelho))
.rename("NDVI"), # NDVI será o nome da nova banda
["NDVI"],
)
Podemos conferir que no objeto clipped
foi adicionada uma banda NDVI.
[ ]:
clipped
Adicionar o NDVI como um layer no mapa
[ ]:
my_map.addLayer(clipped, {"bands": ["NDVI"], "min": -1, "max": 1}, "NDVI")
Visualizar o mapa novamente
[ ]:
my_map
Execução do k-means#
Há duas maneiras de se executar o k-means junto do GEE:
Uma delas está dentro de
ee.Algorithms
.A outra é baseada na implementação do pacote WEKA dentro dos algoritmos da classe
ee.Clusterer
.
Vamos trabalhar com a segunda opção por enquanto, e depois podemos experimentar a primeira.
[ ]:
escala = 30 # 30 metros, ver descrição da coleção de imagens
bandas = ee.List(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"])
roi = clipped.select(bandas) # Region of interest
roi
Primeiramente, vamos selecionar uma amostra de pixels com o método sample()
chamado a partir da imagem que será classificada, que neste caso será a imagem clipped
[ ]:
# Selecionar uma amostra para executar o k-means
amostra_treinamento = roi.sample(
region=roi.geometry(), # redundante fazer isso, mas é para ensinar que é possível selecionar uma região menor
scale=escala,
numPixels=5000,
)
O resultado deve ser uma FeatureCollection com numPixels
amostras.
[ ]:
amostra_treinamento
# TODO: acho que seria possível criar um pd.Dataframe com os dados da amostra abaixo,
# Isso facilitaria bastante a fazer o k-means lá na frente, mas o desafio maior é como voltar para o ee.ImageCollection depois
Executando a clusterização de k-means
[ ]:
k = 5 # número de clusters (altere se quiser testar diferentes valores)
clusters_imagem = ee.Clusterer.wekaKMeans(k).train(amostra_treinamento)
[ ]:
clusters_imagem
Agora classificamos os pixels de acordo com o resultado do k-means
[ ]:
# Classificando os pixels de acordo com o resultado do k-means da amostra
imagem_classificada_weka = roi.cluster(clusters_imagem)
[ ]:
imagem_classificada_weka
Vamos adicionar a nova imagem ao mapa para visualizarmos o resultado da classificação
[ ]:
# Adicionando a imagem classificada no mapa do GEE
# Vamos colorir aleatoriamente, mas você pode alterar as cores depois
my_map.addLayer(imagem_classificada_weka.randomVisualizer(), {}, "k-means")
Visualizar o mapa novamente
[ ]:
my_map
Indo além#
Seleção de número de clusters ótimo#
Vamos tentar encontrar, de forma programática, o número de clusters ótimo para a classificação. Para tanto, precisamos rodar o k-means para diferentes valores de k e calcular o erro quadrático médio (MSE) para cada um deles.
Antes de mais nada, vamos criar uma função que realiza o k-means dado um número de clusters, a amostra de treinamento e uma imagem.
[ ]:
def executa_kmeans(k: int, amostra: ee.FeatureCollection, img: ee.Image):
"""Executa o k-means na imagem 'img' com 'k' clusters, usando 'amostra'
como amostra de treinamento."""
# Initialization method to use.0 = random, 1 = k-means++, 2 = canopy, 3 = farthest first.
return img.cluster(
ee.Clusterer.wekaKMeans(nClusters=k, init=1, fast=True).train(amostra)
)
Agora vamos para a parte mais legal: uma função para calcular a soma dos quadrados das distâncias dos pixels para o centroide do cluster ao qual ele pertence. Essa função é chamada de soma dos quadrados dentro do cluster (within cluster sum of squares, ou WCSS).
[ ]:
def wcss(img_kmeans):
"""
Esta função calcula a soma dos quadrados intra-clusters (Within Cluster Sum of Squares)
para cada imagem clusterizada. Créditos: Leonardo Godoy
"""
img_kmeans = ee.Image(img_kmeans)
# Encontra o ID de cluster máximo na imagem
max_id_cluster = img_kmeans.reduceRegion(
reducer=ee.Reducer.max(), maxPixels=1e12, bestEffort=False
)
min_id_cluster = img_kmeans.reduceRegion(
reducer=ee.Reducer.min(), maxPixels=1e12, bestEffort=False
)
# Cria uma lista de IDs de cluster
lista_id_clusters = ee.List.sequence(
min_id_cluster.get("cluster"), max_id_cluster.get("cluster")
)
# Função para calcular a diferença quadrática para um cluster
def calcula_diff_quad_cluster(id_cluster):
# Isola os pixels pertencentes ao cluster atual
pixels_cluster = roi.mask(img_kmeans.eq(ee.Number(id_cluster).toInt()))
# Calcula a média dos pixels do cluster
media_pixels_cluster = pixels_cluster.reduceRegion(
ee.Reducer.mean(), maxPixels=1e12, bestEffort=False
)
# Calcula a diferença quadrática
diff_quad = pixels_cluster.subtract(media_pixels_cluster.toImage()).pow(2)
# Soma a diferença quadrática sobre todas as bandas
diff_quad_soma_bandas = diff_quad.reduceRegion(
ee.Reducer.sum(), maxPixels=1e12, bestEffort=False
)
# Retorna a soma das diferenças quadráticas para todas as bandas
return diff_quad_soma_bandas.values().reduce(ee.Reducer.sum())
# Mapeia a função sobre a lista de IDs de cluster e reduz os resultados
soma_wcss = lista_id_clusters.map(calcula_diff_quad_cluster).reduce(
reducer=ee.Reducer.sum()
)
value = soma_wcss.getInfo()
print(f"\t>>> WCSS: {value:.2f}")
return value
Perfeito, agora vamos iterar sobre uma lista de valores de k, fazer o agrupamento e calcular o WCSS para cada um deles.
[ ]:
# TODO: código está bastante lento, mas não consegui fazer funcionar usando map (erro 429 na API do GEE)
# Gera lista com uma sequência dos números de cluster
numero_clusters = range(2, 15, 1) # start, stop, step
# Lista para armazenar os resultados
k_values = [] # Lista para armazenar os valores de k
wcss_values = [] # Lista para armazenar os valores WCSS
# Loop para aplicar k-means com diferentes números de clusters
# Obs.: Existem várias informações de tempo aqui, mas isso é só para debug
for k in numero_clusters:
print(f"Executando k-means com k = {float(k):2.0f}")
start_time = time.time() # Captura o tempo de início da iteração
img = executa_kmeans(k, amostra_treinamento, roi)
k_values.append(k)
wcss_values.append(wcss(img))
# Calcula o tempo e imprime no console
end_time = time.time()
elapsed_time = end_time - start_time
print(f"\tK-means concluído em {elapsed_time:.2f} s.\n")
Plotar o gráfico de MSE para cada valor de k, e identificar o ponto de inflexão, que é o número de clusters ótimo.
[ ]:
# Plota o gráfico WCSS
from scipy.optimize import curve_fit
import numpy as np
# Definindo a forma da função exponencial decrescente.
def exp_decreasing(x, a, b, c):
return a * np.exp(-b * x) + c
k_values = np.array(k_values)
wcss_values = np.array(wcss_values)
# Ajustando a função aos dados. popt são os valores otimizados para os parâmetros da função (a, b e c)
popt, _ = curve_fit(
exp_decreasing, k_values, wcss_values, p0=(1, 1e-6, 1)
) # p0 é o palpite inicial para os valores dos parâmetros
# Usando os parâmetros otimizados para prever y com base em x
y_pred = exp_decreasing(k_values, *popt)
# Imprimindo os parâmetros otimizados
print("Parâmetros otimizados: a =", popt[0], "b =", popt[1], "c =", popt[2])
plt.plot(k_values, y_pred, "r-", label="Ajuste exponencial")
plt.plot(k_values[:], wcss_values, "bx-", label="WCSS")
plt.title("Método do Cotovelo para K-Means Clustering")
plt.xlabel("Número de clusters - k")
plt.ylabel("WCSS")
plt.xlim(xmin=1.8)
plt.grid()
plt.legend()
plt.show()
A partir do gráfico acima, podemos analisar visualmente qual o número ideal de clusters, que será o ponto de inflexão da curva.
Alternativa para execução do k-means no GEE#
[ ]:
imagem_classificada = ee.Algorithms.Image.Segmentation.KMeans(
image=roi, numClusters=5, uniqueLabels=False
)
[ ]:
my_map.addLayer(imagem_classificada.randomVisualizer(), {}, "k-means3")
my_map
Atividade#
[ ]:
[ ]: