Florence-2 es un modelo desarrollado por Microsoft, que analiza imágenes de distintas maneras: puede describirlas, sugerir áreas de interés, detectar objetos, vincular palabras o frases a objetos de la imagen, segmentar a nivel de píxeles o extraer el texto de una imagen. Esto nos puede servir para crear motores de búsqueda de imágenes, software de accesibilidad o analizar datos, por ejemplo.
Es un modelo bastante potente y versátil, y lo mejor es que lo podemos ejecutar en nuestro ordenador, incluso sin GPU. En este tutorial vamos a usar el modelo para obtener las descripciones del contenido (captions) de las imágenes que tenemos en una carpeta.
El código y una carpeta con imágenes de ejemplo están disponibles en nuestro Github.
- Cómo configurar y usar un modelo de visión por ordenador de última generación.
- Cómo procesar múltiples imágenes automáticamente y obtener su descripción.
- Cómo guardar los resultados en varios formatos, y aprovecharlos en futuros proyectos.
- Editor de código: Un editor de texto o IDE para Python. Por aquí nos gusta Visual Studio Code, pero puedes usar cualquier editor con el que te sientas cómoda.
- Familiaridad con entornos virtuales: Necesitaremos crear y activar un entorno virtual en Python (sea con
venv
oconda
). - Conocimientos básicos de Python: Variables, funciones, bucles y manejo de archivos en Python.
- Espacio en disco: Necesitaremos unos 2GB de espacio en disco, para instalar las dependencias y el modelo.
- Conexión a Internet: Necesitamos una conexión a Internet estable para descargar las librerías y el modelo.
Os recomiendo crear un entorno virtual (con conda
o venv
) para no tener conflictos entre las librerías. En cuanto a la versión de Python, la 3.11 va de maravilla, que es la que nos sugiere Visual Studio Code.
A continuación, hay que instalar las siguientes librerías
- PIL (Python Imaging Library): para manipular y procesar imágenes.
- Transformers: para acceder a modelos de lenguaje y visión avanzados.
- Torch (PyTorch): para todos los temas de ML.
- Einops: para simplificar las operaciones con tensores.
- Timm (PyTorch Image Models): una colección de modelos preentrenados de visión por ordenador.
pip install pillow 'transformers[torch]' einops timm
Cargamos las librerías, detectamos si el ordenador tiene GPU o no, y según eso usamos un tipo de datos u otro (16 o 32 bits), definimos el nombre del modelo, la carpeta donde están las imágenes, el nombre del fichero de salida y la tarea que queremos que haga el modelo, en este caso <CAPTION>
, vamos, que nos describa la imagen. Dejo comentadas dos opciones más, que nos devolverían descripciones cada vez más detalladas. Aquí ya es cuestión de probar y usar la que mejor nos sirva.
import glob
import json
import os
import torch
from PIL import Image
from transformers import AutoProcessor, AutoModelForCausalLM
from transformers.dynamic_module_utils import get_imports
from unittest.mock import patch
device = "cuda:0" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
model_id = "microsoft/Florence-2-large"
image_folder = "demo/"
output_file = "demo.jsonl"
task = "<CAPTION>"
# task = "<DETAILED_CAPTION>"
# task = "<MORE_DETAILED_CAPTION>"
Bueno, este paso es un poco movida: resulta que el modelo se va a quejar porque no encuentra una librería, pero resulta que esa librería no la usa para nada y además solo funciona con GPU, así que hay que hacer una chapuza un truco para ignorar esa librería.
Básicamente, con el siguiente bloque de código, estamos diciendo que cuando se cargue modeling_florence2.py
, pase totalmente del requisito de flash_attn
y que, en otro caso, respete los imports.
def fixed_get_imports(filename):
if not str(filename).endswith("modeling_florence2.py"):
return get_imports(filename)
imports = get_imports(filename)
imports.remove("flash_attn")
return imports
def get_model_processor(model_id, device, torch_dtype):
with patch("transformers.dynamic_module_utils.get_imports", fixed_get_imports):
model = AutoModelForCausalLM.from_pretrained(
model_id,
attn_implementation="sdpa",
torch_dtype=torch_dtype,
trust_remote_code=True,
).to(device)
processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
return model, processor
Esta función devuelve la lista de ficheros de imagen que haya en la carpeta que le indiquemos. Eso sí, comprobamos antes que la carpeta existe.
def get_image_files(folder):
if not os.path.exists(folder):
print(f"Error: La carpeta '{folder}' no existe.")
return []
image_files = (
glob.glob(os.path.join(folder, "*.jpg"))
+ glob.glob(os.path.join(folder, "*.jpeg"))
+ glob.glob(os.path.join(folder, "*.png"))
)
return image_files
Aquí está el meollo del programa. Antes de nada, cargamos el modelo. La primera vez tardará un ratillo, ya que se lo baja, y ocupa un giga y pico. El siguiente paso es sacar la lista de imágenes y finalmente, para cada imagen, pasarla por el modelo. El modelo toma la imagen (convertida en un tensor) y un prompt (convertido en tokens) como entrada y devuelve una secuencia de tokens de salida que se convierten en texto, obteniéndose así la descripción (caption) de la imagen.
Entrando en detalle, estos son los pasos que se dan en el bucle principal:
- Se carga la imagen.
- La imagen y el prompt se pasan a un formato que la red neuronal es capaz de interpretar. El prompt lo convierte en la siguiente lista de tokens (valores numéricos que representan a palabras o partes de palabras)
[0, 2264, 473, 5, 2274, 6190, 116, 2]
, y la imagen, en un tensor (array multidimensional) de 3 canales (RGB) y 768 x 768 valores. - La imagen y el prompt, transformados en el paso anterior, se presentan a la red neuronal y obtenemos una serie de tokens de vuelta, algo de este palo:
[[2, 0, 250, 3034, 5271, 2828, 15, 5, 1255, 220, 7, 10, 8875, 64, 4, 2]]
- Los tokens se convierten en texto:
</s><s>A computer monitor sitting on the ground next to a trash can.</s>
- Y el texto finalmente se convierte en una linea de JSON:
{'<CAPTION>': 'A computer monitor sitting on the ground next to a trash can.'}
- Dejamos listo el JSON para grabarlo posteriormente:
{'text': 'A computer monitor sitting on the ground next to a trash can.', 'file_name': '753534820828741632_0.jpg'}
model, processor = get_model_processor(model_id, device, torch_dtype)
image_files = get_image_files(image_folder)
results = []
for index, image_file in enumerate(image_files, 1):
# 1
image = Image.open(image_file)
# 2
inputs = processor(text=task, images=image, return_tensors="pt").to(
device, torch_dtype
)
# 3
generated_ids = model.generate(
input_ids=inputs["input_ids"],
pixel_values=inputs["pixel_values"],
max_new_tokens=1024,
num_beams=3,
do_sample=False,
early_stopping=False,
)
# 4
gen_text = processor.batch_decode(generated_ids, skip_special_tokens=False)[0]
# 5
parsed_answer = processor.post_process_generation(
gen_text, task=f"{task}", image_size=(image.width, image.height)
)
# 6
value = parsed_answer.pop(task)
parsed_answer["text"] = value
parsed_answer["file_name"] = os.path.basename(image_file)
print(
f"{index}/{len(image_files)} {parsed_answer['file_name']}: {parsed_answer['text']}"
)
results.append(parsed_answer)
Vamos a utilizar el formato JSONL (JSON Lines), que es un formato de texto para almacenar datos estructurados donde cada línea es un objeto JSON válido. Combina la estructuración del JSON con la facilidad de procesar un fichero línea por línea, de manera independiente.
with open(output_file, "w") as jsonl_file:
for line in results:
jsonl_file.write(json.dumps(line) + "\n")
Tras ejecutar el código con las imágenes de ejemplo, habremos obtenido el siguiente fichero JSONL.
{"text": "A computer monitor sitting on the ground next to a trash can.", "file_name": "753534820828741632_0.jpg"}
{"text": "A trash can sitting on the side of a street.", "file_name": "752079462820200448_0.jpg"}
{"text": "A television sitting on the ground next to a green trash can.", "file_name": "754066664700641280_0.jpg"}
{"text": "A portable air conditioner sitting on the ground next to a wall.", "file_name": "756467765369520128_0.jpg"}
{"text": "A metal box sitting on the ground in the woods.", "file_name": "754617998299512832_0.jpg"}
A portable air conditioner sitting on the ground next to a wall. | A computer monitor sitting on the ground next to a trash can. |
La idea que tengo es usar estas descripciones en otros sitios y, como cada modelo o workflow te las pide en un formato distinto, he desarrollado un conversor la mar de sencillo que a partir del JSONL genera un JSON, un CSV y una lista de ficheros de texto con el mismo nombre que las imágenes pero con extensión txt.
import csv
import json
import os
input_file = "spup01.jsonl"
base_name = os.path.splitext(input_file)[0]
json_output_file = f"{base_name}.json"
csv_output_file = f"{base_name}.csv"
data = []
with open(input_file, "r") as f:
for line in f:
data.append(json.loads(line))
with open(json_output_file, "w") as f:
json.dump(data, f, indent=4)
with open(csv_output_file, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["text", "file_name"])
writer.writeheader()
writer.writerows(data)
for entry in data:
text_content = entry["text"]
file_name = entry["file_name"].replace(".jpg", ".txt")
with open(file_name, "w") as f:
f.write(text_content)
Algunas ideas para continuar el proyecto:
- Probar con los prompts
<DETAILED_CAPTION>
y<MORE_DETAILED_CAPTION>
para ver cómo varia el detalle de la descripción. Estos prompts le indican al modelo que genere descripciones más detalladas de las imágenes, lo que puede ser útil para un análisis más profundo. - Tratar las imágenes por grupos, en vez de de una en una como ahora. En determinado casos, esto puede hacer que el procesado sea más rápido.
- Modificar el código para que el prompt, la carpeta de entrada y el fichero de salida se puedan configurar desde la línea de comando. Esto nos permite usar el script sin tener que modificar el código fuente.
- Modificar el código para hacerlo más robusto: por ejemplo, comprobando que la carpeta de entrada exista, que las imágenes se puedan leer...
- Adaptar el código para que funcione también en una GPU.
Paper: https://arxiv.org/abs/2311.06242
Página oficial: https://huggingface.co/microsoft/Florence-2-large
Código del tutorial: https://github.com/BothRocks/SPUP-01