Plan y arquitectura de CBIR para recuperación de imágenes

Plan y arquitectura de CBIR para recuperación de imágenes

Durante décadas, la gestión de activos digitales se ha basado en una base frágil: etiquetado manual. Buscamos imágenes utilizando palabras clave, nombres de archivo y metadatos. Este enfoque es fundamentalmente no escalable, subjetivo y falla por completo cuando los metadatos están ausentes o son incorrectos. A medida que los datos empresariales crecen con información visual no estructurada (desde contenido generado por el usuario hasta fotos de productos e imágenes satelitales), este paradigma basado en texto colapsa.

Recuperación de imágenes basada en contenido (CBIR) soluciona esto. En lugar de buscar metadatos sobre una imagen, CBIR busca el contenido visual de la propia imagen. Te permite "encontrar más imágenes que se parezcan a esta."

Implementar un sistema robusto de CBIR ya no es un problema de investigación; es un componente fundamental de los servicios de ingeniería de IA para empresas. Permite nuevas experiencias de usuario (búsqueda visual), automatiza la moderación (encontrar contenido visualmente similar dañino), facilita la deduplicación y potencia los motores de recomendación.

Este artículo es un manual técnico para directores de tecnología (CTOs) y ingenieros senior, encargados de desarrollar un sistema CBIR (Búsqueda de Imagen basada en Contenido) escalable y de alto rendimiento. Pasaremos de diagramas de alto nivel y nos centraremos en las decisiones arquitectónicas, los componentes centrales y el código de implementación necesarios para construirlo.

Servicios de Ingeniería de LLM y IA

Ofrecemos una completa gama de soluciones impulsadas por IA, que incluyen IA generativa, visión artificial, aprendizaje automático, procesamiento del lenguaje natural y automatización basada en IA.

Learn more

Una tubería de tres fases

En esencia, un sistema de CBIR (Búsqueda por Imagen) es una línea de procesamiento de datos que transforma píxeles no estructurados en un índice estructurado y de búsqueda de vectores de alta dimensión.

Todo el sistema se puede descomponer en tres fases distintas:

  1. Fase 1: Extracción de Características: Un modelo de Deep Learning (típicamente una CNN) "analiza" una imagen y resume su contenido visual en una lista numérica llamada un vector de características o incrustación. Este vector, que puede tener hasta 2048 dimensiones, es la "huella visual" de la imagen.
  2. Fase 2: Indexación: Almacenar millones o miles de millones de estos vectores de alta dimensionalidad no es tarea de una base de datos SQL tradicional. Utilizamos una base de datos especializada de Vectores que utiliza algoritmos de "Vecino Más Cercano Aproximado" (ANN) para crear un índice de búsqueda.
  3. Fase 3: Consulta y Recuperación: Cuando un usuario proporciona una imagen de consulta, ésta pasa por el mismo extractor de características (Fase 1). Su vector resultante se utiliza para buscar en la Base de Datos de Vectores (Fase 2), que devuelve los IDs de los vectores "más cercanos" (es decir, las imágenes visualmente más similares) en milisegundos.

Analicemos la implementación de cada fase.

Fase 1: Extracción de características con aprendizaje profundo

El objetivo es convertir una imagen $I$ en un vector $v \in \mathbb{R}^D$ de manera que imágenes visualmente similares como $I_1$ e $I_2$ produzcan vectores $v_1$ y $v_2$ que sean "similares" en distancia euclidiana ($L_2$).

Intentar utilizar valores de píxeles brutos es inútil debido a la curse of dimensionality. Una simple imagen de 224x224x3 es un vector de 150,528 dimensiones, donde la mayor parte de los datos son ruido espacialmente correlacionado.

La Herramienta: Redes Neuronales Convolucionales (CNNs)

Utilizamos aprendizaje por transferencia con una CNN pre-entrenada (por ejemplo, ResNet-50, EfficientNet, VGG-16) que fue entrenada en un conjunto de datos masivo como ImageNet. Estos modelos ya han aprendido una rica jerarquía de características visuales: desde bordes y texturas simples en las primeras capas hasta partes de objetos complejas en las capas más profundas.

Nos no importa la clasificación final del modelo (p. ej., "gato", "perro"). Nos importa la capa de "clasificación previa", que representa el resumen interno del modelo de la imagen. Lo logramos al eliminar la última capa totalmente conectada (de clasificación) y utilizando la salida de la capa anterior (a menudo una capa de Global Average Pooling) como nuestro vector de características.

