La detección de objetos en imágenes es una de las aplicaciones más comunes de la Visión por Computador en la actualidad.
Y YOLO (del Inglés You Only Look Once) es, tal vez, el algoritmo y modelo de Deep Learning más usado en la actualidad para realizar este tipo de tarea. Así que en este artículo explico todos los detalles de cómo funciona la versión original de este algoritmo, comenzando con lo esencial (entender qué es la detección de objetos) hasta llegar a los detalles de cómo se entrena la Red Convolucional usada por este algoritmo para generar las predicciones y detectar objetos en una imagen.
Conceptos básicos: clasificación, localización y detección
Para entender qué es y cómo funciona YOLO, tenemos que comenzar entendiendo cada uno de sus elementos básicos.
Así que comenzaremos analizando qué es la detección de objetos y cómo se codifica numéricamente la información de los objetos que queremos detectar. Y para ello es clave entender tres sencillos conceptos de Visión por Computador que están muy relacionados pero que a la vez tienen ligeras diferencias: clasificación, localización y detección.
Clasificación
Supongamos que tenemos un modelo al cual le introducimos una imagen, por ejemplo una imagen que contiene una silla. Lo que nos interesa en la clasificación es que el modelo le asigne una categoría a esa imagen (la categoría «silla»):

Así que lo que busca la clasificación es asignar una categoría a la totalidad de la imagen de entrada.
Localización
Si vamos un poco más allá y hablamos de la localización, la idea es que el modelo acepte la imagen pero a la salida nos entregue dos elementos. El primero de ellos nos tiene que indicar que lo que hay en la imagen es una silla. Pero lo más importante de la localización es que nos tiene que indicar en qué región de la imagen se encuentra ubicada dicha silla:

Visualmente, esto se representa enmarcando el objeto con un recuadro que se conoce como bounding box (una caja delimitadora) que enmarca la ubicación exacta del objeto.
Detección de objetos
La detección de objetos va un paso más allá de la clasificación y de la localización.
En una imagen de entrada podemos tener no solo una categoría o un único tipo de objeto, sino múltiples objetos. Así que la idea de la detección de objetos en imágenes es que, a la salida, el sistema nos tiene que entregar la categoría a la que pertenece cada objeto presente en la imagen y la ubicación exacta de cada uno de ellos:

Es decir, nos va a entregar múltiples bounding boxes, indicando para cada caja qué tipo de objeto contiene.
¡Y esto es precisamente lo que hace YOLO: una tarea de detección de múltiples objetos en imágenes!
Codificación numérica de los bounding boxes
En realidad, las Redes Convolucionales que usamos para detectar objetos no nos van a entregar a la salida una imagen con los recuadros dibujados y la categoría explícita. Lo que realmente entregan es la información codificada como un vector; es decir, un listado de valores numéricos.
Este vector contiene un total de siete elementos fundamentales para codificar un bounding box.
Probabilidad del objeto ($P_r$)
Este primer elemento nos indica la probabilidad de que haya o no un objeto de interés en la imagen. Y por tanto es un valor entre 0 y 1: si es muy cercano a 0, probablemente no hay objetos de interés (por más que haya árboles o césped en el fondo, si buscamos una silla, la probabilidad será baja), mientras que si es cercano a 1, tendremos un objeto de interés.
Así, para un ejemplo ideal con un solo objeto (como en la figura de abajo), codificamos este valor con un 1:

Coordenadas del centro ($B_x, B_y$)
Estas son las coordenadas del centro geométrico del bounding box y están normalizadas con respecto a la imagen total.
Por ejemplo, si enmarcamos la imagen en un eje X horizontal y un eje Y vertical (como se muestra en la figura de abajo), el origen (parte superior izquierda) es la coordenada (0,0) y el extremo inferior derecho es (1,1). Esto asume que el ancho y el alto de la imagen se normalizan a 1.
Y este escalamiento resulta clave pues al usar un detector de objetos en muchas aplicaciones tendremos imágenes de diferentes tamaños y el escalamiento garantiza que las coordenadas del centro del bounding box siempre estarán en el rango de 0 a 1:

Por ejemplo, en la figura de arriba el centro está aproximadamente en las coordenadas $B_x = 0.4$ y $B_y = 0.5$.
Dimensiones de la caja ($B_w, B_h$)
Estos valores representan el ancho y el alto del bounding box y también están normalizados con respecto al tamaño de la imagen original (por las mismas razones que vimos en el caso de anterior), así que sus valores oscilan entre 0 y 1:

Por ejemplo, si la caja ocupa la mitad de la imagen a lo ancho, $B_w$ será 0.5. Si ocupa un 70% del alto, $B_h$ será 0.7.
Categoría del objeto ($C_1, C_2, \dots, C_n$)
Los últimos elementos codifican la categoría del objeto.
Por ejemplo, si buscamos detectar dos tipos de objetos (sillas y mesas), usaremos dos números ($C_1$ y $C_2$). Y si tuviésemos 100 objetos, usaríamos desde $C_1$ hasta $C_{100}$.
Para el caso de dos objetos, si tenemos una silla, se codifica como 1 para $C_1$ y 0 para $C_2$. Y si fuese una mesa, la codificación sería 0 y 1:

¿Y si la imagen no contiene objetos a detectar?
En este caso el vector tendrá la misma cantidad de elementos, pero el componente de probabilidad ($P_r$) será etiquetado con un valor de 0. Este cero indica que no hay un objeto de interés, por lo cual los valores de todos los demás elementos del vector (coordenadas, dimensiones y clases) son irrelevantes y no se realizará un procesamiento posterior sobre ellos:

Operaciones fundamentales en YOLO
Para que YOLO realice la detección correctamente, se basa en un par de operaciones matemáticas y algorítmicas que permiten cuantificar el error y limpiar las predicciones redundantes. Veamos en detalle estas dos operaciones.
Intersección sobre Unión (Intersection over Union, IoU)
Al generar predicciones, los modelos de Deep Learning (como es el caso de YOLO) no son totalmente precisos.
El bounding box ideal (es decir demarcado por un humano) rara vez coincidirá a la perfección con el bounding box predicho por el modelo. Esto lo vemos en la figura de abajo del lado izquierdo, donde el bounding box ideal es de color fucsia y el predicho por el modelo es el amarillo:

Para cuantificar qué tan parecida es la predicción al valor ideal de referencia, se usa la Intersección sobre Unión (IoU).
- Primero, se calcula el área de la intersección entre las dos cajas (la región donde ambas se superponen).
- Luego, se calcula el área de la unión de ambos objetos.
- Finalmente, se divide el área de la intersección entre el área de la unión.
El ejemplo de este cálculo lo vemos en la parte derecha de la figura de arriba.
Así, si la predicción es pésima y los bounding boxes no se tocan, la intersección es 0 y, por tanto, el IoU será exactamente 0 (el peor caso):

Si la predicción es perfecta las dos cajas estarán superpuestas y serán exactamente iguales. En este caso, el área de intersección es igual a la de unión, dando como resultado un IoU de 1:

En la práctica, entre más cercano a 1 sea el IoU mejor será el bounding box predicho por el modelo.
Algoritmo de Supresión de No-Máximos (Non-Max Suppression)
En la práctica, un modelo suele detectar múltiples bounding boxes para un único objeto.
Por ejemplo, en la figura de abajo (a la derecha) tenemos dos sillas a detectar pero el modelo detecta dos bounding boxes para la primera y tres para la segunda:

Para limpiar este exceso de cajas redundantes y dejar solo un bounding box definitivo por cada objeto, se utiliza el algoritmo de Supresión de No Máximos (NMS).
El algoritmo funciona evaluando el vector de predicciones de cada caja, específicamente el valor de probabilidad $P_r$ mencionado anteriormente, y comparando el IoU entre las cajas. Veamos el paso a paso suponiendo que tenemos varias cajas para el mismo tipo de objeto.
Filtrado inicial por umbral de probabilidad
Recordemos que cada bounding box tiene asociada una probabilidad $P_r$. Por ejemplo, una caja en la parte inferior derecha podría tener $0.27$ y otra en la parte superior derecha $0.92$:

Así que en este primer paso se establece un umbral (generalmente de $0.6$) y todos los objetos cuyo bounding box tenga un $P_r < 0.6$ se eliminan. Esto quiere decir que, si analizamos la figura anterior, el bounding box con probabilidad de 0.27 sería eliminado:

Selección del máximo
En este segundo paso, se analizan los bounding boxes restantes y se toma el que tenga la probabilidad más alta (en nuestro ejemplo, el de $0.92$, marcado en color verde en la figura de abajo). Este se establece como una predicción definitiva y no se descartará en pasos posteriores:

Cálculo de similitud
A continuación se calcula la IoU entre el bounding box recién seleccionado y el resto de cajas.
Supongamos que evaluamos tres cajas restantes: una arroja un IoU de $0.8$ (ya que coincide casi perfectamente con la verde), otra de $0.2$ (corresponde a un objeto distinto) y otra de $0$:

Descarte por solapamiento
En este cuarto paso se descartan los bounding boxes cuyo IoU supere un umbral (por ejemplo, $0.5$), pues serán aquellas cajas más similares a la caja de referencia.
En el paso anterior, la caja que arrojó un IoU de $0.8$ supera el umbral, indicando que es altamente probable que corresponda al mismo objeto que la caja verde. Así que en este paso se elimina precisamente esta caja:

Así que en este punto tenemos tres «bounding boxes»: uno que será definitivo (en verde en la figura de arriba) y dos que aún nos resta por analizar (en gris).
Iteración
Y ahora sólo nos resta repetir los pasos anteriores con los bounding boxes que aún no han sido analizados.
Hasta este punto tenemos dos de estas cajas: una con $P_r$ de $0.85$ y $0.78$ (las de color gris en la figura de arriba).
Así que seleccionamos la de mayor probabilidad (es de cir la de $0.85$) y la marcamos como definitiva (en verde a la izquierda en la figura de abajo):

Y ahora calculamos el IoU con el «bounding box» de $0.78$, lo cual nos arroja un valor de 0.9. Este valor alto quiere decir que esta última caja es muy similar a la que acabamos de marcar como definitiva, así que procedemos a eliminarla:

¡Así que en este punto tenemos los «bounding boxes» totalmente depurados!: las dos cajas que aparecen en la figura de arriba serían las obtenidas tras el proceso de detección con YOLO.
Entonces, como acabamos de ver en este ejemplo, el algoritmo de supresión de no-máximos se detiene cuando no hay más cajas por eliminar o preservar.
Además, cuando tenemos múltiples objetos de múltiples categorías (por ejemplo, sillas y sofás), la limpieza mediante Non-Max Suppression se ejecuta de manera independiente por cada tipo de objeto.
Así por ejemplo, en la imagen de abajo se han detectado tres posibles cajas para el objeto «silla» y del lado derecho vemos el resultado de su depuración con non-max suppression:

Y este sería el resultado si aplicamos la depuración a los «bounding boxes» asociados al objeto tipo «sofá»:

Así que, en este caso, el resultado final de la detección sería un «bounding box» para «silla» y uno para «sofá»:

Superposición de objetos
Ocasionalmente, los objetos a detectar pueden ocupar la misma región espacial de la imagen.
Por ejemplo, una mesa y una silla que se superponen (como se observa en la figura de abajo). Dado que un vector simple de 7 elementos solo puede codificar un único objeto (si es silla $C_1=1, C_2=0$, pero si es mesa $C_1=0, C_2=1$ y no pueden coexistir ambas clases simultáneamente en los mismos bits de un solo vector básico), se debe buscar una alternativa:

Para solucionar esto, concatenamos los vectores. Es decir que ahora el vector de predicciones pasa a ser una combinación. La primera mitad del vector codifica el primer objeto y la segunda mitad el segundo.
Por ejemplo, si queremos codificar una mesa que se superpone a una silla, entonces:
- La mitad superior del vector corresponderá al primer objeto (por ejemplo «mesa»). Así que tendrá asociados estos valores: probabilidad
1, coordenadas del centro0.4y0.4, ancho y alto0.6y0.8y categoría1y0(indicando mesa). - Mientras que la mitad inferior del vector corresponderá al segundo objeto («silla») y tendrá los siguientes valores: probabilidad 1, coordenadas del centro de la silla
0.3, ancho y alto de 0.6 y 0.5 y categoría0y1(indicando silla).

Y si, por ejemplo, en la imagen tenemos sólo un objeto codificamos solo la mitad correspondiente del vector de predicción. Por ejemplo, si es una «silla» codificamos sólo la mitad inferior empezando con un 1 de probabilidad y un (0,1) en las clases, mientras que la mitad superior (correspondiente a «mesa») tendría un $0$ en la probabilidad e ignoramos el resto de sus valores:

Ahora sí: !el algoritmo YOLO (You Only Look Once)!
Antes de YOLO, la detección de objetos se basaba en la técnica de ventanas deslizantes. En esta técnica se tomaban bloques de diferentes tamaños y proporciones y se hacía un barrido completo por toda la imagen, múltiples veces, para intentar detectar objetos de diferentes tamaños. Esto requería enormes tiempos de cómputo:
YOLO cambió este paradigma: su nombre proviene de You Only Look Once («solo lo miras una vez»). Esto quiere decir que analiza la imagen por bloques pero en una única pasada (de allí el término «only look once», o sólo se mira una vez) logrando un proceso mucho más rápido y con menos cálculos. Y este sencillo cambio introdujo una revolución en la Visión por Computador y en particular en la detección de objetos.
Así que entendamos cómo se realiza el entrenamiento de un modelo YOLO.
La grilla y el set de entrenamiento
Para entrenar y ejecutar YOLO, se divide la imagen en una grilla (una cuadrícula de sub-imágenes):

Por ejemplo, supongamos que queremos detectar tres tipos de objeto: «silla», «mesa» y «sofá» y supongamos además que como máximo tendremos dos objetos superpuestos por cada cuadrante (o elemento de la grilla).
El entrenamiento de YOLO requiere recolectar y etiquetar manualmente cientos de miles de imágenes marcando el bounding box ideal. Y ese etiquetado implica que el vector de predicciones por cada celda en la grilla tendrá una longitud de $2 \cdot (5 + n)$, donde:
- $2$ se refiere a que asumiremos que como máximo hay dos objetos superpuestos a detectar
- $5$ corresponde al vector ($P_r, B_x, B_y, B_w, B_h$) (probabilidad, coordenadas x e y del centro y ancho y alto del bounding box)
- Y $n$ es el número de diferentes objetos a detectar ($N=3$ en el ejemplo que estamos analizando)

A continuación entran en juego precisamente las celdas de la grilla definida anteriormente. Y acá hay una regla de oro: una celda de la grilla solo es responsable de codificar y detectar un bounding box sí y solo sí dicha celda contiene el punto central exacto de dicho bounding box.
Entendamos esta regla con un ejemplo. Supongamos que tenemos la siguiente imagen etiquetada, la cual contiene dos objetos a detectar («silla» y «mesa») los cuales a su vez están superpuestos:

En la figura anterior, los círculos representan la ubicación del centro de cada bounding box.
Y supongamos que para este ejemplo usaremos una grilla de 4×4 (4 filas y 4 columnas):

Veamos por ejemplo cómo se codificaría el vector de predicciones para la celda de la esquina superior izquierda (resaltada en color gris en la figura de abajo, a la izquierda):

En este caso, a pesar de que la celda contiene fragmentos visuales de la mesa, no contiene el centro geométrico de su bounding box. Por este motivo, esta celda se codifica con ceros en sus campos de probabilidad (lo cual se aprecia en detalle del lado derecho en la figura de arriba).
Veamos ahora un segundo ejemplo: en la figura de abajo la celda de interés (en gris) contiene únicamente el centro geométrico del bounding box de la mesa:

En este caso sólo se codifica la porción superior del vector de predicción, pues solo tenemos el centro del bounding box correspondiente a un objeto (el objeto «mesa»). Así que:
- Asignamos un valor de 1 al primer elemento (correspondiente a la probabilidad $P_r$)
- Luego codificamos las coordenadas $B_x$ y $B_y$ (elementos 2 y 3) correspondientes al centro del bounding box pero normalizadas, es decir calculadas con respecto al tamaño de la celda. En este caso estos valores son de aproximadamente 0.9 y 0.9
- Y al codificar el alto y el ancho del bounding box ($B_w$ y $B_h$, elementos 4 y 5) los calculamos con respecto a la imagen completa, pues dicho bounding box se extiende más allá de la celda que estamos analizando. Y como la caja es más grande que la celda, sus valores pueden ser mayores a 1 (por ejemplo, un ancho de
2.5veces el ancho de la celda y un alto de2.4) - Finalmente, la clase (elementos 6, 7 y 8) se codifica como 0, 1, 0 (que corresponde a la categoría «mesa»)
Y la segunda mitad, que corresponde a un segundo objeto, se codifica con un 0 en el elemento $P_r$, pues recordemos que la celda sólo contiene el centro del objeto «mesa».
Así que el vector resultante se muestra a continuación:

