Caixa grande "engolindo" uma caixa menor do mesmo produto continuava aparecendo mesmo com sobreposição óbvia — IoU sozinho não resolve isso porque a união fica dominada pela caixa maior, então o IoU continua baixo mesmo com contenção quase total (ex.: caixa 1/5 da área da outra, 100% contida, IoU ≈ 0.2). - iou=0.5 explícito no NMS do YOLO (antes usava o default 0.7, permissivo demais) - novo pós-filtro _suprimir_boxes_contidas(): entre duas boxes da mesma classe onde a intersecção cobre ≥90% da área da MENOR das duas, mantém só a de maior confiança Afeta tanto as boxes da revisão quanto as do TrainingTab, já que ambas vêm do mesmo /detectar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3.4 KiB
IA Gôndola Engine
Serviço isolado de detecção YOLO e treinamento. Consumido exclusivamente pela ia-gondola-api via IA_MOTOR_URL.
Arquitetura
ia-gondola-api → POST /detectar → YOLO → [{box, conf, class}]
→ POST /treinar → triagem + treino → modelo atualizado no S3
Estrutura
main.py — todo o código (sem sub-módulos)
Dockerfile
requirements.txt
Endpoints
POST /detectar
Form-data: ambiente (str), file (imagem). Confidence threshold: 0.25, NMS iou=0.5.
ia-gondola-api também manda um campo conf no form (ver gondola_service.py), mas esse endpoint
não declara/lê esse campo — é ignorado, o threshold usado é sempre o 0.25 hardcoded aqui.
Depois do NMS do próprio YOLO, passa por _suprimir_boxes_contidas(): remove boxes da mesma classe
quase totalmente contidas em outra (caixa grande "engolindo" uma caixa menor do mesmo produto —
comum em prateleira lotada). IoU sozinho não resolve isso: quando uma caixa é muito maior que a
outra, a união fica dominada pela caixa grande e o IoU continua baixo mesmo com contenção quase
total (ex.: caixa pequena 1/5 da área da grande, 100% contida → IoU ≈ 0.2, seguiria sobrevivendo
mesmo com iou threshold bem mais agressivo que o do NMS padrão). A métrica usada aqui é
intersecção / área da MENOR caixa do par, limiar 0.90 — entre duas boxes da mesma classe acima
desse limiar, fica só a de maior confiança.
{ "status": "sucesso", "deteccoes": [{ "box": [x1,y1,x2,y2], "conf": 0.87, "class": 0 }] }
POST /treinar
Body JSON: { "ambiente": "gondola", "pular_triagem": false }
- Lista
treinamento/{ambiente}/novos-treinamentos/no S3 - Triagem (salvo
pular_triagem: true): conf média ≥ 0.15 →base-oficial, < 0.15 →descartados - Baixa
base-oficial, redimensiona imagens > 4096px - Treina: 20 épocas fine-tuning / 30 épocas do zero, imgsz=640, batch=8, device=cpu
- Salva
best.ptemmodelos/{ambiente}/atual/cerebro.pt+ histórico versionado
Retorna { "status": "iniciado", "ambiente": "..." } imediatamente — treino roda em background thread.
Quando não há modelo base, usa yolov8n.pt e pula triagem automaticamente.
GET /treinar/status
Retorna estado do treino em andamento + últimas 30 linhas de log.
{ "status": "running|concluido|erro|vazio|idle", "ambiente": "gondola", "versao": "20260627_1430", "detalhe": null, "logs": ["[1/20] box=0.432 ..."] }
logs inclui tanto as métricas por época (_on_fit_epoch_end callback do YOLO) quanto mensagens de progresso de triagem/download via log_print().
Organização S3
modelos/{ambiente}/atual/cerebro.pt
modelos/{ambiente}/versionamento/cerebro_YYYYMMDD_HHMM.pt
treinamento/{ambiente}/novos-treinamentos/
treinamento/{ambiente}/base-oficial/
treinamento/{ambiente}/descartados/
Convenções
MODELOS_CARREGADOS= dict em memória{ambiente: YOLO}— cache local em/tmp/cerebro_{ambiente}.pt- Se S3 tiver versão mais nova (LastModified), descarta cache e baixa novamente
- Após treino, remove o ambiente de
MODELOS_CARREGADOSpara forçar reload - Todo o código em
main.py— não criar sub-módulos log_print(msg)escreve no stdout e em_training_logs(buffer circular 60 linhas) — não usarprint()diretamente
Como rodar
pip install -r requirements.txt
uvicorn main:app --reload --port 8001