Implementación: Modelo Extractor de Características (Python / TensorFlow)

Aquí se explica cómo construir un extractor de características utilizando un ResNet-50 pre-entrenado con TensorFlow/Keras. Este modelo recibirá una imagen de 224x224x3 y producirá un vector de características de 2048 dimensiones.

import tensorflow as tf
from tensorflow.keras.applications import resnet50
from tensorflow.keras.models import Model
from tensorflow.keras.layers import GlobalAveragePooling2D, Input
from tensorflow.keras.preprocessing import image
import numpy as np

def get_feature_extractor(model_input_shape=(224, 224, 3)):
    """
    Creates a feature extractor model from a pre-trained ResNet-50.
    """
    # Load ResNet-50 pre-trained on ImageNet
    # `include_top=False` strips the final classification layer
    base_model = resnet50.ResNet50(weights='imagenet', 
                                   include_top=False, 
                                   input_shape=model_input_shape)
    
    # We don't need to re-train the model
    base_model.trainable = False

    # Define the new model
    # Use the base_model's input
    model_input = base_model.input
    
    # Add a Global Average Pooling layer to flatten the features
    # This converts the (7, 7, 2048) output of ResNet's conv_base
    # into a (1, 2048) vector.
    x = base_model.output
    x = GlobalAveragePooling2D()(x)
    
    # Create the new model mapping input image to 2048-dim vector
    feature_extractor_model = Model(inputs=model_input, outputs=x)
    
    return feature_extractor_model

def extract_features(img_path, model):
    """
    Loads an image, preprocesses it, and extracts its feature vector.
    """
    # Load image, ensuring target size matches model input
    img = image.load_img(img_path, target_size=(224, 224))
    
    # Convert image to array
    img_array = image.img_to_array(img)
    
    # Expand dimensions to create a "batch" of 1
    img_batch = np.expand_dims(img_array, axis=0)
    
    # Pre-process the image for ResNet-50
    # (e.g., mean subtraction, channel re-ordering)
    processed_img = resnet50.preprocess_input(img_batch)
    
    # Get the feature vector (prediction)
    features = model.predict(processed_img)
    
    # Normalize the vector (L2 normalization)
    # This is critical for many vector search algorithms
    features_normalized = features / np.linalg.norm(features)
    
    return features_normalized.flatten()

# --- Example Usage ---
#
# 1. Initialize the model (do this once)
# extractor = get_feature_extractor()
#
# 2. Extract features for an image
# vector = extract_features('path/to/my_image.jpg', extractor)
# print(f"Extracted vector of shape: {vector.shape}") 
# Output: Extracted vector of shape: (2048,)

Consideraciones para el CTO: Este paso de extracción de características es extremadamente paralelo. Es una función sin estado. Esto lo convierte en un candidato perfecto para la escalabilidad horizontal utilizando funciones sin servidor (como AWS Lambda o Google Cloud Run) tanto para la inicialización de imágenes existentes como para el procesamiento de nuevas subidas.

Fase 2: Indexación con una base de datos vectorial

Ahora tiene una función para generar vectores de dimensión 2048. Su tarea es procesar toda su biblioteca (por ejemplo) de 50 millones de imágenes, generando 50 millones de vectores.

El problema: ¿Cómo se pueden buscar?

Una búsqueda exhaustiva requiere calcular la distancia $L_2$ entre tu vector de consulta $q$ y todos los 50 millones de vectores en la base de datos. Esto es una operación de $O(N \cdot D)$ – mucho demasiado lenta para una aplicación en tiempo real.

La Solución: Vecino más cercano aproximado (ANN)

Debemos sacrificar una precisión perfecta por una velocidad enorme. Utilizamos un algoritmo de Vecinos más cercanos más aproximados (ANN).10 vectores más cercanos, garantiza que probablemente los 10 vectores más cercanos, o quizás 9 de ellos más uno que es el 11º más cercano. Para la búsqueda de imágenes, esta compensación es casi siempre aceptable.

Los algoritmos ANN preprocesan los vectores en una estructura de datos inteligente (un índice) que permite búsquedas en tiempo sublineal, a menudo $O(\log N)$.

Algoritmo clave: HNSW (Mundos Pequeños Navegables Jerárquicos)