Siguiendo la misma lógica anterior, si ahora analizamos la celda que contiene el centro del bounding box correspondiente a la silla (figura de abajo, a la izquierda), veremos que la mitad de arriba del vector se codifica con los elementos correspondientes al bounding box del objeto «silla», mientras que la mitad de abajo se codifica con un valor 0 en la posición $P_r$ (pues no tenemos más objetos en dicha celda):

Pero tengamos en cuenta que esta codificación es válida sólo si la grilla es de 4×4.
Si por ejemplo ahora la grilla es de 3×3, en la figura de abajo (a la izquierda) podemos ver que los centros de ambos bounding boxes se encuentran en la misma celda (resaltada en color gris):

Así que en este caso codificamos ambas mitades del vector de esta forma: la mitad superior contendrá la información asociada al bounding box del objeto «mesa», mientras que la mitad inferior tendrá los datos del bounding box asociado a «silla» (parte derecha de la figura anterior).
El entrenamiento de YOLO
Al final, este etiquetado manual que acabamos de ver genera un arreglo numérico de múltiples dimensiones de tamaño $S \times S \times (2 \times (5+n))$ donde:
- El tamaño de la grilla es $S \times S$
- El $2$ se refiere al número de objetos que se pueden superponer
- El $5$ corresponde al vector de codificación del bounding box de cada objeto (que contiene exactamente 5 elementos: $P_r$, $B_x$, $B_y$, $B_w$ y $B_h$)
- Y $n$ es el número de categorías o tipos de objeto que queremos detectar (por ejemplo, si queremos detectar «silla» y «mesa», entonces $n=2$)
Entonces, cada dato de entrenamiento contendrá un par imagen – arreglo numérico. Y el objetivo del entrenamiento es que la Red Convolucional de YOLO tome como entradas las imágenes y aprenda a predecir el arreglo numérico correspondiente, con lo cual en últimas habrá aprendido a predecir precisamente los bounding boxes de los objetos a detectar.
Generación de predicciones
Con el modelo entrenado, ya podemos generar predicciones. Es decir, podemos presentar al modelo una imagen de entrada y este debería entregarnos a la salida el arreglo con la información de los objetos detectados en la imagen.
Este es el flujo de inferencia (o predicción) en detalle:
- Tomamos la imagen que queremos procesar y establecemos el tamaño de grilla a usar
- Presentamos la imagen a la Red Convolucional (es decir al modelo YOLO entrenado).
- La red genera el volumen de predicciones en una sola pasada. Y recordemos que estas predicciones contienen toda la información asociada a los objetos detectados (categorías y ubicación dentro de la imagen)
- Generalmente, este proceso crudo arroja múltiples bounding boxes redundantes para un mismo objeto, así que se aplica el algoritmo de Non-Max Suppression visto previamente para limpiar las redundancias.
- Y con esto, a la salida, obtenemos el resultado esperado: la imagen de entrada con un único y preciso bounding box por cada objeto de interés detectado.

Conclusión
Y este es el algoritmo YOLO, concebido en 2016 y que ha evolucionado en múltiples versiones modernas.
Se trata de un enfoque que representa un antes y un después en la Visión por Computador, pues, como acabamos de ver, permite analizar una imagen y realizar la detección de objetos en una sola pasada.
Con el tiempo, este modelo ha venido evolucionando hasta versiones como las implementadas por la empresa Ultralytics, que ha logrado progresivamente mejorar tanto la precisión del modelo al momento de detectar objetos así como los tiempos de inferencia.
Es así como, en la actualidad, los modelos YOLO son uno de los estándares de la industria al momento de implementar aplicaciones de detección de objetos en diferentes entornos y no sólo en imágenes estáticas sino también en secuencias de video.
De hecho, si te interesa te invito a darle una mirada al curso Detección de Objetos con Deep Learning, donde vemos cómo funcionan los modelos YOLO modernos y cómo realizar la afinación de estos modelos YOLO para diferentes retos de detección de objetos en imágenes.
