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": ["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.
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.
| Campo | Tipo | Descrição |
|---|---|---|
| name | string | Identificador do modelo (resnet50-ms-cerrado ou resnet50-ms-brasil2) |
| label | string | Nome legível (ex: ResNet-50 · MS · Brasil 2.0) |
| available | boolean | Se o modelo está registrado localmente (pronto para lazy load / inferência) |
| mode | string | Sempre ms para os modelos listados — input do /predict em NPY |
[
{ "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" }
]
| 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 | 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) |
// 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" }
| 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. 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 }
|
{
"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 }
]
}
image_format informado incompatível com o modelo (ex.: "image_format 'png' incompatível com o modelo 'resnet50-ms': ele requer 'npy'")
# 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; }
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.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| geojson | GeoJSON | sim | FeatureCollection com polígonos potencialmente sobrepostos (WGS84) |
{
"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.
| Campo | Tipo | Descrição |
|---|---|---|
| name | string | Identificador do modelo SR (ex: sen2sr-lite) |
| label | string | Nome legível |
| scale | int | Fator de escala (4 = 10 m → 2,5 m) |
| available | boolean | Se o modelo está utilizável neste deploy (dependências SR instaladas) |
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.
| Campo | Tipo | Obrigatório | Descriçã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 |
{
"sr_png_base64": "iVBORw0KGgoAAAANSUhEUgAABAAAAA...", // PNG 1024×1024 (4× do chip)
"bbox": [-40.3821, -9.7654, -40.2190, -9.6023],
"model": "sen2sr-lite",
"scale": 4
}
image_npy — esperado NPY de 10 bandas (B2–B8A, B11, B12)
requirements-sr.txt não instaladas neste deploy)
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).
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| image_id | string | sim | ID da imagem no formato GEE |
| bbox | number[4] | sim | [minLon, minLat, maxLon, maxLat] WGS84 |
| size | int | opcional | Lado do PNG de saída. Default: 1024 |
| min_dn / max_dn | int | opcional | Janela de contraste em DN. Default: 0 / 3000 |
| gamma | float | opcional | Correção gamma. Default: 1.4 |
{
"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
}
Os três compartilham o mesmo request (image_id + bbox)
e produzem chips 256×256 compatíveis com os inputs do /predict:
| Endpoint | Retorno | Uso 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 S2 | before / after com image_format: npy (modelos ms) |
| /pc/image-sar | { npy_base64 } — NPY S2 + VV/VH do Sentinel-1 | before / after com image_format: npy (modelos sar); 404 também quando não há cena S1 casada |
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| tile_id | string | sim | Tile MGRS do Sentinel-2 (ex: 24MVB) |
| date_start / date_end | string | sim | Período de busca (YYYY-MM-DD) |
| per_quarter | boolean | opcional | Se true, aplica o limite por trimestre (carrossel ANTES). Default: false |
| limit | int | opcional | Má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.
Mesmo request dos endpoints de imagem (image_id + bbox).
Retorna o NPY de 10 bandas esperado pelo POST /sr.
{
"npy_base64": "k05VTVBZAQB2AHsnZGVzY3InOiAnfGY0...",
"used_bbox": [-40.3821, -9.7654, -40.2190, -9.6023] // pode diferir do pedido: clip ao footprint da cena
}
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.
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 / 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).
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.
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.
Detecta automaticamente: MPS (Apple Silicon) →
CUDA (GPU NVIDIA) → CPU.
Inferência de uma imagem leva ~80 ms em MPS / ~200 ms em CPU.