# Búsqueda de imágenes

# Introducción

La búsqueda de imágenes se ha convertido en una aplicación popular y poderosa, que permite a los usuarios encontrar imágenes similares mediante la coincidencia de características o contenido visual. Con el rápido desarrollo de la visión por computadora y el aprendizaje profundo, esta capacidad se ha mejorado considerablemente.

Esta guía está diseñada para ayudarte a aprovechar las últimas técnicas y herramientas para la búsqueda de imágenes. En esta guía, aprenderás cómo:

  • Crear un conjunto de datos con vectores de incrustación utilizando un conjunto de datos y un modelo públicos
  • Realizar una búsqueda de similitud de imágenes con MyScale, una plataforma potente que agiliza el proceso de búsqueda y proporciona resultados rápidos y precisos

Si estás más interesado en explorar las capacidades de MyScale, siéntete libre de saltar la sección Creación del conjunto de datos y sumergirte directamente en la sección Población de datos en MyScale.

Puedes importar este conjunto de datos en la consola de MyScale siguiendo las instrucciones proporcionadas en la sección Importar datos para el conjunto de datos Búsqueda de imágenes. Una vez importado, puedes pasar directamente a la sección Consulta a MyScale para disfrutar de esta aplicación de muestra.

# Prerrequisitos

Antes de comenzar, debemos instalar el cliente de Python de ClickHouse (opens new window) y la biblioteca datasets de HuggingFace para descargar datos de muestra.

pip install datasets clickhouse-connect

Para seguir los pasos descritos en la sección Creación del conjunto de datos, necesitamos instalar transformers y otras dependencias necesarias.

pip install requests transformers torch tqdm

# Creación del conjunto de datos

# Descarga y procesamiento de datos

Descargamos los datos del conjunto de datos de Unsplash (opens new window) y utilizamos el conjunto de datos Lite.

wget https://unsplash-datasets.s3.amazonaws.com/lite/latest/unsplash-research-dataset-lite-latest.zip
# descomprimimos los archivos descargados en un directorio temporal
unzip unsplash-research-dataset-lite-latest.zip -d tmp

Leemos los datos descargados y los transformamos en marcos de datos de Pandas.

import numpy as np
import pandas as pd
import glob
documents = ['photos', 'conversions']
datasets = {}
for doc in documents:
    files = glob.glob("tmp/" + doc + ".tsv*")
    subsets = []
    for filename in files:
        df = pd.read_csv(filename, sep='\t', header=0)
        subsets.append(df)
    datasets[doc] = pd.concat(subsets, axis=0, ignore_index=True)
df_photos = datasets['photos']
df_conversions = datasets['conversions']

# Generación de incrustaciones de imágenes

Para extraer incrustaciones de imágenes, definimos una función extract_image_features que utiliza el modelo clip-vit-base-patch32 (opens new window) de HuggingFace. Las incrustaciones resultantes son vectores de 512 dimensiones.

import torch
from transformers import CLIPProcessor, CLIPModel
model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
def extract_image_features(image):
    inputs = processor(images=image, return_tensors="pt")
    with torch.no_grad():
        outputs = model.get_image_features(**inputs)
        outputs = outputs / outputs.norm(dim=-1, keepdim=True)
    return outputs.squeeze(0).tolist()

Después de eso, seleccionamos los primeros 1000 ID de fotos del marco de datos df_photos, descargamos las imágenes correspondientes y extraemos sus incrustaciones de imágenes utilizando la función extract_image_features.

from PIL import Image
import requests
from tqdm.auto import tqdm
# seleccionamos los primeros 1000 ID de fotos
photo_ids = df_photos['photo_id'][:1000].tolist()
# creamos un nuevo marco de datos solo con los ID de fotos seleccionados
df_photos = df_photos[df_photos['photo_id'].isin(photo_ids)].reset_index(drop=True)
# mantenemos solo las columnas 'id_photo' y 'photo_image_url' en el marco de datos
df_photos = df_photos[['photo_id', 'photo_image_url']]
# agregamos una nueva columna 'photo_embed' al marco de datos
df_photos['photo_embed'] = None
# descargamos las imágenes y extraemos sus incrustaciones utilizando la función 'extract_image_features'
for i, row in tqdm(df_photos.iterrows(), total=len(df_photos)):
      # construimos una URL para descargar una imagen con un tamaño más pequeño modificando la URL de la imagen
    url = row['photo_image_url'] + "?q=75&fm=jpg&w=200&fit=max"
    try:
        res = requests.get(url, stream=True).raw
        image = Image.open(res)
    except:
        # eliminamos la foto si falla la descarga de la imagen
        photo_ids.remove(row['photo_id'])
        continue
    # extraemos la incrustación de características
    df_photos.at[i, 'photo_embed'] = extract_image_features(image)

# Creación del conjunto de datos

Ahora tenemos dos marcos de datos: uno para la información de las fotos con las incrustaciones y otro para la información de las conversiones.

