julio 21, 2021 10:00 am

Jesús

Otra de las aplicaciones útiles de los hashes de imágenes, especialmente aquellos que comparan perceptualmente y no estructuralmente, como dHash, son los motores de búsqueda.

En el presente artículo crearemos un motor de búsqueda de imágenes basado en hashes de imágenes, almacenados en una estructura de datos especial, conocida como VP-Tree

¿Por qué? ¿Qué tiene de especial este tipo de árbol? ¡Sigue leyendo para descubrirlo!

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

  • Cuáles son los ingredientes básicos de un motor de búsqueda.
  • Qué son VP-Trees y cómo usarlos para acelerar el tiempo de respuesta de un motor de búsqueda.
  • Cómo construir un índice de un motor de búsqueda.
  • Cómo llevar a cabo una búsqueda usando dicho índice y un VP-Tree.

¿Preparado? ¡Comencemos!

Los Motores de Búsqueda y la Escalabilidad

La clave para entender las decisiones de implementación tomadas en este artículo radican en esta sección.

En primer lugar, tenemos que repasar cuáles son los elementos básicos de un motor de búsqueda de imágenes:

1

Un descriptor de imágenes o, en términos más sencillos, un mecanismo para convertir imágenes en vectores con mínima pérdida de información

2

Un índice de imágenes

3

Una métrica o función de similitud apropiada para ver qué tan cerca se encuentran dos imágenes con base en su descriptor.

4

Un buscador como tal

¡ATENTO!

Si deseas ahondar más en los componentes de un motor de búsqueda, te recomiendo que leas este artículo.

Parece una lista de componentes bastante razonable, ¿no? 

Sí, indudablemente. Sin embargo, no hay mejor manera de añadir ingentes cantidades de complejidad a un problema aparentemente sencillo que escalándolo, y es ahí cuando crear un motor de búsqueda de imágenes usable, responsivo, se vuelve una tarea mucho más difícil.

El problema de la escalabilidad se reduce a esto:

Mientras más imágenes tienes, más se demora la búsqueda.

No hay manera de escapar a esta realidad; lo único que podemos hacer es mitigarla mediante una serie de decisiones inteligentes, empezando por cómo indexamos y cómo almacenamos las entradas del motor.

Imagina que tenemos este escenario:

  • 500.000 imágenes.
  • Hemos calculado los hashes de las 500.000 imágenes.

Ahora, un usuario nos pasa una imagen como término de búsqueda. El proceso para obtener los resultados sería el siguiente:

  1. 1
    Calculamos el hash de la imagen de entrada.
  2. 2
    Comparamos el hash con cada una de las 500.000 imágenes.
  3. 3
    Mantenemos una lista de las N imágenes más parecidas a la de entrada.
  4. 4
    Retornamos el resultado.

El hecho de que tengamos que comparar cada hash en nuestro índice con el de la imagen de entrada es una mala señal, ya que aún si la comparación tardase 0,00001 segundos, nos tardaríamos 5 segundos en devolver los resultados.

Si nuestro motor de búsqueda es un éxito y decidimos expandirlo con otras 500.000 imágenes, estamos hablando de 10 segundos por búsqueda.

INACEPTABLE.

¿Qué hacemos? Podemos empezar por utilizar estructuras de datos especializadas en la escalabilidad, como los VP-Trees.

Una Breve Introducción a los VP-Trees

Una de las estructuras de datos que nos facilita reducir de manera drástica el tiempo de búsqueda (concretamente, de O(n) a O(log n)) es el VP-Tree, cuyo nombre completo en inglés es Vantage-point Trees y, en español, Árboles de Punto de Ventaja.

Estructura de un VP-Tree. Los rectángulos verdes corresponden a los puntos de ventaja. Fuente: https://everyhue.me/media/3016/vptree.png

Los VP-Trees son árboles métricos, que separan los datos en un espacio métrico, donde se escoge un determinado punto en ese espacio, conocido como “vantage point” o “punto de ventaja”, para particionar los elementos en dos conjuntos:

  1. 1
    Puntos cercanos al punto de ventaja.
  2. 2
    Puntos lejanos al punto de ventaja.

El punto de ventaja es el verde. Aquellos que están fuera del círculo se consideran elementos lejanos, mientras que los que están dentro de la circunferencia se consideran cercanos. Fuente: https://everyhue.me/media/3016/left_right_child.png

Para construir el árbol, aplicamos el procedimiento anterior recursivamente, lo que conduce a una partición de los puntos en conjuntos cada vez más pequeños. El resultado es un árbol donde los nodos vecinos tienen distancias pequeñas entre sí.

¡ATENTO!

Un espacio métrico, en matemáticas, es un conjunto asociado a una métrica. Los puntos o elementos en dicho conjunto están organizados con base a esta métrica, usualmente conocida como función de distancia.


Un ejemplo clásico de un espacio métrico es el espacio tridimensional euclidiano (es decir, el conjunto de puntos del estilo (x, y, z), siendo la métrica de dicho espacio la función de distancia euclidiana).

En nuestro proyecto de Python usaremos una librería de Python que implementa los VP-Trees en un API muy fácil de usar. Más concretamente:

  • Nuestros puntos serán los hashes de las imágenes.
  • La métrica será la distancia Hamming.

CALTECH 101

Muestra de imágenes de CALTECH 101.

El conjunto de datos sobre el que indexaremos nuestro motor de búsqueda, no es otro que el célebre CALTECH 101, el cual consiste de 9.146 imágenes, distribuidas en, evidentemente, 101 categorías entre las que destacan aviones, motocicletas, camiones, cámaras fotográficas e incluso rostros.

Como ya mencioné en el párrafo anterior, la idea del post de hoy es implementar un motor de búsqueda sobre estas imágenes.

