# Descomente no Colab:
# !pip install -q pystac-client planetary-computer geopandas rioxarray matplotlibBuscando imagens Sentinel-2 via STAC
Encontre cenas Sentinel-2 por área, data e cobertura de nuvens no Microsoft Planetary Computer, sem login e sem download manual.
Objetivo
Antes de calcular qualquer índice, é preciso achar a imagem certa. Neste tutorial você vai usar o padrão STAC (SpatioTemporal Asset Catalog) para buscar cenas do Sentinel-2 no catálogo público do Microsoft Planetary Computer.
Ao final, você saberá:
- o que é um catálogo STAC e como ele organiza coleções, itens e assets;
- buscar cenas por área, intervalo de datas e cobertura de nuvens;
- transformar o resultado em uma tabela (
GeoDataFrame) para filtrar e ordenar; - abrir só o recorte da cena que interessa, sem baixar o arquivo inteiro.
A área de estudo é o entorno de Sorriso (MT), um dos maiores produtores de grãos do país.
Pré-requisitos
- Python básico (listas, dicionários, funções).
- Nenhuma conta ou chave de API: o Planetary Computer assina as URLs automaticamente.
Bibliotecas usadas (todas no environment.yml do site):
| Biblioteca | Para quê |
|---|---|
pystac-client |
conversar com a API STAC |
planetary-computer |
assinar as URLs dos arquivos |
geopandas |
tabela com geometrias dos resultados |
rioxarray |
ler o recorte da imagem (COG) |
matplotlib |
gráficos |
Use o botão Abrir no Colab no topo da página e rode a célula abaixo antes das demais. No seu computador, com o ambiente do site ativo, ela não é necessária.
import geopandas as gpd
import matplotlib.pyplot as plt
import planetary_computer
import pystac_client
import rioxarray # noqa: F401 (registra o acessor .rio)
from shapely.geometry import box, shape
plt.style.use("dark_background") # opcional: gráficos no estilo escuro do siteAcesso aos dados: o catálogo STAC
Um catálogo STAC tem três níveis:
- Coleção: um conjunto de dados, como
sentinel-2-l2a(reflectância de superfície já corrigida da atmosfera); - Item: uma cena, com data, geometria e metadados (nuvens, órbita, tile…);
- Asset: cada arquivo do item, como uma banda (
B04,B08) ou a composição colorida (visual).
Abrimos o catálogo passando modifier=planetary_computer.sign_inplace, que assina cada URL para que possamos ler os arquivos diretamente.
catalogo = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
s2 = catalogo.get_collection("sentinel-2-l2a")
print(s2.title)
print("Extensão temporal:", s2.extent.temporal.intervals[0][0].date(), "até hoje")Sentinel-2 Level-2A
Extensão temporal: 2015-06-27 até hoje
Definindo a busca
Vamos procurar cenas sobre um quadrado de cerca de 11 km ao redor de Sorriso, durante a estação seca de 2025 (junho a setembro), quando o céu do Mato Grosso costuma estar limpo.
O filtro de nuvens usa a propriedade eo:cloud_cover de cada item, com a extensão query da API.
lat, lon = -12.55, -55.72
d = 0.05 # meio lado do quadrado, em graus (~5,5 km)
bbox = [lon - d, lat - d, lon + d, lat + d]
busca = catalogo.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime="2025-06-01/2025-09-30",
query={"eo:cloud_cover": {"lt": 20}},
)
itens = busca.item_collection()
print(f"{len(itens)} cenas encontradas")27 cenas encontradas
Os resultados como tabela
Cada item é um GeoJSON Feature. O geopandas transforma a coleção inteira em uma tabela com geometria, o que facilita ordenar e filtrar.
gdf = gpd.GeoDataFrame.from_features(itens.to_dict(), crs="EPSG:4326")
gdf["id"] = [i.id for i in itens]
gdf["data"] = gdf["datetime"].str[:10]
gdf["tile"] = gdf["s2:mgrs_tile"]
tabela = gdf[["data", "platform", "tile", "eo:cloud_cover"]].sort_values("data")
tabela.head(10)| data | platform | tile | eo:cloud_cover | |
|---|---|---|---|---|
| 26 | 2025-06-04 | Sentinel-2C | 21LXG | 12.963872 |
| 25 | 2025-06-06 | Sentinel-2A | 21LXG | 0.138244 |
| 24 | 2025-06-09 | Sentinel-2B | 21LXG | 0.002827 |
| 23 | 2025-06-14 | Sentinel-2C | 21LXG | 0.611727 |
| 22 | 2025-06-16 | Sentinel-2A | 21LXG | 8.199535 |
| 21 | 2025-06-19 | Sentinel-2B | 21LXG | 8.898673 |
| 20 | 2025-06-26 | Sentinel-2A | 21LXG | 0.083600 |
| 19 | 2025-06-29 | Sentinel-2B | 21LXG | 6.090753 |
| 18 | 2025-07-06 | Sentinel-2A | 21LXG | 0.123832 |
| 17 | 2025-07-09 | Sentinel-2B | 21LXG | 0.001553 |
O Sentinel-2 é distribuído em tiles da grade MGRS, quadrados de 110 × 110 km. A coluna tile diz em qual deles cada cena está. Se a sua área cair na borda entre dois tiles, a mesma data aparece duas vezes, uma por tile. Vamos ver onde a nossa área fica.
aoi = gpd.GeoDataFrame(geometry=[box(*bbox)], crs="EPSG:4326")
fig, ax = plt.subplots(figsize=(7, 6))
gdf.drop_duplicates("tile").plot(
ax=ax,
column="tile",
alpha=0.35,
edgecolor="white",
legend=True,
legend_kwds={"title": "Tile MGRS", "loc": "upper left"},
)
aoi.boundary.plot(ax=ax, color="#FF7A45", linewidth=2.5)
ax.set_title("Footprints dos tiles e área de interesse (laranja)")
ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude")
plt.show()
E a cobertura de nuvens ao longo da estação seca:
por_data = gdf.groupby("data")["eo:cloud_cover"].min()
fig, ax = plt.subplots(figsize=(8, 3.2))
ax.bar(por_data.index, por_data.values, color="#22D3EE")
ax.axhline(5, color="#FF7A45", ls="--", lw=1, label="5%")
ax.set_ylabel("Nuvens (%)")
ax.set_title("Cobertura de nuvens por data")
ax.tick_params(axis="x", rotation=60, labelsize=8)
ax.legend()
plt.tight_layout()
plt.show()
Escolhendo a melhor cena
Ficamos com a cena de menor cobertura de nuvens cujo footprint contém toda a área de interesse (assim não precisamos fazer mosaico).
area = box(*bbox)
cobrem = [i for i in itens if shape(i.geometry).contains(area)]
print(f"{len(cobrem)} de {len(itens)} cenas cobrem toda a área")
melhor = min(cobrem, key=lambda i: i.properties["eo:cloud_cover"])
print("Cena:", melhor.id)
print("Data:", melhor.datetime.date())
print(f"Nuvens: {melhor.properties['eo:cloud_cover']:.2f}%")
print("Assets disponíveis:", ", ".join(sorted(melhor.assets)))27 de 27 cenas cobrem toda a área
Cena: S2A_MSIL2A_20250726T140111_R067_T21LXG_20250726T183013
Data: 2025-07-26
Nuvens: 0.00%
Assets disponíveis: AOT, B01, B02, B03, B04, B05, B06, B07, B08, B09, B11, B12, B8A, SCL, WVP, datastrip-metadata, granule-metadata, inspire-metadata, product-metadata, rendered_preview, safe-manifest, tilejson, visual
Cada asset é um COG (Cloud Optimized GeoTIFF): um GeoTIFF organizado em blocos, que permite ler só a janela que interessa via HTTP. É isso que vamos fazer com o asset visual, a composição em cor verdadeira (B04, B03, B02) já pronta, em 8 bits.
visual = rioxarray.open_rasterio(melhor.assets["visual"].href)
print("Cena completa:", visual.shape, "pixels — CRS", visual.rio.crs)
recorte = visual.rio.clip_box(*bbox, crs="EPSG:4326")
print("Recorte:", recorte.shape)Cena completa: (3, 10980, 10980) pixels — CRS EPSG:32721
Recorte: (3, 1112, 1093)
Visualização do resultado
O recorte tem pouco mais de mil pixels de lado (10 m de resolução). Os talhões retangulares das lavouras e as matas ciliares ao longo dos rios ficam bem visíveis.
fig, ax = plt.subplots(figsize=(8, 8))
recorte.plot.imshow(ax=ax)
ax.set_title(f"Sentinel-2 · cor verdadeira · {melhor.datetime.date()} · Sorriso (MT)")
ax.set_xlabel("x (m, UTM)")
ax.set_ylabel("y (m, UTM)")
ax.set_aspect("equal")
plt.show()
Por fim, salvamos uma miniatura sem eixos. É ela que aparece no card deste tutorial no site.
img = recorte.transpose("y", "x", "band").values
altura = img.shape[0]
faixa = img[int(altura * 0.2) : int(altura * 0.2) + int(img.shape[1] * 9 / 16)] # recorte 16:9
fig = plt.figure(figsize=(8, 4.5), dpi=150)
ax = fig.add_axes([0, 0, 1, 1])
ax.imshow(faixa)
ax.axis("off")
fig.savefig("thumbnail.webp", dpi=150)
plt.close(fig)Contains modified Copernicus Sentinel data 2025, acessados pelo Microsoft Planetary Computer.
Exercícios
- Troque a área para o entorno de Brasília (
lat, lon = -15.78, -47.93) e repita a busca. Quantas cenas com menos de 5% de nuvens existem entre junho e setembro? - Busque cenas da estação chuvosa (dezembro a fevereiro) em Sorriso. Como muda a cobertura de nuvens?
- Use
query={"s2:mgrs_tile": {"eq": "21LXG"}}(ou o tile que apareceu no seu mapa) para restringir a busca a um único tile. - Repita o recorte com o asset
B08(infravermelho próximo). O que acontece com a vegetação?