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":    ["resnet50-ms"],
  "models_available": ["resnet50", "resnet50-ms", "resnet50-ms-brasil2", "resnet50-sar", "..."]
}

models_loaded = já em memória (lazy load); models_available = registrados localmente ou baixáveis do GCS.

GET /models Lista os modelos expostos no seletor (Brasil e Brasil 2.0)

Lista apenas os modelos expostos no seletor do validador: resnet50-ms-cerrado (Brasil) e resnet50-ms-brasil2 (Brasil 2.0), nessa ordem. Demais modelos registrados (RGB, MS genérico, SAR) ficam fora da listagem, mas continuam aceitos pelo POST /predict.

Resposta 200 — Array de ModelInfo
CampoTipoDescrição
namestringIdentificador do modelo (resnet50-ms-cerrado ou resnet50-ms-brasil2)
labelstringNome legível (ex: ResNet-50 · MS · Brasil 2.0)
availablebooleanSe o modelo está registrado localmente (pronto para lazy load / inferência)
modestringSempre ms para os modelos listados — input do /predict em NPY
Exemplo
JSON
[
  { "name": "resnet50-ms-cerrado", "label": "ResNet-50 · MS · Brasil",    "available": true,  "mode": "ms"  },
  { "name": "resnet50-ms-brasil2", "label": "ResNet-50 · MS · Brasil 2.0", "available": true,  "mode": "ms"  }
]
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 string sim Conteúdo binário do chip anterior ao evento, codificado em base64 (não é um ID ou URL — são os bytes do arquivo em si).
Modelos rgb: PNG 256×256 gerado via GEE image:computePixels com bandas B4, B3, B2 (min=0, max=3000, EPSG:3857).
Modelos ms: NumPy NPY 256×256 com 9 bandas (B4, B3, B2, B5, B6, B7, B8, B8A, B11) em valores DN brutos — o servidor calcula NDFIa via unmixing espectral.
Modelos sar: mesmo NPY acrescido das bandas VV e VH (Sentinel-1, em dB).
after string sim Mesma especificação de before, para a imagem posterior ao evento.
image_format string opcional png | npy — formato dos chips em before/after. Se omitido, é inferido do modelo (rgb → png; ms/sar → npy). Se informado e incompatível com o modelo → 400.
model_name string opcional Backbone a usar. Default do schema: resnet18 — como esse id normalmente não está carregado, o serviço faz fallback automático para o primeiro modelo disponível. Recomenda-se informar explicitamente: consulte GET /models e o campo mode para saber se enviar PNG (rgb) ou NPY (ms/sar)
Exemplo de request
JSON
// Modelo RGB — before/after com os bytes do PNG em base64
{
  "alert_id":     "SAD_20240115_T24MVB_0042",
  "bbox":         [-40.3821, -9.7654, -40.2190, -9.6023],
  "before":       "iVBORw0KGgoAAAANSUhEUgAAAQAAAA...",
  "after":        "iVBORw0KGgoAAAANSUhEUgAAAQAAAA...",
  "image_format": "png",
  "model_name":   "resnet50"
}

// Modelo MS — before/after com os bytes do NPY (9 bandas) em base64
{
  "alert_id":     "SAD_20240115_T24MVB_0042",
  "bbox":         [-40.3821, -9.7654, -40.2190, -9.6023],
  "before":       "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...",
  "after":        "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...",
  "image_format": "npy",
  "model_name":   "resnet50-ms"
}

// Modelo SAR — NPY com as 9 bandas ópticas + VV, VH; image_format omitido (inferido: npy)
{
  "alert_id":   "SAD_20240115_T24MVB_0042",
  "bbox":       [-40.3821, -9.7654, -40.2190, -9.6023],
  "before":     "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...",
  "after":      "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...",
  "model_name": "resnet50-sar"
}
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. Vetorização direta da máscara (sem suavização morfológica). Manchas com área < 1 ha são descartadas
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 georreferenciar a máscara — idêntico ao bbox enviado na request. Este serviço NÃO adiciona margem (a margem de 30% existe apenas no backend Node, não aqui)
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.3821, -9.7654, -40.2190, -9.6023],
  "model_name":      "resnet50-ms",
  "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 Imagem não decodificável (base64 inválido ou conteúdo que não bate com o formato esperado — ex.: "Falha ao decodificar: before, after. Este modelo requer image_format 'npy'"), ou image_format informado incompatível com o modelo (ex.: "image_format 'png' incompatível com o modelo 'resnet50-ms': ele requer 'npy'")
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\":       \"$BEFORE_B64\",
    \"after\":        \"$AFTER_B64\",
    \"image_format\": \"png\",
    \"model_name\":   \"resnet50\"
  }" | 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":       to_b64("before.png"),
    "after":        to_b64("after.png"),
    "image_format": "png",
    "model_name":   "resnet50",
}

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:       beforeB64,
      after:        afterB64,
      image_format: 'png',
      model_name:   'resnet50',
    }),
  });

  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
POST /merge Funde polígonos sobrepostos de um FeatureCollection

Recebe um FeatureCollection (ex.: resultado de múltiplos /predict em janelas vizinhas) e aplica shapely.unary_union: polígonos que se tocam ou se sobrepõem viram uma única feature. Geometrias inválidas são descartadas silenciosamente. Com 0 ou 1 feature, retorna o GeoJSON de entrada inalterado.

Request Body
CampoTipoObrigatórioDescrição
geojson GeoJSON sim FeatureCollection com polígonos potencialmente sobrepostos (WGS84)
Resposta 200
JSON
{
  "type": "FeatureCollection",
  "features": [{
    "type":     "Feature",
    "geometry": { "type": "Polygon", "coordinates": [[...]] },
    "properties": {
      "area_ha":      4.60,
      "centroid_lat": -9.6891,
      "centroid_lon": -40.3104
    }
  }]
}

Propriedades originais das features são substituídas por area_ha (aproximação planar corrigida pela latitude do centroide), centroid_lat e centroid_lon.

GET /sr/models Lista modelos de super-resolução
Resposta 200 — Array de SRModelInfo
CampoTipoDescrição
namestringIdentificador do modelo SR (ex: sen2sr-lite)
labelstringNome legível
scaleintFator de escala (4 = 10 m → 2,5 m)
availablebooleanSe o modelo está utilizável neste deploy (dependências SR instaladas)
POST /sr Super-resolução Sentinel-2 (10 m → 2,5 m)

Aplica SEN2SR (4×) sobre um chip Sentinel-2 e retorna um PNG RGB super-resolvido, usado pelo botão "HD" da interface de validação.

Request Body — SRRequest
CampoTipoObrigatórioDescrição
image_npy string sim Conteúdo binário de um NPY 256×256 com 10 bandas (B2, B3, B4, B5, B6, B7, B8, B8A, B11, B12) em base64 — obtenível via GEE ou POST /pc/sr-bands
bbox number[4] sim [minLon, minLat, maxLon, maxLat] em WGS84 (retornado como veio, para georreferenciamento)
model string opcional Modelo SR. Default: sen2sr-lite. Consulte GET /sr/models
Resposta 200 — SRResponse
JSON
{
  "sr_png_base64": "iVBORw0KGgoAAAANSUhEUgAABAAAAA...",  // PNG 1024×1024 (4× do chip)
  "bbox":          [-40.3821, -9.7654, -40.2190, -9.6023],
  "model":         "sen2sr-lite",
  "scale":         4
}
Respostas de erro
400 Falha ao decodificar image_npy — esperado NPY de 10 bandas (B2–B8A, B11, B12)
503 Modelo SR indisponível ou falhou (dependências requirements-sr.txt não instaladas neste deploy)

Endpoints — Planetary Computer

Alternativa ao GEE para obtenção de imagens Sentinel-1/2, lendo COGs do catálogo STAC do Microsoft Planetary Computer. Todos retornam 503 se as dependências PC (pystac-client / stackstac / planetary-computer) não estiverem instaladas, e 404 se a imagem não existir no catálogo. O image_id usa o mesmo formato de ID do GEE (ex: 20240115T130251_20240115T130249_T24MVB).