df_photos = df_photos[df_photos['photo_id'].isin(photo_ids)].reset_index().rename(columns={'index': 'id'})
df_conversions = df_conversions[df_conversions['photo_id'].isin(photo_ids)].reset_index(drop=True)
df_conversions = df_conversions[['photo_id', 'keyword']].reset_index().rename(columns={'index': 'id'})

Finalmente, convertimos los marcos de datos en archivos Parquet y luego procedemos a cargarlos en el repositorio de Hugging Face myscale/unsplash-examples (opens new window) siguiendo los pasos (opens new window). Esto facilita el acceso y el intercambio de los datos.

import pyarrow as pa
import pyarrow.parquet as pq
import numpy as np
# creamos un objeto Table a partir de los datos y el esquema
photos_table = pa.Table.from_pandas(df_photos)
conversion_table = pa.Table.from_pandas(df_conversions)
# escribimos la tabla en un archivo Parquet
pq.write_table(photos_table, 'photos.parquet')
pq.write_table(conversion_table, 'conversions.parquet')

# Población de datos en MyScale

# Carga de datos

Para poblar datos en MyScale, primero cargamos los datos del conjunto de datos de HuggingFace myscale/unsplash-examples (opens new window) creado en la sección anterior. El siguiente fragmento de código muestra cómo cargar los datos y transformarlos en marcos de datos de panda.

Nota: incrustacion_foto es un vector de punto flotante de 512 dimensiones que representa las características de la imagen extraídas de una imagen utilizando el modelo CLIP (opens new window).

from datasets import load_dataset
photos = load_dataset("myscale/unsplash-examples", data_files="photos-all.parquet", split="train")
conversions = load_dataset("myscale/unsplash-examples", data_files="conversions-all.parquet", split="train")
# transformamos los conjuntos de datos en marcos de datos de panda
photo_df = photos.to_pandas()
conversion_df = conversions.to_pandas()
# convertimos incrustacion_foto de np array a lista
photo_df['photo_embed'] = photo_df['photo_embed'].apply(lambda x: x.tolist())

# Creación de tablas

A continuación, creamos tablas en MyScale. Antes de comenzar, deberás obtener la información de host, nombre de usuario y contraseña de tu clúster de MyScale desde la consola de MyScale. El siguiente fragmento de código crea dos tablas, una para la información de las fotos y otra para la información de las conversiones.

import clickhouse_connect
# inicializamos el cliente
client = clickhouse_connect.get_client(host='YOUR_CLUSTER_HOST', port=443, username='YOUR_USERNAME', password='YOUR_CLUSTER_PASSWORD')
# eliminamos la tabla si existe
client.command("DROP TABLE IF EXISTS default.myscale_photos")
client.command("DROP TABLE IF EXISTS default.myscale_conversions")
# creamos la tabla para las fotos
client.command("""
CREATE TABLE default.myscale_photos
(
    id UInt64,
    photo_id String,
    photo_image_url String,
    photo_embed Array(Float32),
    CONSTRAINT vector_len CHECK length(photo_embed) = 512
)
ORDER BY id
""")
# creamos la tabla para las conversiones
client.command("""
CREATE TABLE default.myscale_conversions
(
    id UInt64,
    photo_id String,
    keyword String
)
ORDER BY id
""")

# Carga de datos

Después de crear las tablas, insertamos los datos cargados de los conjuntos de datos en las tablas y creamos un índice vectorial para acelerar las consultas de búsqueda de vectores posteriores. El siguiente fragmento de código muestra cómo insertar datos en las tablas y crear un índice vectorial con la métrica de distancia del coseno.

# cargamos los datos de los conjuntos de datos
client.insert("default.myscale_photos", photo_df.to_records(index=False).tolist(),
              column_names=photo_df.columns.tolist())
client.insert("default.myscale_conversions", conversion_df.to_records(index=False).tolist(),
              column_names=conversion_df.columns.tolist())
# comprobamos la cantidad de datos insertados
print(f"Cantidad de fotos: {client.command('SELECT count(*) FROM default.myscale_photos')}")
print(f"Cantidad de conversiones: {client.command('SELECT count(*) FROM default.myscale_conversions')}")
# creamos un índice vectorial con distancia del coseno
client.command("""
ALTER TABLE default.myscale_photos 
ADD VECTOR INDEX photo_embed_index photo_embed
TYPE MSTG
('metric_type=Cosine')
""")
# comprobamos el estado del índice vectorial, asegurándonos de que el índice vectorial esté listo con el estado 'Built'
get_index_status="SELECT status FROM system.vector_indices WHERE name='photo_embed_index'"
print(f"Estado de construcción del índice: {client.command(get_index_status)}")

# Consulta a MyScale

# Encontrar las K imágenes más similares

Para encontrar las K imágenes más similares utilizando la búsqueda de vectores, sigue estos pasos:

Primero, seleccionemos aleatoriamente una imagen y la mostremos utilizando la función show_image().

import requests
import matplotlib.pyplot as plt
from PIL import Image
from io import BytesIO
# descargamos la imagen con su URL
def download(url):
    response = requests.get(url)
    return Image.open(BytesIO(response.content))
