octubre 27, 2021 10:00 am

Jesús

Una de las grandes ventajas de OpenCV, especialmente de las versiones superiores a las 3.0, es su interoperabilidad con infinidad de arquitecturas de deep learning ya entrenadas, y enfocadas en aplicaciones concretas de computer vision.

A pesar de que hemos redactado un número importante de tutoriales sobre deep learning en OpenCV, aún no sabemos del todo bien cómo funciona el método cv2.dnn.blobFromImage(), el cual es un requisito fundamental para poder usar redes neuronales en esta librería.

Pues, ¡adivina!

Hoy se acaba el misterio, ya que dedicaremos el resto de este artículo a entender en profundidad lo que hace cv2.dnn.blobFromImage(), y por qué es tan importante.

¡Sigue leyendo!

Al final de este artículo habrás aprendido:

  • Qué es la sustracción de la media en imágenes, y para qué se usa.
  • Qué es el escalado en imágenes y para qué sirve.
  • Cómo usar apropiadamente el método cv2.dnn.blobFromImage() para preparar las imágenes que le pasamos a las redes neuronales mediante OpenCV.

Comencemos ?.

¿Para Qué Sirve cv2.dnn.blobFromImage()?

Este método tan frecuentemente visto en nuestros tutoriales de deep learning con OpenCV, en esencia, lo que hace es lo siguiente:

  • Lleva a cabo sustracción de media en la imagen.
  • Lleva a cabo sustracción de media en la imagen.
  • Opcionalmente, intercambia el orden de los canales de color.

Esta pequeña lista de operaciones que cv2.dnn.bloblFromImage() ejecuta sobre una imagen se conoce como preprocesamiento.

En pocas palabras, cv2.dnn.blobFromImage() lo que hace es poner la imagen en el estado ideal para que la red pueda realizar predicciones sobre ella. De forma más concreta, a esta imagen (o grupo de imágenes procesadas), las denominamos blob.

Sustracción de la Media

¿Recuerdas que muchas veces hemos dicho que la iluminación es uno de los factores que más afecta nuestros algoritmos de computer vision?

Bueno, este hecho se mantiene en deep learning.

¿También recuerdas que el valor o magnitud de un píxel no es más que una medida de la luminosidad o intensidad de color en ese punto? 

¿Y que a medida que el valor de un píxel se aproxima a 255 (en el espacio RGB), mayor es su intensidad, y mientras más se aproxima a 0, menor es su brillo?

Si te acuerdas, fabuloso, estoy orgulloso de ti. Si no, no importa porque ya te refresqué la memoria ?.

Teniendo en cuenta estos datos, la sustracción de la media se utiliza para reducir el rango de valores de los píxeles y, en consecuencia, disminuir el impacto de los cambios de iluminación en las imágenes que componen nuestro dataset.

¿Cómo hacemos esto? Así:

  1. 1
    Calculamos el valor promedio de cada canal de color tomando en cuenta todas las imágenes de nuestro dataset.
  2. 2
    El resultado será una terna (µr, µg, µb), correspondiente a la media de los canales rojo, verde y azul, respectivamente.
  3. 3
    Restamos de cada píxel, el valor de la media correspondiente a cada canal. Es decir, la fórmula para el nuevo valor de un píxel es: Pr = Pr - µr, Pg = Pg - µg, Pb = Pb - µb.

Facilito, ¿no?  

Escalado

En ocasiones queremos escalar los valores de los píxeles a un rango específico (típicamente entre 0 y 1). 

En esas circunstancias, lo que podemos hacer es computar la desviación estándar (σ) del dataset, y dividir el resultado de la fórmula de la sustracción de la media entre σ.

Entonces, ¿cómo quedarían las fórmulas? 

Así, mira:

  • Pr = (Pr - µr) / σ
  • Pg = (Pg - µg) / σ
  • Pb = (Pb - µb) / σ

¿Todo claro hasta acá? Genial, continuemos.

¡ATENTO!

Es importante mencionar que no hay una forma “estándar” de preprocesar imágenes. Dependiendo de la arquitectura, es posible que se aplique únicamente sustracción de media, o que también se escalen los datos.


De hecho, está dentro de las posibilidades que los autores de un determinado paper hayan decidido obviar cualquier tipo de preprocesamiento.


¡Es fundamental que leas las publicaciones de las arquitecturas que vayas a usar en OpenCV para saber qué técnicas de preprocesamiento aplicar!

Creación del Entorno de Desarrollo con Anaconda

Como siempre, empecemos viendo la estructura del proyecto que nos ocupa:

.
├── datasmarts
│   └── blobfromimage.py
├── env.yml
├── images
│   ├── boots.jpeg
│   ├── car.jpg
│   └── polaroid.jpg
└── resources
    ├── bvlc_googlenet.caffemodel
    ├── bvlc_googlenet.prototxt
    └── synset_words.txt