HNSW es el estándar de la industria. Construye un grafo de múltiples capas. Las capas superiores contienen enlaces "de largo alcance" entre vectores distantes, y las capas inferiores contienen enlaces detallados entre vecinos cercanos. Una búsqueda comienza en la capa superior, "navega" el grafo para llegar al "vecindario" correcto, y luego realiza una búsqueda detallada en la capa inferior. Es increíblemente rápido y eficiente en memoria.

Servicios de Ingeniería de Productos

Trabaje con nuestros gestores de proyectos, ingenieros de software y testers de calidad internos para desarrollar su nuevo producto de software personalizado o para apoyar su flujo de trabajo actual, siguiendo metodologías Agile, DevOps y Lean.

Build with 4Geeks

Opciones de tecnología: La base de datos vectorial

Esta es su decisión arquitectónica más importante.

  1. Bibliotecas (Autoadministradas):
    • Faiss (Búsqueda de similitud de Facebook AI): Una biblioteca increíblemente rápida, escrita en C++/Python. No es una "base de datos", sino una herramienta para construir un índice, que usted es responsable de administrar, persistir y envolver en una API. Ideal para tener el máximo control y rendimiento.
    • ScaNN (Google): Otra biblioteca conocida por su rendimiento de última generación, especialmente con la cuantización.
  2. Bases de datos (Autohospedadas o gestionadas):
    • Milvus: Una base de datos vectorial de código abierto, nativa en la nube. Maneja el particionado, la replicación, la persistencia de datos y proporciona una API similar a la de una base de datos. Esta es la opción de código abierto "lista para producción".
    • Weaviate, Pinecone, Qdrant: Proveedores de "base de datos vectorial como servicio", completamente gestionados. Manejan toda la infraestructura, la escalabilidad y el MLOps, lo que permite a su equipo centrarse únicamente en la lógica de la aplicación. Esta es una excelente opción para equipos empresariales que necesitan avanzar rápidamente y externalizar la gestión de infraestructuras complejas.

Implementación: Indexación de vectores (Python / Faiss)

Aquí hay un ejemplo simplificado de cómo construir y buscar un índice Faiss en memoria. En un sistema real, guardarías este índice en el disco.

import faiss
import numpy as np

# Let's assume we have 10,000 vectors, each 2048 dimensions
D = 2048       # Vector dimensionality
N = 10000      # Number of vectors in the index

# 1. Generate some random fake vectors (for demonstration)
# In reality, these would come from your feature extractor
np.random.seed(123) 
vectors_to_index = np.random.random((N, D)).astype('float32')
# L2 normalize them, as we did with the extractor output
faiss.normalize_L2(vectors_to_index)

# 2. Build the Faiss Index (using HNSW)
# We choose an HNSW (Hierarchical Navigable Small Worlds) index
# `faiss.IndexHNSWFlat` stores the full vectors (high accuracy, high RAM)
# M = 32 is the number of neighbors per node in the graph
index = faiss.IndexHNSWFlat(D, 32, faiss.METRIC_L2)

# 3. Add vectors to the index
print("Training and adding vectors to the index...")
# For HNSW, "training" is optional or can be done on the fly
# We just add the vectors directly.
index.add(vectors_to_index)
print(f"Index built. Total vectors: {index.ntotal}")

# --- This index is now ready to be searched ---
# (See Phase 3 for the search part)

# --- Persisting the Index ---
# faiss.write_index(index, "my_image_index.faiss")
# ... later ...
# index = faiss.read_index("my_image_index.faiss")

Fase 3: El flujo de consulta y recuperación

Con nuestro extractor de características implementado (Fase 1) y nuestro índice de vectores poblado (Fase 2), ahora podemos implementar la búsqueda en tiempo real.

El proceso es sencillo:

  1. Un usuario sube una imagen de consulta $I_q$.
  2. $I_q$ se pasa a través de la misma función extract_features para generar un vector de consulta $q$.Crucialmente, se deben aplicar las mismas etapas de preprocesamiento y normalización.
  3. El vector $q$ se envía a la Base de Datos Vectorial (o al índice Faiss).
  4. La base de datos realiza una búsqueda ANN para los "k vectores más cercanos" (p. ej., k=20).
  5. La base de datos devuelve una lista de IDs de vectores (que corresponden a tus IDs internos image_id), así como sus distancias.
  6. Tu servidor de aplicaciones busca estos image_id en una base de datos estándar (p. ej., PostgreSQL) para obtener metadatos como la URL de la imagen, el título, etc., y los devuelve al usuario.

