Serviço de inferência deep learning para detecção e delimitação de desmatamento na Caatinga. Recebe um par de imagens Sentinel-2 (antes/depois) e retorna a máscara de mudança.
http://localhost:8000
{
"status": "ok",
"models_loaded": ["resnet18", "resnet34"]
}
Consulta o bucket GCS (dl-models-alerts-caatinga)
em busca de latest.pt por backbone,
combinando com os modelos já carregados em memória.
| Campo | Tipo | Descrição |
|---|---|---|
| name | string | Identificador do backbone (ex: resnet18) |
| label | string | Nome legível (ex: ResNet-18) |
| available | boolean | Se o modelo está carregado em memória e pronto para inferência |
[
{ "name": "resnet18", "label": "ResNet-18", "available": true },
{ "name": "resnet34", "label": "ResNet-34", "available": true },
{ "name": "resnet50", "label": "ResNet-50", "available": false },
{ "name": "efficientnet_b4", "label": "EfficientNet-B4", "available": false },
{ "name": "convnext_tiny", "label": "ConvNeXt-Tiny", "available": false }
]
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| alert_id | string | sim | ID do alerta SAD (retornado no response para rastreabilidade) |
| bbox | number[4] | sim | [minLon, minLat, maxLon, maxLat] — envelope geográfico em WGS84 |
| before_png | string | condicional |
Conteúdo binário de um arquivo PNG 256×256 RGB codificado em base64
(não é um ID ou URL — são os bytes do arquivo em si). Gerado via GEE image:computePixels com bandas B4, B3, B2
(min=0, max=3000, EPSG:3857). Obrigatório para modelos RGB.
|
| after_png | string | condicional |
Mesma especificação de before_png,
para a imagem posterior ao evento. Obrigatório para modelos RGB.
|
| before_npy | string | condicional |
Conteúdo binário de um arquivo NumPy NPY 256×256 com
9 bandas (B4, B3, B2, B5, B6, B7, B8, B8A, B11) codificado em base64. Gerado via GEE image:computePixels em formato NPY
(valores DN brutos). O servidor calcula NDFIa via unmixing espectral internamente.
Obrigatório para modelos MS.
|
| after_npy | string | condicional |
Mesma especificação de before_npy,
para a imagem posterior ao evento. Obrigatório para modelos MS.
|
| model_name | string | opcional | Backbone a usar. Default: resnet50-ms. Se indisponível, usa fallback automático |
// Modelo RGB — envia before_png / after_png (bytes do PNG em base64) { "alert_id": "SAD_20240115_T24MVB_0042", "bbox": [-40.3821, -9.7654, -40.2190, -9.6023], "before_png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAA...", "after_png": "iVBORw0KGgoAAAANSUhEUgAAAQAAAA...", "model_name": "resnet50" } // Modelo MS — envia before_npy / after_npy (bytes do NPY em base64, 9 bandas) { "alert_id": "SAD_20240115_T24MVB_0042", "bbox": [-40.3821, -9.7654, -40.2190, -9.6023], "before_npy": "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...", "after_npy": "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...", "model_name": "resnet50-ms" }
| Campo | Tipo | Descrição |
|---|---|---|
| alert_id | string | Mesmo alert_id enviado na request |
| mask_png_base64 | string | PNG RGBA 256×256 em base64 — pixels desmatados em vermelho semitransparente (RGBA 220,50,50,180), restante transparente |
| mask_geojson | GeoJSON | FeatureCollection com polígono(s) vetorizado(s) da mancha, em coordenadas WGS84. Inclui buffer+closing morfológico para suavizar bordas |
| prediction_score | float | Fração de pixels classificados como desmatamento (0–1). Score 0.05 = 5% do chip afetado |
| mask_bbox | number[4] | Bbox usada para georreferenciamento da máscara (igual ao bbox da request + margem 30%) |
| model_name | string | Backbone efetivamente usado (pode diferir do pedido se ocorreu fallback) |
| model_version | string | Versão da arquitetura do serviço (atualmente "2.0") |
| debug_components | object[]? |
Lista de componentes conectados sobreviventes ao filtro de área mínima (≥ 1 ha).
Cada item: { area_ha, centroid_lon, centroid_lat }
|
{
"alert_id": "SAD_20240115_T24MVB_0042",
"mask_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAQAAAA...",
"mask_geojson": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[...]] },
"properties": {}
}]
},
"prediction_score": 0.0842,
"mask_bbox": [-40.4332, -9.8165, -40.1679, -9.5512],
"model_name": "resnet34",
"model_version": "2.0",
"debug_components": [
{ "area_ha": 3.42, "centroid_lon": -40.3104, "centroid_lat": -9.6891 },
{ "area_ha": 1.18, "centroid_lon": -40.2751, "centroid_lat": -9.7243 }
]
}
# Converte imagens para base64 e chama o endpoint BEFORE_B64=$(base64 -i before.png | tr -d '\n') AFTER_B64=$(base64 -i after.png | tr -d '\n') curl -s -X POST http://localhost:8000/predict \ -H "Content-Type: application/json" \ -d "{ \"alert_id\": \"SAD_20240115_T24MVB_0042\", \"bbox\": [-40.3821, -9.7654, -40.2190, -9.6023], \"before_png\": \"$BEFORE_B64\", \"after_png\": \"$AFTER_B64\", \"model_name\": \"resnet34\" }" | python3 -m json.tool
import base64, requests # Lê e codifica as imagens def to_b64(path): with open(path, "rb") as f: return base64.b64encode(f.read()).decode() payload = { "alert_id": "SAD_20240115_T24MVB_0042", "bbox": [-40.3821, -9.7654, -40.2190, -9.6023], "before_png": to_b64("before.png"), "after_png": to_b64("after.png"), "model_name": "resnet34", } r = requests.post("http://localhost:8000/predict", json=payload) r.raise_for_status() data = r.json() print(f"Score: {data['prediction_score']:.2%}") print(f"Modelo usado: {data['model_name']}") print(f"Componentes: {len(data['debug_components'])} mancha(s)") # Salva a máscara PNG import io from PIL import Image png = base64.b64decode(data["mask_png_base64"]) Image.open(io.BytesIO(png)).save("mask.png") # Salva o GeoJSON import json with open("mask.geojson", "w") as f: json.dump(data["mask_geojson"], f, indent=2)
// Lê arquivos via FileReader e chama o endpoint async function toBase64(file) { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(r.result.split(',')[1]); r.onerror = reject; r.readAsDataURL(file); }); } async function predict(beforeFile, afterFile) { const [beforeB64, afterB64] = await Promise.all([ toBase64(beforeFile), toBase64(afterFile), ]); const res = await fetch('http://localhost:8000/predict', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alert_id: 'SAD_20240115_T24MVB_0042', bbox: [-40.3821, -9.7654, -40.2190, -9.6023], before_png: beforeB64, after_png: afterB64, model_name: 'resnet34', }), }); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); console.log(`Score: ${(data.prediction_score * 100).toFixed(1)}%`); console.log(`Modelo: ${data.model_name}`); return data; }
Siamese U-Net com encoder compartilhado. O encoder processa antes e depois em ramos paralelos; o decoder faz a diferença das features e produz uma máscara binária 256×256.
Os campos before_png / after_png e
before_npy / after_npy transportam o
conteúdo binário completo do arquivo de imagem
codificado em base64 — não são IDs nem URLs.
RGB: PNG 256×256 (B4, B3, B2) obtido do GEE via
image:computePixels com Image.visualize(min=0, max=3000).
Normalizado internamente com p2–p98 para float 0–1.
MS: NPY 256×256×9 bandas (B4, B3, B2, B5, B6, B7, B8, B8A, B11)
em valores DN brutos. O servidor calcula NDFIa via unmixing espectral (Souza 2005)
e entrega ao modelo 6 bandas (RGB + B8 + B11 + NDFIa).
Componentes conectados com área geográfica inferior a
1 ha são removidos da máscara final.
O cálculo corrige distorção longitudinal pela latitude central do bbox.
O backend expande automaticamente o bbox do alerta em
30% em cada direção antes de enviar ao serviço ML,
garantindo contexto espacial adequado para o modelo.
Modelos armazenados no GCS como
caatinga-rgb/{backbone}/latest.pt (ponteiro) e
best_{backbone}_vN_dice{score}.pt (versão fixa).
Download automático no startup.
Detecta automaticamente: MPS (Apple Silicon) →
CUDA (GPU NVIDIA) → CPU.
Inferência de uma imagem leva ~80 ms em MPS / ~200 ms em CPU.