3 directories, 8 files
  • El archivo datasmarts/blobfromimage.py contiene el script que usaremos para demostrar el funcionamiento de la función cv2.dnn.blobFromImage().
  • En la carpeta resources tenemos los archivos necesarios para cargar en OpenCV la red GoogLeNet implementada y entrenada en Caffe.
  • Finalmente, en el directorio images contamos con varias imágenes de ejemplo que usaremos para poner a prueba nuestro programa.

Para crear el ambiente de Anaconda, corre este comando:

conda env create -f opencv-blobfromimage

Esta instrucción habrá creado un ambiente llamado opencv-blobfromimage, configurado de la siguiente manera:

Puedes activar el ambiente así:

conda activate opencv-blobfromimage

Cool, prosigamos.

Demostración de cv2.dnn.blobFromImage() en OpenCV

Abre el archivo datasmarts/blobfromimage.py e inserta estas líneas para importar las dependencias del programa:

Para poder etiquetar correctamente las imágenes de prueba con las predicciones de GoogLeNet, tenemos que cargar en memoria las categorías de ImageNet, el conjunto de datos en el que se entrenó la red:

Por completitud, he aquí una muestra del contenido de synset_words.txt:

El siguiente paso es cargar GoogLeNet, usando la ya familiar función cv2.dnn.readNetFromCaffe(), a la que debemos pasarle los correspondientes archivos prototxt y caffemodel:

Cargamos las rutas de las imágenes de prueba:

Iteramos sobre cada imagen. El primer paso consiste en cargar la imagen en memoria, y redimensionarla para que tenga un tamaño de 224x224x3, que es el volumen que espera GoogLeNet:

En el siguiente extracto procesamos la imagen usando cv2.dnn.blobFromImage, la estrella de este post. He aquí una breve descripción de cada parámetro, en orden de aparición, de izquierda a derecha:

  • image: Imagen a procesar.
  • scalefactor: Factor a utilizar para escalar después de restar la media. En este caso, pasamos 1, lo que implica que no vamos a escalar.
  • size: Dimensiones de la imagen de entrada que la red espera.
  • mean: Tupla o un valor único correspondiente a la media por canal, o global, respectivamente. Si swapRB=True (por defecto), hay que pasar la tupla en orden (R, G, B). Si no, el orden es (B, G, R).
  • swapRB: Si se debe cambiar el orden de los canales R y B. Por defecto es True.

Pasamos el blob (la imagen procesada) por la red para obtener las predicciones:

Extraemos el índice de la clase más probable:

Etiquetamos la imagen con la clase más probable y la mostramos:

Por último, limpiamos y liberamos recursos:

Para ejecutar el programa, corre este comando:

python datasmarts/blobfromimage.py

En la consola veremos la dimensión de cada blob procesado:

Blob #1: (1, 3, 224, 224) Blob #2: (1, 3, 224, 224) Blob #3: (1, 3, 224, 224)

Un blob, entonces, es un “batch” o lote compuesto de una sola imagen (de ahí el 1 en la tupla), de 224x224x3. 

¡ATENTO!

Si quisiéramos procesar varias imágenes de una sola vez, llamaríamos a la función cv2.dnn.blobFromImages(), que recibe exactamente los mismos parámetros que su versión en singular, cv2.dnn.blobFromImage().

En pantalla veremos las imágenes de ejemplo etiquetadas con la categoría que GoogLeNet considera más probable en cada caso:

cowboy boot, 99.99%

sports car, 53,28%

Polaroid camera, 98,49%

Si bien todos los resultados son técnicamente acertados, claramente un Toyota Corolla no es un carro deportivo ?.

Resumen

El día de hoy aprendimos exactamente qué hace la función cv2.dnn.blobFromImage() (y su contraparte para varias imágenes, cv2.dnn.blobFromImages()): preprocesa una imagen de la manera exacta que una red neuronal la espera para poder realizar predicciones sobre ella.

¿De qué clase de preprocesamiento hablamos?

Típicamente de:

  • Sustracción de media.
  • Escalado.
  • Cambio en el orden de los canales de color.

¿Por qué tenemos que hacer esto?

Diferentes arquitecturas, ideadas por diversos equipos de trabajo, requieren distintas condiciones iniciales. Algunas redes funcionan sin ninguna clase de procesamiento previo, mientras que otros modelos son más estables cuando los píxeles de una imagen están en un rango numérico corto.

Es esta variedad la que nos obliga a leer las publicaciones de las redes que queramos usar en OpenCV, de manera que podamos sacarles el máximo provecho posible. El primer paso en estos casos es, precisamente, procesar adecuadamente las imágenes, cosa que se nos facilita enormemente gracias a la existencia de cv2.dnn.blobFromImage().

¿Qué te pareció el post de hoy? Espero que haya sido de utilidad para ti. 

Como de costumbre, puedes descargar el código en el formulario de abajo:

¡Adiós!

Sobre el Autor

Jesús Martínez es el creador de DataSmarts, un lugar para los apasionados por computer vision y machine learning. Cuando no se encuentra bloggeando, jugando con algún algoritmo o trabajando en un proyecto (muy) cool, disfruta escuchar a The Beatles, leer o viajar por carretera.