diff --git a/CLAUDE.md b/CLAUDE.md index ec99489..f17c1a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,18 @@ requirements.txt ### `POST /detectar` -Form-data: `ambiente` (str), `file` (imagem). Confidence threshold: 0.25. +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. ```json { "status": "sucesso", "deteccoes": [{ "box": [x1,y1,x2,y2], "conf": 0.87, "class": 0 }] } diff --git a/main.py b/main.py index 66613f8..57bb584 100644 --- a/main.py +++ b/main.py @@ -66,6 +66,33 @@ def log_print(msg): if len(_training_logs) > 60: _training_logs.pop(0) +def _suprimir_boxes_contidas(deteccoes: list, limiar: float = 0.90) -> list: + """Remove boxes da mesma classe quase totalmente contidas em outra (ex.: caixa grande + envolvendo uma caixa menor do mesmo produto). IoU sozinho não pega esse caso: a união fica + dominada pela caixa maior, então o IoU continua baixo mesmo com contenção quase total. + Aqui a métrica é intersecção / área da MENOR das duas caixas do par.""" + ordenadas = sorted(deteccoes, key=lambda d: d["conf"], reverse=True) + mantidas: list = [] + for d in ordenadas: + x1, y1, x2, y2 = d["box"] + area_d = max(0.0, x2 - x1) * max(0.0, y2 - y1) + contida = False + for m in mantidas: + if m["class"] != d["class"]: + continue + mx1, my1, mx2, my2 = m["box"] + area_m = max(0.0, mx2 - mx1) * max(0.0, my2 - my1) + ix1, iy1 = max(x1, mx1), max(y1, my1) + ix2, iy2 = min(x2, mx2), min(y2, my2) + inter = max(0.0, ix2 - ix1) * max(0.0, iy2 - iy1) + menor_area = min(area_d, area_m) + if menor_area > 0 and inter / menor_area >= limiar: + contida = True + break + if not contida: + mantidas.append(d) + return mantidas + def carregar_modelo_do_s3(ambiente: str): s3_key = f"modelos/{ambiente}/atual/cerebro.pt" local_path = f"/tmp/cerebro_{ambiente}.pt" @@ -258,11 +285,12 @@ async def detectar(ambiente: str = Form(...), file: UploadFile = File(...)): modelo = carregar_modelo_do_s3(ambiente) conteudo = await file.read() imagem = Image.open(io.BytesIO(conteudo)) - results = modelo(imagem, conf=0.25) + results = modelo(imagem, conf=0.25, iou=0.5) deteccoes = [ {"box": [round(x, 2) for x in b.xyxy[0].tolist()], "conf": round(float(b.conf), 2), "class": int(b.cls)} for r in results for b in r.boxes ] + deteccoes = _suprimir_boxes_contidas(deteccoes, limiar=0.90) return {"status": "sucesso", "deteccoes": deteccoes} except Exception as e: log_print(f"Erro deteccao: {str(e)}")