POST /pc/overlay PNG RGB do bbox p/ exibição no mapa (substitui tile GEE)
Request Body — PCOverlayRequest
CampoTipoObrigatórioDescrição
image_idstringsimID da imagem no formato GEE
bboxnumber[4]sim[minLon, minLat, maxLon, maxLat] WGS84
sizeintopcionalLado do PNG de saída. Default: 1024
min_dn / max_dnintopcionalJanela de contraste em DN. Default: 0 / 3000
gammafloatopcionalCorreção gamma. Default: 1.4
Resposta 200
JSON
{
  "png_base64": "iVBORw0KGgo...",
  "bbox":       [-40.3821, -9.7654, -40.2190, -9.6023],
  "stats":      { "cloud": 0.02, "veg": 0.71, "soil": 0.18 }  // da banda SCL, quando disponível
}
POST /pc/image-rgb · /pc/image-ms · /pc/image-sar Chips de inferência p/ os 3 modos de modelo

Os três compartilham o mesmo request (image_id + bbox) e produzem chips 256×256 compatíveis com os inputs do /predict:

EndpointRetornoUso no /predict
/pc/image-rgb{ png_base64 } — PNG RGB (B4, B3, B2)before / after com image_format: png (modelos rgb)
/pc/image-ms{ npy_base64 } — NPY 9 bandas S2before / after com image_format: npy (modelos ms)
/pc/image-sar{ npy_base64 } — NPY S2 + VV/VH do Sentinel-1before / after com image_format: npy (modelos sar); 404 também quando não há cena S1 casada
POST /pc/quarter-chips Lista imagens S2 de um tile/período (substitui consulta BQ)
Request Body — PCQuarterRequest
CampoTipoObrigatórioDescrição
tile_idstringsimTile MGRS do Sentinel-2 (ex: 24MVB)
date_start / date_endstringsimPeríodo de busca (YYYY-MM-DD)
per_quarterbooleanopcionalSe true, aplica o limite por trimestre (carrossel ANTES). Default: false
limitintopcionalMáximo de imagens (por trimestre ou total). Default: 8

Retorna a lista de chips candidatos (ID, data, cobertura de nuvens) ordenados por qualidade, no mesmo formato consumido pelo carrossel do frontend.

POST /pc/sr-bands NPY 10 bandas p/ super-resolução

Mesmo request dos endpoints de imagem (image_id + bbox). Retorna o NPY de 10 bandas esperado pelo POST /sr.

Resposta 200
JSON
{
  "npy_base64": "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...",
  "used_bbox":  [-40.3821, -9.7654, -40.2190, -9.6023]  // pode diferir do pedido: clip ao footprint da cena
}

Modelos disponíveis

Backbones: resnet50, efficientnet_b4 e convnext_tiny, em quatro famílias por tipo de input/dataset. A lista efetiva do deploy vem de GET /models.

{backbone}
RGB — 3 bandas (B4, B3, B2), input PNG
caatinga-rgb
{backbone}-ms
Multiespectral — 6 bandas (RGB + B8 + B11 + NDFIa), input NPY
caatinga-ms
{backbone}-ms-cerrado · resnet50-ms-brasil2
"Brasil": MS treinado com dados de outros biomas (prefixo caatinga-ms-cerrado). "Brasil 2.0": alias do resnet50-ms mais recente
caatinga-ms-cerrado
{backbone}-sar
SAR — 9 bandas (6 MS + VV, VH e razão VV/VH do Sentinel-1), input NPY
caatinga-sar

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

SAR: mesmo NPY do modo MS acrescido das bandas VV e VH do Sentinel-1 (em dB); o modelo recebe 9 bandas (6 MS + VV, VH e a razão VV/VH).

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.

Rate limiting

Limite genérico de 30 req/min por IP (janela deslizante) nas rotas POST /predict, POST /sr, POST /merge e POST /pc/*. GET /health, GET / e GET /models são livres. Estouro retorna 429 com header Retry-After.

Requests cujo IP real é privado/loopback (backend interno via rede Docker, localhost) são isentas automaticamente; tráfego via reverse proxy é identificado pelo X-Forwarded-For. Override via env ML_RATE_LIMIT (formato N/minute) e ML_RATE_LIMIT_ENABLED=false para desligar.

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.