Implementación: Punto final de búsqueda (Python / Faiss + Flask)

Este código combina todas las piezas en una API de búsqueda mínima y funcional. Asume que los índices y extensores de los ejemplos anteriores están cargados en memoria.

from flask import Flask, request, jsonify
# Assume previous functions `get_feature_extractor` and 
# `extract_features` are defined here.
# Assume the `index` from Phase 2 is loaded.

app = Flask(__name__)

# --- Load models on startup ---
print("Loading feature extractor model...")
FEATURE_EXTRACTOR = get_feature_extractor()
print("Model loaded.")

# In a real app, load the pre-built Faiss index
# index = faiss.read_index("my_image_index.faiss")
# For this example, we'll use the 'index' from the Phase 2 code.

# Create a mapping from index-position (0, 1, 2...) to your
# actual database image ID (e.g., 'img_abc_123.jpg')
# For this demo, we'll just map 0 -> 'image_0.jpg'
image_id_map = [f"image_{i}.jpg" for i in range(N)]

@app.route("/search/visual", methods=["POST"])
def visual_search():
    if 'file' not in request.files:
        return "No file part", 400
    
    file = request.files['file']
    if file.filename == '':
        return "No selected file", 400

    try:
        # 1. Save file temporarily (in real app, use in-memory stream)
        img_path = "temp_query.jpg"
        file.save(img_path)

        # 2. Phase 1: Extract features from query image
        query_vector = extract_features(img_path, FEATURE_EXTRACTOR)
        
        # Reshape for Faiss search (batch of 1)
        query_vector = np.expand_dims(query_vector, axis=0)

        # 3. Phase 3: Search the index
        K = 10 # We want the top 10 results
        
        # D = distances, I = indices (plural of index)
        distances, indices = index.search(query_vector, K)
        
        # 4. Format and return results
        results = []
        for i, dist in zip(indices[0], distances[0]):
            # Map Faiss index 'i' back to our real image ID
            image_id = image_id_map[i]
            results.append({
                "image_id": image_id,
                "similarity_score": 1.0 - dist # Convert L2 dist to a 0-1 score
            })

        return jsonify(results)

    except Exception as e:
        return str(e), 500

# if __name__ == "__main__":
#    app.run(debug=True)

Consideraciones Arquitectónicas y de Rendimiento Clave

Crear un prototipo es fácil. Crear un sistema de nivel empresarial requiere centrarse en estas consideraciones clave.

1. El triángulo Recall-Latencia-Costo

Este es el principal desafío de ANN.

  • Recuerda: "¿Qué porcentaje de los resultados verdaderos¿Cuáles fueron los resultados de los K principales que encontramos? Un 95% es excelente, un 60% es malo.
  • Latencia: ¿Qué tan rápido es la búsqueda? (Un objetivo común es que la latencia (p99) sea inferior a 100 ms).
  • Costo: ¿Cuánta memoria RAM y CPU requiere el índice?

En HNSW, esto se controla mediante parámetros como efConstruction (calidad en tiempo de construcción) y efSearch (calidad en tiempo de consulta). Los valores más altos aumentan la precisión y la latencia. Para las bases de datos administradas, esto a menudo se presenta como un simple control deslizante de "precisión" frente a "velocidad". Su tarea es realizar pruebas y encontrar la configuración más económica que cumpla con los requisitos de precisión y latencia de su producto.

2. Indexing: El problema del "Inicio Frío" frente al "Delta"

Tiene dos cargas de trabajo de indexación:

  • El proceso de "Backfill" (Inicio en frío): Una tarea masiva para procesar todas las imágenes existentes. Esto debe ejecutarse en un entorno de cómputo escalable y predecible (por ejemplo, Spark, AWS Batch).
  • El proceso "Delta" (Indexación en tiempo real): Un flujo de datos en tiempo real (por ejemplo, a través de Kafka, SQS) que procesa las nuevas subidas de imágenes de forma individual. Esto debe ser rápido para garantizar que el nuevo contenido esté inmediatamente disponible.

Una arquitectura común utiliza una "arquitectura lambda" (que no debe confundirse con AWS Lambda), donde tienes un índice principal grande, estático y optimizado, y un índice delta pequeño, dinámico y menos optimizado. Se envían consultas a ambos, y los resultados se combinan. El índice delta se fusiona periódicamente con el índice principal.página principaly una pequeña, dinámica y menos optimizadaíndice delta. Se envían consultas a ambos, y los resultados se combinan. El índice delta se fusiona periódicamente con el índice principal.