# definimos un método para mostrar una imagen en línea con una URL
def show_image(url, title=None):
    img = download(url)
    fig = plt.figure(figsize=(4, 4))
    plt.imshow(img)
    plt.show()
# mostramos el número de filas en cada tabla
print(f"Cantidad de fotos: {client.command('SELECT count(*) FROM default.myscale_photos')}")
print(f"Cantidad de conversiones: {client.command('SELECT count(*) FROM default.myscale_conversions')}")
# seleccionamos una imagen aleatoria de la tabla como objetivo
random_image = client.query("SELECT * FROM default.myscale_photos ORDER BY rand() LIMIT 1")
assert random_image.row_count == 1
target_image_id = random_image.first_item["photo_id"]
target_image_url = random_image.first_item["photo_image_url"]
target_image_embed = random_image.first_item["photo_embed"]
print("imagen actualmente seleccionada id={}, url={}".format(target_image_id, target_image_url))
# mostramos la imagen objetivo
print("Cargando imagen objetivo...")
show_image(target_image_url)

Una imagen de muestra:

Luego, utilizamos la búsqueda de vectores para identificar los K candidatos que son más similares a la imagen seleccionada y mostramos estos candidatos:

# consultamos la base de datos para encontrar las K imágenes más similares a la imagen dada
top_k = 10
results = client.query(f"""
SELECT photo_id, photo_image_url, distance(photo_embed, {target_image_embed}) as dist
FROM default.myscale_photos
WHERE photo_id != '{target_image_id}'
ORDER BY dist
LIMIT {top_k}
""")
# descargamos las imágenes y las agregamos a una lista
images_url = []
for r in results.named_results():
    # construimos una URL para descargar una imagen con un tamaño más pequeño modificando la URL de la imagen
    url = r['photo_image_url'] + "?q=75&fm=jpg&w=200&fit=max"
    images_url.append(download(url))
# mostramos las imágenes candidatas
print("Cargando imágenes candidatas...")
for row in range(int(top_k / 5)):
    fig, axs = plt.subplots(1, 5, figsize=(20, 4))
    for i, img in enumerate(images_url[row * 5:row * 5 + 5]):
        axs[i % 5].imshow(img)
    plt.show()

Imágenes candidatas similares:

# Análisis de la información de conversión para cada imagen candidata

Después de identificar las K imágenes más similares, puedes utilizar consultas SQL que combinan campos estructurados y campos vectoriales para realizar análisis de la información de conversión para cada candidato.

Para calcular el recuento total de conversiones para cada imagen candidata, puedes utilizar la siguiente consulta SQL para unir los resultados de búsqueda de imágenes con la tabla conversiones:

# mostrar el recuento total de descargas para cada imagen candidata
results = client.query(f"""
SELECT photo_id, count(*) as count
FROM default.myscale_conversions
JOIN (
    SELECT photo_id, distance(photo_embed, {target_image_embed}) as dist
    FROM default.myscale_photos
    ORDER BY dist ASC
    LIMIT {top_k}
    ) AS target_photos
ON default.myscale_conversions.photo_id = target_photos.photo_id
GROUP BY photo_id
ORDER BY count DESC
""")
print("Descargas totales para cada imagen candidata")
for r in results.named_results():
    print("- {}: {}".format(r['photo_id'], r['count']))

Salida de ejemplo:

Descargas totales para cada imagen candidata
- Qgb9urMZ8lw: 1729
- f0OL01IHbCM: 1444
- Bgae-sqbe_g: 313
- XYg2zLjxxa0: 207
- BkW8I1n354I: 184
- 5yFOvJZp7Rg: 63
- sKPPBn6OkJg: 48
- joL0nSbZ-lI: 20
- fzDtQWW8dV4: 8
- DCAERnaj31U: 3

Después de calcular el recuento total de conversiones para cada imagen candidata, puedes identificar la imagen candidata con más descargas y examinar la información detallada de conversión por palabra clave de descarga para esa imagen. Utiliza la siguiente consulta SQL:

# mostrar la imagen candidata más popular y las 5 principales palabras clave de descarga relacionadas
most_popular_candidate = results.first_item['photo_id']
# mostrar la imagen más popular
candidate_url = client.command(f"""
SELECT photo_image_url FROM default.myscale_photos WHERE photo_id = '{most_popular_candidate}'
""")
print("Cargando la imagen candidata más popular...")
show_image(candidate_url)
# encontrar las 5 principales palabras clave de descarga
results = client.query(f"""
SELECT keyword, count(*) as count
FROM default.myscale_conversions
WHERE photo_id='{most_popular_candidate}'
GROUP BY keyword
ORDER BY count DESC
LIMIT 5
""")
print("Palabras clave relacionadas y recuentos de descarga para la imagen más popular")
for r in results.named_results():
    print(f"- {r['keyword']}: {r['count']}") 

La imagen candidata más popular en el top 10:

Salida de ejemplo:

Palabras clave relacionadas y recuentos de descarga para la imagen más popular
- bee: 1615
- bees: 21
- bumblebee: 13
- honey: 13
- honey bee: 12
Last Updated: Sat Apr 13 2024 10:45:55 GMT+0000