Creación del Entorno de Programación con virtualenv

Empecemos ojeando la estructura del proyecto:

Estructura del proyecto

El código fuente se compone de estos tres archivos:

  • datasmarts/utils.py, donde tenemos las funciones auxiliares dhash() y hamming(), para calcular el dHash de una foto y computar la distancia hamming entre dos vectores, respectivamente. Como ya explicamos estas funciones en otro artículo, te recomiendo que lo leas si estás interesado en profundizar en sus detalles.
  • datasmarts/index.py, el cual construye el índice de hashes de cada imagen, y los organiza en un VP-Tree.
  • datasmarts/search.py, que usa el índice y el VP-Tree para obtener los resultados más cercanos a una imagen de búsqueda proporcionada por el usuario.

Debido a que la librería vptree no está disponible en Anaconda, usaremos una herramienta familiar para aislar nuestro ambiente de desarrollo: virtualenv.

Para crear y activar el ambiente, ejecuta estas instrucciones en tu terminal:

Después, tendrás que instalar las siguientes librerías:

Para ello, apóyate en pip, así:

pip install -r requirements.txt

Voilà.

Creando un Motor de Búsqueda con Hashing de Imágenes en OpenCV

Como nuestro motor de búsqueda se fundamenta en los hashes perceptuales (calculados con el algoritmo dHash) de las imágenes, usaremos las funciones auxiliares dhash() y hamming() para, respectivamente, calcular el dHash de una imagen digital, y luego calcular la distancia Hamming entre dos hashes. Ambas funciones están definidas en datasmarts/utils.py:

¡ATENTO!

Cubrí los detalles de estas dos funciones (y mucho más) en este artículo.

El siguiente paso es crear nuestro programa de indexación. Abre datasmarts/index.py e importa las dependencias del script:

Define los parámetros del programa:

  • -i/--images: Ruta al directorio con las imágenes a indexar.
  • -t/--tree: Ruta donde se guardará el VP-Tree.
  • -a/--hashes: Ruta donde se guardará el índice de hashes.

Creamos una lista de las rutas a las imágenes a indexar:

Iteraremos sobre cada ruta, abriendo la imagen en la ubicación indicada por la misma con el fin de calcular su dhash y añadirlo al índice hashes:

El siguiente paso es construir el VP-Tree. Fíjate que tenemos que pasarle dos parámetros al constructor:

  • La lista de puntos (los hashes).
  • La función de distancia o métrica (hamming()).

Por último, serializamos y guardamos los hashes y el VP-Tree. Estos serán útiles en el siguiente programa:

Ahora abre el archivo datasmarts/search.py, que como su nombre indica, contiene la lógica del buscador, e inserta estas líneas para importar las dependencias del programa:

Define los parámetros del programa:

  • -t/--tree: Ruta al VP-Tree construido con index.py.
  • -a/--hashes: Ruta a los hashes construidos con index.py.
  • -q/--query: Ruta a la imagen de consulta.
  • -d/--distance: Máxima distancia Hamming. Imágenes con una distancia mayor serán descartadas (por defecto es 10).

Cargamos el VP-Tree y los hashes:

Leemos la imagen de consulta y la mostramos en pantalla:

Calculamos el dhash de la imagen de consulta:

Usamos el método get_all_in_range() del VP-Tree para traernos los hashes que están a lo sumo 10 unidades (si usamos el valor por defecto de -d/--distance) Hamming de distancia de la imagen de consulta, y ordenamos los resultados de menor a mayor distancia:

Buscamos las imágenes asociadas a los hashes que obtuvimos del árbol, y las mostramos en pantalla, ya que constituyen el resultado del motor de búsqueda:

Para indexar el conjunto de datos, podemos correr este comando:

python datasmarts/index.py -i D:\Data\101_ObjectCategories -t vptree.pickle -a hashes.pickle

Este proceso se demorará en proporción a la cantidad de imágenes. En mi caso, se tomó aproximadamente 30 segundos.

Luego, podemos llevar a cabo una búsqueda así:

python datasmarts/search.py -t vptree.pickle -a hashes.pickle -q queries/image_0009.jpg

Puedes ver el resultado en este video:

Genial, ¿no crees? Aunque algunas de las imágenes resultantes no parecen contener el mismo objeto de la imagen de consulta. Es posible que tengamos que probar con distintos valores de -d/--distance hasta obtener resultados más convincentes.

Resumen

Hoy aprendimos que los motores de búsqueda de imágenes constituyen el contexto ideal para usar hashes perceptuales de imágenes, ya que como vimos en la introducción de este artículo, uno de los ingredientes principales de un buscador es un descriptor de imágenes… qué es lo que, al final del día, es un hash.

Sin embargo, por muy bueno que sea dHash creando representaciones vectoriales de las imágenes indexadas por nuestro buscador, el mayor desafío tiene que ver con la escalabilidad, puesto que si diseñamos nuestro sistema de manera tal que retorne resultados lo más rápido posible, su usabilidad se verá severamente afectada.

Afortunadamente, podemos incrementar la velocidad de respuesta usando estructuras de datos especialmente diseñadas para la escalabilidad, como los VP-Trees, un árbol que almacena los puntos (en este caso, hashes) en ubicaciones estratégicas basadas en puntos de ventaja, de forma que los nodos vecinos tienen distancias pequeñas entre sí.

El mayor beneficio de usar un VP-Tree como espina dorsal de nuestro motor de búsqueda, es que pasamos de una complejidad lineal (O(n)) a una logarítmica (O(log n)). Cool, ¿no?

¿Por qué no te descargas el código para seguir experimentando, y ver si puedes conseguir mejorar los resultados del buscador?

¡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.

Paso 2/2: Celebra tu NUEVO EMPLEO en Machine Learning ?

A %d blogueros les gusta esto: