Caatinga ML — API de Segmentação

v2.0

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.

Base URL http://localhost:8000

Fluxo de uso

📡
GEE — busca tiles S2
🖼
PNG base64 antes/depois
⚙️
POST /predict
🗺
Máscara PNG + GeoJSON
Validação humana

Endpoints

GET /health Verifica se o serviço está ativo
Resposta 200
JSON
{
  "status": "ok",
  "models_loaded": ["resnet18", "resnet34"]
}
GET /models Lista modelos disponíveis no GCS e em memória

Consulta o bucket GCS (dl-models-alerts-caatinga) em busca de latest.pt por backbone, combinando com os modelos já carregados em memória.

Resposta 200 — Array de ModelInfo
CampoTipoDescrição
namestringIdentificador do backbone (ex: resnet18)
labelstringNome legível (ex: ResNet-18)
availablebooleanSe o modelo está carregado em memória e pronto para inferência
Exemplo
JSON
[
  { "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 }
]
POST /predict Segmenta desmatamento a partir de par de imagens
Request Body — PredictRequest
CampoTipoObrigatórioDescriçã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
Exemplo de request
JSON
// 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"
}
Resposta 200 — PredictResponse
CampoTipoDescriçã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 }
Exemplo de resposta
JSON
{
  "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 }
  ]
}
Respostas de erro
400 PNG inválido ou não decodificável — verifique o encoding base64 das imagens
503 Nenhum modelo carregado em memória — aguarde o startup completar ou verifique o GCS
Exemplos de código
# 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;
}
Testar agora
POST /predict — execução ao vivo requer o servidor rodando localmente

Modelos disponíveis

resnet18
ResNet-18
treinado · Dice 0.900
resnet34
ResNet-34
treinado · Dice 0.903
resnet50
ResNet-50
em treinamento
efficientnet_b4
EfficientNet-B4
em treinamento
convnext_tiny
ConvNeXt-Tiny
em treinamento

Notas técnicas

Arquitetura

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.

Input das imagens

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).

Filtro de componentes

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.

Bbox expandido

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.

Versionamento de modelos

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.

Dispositivo de inferência

Detecta automaticamente: MPS (Apple Silicon) → CUDA (GPU NVIDIA) → CPU. Inferência de uma imagem leva ~80 ms em MPS / ~200 ms em CPU.