Servicios de Ingeniería de Productos

Trabaje con nuestros gestores de proyectos, ingenieros de software y probadores de calidad internos para desarrollar su nuevo producto de software personalizado o para apoyar su flujo de trabajo actual, siguiendo metodologías Agile, DevOps y Lean.

Build with 4Geeks

3. Dimensionalidad vectorial frente al rendimiento

Un vector de 2048 dimensiones (de ResNet) es muy descriptivo, pero "pesado". Un vector de 512 dimensiones (de un modelo MobileNet más pequeño o un modelo modificado) es "más ligero".

  • Vectores de alta dimensionalidad: Mayor precisión, más memoria RAM, búsqueda más lenta.
  • Vectores de baja dimensionalidad: Menor precisión, menos memoria RAM, búsqueda más rápida.

Si sus vectores de 2048 dimensiones son demasiado exigentes en términos de memoria, no simplemente los recorte. Utilice una técnica de reducción de dimensionalidad como PCA (Análisis de Componentes Principales). Puede entrenar un modelo de PCA en una muestra de 1 millón de vectores para aprender una proyección de 2048 dimensiones a 256 dimensiones. Este vector de 256 dimensiones conservará mucha más información que simplemente tomar los primeros 256 valores. Esta es una etapa de preprocesamiento común antes de la indexación.

Conclusión

Implementar un sistema de recuperación de imágenes basado en contenido es una tarea de ingeniería concreta que permite a una organización pasar de una búsqueda basada en texto a un paradigma moderno, centrado en el contenido. Al separar la arquitectura en tres fases distintas—Extracción, Indexación y Recuperación—cualquier equipo de ingeniería de alto rendimiento puede desarrollar esta capacidad.

Las decisiones clave no son si debe utilizar una red neuronal convolucional (CNN) o una base de datos vectorial, sino qué modelo pre-entrenado utilizar (ResNet-50 es una apuesta segura), qué tecnología de base de datos vectorial (Faiss, Milvus, o un servicio gestionado) se adapta a la madurez operativa de su equipo, y cómo ajustará la decisión crucial entre la precisión de la búsqueda y la latencia.

Este sistema es un elemento fundamental para una amplia gama de productos, y dominar su implementación es un factor diferenciador clave para cualquier empresa que invierta en servicios de IA aplicada.

Preguntas frecuentes

¿Qué es la Recuperación de Imágenes Basada en Contenido (CBIR)?

La Recuperación de Imágenes Basada en Contenido, o CBIR, es una tecnología de búsqueda que encuentra imágenes basándose en su contenido visual real, en lugar de depender de palabras clave, nombres de archivo o etiquetas manuales. En lugar de buscar texto sobre una imagen, permite a un usuario proporcionar una imagen de consulta y encontrar otras imágenes que "se parezcan" a ella. Este sistema analiza características visuales como colores, formas y texturas para determinar la similitud.

¿Cómo funciona un sistema de CBIR?

Un sistema de CBIR normalmente funciona en un proceso de tres fases. Primero, en la fase de Extracción de Características , un modelo de aprendizaje profundo (como una CNN) analiza una imagen y convierte su contenido visual en un "huella" numérica única llamada vector de características. Segundo, en la fase de Indexación , estos vectores se almacenan en una base de datos vectorial especializada que utiliza algoritmos como HNSW (Hierarchical Navigable Small Worlds) para una búsqueda eficiente. Finalmente, en la fase de Consulta y Recuperación , la imagen de consulta del usuario se pasa a través del mismo extractor de características y su vector se utiliza para buscar en la base de datos los vectores más cercanos y visualmente similares, devolviendo las imágenes correspondientes.

¿Por qué es importante la recuperación de imágenes basada en contenido?

CBIR (Recuperación de imágenes basada en contenido) resuelve las principales limitaciones de la búsqueda tradicional basada en texto, que es no escalable, subjetiva y falla por completo si falta o es incorrecta la información. A medida que aumenta el volumen de datos visuales no estructurados (como fotos de productos o contenido generado por usuarios), CBIR proporciona una forma escalable y precisa para organizar, buscar y gestionar estos activos. Permite funciones críticas como la búsqueda visual, la moderación automática de contenido, la detección de imágenes duplicadas y los motores de recomendación visual.