Agentes y percepción · Media · 20 min

Detecta objetos: IoU y NMS

Un detector propone 81 cajas para una mesa con seis cosas. Escribe el IoU y la supresión de no máximos que hay bajo cualquier YOLO y quédate con las que valen.

DETECTIONS.taller y DETECTIONS.calle traen las cajas en bruto que YOLOS-tiny propuso para dos fotos nuestras: { label, score, box }, con box = [x1, y1, x2, y2] entre 0 y 1. Hay muchas repetidas. Escribe cuatro funciones:

iou(a, b): intersección entre unión de dos cajas. Si no se tocan, 0.

filterByScore(boxes, min): las cajas con score >= min, en el mismo orden.

nms(boxes, limit): ordena por score de mayor a menor y recorre: una caja se queda si ningún resultado ya aceptado **de la misma etiqueta** la solapa con iou > limit. Devuelve las aceptadas en ese orden.

countByLabel(boxes): { etiqueta: cuántas } con las claves ordenadas alfabéticamente. La vista previa pinta las cajas sobre la foto y deja mover los dos umbrales.

Retos 0/6

  • IoU: mitad solapadas, iguales y separadas
  • El umbral de confianza conserva el orden
  • NMS: tres cajas de una persona y un perro encima se quedan en dos
  • La mesa del taller, con confianza 0,5 e IoU 0,5
  • La calle: dos personas, un perro, un coche, una bici
  • Con las cajas del modelo (tu foto) o la mesa del taller: ordenadas y sin duplicados

function iou(a, b) {
  // área de intersección / (áreaA + áreaB - intersección); 0 si no se tocan
  return 0;
}

function filterByScore(boxes, min) {
  // score >= min, mismo orden
  return boxes;
}

function nms(boxes, limit) {
  // de mayor a menor score; una caja se queda salvo que una aceptada con la misma etiqueta la solape con iou > limit
  return boxes;
}

function countByLabel(boxes) {
  // { etiqueta: cuántas }, claves ordenadas
  return {};
}

console.log(countByLabel(nms(filterByScore(DETECTIONS.taller.boxes, 0.5), 0.5)));
La consola aparece aquí (console.log).

La idea

Un detector no devuelve «un portátil». Devuelve cientos de propuestas, cada una con una etiqueta, una confianza y una caja, y muchas apuntan al mismo objeto. Dos pasos ordenan eso: un umbral de confianza, que tira las dudosas, y la supresión de no máximos (NMS), que de cada grupo de cajas solapadas se queda con la mejor. El IoU (intersección entre unión) es la regla para decir «solapadas».

Lo que medimos: YOLOS-tiny cuantizado (9,7 MB) tardó 711 ms por foto de 1024 × 768 en la CPU del DGX Spark el 18-10-2026, con transformers.js 3.8.1. En la mesa del taller propuso 81 cajas con confianza ≥ 0,05; con umbral 0,5 e IoU 0,5 quedan las que ves en la vista previa. También se equivoca: a las llaves colgadas de la pared las llama «knife», cuenta diez «book» entre cuadernos y carpetas, y ve cuatro «cup» donde hay una taza, un lapicero y un altavoz. Las fotos las generamos con FLUX.1-schnell.

En esta página puedes pasarlo tú: el recuadro «Un modelo en tu navegador» carga YOLOS-tiny con ONNX Runtime Web (24 MB, todo desde este sitio) y en el navegador del DGX Spark tardó unos 730 ms por foto el 19-10-2026. Sus cajas en bruto, de una muestra o de tu propia foto, entran en el ejercicio como una tercera escena.

El umbral de IoU es una decisión de negocio, no un número mágico. Con 0,3 pierdes objetos pegados (dos sillas juntas); con 0,7 te quedas con duplicados. Mira la vista previa con cada valor antes de fijarlo.

Todo esto corre sin GPU y sin nube. Un detector de 10 MB en el navegador o en un mini-PC sirve para contar piezas en una estantería o detectar un hueco en una línea, y la imagen nunca sale del sitio donde se tomó.

Hazlo en tu máquina

Con Python y NumPy, sobre las cajas en bruto de este ejercicio (CC BY 4.0):

curl -O https://vorluxai.com/data/labs/detecciones-taller.json
python3 -c "
import json, numpy as np
boxes = [b for b in json.load(open('detecciones-taller.json'))['boxes'] if b['score'] >= 0.5]
def iou(a, b):
    ix = max(0, min(a[2], b[2]) - max(a[0], b[0])); iy = max(0, min(a[3], b[3]) - max(a[1], b[1]))
    inter = ix * iy; return inter / ((a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter)
kept = []
for b in sorted(boxes, key=lambda b: -b['score']):
    if not any(k['label'] == b['label'] and iou(k['box'], b['box']) > 0.5 for k in kept): kept.append(b)
print(len(boxes), 'cajas ->', len(kept), 'tras NMS:', sorted(k['label'] for k in kept))
"

Comprueba lo que has entendido

1. ¿Por qué un detector propone varias cajas para un mismo objeto?
2. Dos cajas de etiquetas distintas se solapan al 90 %. ¿Qué hace el NMS de este ejercicio?
3. ¿Qué pasa al subir el umbral de IoU de 0,5 a 0,8?
4. El detector llama «knife» a las llaves de la pared. ¿Qué haces en producción?

Para profundizar: la referencia de Hugging Face (en inglés) →

¿Esto en producción, con tus datos? Hablemos 15 minutos →