fix: reduzir sobreposição de boxes em /detectar
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>
This commit is contained in:
@@ -21,7 +21,18 @@ requirements.txt
|
|||||||
|
|
||||||
### `POST /detectar`
|
### `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
|
```json
|
||||||
{ "status": "sucesso", "deteccoes": [{ "box": [x1,y1,x2,y2], "conf": 0.87, "class": 0 }] }
|
{ "status": "sucesso", "deteccoes": [{ "box": [x1,y1,x2,y2], "conf": 0.87, "class": 0 }] }
|
||||||
|
|||||||
@@ -66,6 +66,33 @@ def log_print(msg):
|
|||||||
if len(_training_logs) > 60:
|
if len(_training_logs) > 60:
|
||||||
_training_logs.pop(0)
|
_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):
|
def carregar_modelo_do_s3(ambiente: str):
|
||||||
s3_key = f"modelos/{ambiente}/atual/cerebro.pt"
|
s3_key = f"modelos/{ambiente}/atual/cerebro.pt"
|
||||||
local_path = f"/tmp/cerebro_{ambiente}.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)
|
modelo = carregar_modelo_do_s3(ambiente)
|
||||||
conteudo = await file.read()
|
conteudo = await file.read()
|
||||||
imagem = Image.open(io.BytesIO(conteudo))
|
imagem = Image.open(io.BytesIO(conteudo))
|
||||||
results = modelo(imagem, conf=0.25)
|
results = modelo(imagem, conf=0.25, iou=0.5)
|
||||||
deteccoes = [
|
deteccoes = [
|
||||||
{"box": [round(x, 2) for x in b.xyxy[0].tolist()], "conf": round(float(b.conf), 2), "class": int(b.cls)}
|
{"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
|
for r in results for b in r.boxes
|
||||||
]
|
]
|
||||||
|
deteccoes = _suprimir_boxes_contidas(deteccoes, limiar=0.90)
|
||||||
return {"status": "sucesso", "deteccoes": deteccoes}
|
return {"status": "sucesso", "deteccoes": deteccoes}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_print(f"Erro deteccao: {str(e)}")
|
log_print(f"Erro deteccao: {str(e)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user