antes de comenzar:
Soy desarrollador en Google Cloud. Los pensamientos y opiniones expresados aquí son enteramente míos. El código fuente completo de este artículo, incluidas las actualizaciones futuras, está disponible en este cuaderno bajo la licencia Apache 2.0. Todas las imágenes nuevas de este artículo se generaron con Gemini Nano Banana utilizando la prueba de concepto explorada. Todas las imágenes fuente son de dominio público o de uso gratuito (los enlaces de referencia se proporcionan en el resultado del código). Puedes experimentar con modelos Gemini de forma gratuita en Google AI Studio. Para el acceso API programático, tenga en cuenta que, si bien hay un nivel gratuito disponible para algunos modelos (es decir, puede realizar la detección de objetos), la generación de imágenes es un servicio de pago por uso.
✨ Descripción general
Los modelos tradicionales de visión por computadora suelen estar entrenados para detectar un conjunto fijo de clases de objetos, como "persona", "gato" o "automóvil". Si desea detectar algo específico que no estaba en el conjunto de entrenamiento, como una "ilustración" en la fotografía de un libro, generalmente debe recopilar un conjunto de datos, etiquetarlo manualmente y entrenar un modelo personalizado, lo que puede llevar horas o incluso días.
En esta exploración, probaremos un enfoque diferente utilizando Gemini. Aprovecharemos sus capacidades de comprensión espacial para realizar la detección de objetos de vocabulario abierto. Esto nos permite encontrar objetos basándonos únicamente en una descripción en lenguaje natural, sin ningún tipo de formación.
Una vez que se detectan los objetos visuales, los extraeremos y luego usaremos las capacidades de edición de imágenes de Gemini (específicamente los modelos Nano Banana) para restaurarlos y transformarlos creativamente.
🔥 Desafío
Se trata de datos no estructurados: fotografías de libros, revistas y objetos en la naturaleza. Estas imágenes presentan varias dificultades para la visión por computadora tradicional:
Variedad: Los objetos que queremos encontrar (ilustraciones, grabados y cualquier elemento visual en general) varían enormemente en estilo y contenido. Distorsión: las páginas están curvadas, las fotografías se toman en ángulo y la iluminación es desigual. Ruido: Los libros viejos tienen manchas, granos de papel y texto que se traspasa desde el otro lado.
Nuestro desafío es construir una canalización sólida que pueda detectar estos objetos a pesar de las distorsiones, extraerlos limpiamente y editarlos para que parezcan activos digitales de alta calidad… todo usando indicaciones de texto simples.
🏁 Configuración
🐍 Paquetes de Python
Usaremos los siguientes paquetes:
google-genai: el SDK de Google Gen AI Python nos permite llamar a Gemini con unas pocas líneas de código almohada para la gestión de imágenes matplotlib para la visualización de resultados
También usaremos estos paquetes (dependencias de google-genai):
pydantic para la gestión de datos tenacidad para la gestión de solicitudes pip install –quiet "google-genai>=1.63.0" "pillow>=11.3.0" "matplotlib>=3.10.0"
🔗 API de Géminis
Para utilizar la API de Gemini, tenemos dos opciones principales:
A través de Vertex AI con un proyecto de Google Cloud A través de Google AI Studio con una clave API de Gemini El SDK de Google Gen AI proporciona una interfaz unificada para estas API y podemos usar variables de entorno para la configuración. 🔽
🛠️ Opción 1: API Gemini a través de Vertex AI
Requisitos:
Variables de entorno del SDK de IA de generación:
GOOGLE_GENAI_USE_VERTEXAI="Verdadero" GOOGLE_CLOUD_PROJECT="" GOOGLE_CLOUD_LOCATION=""
💡 Para los modelos de vista previa, la ubicación debe configurarse en global. Para los modelos disponibles con carácter general, podemos elegir la ubicación más cercana entre las ubicaciones de los puntos finales del modelo de Google.
ℹ️ Obtenga más información sobre cómo configurar un proyecto y un entorno de desarrollo.
🛠️ Opción 2: API Gemini a través de Google AI Studio
Requisito:
Variables de entorno del SDK de IA de generación:
GOOGLE_GENAI_USE_VERTEXAI="Falso" GOOGLE_API_KEY=""
ℹ️ Obtenga más información sobre cómo obtener una clave API Gemini de Google AI Studio.
💡 Puede almacenar la configuración de su entorno fuera del código fuente:
Defina las siguientes funciones de detección de entorno. También puede definir su configuración manualmente si es necesario. 🔽 import os import sys from collections.abc import Llamable desde google import genai # Configuración manual (no se modifica si la configuración está definida por el entorno) # @markdown **¿Qué API: Vertex AI o Google AI Studio?** GOOGLE_GENAI_USE_VERTEXAI = Verdadero # @param {tipo: "boolean"} # @markdown **Opción A: proyecto de Google Cloud [+ubicación]** GOOGLE_CLOUD_PROJECT = "" # @param {tipo: "cadena"} GOOGLE_CLOUD_LOCATION = "global" # @param {tipo: "cadena"} # @markdown **Opción B – Clave API de Google AI Studio** GOOGLE_API_KEY = "" # @param {tipo: "cadena"} def check_environment() -> bool: check_colab_user_authentication() return check_manual_setup() o check_vertex_ai() o check_colab() o check_local() def check_manual_setup() -> bool: return check_define_env_vars( GOOGLE_GENAI_USE_VERTEXAI, GOOGLE_CLOUD_PROJECT.strip(), # Podría haberse pegado con la línea return GOOGLE_CLOUD_LOCATION, GOOGLE_API_KEY, ) def check_vertex_ai() -> bool: # Workbench y Colab Enterprise coinciden con os.getenv("VERTEX_PRODUCT", ""): case "WORKBENCH_INSTANCE": pasa case "COLAB_ENTERPRISE": si no se ejecuta_in_colab_env(): devuelve Falso case _: devuelve Falso devuelve check_define_env_vars( True, os.getenv("GOOGLE_CLOUD_PROJECT", ""), os.getenv("GOOGLE_CLOUD_REGION", ""), "", ) def check_colab() -> bool: si no se ejecuta_in_colab_env(): return False # Colab Enterprise se verificó antes, por lo que este es Colab solo desde google.colab import auth as colab_auth # tipo: ignorar colab_auth.authenticate_user() # Usar Colab Secrets (icono 🗝️ en el panel izquierdo) para almacenar las variables de entorno # Los secretos son privados, visibles solo para usted y los cuadernos que seleccione # – Vertex AI: almacene su configuración como secretos # – Google AI: importe directamente su clave API de Gemini desde la interfaz de usuario vertexai, proyecto, ubicación, api_key = get_vars(get_colab_secret) return check_define_env_vars(vertexai, proyecto, ubicación, api_key) def check_local() -> bool: vertexai, proyecto, ubicación, api_key = get_vars(os.getenv) return check_define_env_vars(vertexai, project, location, api_key) def running_in_colab_env() -> bool: # Colab o Colab Enterprise devuelve "google.colab" en sys.modules def check_colab_user_authentication() -> Ninguno: si running_in_colab_env(): desde google.colab importar autenticación como colab_auth # tipo: ignorar colab_auth.authenticate_user() def get_colab_secret(secret_name: str, predeterminado: str) -> str: de google.colab errores de importación, datos de usuario # tipo: ignorar try: return userdata.get(secret_name) excepto errores.SecretNotFoundError: devuelve el valor predeterminado def enable_colab_cell_scrollbar() -> Ninguno: si se ejecuta_in_colab_env(): from google.colab importar salida # tipo: ignorar salida.no_vertical_scroll() def get_vars(getenv: Callable[[str, str], str]) -> tuple[bool, str, str, str]: # Limitar las llamadas getenv al mínimo (puede activar la confirmación de la interfaz de usuario para acceso secreto) vertexai_str = getenv("GOOGLE_GENAI_USE_VERTEXAI", "") if vertexai_str: vertexai = vertexai_str.lower() en ["true", "1"] else: vertexai = bool(getenv("GOOGLE_CLOUD_PROJECT", "")) proyecto = getenv("GOOGLE_CLOUD_PROJECT", "") if vertexai else "" ubicación = getenv("GOOGLE_CLOUD_LOCATION", "") if proyecto else "" api_key = getenv("GOOGLE_API_KEY", "") si no es proyecto else "" devuelve vertexai, proyecto, ubicación, api_key def check_define_env_vars( vertexai: bool, proyecto: str, ubicación: str, api_key: str, ) -> bool: match (vertexai, bool(proyecto), bool(ubicación), bool(api_key)): case (True, True, _, _): # Vertex AI – Proyecto Google Cloud [+ubicación] ubicación = ubicación o "global" define_env_vars(vertexai, proyecto, ubicación, "") caso (Verdadero, Falso, _, Verdadero): # Vertex AI – Clave API define_env_vars(vertexai, "", "", api_key) caso (Falso, _, _, Verdadero): # Google AI Studio – Clave API define_env_vars(vertexai, "", "", api_key) caso _: return False return True def define_env_vars(vertexai: bool, proyecto: str, ubicación: str, api_key: str) -> Ninguno: os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = str(vertexai) os.environ["GOOGLE_CLOUD_PROJECT"] = proyecto os.environ["GOOGLE_CLOUD_LOCATION"] = ubicación os.environ["GOOGLE_API_KEY"] = api_key def check_configuration(client: genai.Client) -> Ninguno: service = "Vertex AI" if client.vertexai else "Google AI Studio" print(f"✅ Usando la API de {servicio}", end="") if client._api_client.project: print(f' con proyecto "{client._api_client.project[:7]}…"', end="") print(f' en la ubicación "{client._api_client.location}"') elif client._api_client.api_key: api_key = client._api_client.api_key print(f' con clave API "{api_key[:5]}…{api_key[-5:]}"', end="") print(f" (en caso de error, asegúrese de que fue creado para {servicio})") print("✅ Funciones de entorno definidas")
🤖 SDK de IA de generación
Para enviar solicitudes de Gemini, cree un cliente google.genai:
desde google importar genai check_environment() cliente = genai.Client() check_configuration(cliente)
🖼️ Conjunto de pruebas de imágenes
Definamos una lista de imágenes para nuestras pruebas: 🔽 from dataclasses import dataclass from enum import StrEnum Url = str class Fuente(StrEnum): incunable = "https://tile.loc.gov/image-services/iiif/service:rbc:rbc0001:2014:2014rosen0487:0165/full/pct:25/0/default.jpg" grabados = "https://tile.loc.gov/image-services/iiif/service:gdc:gdcscd:00:34:07:66:92:1:00340766921:0121/full/pct:50/0/default.jpg" museum_guidebook = "https://tile.loc.gov/image-services/iiif/service:rbc:rbc0001:2014:2014gen34181:0033/full/pct:75/0/default.jpg" denver_illustrated = "https://tile.loc.gov/image-services/iiif/service:gdc:gdclccn:rc:01:00:04:94:rc01000494:0051/full/pct:50/0/default.jpg" física_textbook = "https://tile.loc.gov/image-services/iiif/service:gdc:gdcscd:00:03:64:87:31:8:00036487318:0103/full/pct:50/0/default.jpg" retrato_miniaturas = "https://tile.loc.gov/image-services/iiif/service:rbc:rbc0001:2024:2024rosen013592v02:0249/full/pct:50/0/default.jpg" Wizard_of_oz_drawings = "https://tile.loc.gov/image-services/iiif/service:rbc:rbc0001:2006:2006gen32405:0048/full/pct:25/0/default.jpg" pinturas = "https://images.unsplash.com/photo-1714146681164-f26fed839692?h=1440" alice_drawing = "https://images.unsplash.com/photo-1630595011903-689853b04ee2?h=800" book = "https://images.unsplash.com/photo-1643451533573-ee364ba6e330?h=800" manual = "https://images.unsplash.com/photo-1623666936367-a100f62ba9b7?h=800" electronics = "https://images.unsplash.com/photo-1757397584789-8b2c5bfcdbc3?h=1440" @dataclass class SourceMetadata: título: str webpage_url: Url credit_line: str LOC = "Biblioteca del Congreso" LOC_RARE_BOOKS = "Biblioteca del Congreso, División de Libros Raros y Colecciones Especiales" LOC_MEETING_FRONTIERS = "Biblioteca del Congreso, Encuentro de Fronteras" metadata_by_source: dict[Source, SourceMetadata] = { Source.incunable: SourceMetadata( "Vergaderinge der historien van Troy (1485)", "https://www.loc.gov/resource/rbc0001.2014rosen0487/?sp=165", LOC_RARE_BOOKS, ), Source.engravings: SourceMetadata( "Catálogo ilustrado de Harper (1847)", "https://www.loc.gov/resource/gdcscd.00340766921/?sp=121", LOC, ), Source.museum_guidebook: SourceMetadata( "Museo americano de Barnum ilustrado (1850)", "https://www.loc.gov/resource/rbc0001.2014gen34181/?sp=33", LOC_RARE_BOOKS, ), Source.denver_illustrated: SourceMetadata( "Denver ilustrado (1893)", "https://www.loc.gov/resource/gdclccn.rc01000494/?sp=51", LOC_MEETING_FRONTIERS, ), Source.physics_textbook: SourceMetadata( "Lecciones de física (1916)", "https://www.loc.gov/resource/gdcscd.00036487318/?sp=103", LOC, ), Source.portrait_miniatures: SourceMetadata( "La historia de los retratos en miniatura (1904)", "https://www.loc.gov/resource/rbc0001.2024rosen013592v02/?sp=249", LOC_RARE_BOOKS, ), Source.wizard_of_oz_drawings: SourceMetadata( "El maravilloso Mago de Oz (1899)", "https://www.loc.gov/resource/rbc0001.2006gen32405/?sp=48", LOC_RARE_BOOKS, ), Source.paintings: SourceMetadata( "Libro abierto que muestra pinturas de Vincent van Gogh", "https://unsplash.com/photos/9hD7qrxICag", "Foto de Trung Manh cong en Unsplash",), Source.alice_drawing: SourceMetadata( "Libro abierto que muestra una ilustración y texto de Las aventuras de Alicia en el país de las maravillas", "https://unsplash.com/photos/bewzr_Q9u2o", "Foto de Brett Jordan en Unsplash",), Source.book: SourceMetadata( "Libro abierto que muestra dos ilustraciones botánicas", "https://unsplash.com/photos/4IDqcNj827I", "Foto de Ranurte en Unsplash", ), Source.manual: SourceMetadata( "Manual de usuario abierto para cámara antigua", "https://unsplash.com/photos/aaFU96eYASk", "Foto de Annie Spratt en Unsplash",), Source.electronics: SourceMetadata( "Placa de circuito con componentes electrónicos", "https://unsplash.com/photos/Aqa1pHQ57pw", "Foto de Albert Stoynov en Unsplash", ), } print("✅ Imágenes de prueba definidas")
🧠 Modelos Géminis
Géminis viene en diferentes versiones. Actualmente podemos utilizar los siguientes modelos:
Para detección de objetos: Gemini 2.5 o Gemini 3, cada uno disponible en versiones Flash o Pro. Para edición de objetos: Gemini 2.5 Flash Image o Gemini 3 Pro Image, también conocido como Nano Banana y Nano Banana Pro.
🛠️ Ayudantes
Ahora, agreguemos clases y funciones auxiliares principales: 🔽 de enum import auto de pathlib import Ruta de escribir import Any, cast import IPython.display import matplotlib.pyplot as plt import pydantic import tenacity de google.genai.errors import ClientError de google.genai.types import ( FinishReason, GenerateContentConfig, GenerateContentResponse, PIL_Image, ThinkingConfig, ThinkingLevel, ) # Modelos multimodales con comprensión espacial y salidas estructuradas clase MultimodalModel(StrEnum): # Generalmente disponible (GA) GEMINI_2_5_FLASH = "gemini-2.5-flash" GEMINI_2_5_PRO = "gemini-2.5-pro" # Vista previa GEMINI_3_FLASH_PREVIEW = "gemini-3-flash-preview" GEMINI_3_1_PRO_PREVIEW = "gemini-3.1-pro-preview" # Modelo predeterminado utilizado para la detección de objetos DEFAULT = GEMINI_3_FLASH_PREVIEW # Clase de modelos de generación y edición de imágenes ImageModel(StrEnum): # Disponible generalmente (GA) GEMINI_2_5_FLASH_IMAGE = "gemini-2.5-flash-image" # Nano Banana 🍌 # Vista previa GEMINI_3_PRO_IMAGE_PREVIEW = "gemini-3-pro-image-preview" # Nano Banana Pro 🍌 # Modelo predeterminado utilizado para la edición de imágenes DEFAULT = GEMINI_2_5_FLASH_IMAGE Modelo = MultimodalModel | ImageModel def generate_content( contenido: lista[Cualquiera], modelo: Modelo, configuración: GenerateContentConfig | Ninguno, debería_display_response_info: bool = False, ) -> GenerateContentResponse | Ninguno: respuesta = Ninguno cliente = check_client_for_model(modelo) para intento en get_retrier(): con intento: respuesta = client.models.generate_content( modelo=model.value, contenidos=contents, config=config, ) if debería_display_response_info: display_response_info(respuesta, configuración) devolver respuesta def check_client_for_model(modelo: Modelo) -> genai.Client: if ( model.value.endswith("-preview") y client.vertexai y client._api_client.location != "global" ): # Los modelos de vista previa solo están disponibles en la ubicación "global" return genai.Client(location="global") return client def display_response_info( respuesta: GenerateContentResponse | Ninguno, configuración: GenerateContentConfig | Ninguno, ) -> Ninguno: si la respuesta es Ninguno: print("❌ Sin respuesta") return if use_metadata := respuesta.usage_metadata: if use_metadata.prompt_token_count: print(f"Tokens de entrada: {usage_metadata.prompt_token_count:9,d}") if use_metadata.candidates_token_count: print(f"Tokens de salida: {usage_metadata.candidates_token_count:9,d}") if use_metadata.thinkts_token_count: print(f"Tokens de Thoughts: {usage_metadata.thinkts_token_count:9,d}") if (la configuración no es Ninguna y config.response_mime_type == "application/json" y Response.parsed es Ninguna): print("❌ No se pudo analizar el Respuesta JSON") regresa si no hay respuesta.candidates: print("❌ No `respuesta.candidates`") regresa si (reason_finish := respuesta.candidates[0].finish_reason) != FinishReason.STOP: print(f"❌ {finish_reason = }") si no respuesta.text: print("❌ No `response.text`") return def generate_image( fuentes: lista[PIL_Image], mensaje: str, modelo: ImageModel, configuración: GenerateContentConfig | Ninguno = Ninguno, ) -> PIL_Image | Ninguno: contenido = [*fuentes, solicitud.strip()] respuesta = generar_contenido(contenido, modelo, configuración) return check_get_output_image_from_response(response) def check_get_output_image_from_response( respuesta: GenerateContentResponse | Ninguno, ) -> PIL_Image | Ninguno: si la respuesta es Ninguna: print("❌ No `response`") return Ninguno si no respuesta.candidates: print("❌ No `response.candidates`") if respuesta.prompt_feedback: if block_reason := respuesta.prompt_feedback.block_reason: print(f"{block_reason = :s}") if block_reason_message := respuesta.prompt_feedback.block_reason_message: print(f"{block_reason_message = }") devuelve Ninguno si no (contenido: = respuesta.candidates[0].content): print("❌ Sin `respuesta.candidatos[0].content`") devuelve Ninguno si no (partes := contenido.partes): print("❌ No `respuesta.candidatos[0].content.parts`") return Ninguno imagen_salida: PIL_Image | Ninguno = Ninguno para parte en partes: if part.text: display_markdown(part.text) continuar sdk_image = part.as_image() afirmar que sdk_image no es Ninguno salida_imagen = sdk_image._pil_image afirmar que salida_imagen no es Ninguno break # Debe haber una única imagen de retorno salida_imagen def get_thinking_config(model: Modelo) -> ThinkingConfig | Ninguno: modelo de coincidencia: caso MultimodalModel.GEMINI_2_5_FLASH: devolver ThinkingConfig(thinking_budget=0) caso MultimodalModel.GEMINI_2_5_PRO: devolver ThinkingConfig(thinking_budget=128, include_thinks=False) caso MultimodalModel.GEMINI_3_FLASH_PREVIEW: devolver ThinkingConfig(thinking_level=ThinkingLevel.MINIMAL) caso MultimodalModel.GEMINI_3_1_PRO_PREVIEW: devolver ThinkingConfig(thinking_level=ThinkingLevel.LOW) caso _: devolver Ninguno # Valor predeterminado def display_markdown(markdown: str) -> Ninguno: IPython.display.display(IPython.display.Markdown(markdown)) def display_image(imagen: PIL_Image) -> Ninguno: IPython.display.display(image) def get_retrier() -> tenacity.Retrying: return tenacity.Retrying( stop=tenacity.stop_after_attempt(7), wait=tenacity.wait_incrementing(start=10, increment=1), retry=should_retry_request, reraise=True, ) def debería_retry_request(retry_state: tenacity.RetryCallState) -> bool: si no retry_state.outcome: devolver False err = retry_state.outcome.exception() si no isinstance(err, ClientError): devolver False print(f"❌ ClientError {err.code}: {err.message}") retry = False coincidencia err.code: caso 400 si err.message no es Ninguno e "inténtalo de nuevo" en err.message: # Taller: acceso por primera vez a Cloud Storage (aprovisionamiento de agente de servicio) reintento = Verdadero caso 429: # Taller: proyecto temporal con 1 cuota de QPM reintento = Verdadero print(f"🔄 Reintento: {reintentar}") return reintento print("✅ Ayudantes definidos")
🔍 Detección de objetos visuales
Para realizar la detección visual de objetos, cree el mensaje para indicar qué desea detectar y cómo se deben devolver los resultados. En la misma solicitud, también es posible extraer información adicional sobre cada objeto detectado. Puede ser prácticamente cualquier cosa, desde etiquetas como “muebles”, “mesa” o “silla”, hasta clasificaciones más precisas como “mamíferos” o “reptiles”, o datos contextuales como leyendas, colores, formas, etc.
Para las próximas pruebas, experimentaremos detectando ilustraciones dentro de fotografías de libros. Aquí hay un posible mensaje:
OBJECT_DETECTION_PROMPT = """ Detecta cada ilustración dentro de la foto del libro y extrae los siguientes datos para cada una: – `box_2d`: coordenadas del cuadro delimitador de la ilustración solamente (ignorando cualquier título). – `caption`: título textual o leyenda como "Figura 1". Utilice "" si no se encuentra. – `label`: etiqueta de una sola palabra que describe la ilustración. Utilice "" si no se encuentra. """
Notas:
Los cuadros delimitadores son muy útiles para localizar o extraer los objetos detectados. Normalmente, para los modelos Gemini, un cuadro delimitador box_2d representa coordenadas normalizadas a un espacio (0, 0, 1000, 1000) para una imagen de entrada (0, 0, ancho, alto). También solicitamos extraer títulos (metadatos que suelen estar presentes en libros de referencia) y etiquetas (metadatos dinámicos).
Para automatizar el procesamiento de respuestas, es conveniente definir una clase de Pydantic que coincida con el mensaje, como por ejemplo:
clase DetectedObject(pydantic.BaseModel): box_2d: lista[int] título: str etiqueta: str DetectedObjects: TypeAlias = lista[DetectedObject]
Luego, solicite una salida estructurada con los campos de configuración Response_mime_type y Response_schema:
config = GenerateContentConfig( #…, respuesta_mime_type="aplicación/json", respuesta_schema=ObjetosDetectados, #…,)
Esto generará una respuesta JSON que el SDK puede analizar automáticamente, permitiéndonos usar directamente instancias de objetos:
objetos_detectados = cast(ObjetosDetectados, respuesta.parsed) Agreguemos algunas clases y funciones específicas de detección de objetos: 🔽 importar io importar urllib.request de colecciones.abc importar Iterador de clases de datos importar campo de fecha y hora importar fecha y hora importar PIL.Image de google.genai.types importar Parte, PartMediaResolutionLevel de PIL.PngImagePlugin importar PngInfo OBJECT_DETECTION_PROMPT = """ Detecta cada ilustración dentro de la foto del libro y extrae los siguientes datos para cada una: – `box_2d`: coordenadas del cuadro delimitador de la ilustración únicamente (ignorando cualquier título). – `caption`: título literal o leyenda como "Figura 1". Utilice "" si no se encuentra. – `label`: etiqueta de una sola palabra que describe la ilustración. Utilice "" si no se encuentra. """ # Margen agregado a los objetos detectados/recortados. brindando más contexto para una mejor comprensión de las distorsiones espaciales CROP_MARGIN_PX = 10 # Establezca en True para guardar cada imagen generada SAVE_GENERATED_IMAGES = False OUTPUT_IMAGES_PATH = Path("./object_detection_and_editing") # Clase coincidente para la clase de generación de salida estructurada DetectedObject(pydantic.BaseModel): box_2d: list[int] caption: str label: str # Clases de datos varios InputImage = Camino | Url DetectedObjects = lista[DetectedObject] WorkflowStepImages = lista[PIL_Image] clase WorkflowStep(StrEnum): FUENTE = auto() RECORTADA = auto() RESTAURADA = auto() COLORIZADA = auto() CINEMATIZADA = auto() @dataclass clase VisualObjectWorkflow: source_image: PIL_Image objetos_detectados: DetectedObjects imágenes_por_paso: dict[WorkflowStep, WorkflowStepImages] = field(default_factory=dict) def __post_init__(self) -> Ninguno: denormalize_bounding_boxes(self) flow_by_image: dict[InputImage, VisualObjectWorkflow] = {} def denormalize_bounding_boxes(self: VisualObjectWorkflow) -> Ninguno: """Convierta las coordenadas box_2d. – Antes: [y1, x1, y2, x2] normalizado a 0-1000, según lo devuelto por Gemini – Después: [x1, y1, x2, y2] en coordenadas de imagen_fuente, como se usa en Pillow """ def to_image_coord(coord: int, dim: int) -> int: return int(coord * dim / 1000 + 0.5) w, h = self.source_image.size para obj en self.detected_objects: y1, x1, y2, x2 = obj.box_2d x1, x2 = to_image_coord(x1, w), to_image_coord(x2, w) y1, y2 = to_image_coord(y1, h), to_image_coord(y2, h) obj.box_2d = [x1, y1, x2, y2] def detect_objects( imagen: InputImage, mensaje: str = OBJECT_DETECTION_PROMPT, modelo: MultimodalModel = MultimodalModel.DEFAULT, configuración: GenerateContentConfig | Ninguno = Ninguno, resolución_media: PartMediaResolutionLevel | Ninguno = Ninguno, display_results: bool = True,) -> Ninguno: display_image_source_info(image) pil_image, content_part = get_pil_image_and_part(imagen, modelo, media_resolución) solicitud = solicitud.strip() contenidos = [content_part, solicitud] config = config o get_object_detection_config(modelo) respuesta = generar_contenido(contenido, modelo, configuración) si la respuesta no es Ninguna y respuesta.parsed no es Ninguna: objetos_detectados = cast(Objetos detectados, respuesta.parsed) else: objetos_detectados = flujo de trabajo de DetectedObjects() = VisualObjectWorkflow(pil_image, objetos_detectados) flujo de trabajo_por_imagen[imagen] = flujo de trabajo add_cropped_objects(flujo de trabajo, imagen, aviso) if display_results: display_detected_objects(workflow) def get_pil_image_and_part( imagen: InputImage, modelo: MultimodalModel, media_resolución: PartMediaResolutionLevel | Ninguno,) -> tupla[PIL_Image, Parte]: if isinstance(imagen, Ruta): image_bytes = image.read_bytes() else: headers = {"User-Agent": "Mozilla/5.0"} req = urllib.request.Request(imagen, headers=headers) con urllib.request.urlopen(req, timeout=10) como respuesta: image_bytes = respuesta.read() pil_image = PIL.Image.open(io.BytesIO(image_bytes)) content_part = Part.from_bytes( data=image_bytes, mime_type="image/*", media_solving=media_solving, ) return pil_image, content_part def get_object_detection_config(model: Model) -> GenerateContentConfig: # Baja aleatoriedad para mayor determinismo return GenerateContentConfig( Temperature=0.0, top_p=0.0, semilla=42, respuesta_mime_type="aplicación/json", respuesta_schema=DetectedObjects, think_config=get_thinking_config(modelo), ) def add_cropped_objects( flujo de trabajo: VisualObjectWorkflow, entrada: InputImage, mensaje: str, crop_margin: int = CROP_MARGIN_PX, ) -> Ninguno: cropped_images: lista[PIL_Imagen] =[]obj_count = len(workflow.detected_objects) para obj_order, obj en enumerate(workflow.detected_objects, 1): cropped_image, _ = extract_object_image(workflow.source_image, obj, crop_margin) cropped_images.append(cropped_image) save_workflow_image( WorkflowStep.SOURCE, WorkflowStep.CROPPED, entrada, obj_order, obj_count, cropped_image, dict(prompt=prompt, crop_margin=str(crop_margin)), ) flujo de trabajo.images_by_step[WorkflowStep.CROPPED] = cropped_images def extract_object_image( imagen: PIL_Image, obj: DetectedObject, margen: int = 0, ) -> tuple[PIL_Image, tuple[int, int, int, int]]: def abrazadera(coord: int, dim: int) -> int: return min(max(coord, 0), dim) x1, y1, x2, y2 = obj.box_2d w, h = imagen.tamaño si margen! = 0: x1, x2 = abrazadera(x1 – margen, w), abrazadera(x2 + margen, w) y1, y2 = abrazadera(y1 – margen, h), abrazadera (y2 + margen, h) cuadro = (x1, y1, x2, y2) imagen_objeto = imagen.crop(cuadro) devuelve imagen_objeto, cuadro def guardar_imagen_flujo de trabajo( paso_fuente: Paso_flujo, paso_destino: Paso_flujo, imagen_entrada: Imagen_entrada, orden_obj: int, cuenta_obj: int, imagen_destino: Imagen_PIL | Ninguno, información_imagen: dict[cadena, cadena] | Ninguno = Ninguno, ) -> Ninguno: si no es SAVE_GENERATED_IMAGES o target_image es Ninguno: devuelve si no OUTPUT_IMAGES_PATH.is_dir(): OUTPUT_IMAGES_PATH.mkdir(parents=True) time_str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") intenta: nombre de archivo = f"{Source(input_image).name}_" excepto ValueError: filename = "" filename += f"{obj_order}o{obj_count}_{source_step}_{target_step}_{time_str}.png" image_path = OUTPUT_IMAGES_PATH.joinpath(filename) params = {} if image_info: png_info = PngInfo() para k, v en image_info.items(): png_info.add_text(k, v) params.update(pnginfo=png_info) target_image.save(image_path, **params) # Matplotlib FIGURE_FG_COLOR = "#F1F3F4" FIGURE_BG_COLOR = "#202124" EDGE_COLOR = "#80868B" rcParams = { "figure.dpi": 300, "text.color": FIGURE_FG_COLOR, "figure.edgecolor": FIGURE_FG_COLOR, "axes.titlecolor": FIGURE_FG_COLOR, "axes.edgecolor": FIGURE_FG_COLOR, "xtick.color": FIGURE_FG_COLOR, "ytick.color": FIGURE_FG_COLOR, "figure.facecolor": FIGURE_BG_COLOR, "axes.edgecolor": EDGE_COLOR, "xtick.bottom": Falso, "xtick.top": Falso, "ytick.left": Falso, "ytick.right": Falso, "xtick.labelbottom": Falso, "ytick.labelleft": Falso, } plt.rcParams.update(rcParams) def display_image_source_info(imagen: InputImage) -> Ninguno: def get_image_info_md() -> str: si la imagen no está en la fuente: devuelve f"[[Imagen de origen]({image})]" fuente = Metadatos de la fuente (imagen) = metadata_by_source.get(fuente) si no son metadatos: devuelve f"[[Imagen de origen]({imagen)]" parts = [ f"[Imagen de origen]({source.value})", f"[Página de origen]({metadata.webpage_url})", metadata.title, metadata.credit_line, ] separator = "•" inside_info = f" {separator} ".join(parts) return f"{separator} {inner_info} {separator}" def rendimiento_md_rows() -> Iterador[str]: horizontal_line = "—" image_info = get_image_info_md() rendimiento línea_horizontal rendimiento f"_{image_info}_" rendimiento línea_horizontal display_markdown(f"{chr(10)}{chr(10)}".join(yield_md_rows())) def display_detected_objects(workflow: VisualObjectWorkflow) -> Ninguno: source_image = flujo de trabajo.source_image objetos_detectados = PIL.Image.new("RGB", source_image.size, "white") para obj en flujo de trabajo.objetos_detectados: obj_image, box = extraer_objeto_imagen(imagen_fuente, obj) objetos_detectados.paste(obj_imagen, (cuadro[0], caja[1])) _, (ax1, ax2) = plt.subplots(1, 2, layout="compressed") ax1.imshow(source_image) ax2.imshow(detected_objects) enable_colab_cell_scrollbar() plt.show() print("✅ Ayudantes de detección de objetos definidos")
🧪 Empecemos simple: ¿podemos detectar la única ilustración en este incunable de 1485?
detectar_objetos(Fuente.incunable)
• Imagen fuente • Página fuente • Vergaderinge der historien van Troy (1485) • Biblioteca del Congreso, División de libros raros y colecciones especiales •
💡 Esto funciona muy bien. El cuadro delimitador es muy preciso y encierra muy bien la ilustración grabada en madera coloreada a mano.
🧪 Ahora, revisemos la detección de las múltiples imágenes en esta guía del museo:
detectar_objetos(Fuente.museum_guidebook)
• Imagen fuente • Página fuente • Museo Americano de Barnum ilustrado (1850) • Biblioteca del Congreso, División de Libros Raros y Colecciones Especiales •
💡 Observaciones:
Los cuadros delimitadores vuelven a ser muy precisos. Los resultados son perfectos: no hay falsos positivos ni falsos negativos. Los títulos debajo de las imágenes no están encerrados dentro de los cuadros delimitadores, lo cual se solicitó específicamente. La granularidad del cuadro delimitador se puede controlar cambiando el mensaje.
🧪 ¿Qué pasa con las imágenes ligeramente deformadas?
detectar_objetos (Fuente.pinturas)
• Imagen fuente • Página fuente • Libro abierto que muestra pinturas de Vincent van Gogh • Foto de Trung Manh cong en Unsplash •
💡 Esto no hace la diferencia. Observe cómo la pintura de abajo a la derecha está parcialmente cubierta por el marcador naranja. Intentaremos solucionarlo en el paso de restauración.
🧪 ¿Qué pasa con las imágenes inclinadas de este libro sobre la arquitectura en Denver?
detectar_objetos(Fuente.denver_illustrated)
• Imagen fuente • Página fuente • Denver ilustrado (1893) • Biblioteca del Congreso, Encuentro de Fronteras •
💡 Cada imagen se detecta perfectamente: la comprensión espacial abarca los objetos inclinados.
🧪 Finalmente, verifiquemos la detección en esta página del libro significativamente deformada de Las aventuras de Alicia en el país de las maravillas:
detectar_objetos(Fuente.alice_drawing)
• Imagen fuente • Página fuente • Libro abierto que muestra una ilustración y un texto de Las aventuras de Alicia en el país de las maravillas • Foto de Brett Jordan en Unsplash •
💡 La curvatura de la página y otras distorsiones no impiden que se detecten objetos no rectangulares. De hecho, la comprensión espacial funciona a nivel de píxeles, lo que explica esta precisión en los objetos deformados. Si desea trabajar en un nivel inferior, también puede solicitar una "máscara de segmentación" en el mensaje y obtendrá un PNG codificado en base64 (cada píxel proporciona una probabilidad de 0 a 255 de que pertenezca al objeto dentro del cuadro delimitador). Consulte el documento de segmentación para obtener más detalles.
🏷️ Extracción de texto y etiquetado dinámico.
Además de localizar cada objeto con su cuadro delimitador, nuestro mensaje solicitaba extraer un título textual y asignar una etiqueta de una sola palabra, cuando fuera posible.
Agreguemos una función simple para mostrar los datos de detección en una tabla: 🔽 de colecciones import defaultdict def display_detection_data(source: Source, show_consolidated: bool = False) -> Ninguno: def string_with_visible_linebreaks(s: str) -> str: return f''''"{s.replace(chr(10), "↩️")}"'' def rendimiento_md_rows_consolidated(flujo de trabajo: VisualObjectWorkflow) -> Iterador[cadena]: rendimiento "| etiqueta | recuento | subtítulos |" rendimiento "| :— | —: | :— |" stats = defaultdict(list) para obj en flujo de trabajo.detected_objects: stats[obj.label].append(string_with_visible_linebreaks(obj.caption)) para etiqueta, subtítulos en stats.items(): count = len(captions) label_captions = " • ".join(sorted(captions)) rendimiento f"| {label} | {count} | {label_captions} |" def rendimiento_md_rows_with_bbox(flujo de trabajo: VisualObjectWorkflow) -> Iterador[cadena]: rendimiento "| box_2d | etiqueta | título |" rendimiento "| :— | :— | :— |" para obj en flujo de trabajo.objetos_detectados: rendimiento f"| {obj.box_2d} | {obj.label} | {string_with_visible_linebreaks(obj.caption)} |" flujo de trabajo = flujo de trabajo_by_image.get(fuente) si el flujo de trabajo es Ninguno: print(f'❌ No se detecta la fuente "{fuente.nombre}"') return md_rows = list( rendimiento_md_rows_consolidated(flujo de trabajo) si show_consolidated else rendimiento_md_rows_with_bbox(flujo de trabajo) ) display_image_source_info(fuente) display_markdown(chr(10).join(md_rows))
En la guía del museo, el etiquetado dinámico es preciso según el contexto, y los títulos debajo de cada ilustración están perfectamente extraídos:
display_detection_data(Fuente.museum_guidebook)
• Imagen fuente • Página fuente • Museo Americano de Barnum ilustrado (1850) • Biblioteca del Congreso, División de Libros Raros y Colecciones Especiales •
En la foto del libro que muestra cuatro pinturas, esto también es perfecto:
display_detection_data(Fuente.pinturas)
• Imagen fuente • Página fuente • Libro abierto que muestra pinturas de Vincent van Gogh • Foto de Trung Manh cong en Unsplash •
En el libro de arquitectura de Denver, los cuatro títulos están asignados a las ilustraciones correctas, lo cual no era una tarea obvia:
display_detection_data(Fuente.denver_illustrated)
• Imagen fuente • Página fuente • Denver ilustrado (1893) • Biblioteca del Congreso, Encuentro de Fronteras •
💡 Si observas más de cerca la imagen de entrada, es difícil saber qué título pertenece a qué ilustración de un vistazo. La mayoría de nosotros necesitaríamos pensar en ello (y podríamos estar equivocados). Preguntarle a Gemini revela que los resultados son intencionales y no pura suerte: descifrar diseños antiguos puede parecer un poco como un rompecabezas, pero generalmente hay una lógica de "orden de lectura" en juego. En este caso específico, los títulos están organizados para corresponder con las imágenes en el sentido de las agujas del reloj o en forma de Z, comenzando desde la parte superior izquierda.
En la página del libro “Las aventuras de Alicia en el país de las maravillas”, había una única ilustración que acompañaba al texto de la historia. Como era de esperar, el título está vacío (es decir, no hay falsos positivos):
display_detection_data(Fuente.alice_drawing)
• Imagen fuente • Página fuente • Libro abierto que muestra una ilustración y un texto de Las aventuras de Alicia en el país de las maravillas • Foto de Brett Jordan en Unsplash •
🔭 Generalización de la detección de objetos
Podemos utilizar los mismos principios para otros tipos de objetos. Generalmente seguiremos solicitando cuadros delimitadores para identificar las posiciones de los objetos dentro de las imágenes. Sin cambiar nuestra estructura de salida actual (es decir, sin cambios de código), podemos usar títulos y etiquetas para extraer diferentes metadatos de objetos según el tipo de entrada.
🧪 Vea cómo podemos detectar componentes electrónicos adaptando el mensaje manteniendo exactamente el mismo código y estructura de salida:
ELECTRONIC_COMPONENT_DETECTION_PROMPT = """ Detecta exhaustivamente todos los componentes electrónicos individuales en la imagen y proporciona los siguientes datos para cada uno: – `box_2d`: coordenadas del cuadro delimitador. – `caption`: texto alfanumérico palabra por palabra visible en el componente (incluidos los saltos de línea originales), o "" si no hay texto presente. – `label`: tipo específico de componente. """ detect_objects( Source.electronics, ELECTRONIC_COMPONENT_DETECTION_PROMPT, resolución_media=PartMediaResolutionLevel.MEDIA_RESOLUTION_ULTRA_HIGH,)
• Imagen fuente • Página fuente • Placa de circuito con componentes electrónicos • Foto de Albert Stoynov en Unsplash •
💡 Observaciones:
Se detectan componentes grandes y pequeños, gracias a la instrucción específica “detectar exhaustivamente…”. Al utilizar la resolución multimedia ultraalta, nos aseguramos de que se tokenicen más detalles y se detecte el componente "P" (un valor atípico visual).
Aquí hay una vista consolidada de los componentes detectados:
display_detection_data(Fuente.electronics, show_consolidated=True)
• Imagen fuente • Página fuente • Placa de circuito con componentes electrónicos • Foto de Albert Stoynov en Unsplash •
💡 Observaciones:
Los componentes se detectan junto con sus marcas de texto, a pesar de las tres orientaciones diferentes del texto (vertical, lateral y al revés), el desenfoque y el ruido de la foto. Eliminamos el grado de libertad para el texto de varias líneas especificando la inclusión de "saltos de línea originales" en el mensaje: las respuestas ahora incluyen consistentemente los saltos de línea para los tres circuitos integrados (que se muestran con el emoji ↩️ para una mejor visibilidad). El último grado de libertad reside en el etiquetado. Si bien la mayoría de los componentes han sido etiquetados correctamente, no está claro si el componente "P" es un diodo, una resistencia o un fusible. Hacer que las instrucciones sean más específicas (por ejemplo, enumerar las posibles etiquetas, usar una enumeración para el campo de etiqueta en la clase Pydantic o proporcionar pautas y más detalles sobre las placas de circuito esperadas) hará que el mensaje sea más "cerrado" y los resultados más deterministas y precisos.
También es posible habilitar/actualizar la configuración de think_config, lo que desencadenará una cadena de pensamientos antes de generar la respuesta final. En todas las detecciones realizadas, nuestro código utilizó ThinkingLevel.MINIMAL, que no consumió ningún token de pensamiento (con Gemini 3 Flash). Actualizar el parámetro a ThinkingLevel.LOW, ThinkingLevel.MEDIUM o ThinkingLevel.HIGH utilizará tokens de pensamiento y puede generar mejores resultados en casos complejos.
Esto demuestra la versatilidad del enfoque. Sin volver a entrenar un modelo, pasamos de detectar grabados en madera e ilustraciones del siglo XV con diseños antiguos a identificar dispositivos electrónicos modernos simplemente cambiando el mensaje. Estas detecciones, incluidos los metadatos de leyendas y etiquetas, podrían usarse para recortar automáticamente componentes para un catálogo de piezas, verificar líneas de ensamblaje o crear esquemas interactivos… todo sin una sola imagen de entrenamiento etiquetada.
🪄 Edición de objetos visuales
Ahora que podemos detectar objetos visuales, podemos imaginar un flujo de trabajo de automatización para extraerlos y reutilizarlos. Para esto, usaremos Gemini 2.5 Flash Image (también conocido como Nano Banana 🍌) por defecto, un modelo de generación y edición de imágenes de última generación.
Nuestras funciones de edición de objetos seguirán la misma plantilla, tomando un paso como entrada y generando una imagen editada para el paso de salida. Definamos los asistentes principales para esto: 🔽 escribiendo import Protocolo clase ObjectEditingFunction(Protocolo): def __call__( self, imagen: InputImage, mensaje: str | Ninguno = Ninguno, modelo: ImageModel | Ninguno = Ninguno, configuración: GenerateContentConfig | Ninguno = Ninguno, display_results: bool = True,) -> Ninguno: … SourceTargetSteps = tuple[WorkflowStep, WorkflowStep] funciones_registradas: dict[SourceTargetSteps, ObjectEditingFunction] = {} DEFAULT_EDITING_CONFIG = GenerateContentConfig(response_modalities=["IMAGE"]) EMPTY_IMAGE = PIL.Image.new("1", (1, 1), "white") def object_editing_function( default_prompt: str, source_step: WorkflowStep, target_step: WorkflowStep, default_model: ImageModel = ImageModel.DEFAULT, default_config: GenerateContentConfig = DEFAULT_EDITING_CONFIG,) -> ObjectEditingFunction: def editing_function(imagen: InputImage, mensaje: str | Ninguno = default_prompt, modelo: ImageModel | Ninguno = default_model, configuración: GenerateContentConfig | Ninguno = default_config, display_results: bool = True,) -> Ninguno: flujo de trabajo, source_images = get_workflow_and_step_images(image, source_step) si el mensaje es Ninguno: mensaje = default_prompt mensaje = mensaje.strip() si el modelo es Ninguno: modelo = default_model # Nota: "config is None" es válido y utilizará la configuración predeterminada del punto final del modelo target_images: list[PIL_Image] =[]display_image_source_info(imagen) obj_count = len(source_images) para obj_order, source_image in enumerate(source_images, 1): target_image = generate_image([source_image], solicitud, modelo, configuración) save_workflow_image( source_step, target_step, imagen, obj_order, obj_count, target_image, dict(prompt=prompt),) target_images.append(target_image if target_image else EMPTY_IMAGE) flowwork.images_by_step[target_step] = target_images if display_results: display_sources_and_targets(workflow, source_step, target_step) protected_functions[(source_step, target_step)] = función_de edición return función_de_edición def get_workflow_and_step_images( imagen: InputImage, paso: WorkflowStep, ) -> tupla[VisualObjectWorkflow, lista[PIL_Image]]: # ¿Objetos detectados? si la imagen no está en flujo de trabajo_por_imagen: detectar_objetos(imagen, display_results=False) flujo de trabajo = flujo de trabajo_por_imagen.get(imagen) afirmar que el flujo de trabajo no es Ninguno # ¿Objetos de paso del flujo de trabajo? (un solo nivel, podría extenderse a un gráfico dinámico) operación = (WorkflowStep.CROPPED, paso) si el paso no está en flujo de trabajo.images_by_step y operación en funciones_registradas: función_fuente = funciones_registradas[operación] función_fuente(image, display_results=False) # Imágenes de origen imágenes_fuente = flujo de trabajo.images_by_step.get(paso) afirmar imágenes_fuente no es Ninguno devolver flujo de trabajo, imágenes_fuente def display_sources_and_targets( flujo de trabajo: VisualObjectWorkflow, source_step: WorkflowStep, target_step: WorkflowStep, ) -> Ninguno: source_images = flujo de trabajo.images_by_step[source_step] target_images = flujo de trabajo.images_by_step[target_step] si no source_images: print("❌ No hay imágenes para mostrar") return fig = plt.figure(layout="compressed") if horizontal := (len(source_images) >= 2): filas, cols = 2, len(source_images) else: filas, cols = len(source_images), 2 gs = fig.add_gridspec(rows, cols) for i, (source_image, target_image) in enumerate( zip(source_images, target_images, estricto=True) ): para tenue, imagen en enumerate([source_images, target_image]): grid_spec = gs[dim, i] if horizontal else gs[i, dim] ax = fig.add_subplot(grid_spec) ax.set_axis_off() ax.imshow(image) enable_colab_cell_scrollbar() plt.show() print("✅ Ayudantes de edición de objetos definidos")
Ahora, definamos un primer paso de edición para restaurar los objetos detectados que pueden contener muchos artefactos de la vida real…
✨ Restaurar objetos visuales
Para este paso de restauración, debemos crear un mensaje que sea lo suficientemente genérico (para cubrir la mayoría de los casos de uso) pero también lo suficientemente específico (para tener en cuenta las necesidades de restauración).
Un mensaje de edición de imágenes se basa en lenguaje natural y, por lo general, utiliza instrucciones imperativas o declarativas. Con un mensaje imperativo, describe las acciones a realizar en la entrada, mientras que con un mensaje declarativo, describe el resultado esperado. Ambos son posibles y proporcionarán resultados equivalentes. Su elección es realmente una cuestión de preferencia, siempre que la indicación tenga sentido.
Nuestro conjunto de pruebas se compone principalmente de fotografías de libros, que pueden contener varios artefactos fotográficos y en papel. Los modelos Nano Banana comprenden estas sutilezas y pueden editar imágenes en consecuencia, lo que simplifica el mensaje.
Aquí hay una posible función de restauración usando un mensaje imperativo:
RESTORATION_PROMPT = """ – Aísle y enderece lo visual sobre un fondo blanco puro, excluyendo cualquier texto circundante. – Limpie todos los artefactos físicos y el ruido mientras conserva cada detalle original. – Centra el resultado y escale para que se ajuste al lienzo con márgenes mínimos y simétricos, asegurando que no haya distorsión ni recorte. """ # Configuración predeterminada con baja aleatoriedad para resultados de restauración más deterministas RESTORATION_CONFIG = GenerateContentConfig( Temperature=0.0, top_p=0.0, seed=42, Response_modalities=["IMAGE"], ) recovery_objects = object_editing_function( RESTORATION_PROMPT, WorkflowStep.CROPPED, WorkflowStep.RESTORED, default_config=RESTORATION_CONFIG, ) print("✅ Función de restauración definida")
🧪 Intentemos restaurar la ilustración del incunable de 1485:
restaurar_objetos(Fuente.incunable)
• Imagen fuente • Página fuente • Vergaderinge der historien van Troy (1485) • Biblioteca del Congreso, División de libros raros y colecciones especiales •
💡 Ahora tenemos una bonita restauración de la ilustración grabada en madera coloreada a mano. Tenga en cuenta que nuestro mensaje es genérico (“limpiar todos los artefactos físicos”) y podría hacerse más específico para eliminar más o menos artefactos. En este ejemplo, quedan artefactos restantes, como la decoloración del papel en la espada o la tinta sangrante en la armadura. Veremos si podemos solucionarlos en el paso de coloración.
🧪 ¿Qué pasa con las ilustraciones de la guía del museo?
restaurar_objetos (Fuente.museum_guidebook)
• Imagen fuente • Página fuente • Museo Americano de Barnum ilustrado (1850) • Biblioteca del Congreso, División de Libros Raros y Colecciones Especiales •
💡 ¡Todo bien!
🧪 ¿Qué pasa con las imágenes ligeramente deformadas?
restaurar_objetos (Fuente.pinturas)
• Imagen fuente • Página fuente • Libro abierto que muestra pinturas de Vincent van Gogh • Foto de Trung Manh cong en Unsplash •
💡 Observaciones:
Observe cómo, en el último cuadro, el marcapáginas naranja se retira correctamente y se pinta la parte oculta para completar el cuadro. Solicitamos “rellenar el lienzo con márgenes mínimos uniformes, sin distorsiones ni recortes”. Dependiendo de la relación de aspecto y el tipo de imagen, este grado de libertad puede dar lugar a diferentes márgenes blancos. Este ejemplo muestra pinturas famosas de Vincent Van Gogh. Nano Banana no recupera ninguna imagen de referencia y solo utiliza la entrada proporcionada. Si se tratara de fotografías de cuadros privados, se restaurarían de la misma forma.
En el libro de arquitectura de Denver, las ilustraciones pueden inclinarse, lo que nuestro mensaje genérico no tiene en cuenta del todo. Cuando se trata de varias transformaciones geométricas, puede resultar complicado elaborar un mensaje imperativo que detalle todas las operaciones a realizar. En cambio, un mensaje descriptivo puede ser más sencillo al describir directamente el resultado esperado.
🧪 A continuación se muestra un ejemplo de un mensaje descriptivo que se centra en la restauración de imágenes inclinadas:
inclinado_visual_prompt = """ Una interpretación vertical y de alta fidelidad de lo visual aislado contra un fondo blanco puro, llenando el lienzo con márgenes uniformes mínimos. La salida es limpia, nítida y libre de artefactos físicos. """ restaurar_objetos(Fuente.denver_illustrated, inclinado_visual_prompt)
• Imagen fuente • Página fuente • Denver ilustrado (1893) • Biblioteca del Congreso, Encuentro de Fronteras •
💡 Observaciones:
Para obtener estos resultados, el mensaje se centra en solicitar una imagen "vertical" que "llene el lienzo", lo que resulta más sencillo de escribir que tratar de tener en cuenta todas las correcciones geométricas posibles. La comprensión visual nativa identifica automáticamente el tipo de contenido (fotografía, ilustración, etc.) y los diferentes artefactos (fotográficos, en papel, impresos, escaneados…), lo que permite realizar restauraciones precisas desde el primer momento. Observe cómo se conserva la coherencia: la última imagen se restaura como una ilustración, mientras que las primeras imágenes mantienen su estilo fotográfico. Los resultados, con esta sugerencia bastante genérica, son impresionantes. Por supuesto, es posible ser más específico y solicitar iluminación, estilos, colores particulares…
En esta última prueba, la imagen de entrada tiene distorsiones no solo por la curvatura de la página sino también por la perspectiva de la foto.
🧪 A continuación se muestra un ejemplo de un mensaje descriptivo que se centra en restaurar ilustraciones deformadas:
warped_visual_prompt = """ Una extracción digital de borde a borde de la ilustración de la fotografía del libro proporcionada, excluyendo cualquier texto periférico. Se corrigen todas las curvaturas de la página y las distorsiones de perspectiva, lo que da como resultado una imagen enmarcada en un rectángulo perfecto, sobre un lienzo blanco puro con márgenes mínimos. """ recovery_objects(Source.alice_drawing, warped_visual_prompt)
• Imagen fuente • Página fuente • Libro abierto que muestra una ilustración y un texto de Las aventuras de Alicia en el país de las maravillas • Foto de Brett Jordan en Unsplash •
💡 Es realmente impresionante que una restauración así se pueda realizar en un solo paso. Tenga en cuenta que este mensaje no es estable y puede generar resultados menos óptimos (se beneficiaría si fuera más preciso). Si tiene transformaciones complejas, pruebe las indicaciones descriptivas de forma iterativa, utilizando instrucciones precisas y concisas, y es posible que se lleve una grata sorpresa. En el peor de los casos, también es posible procesar las transformaciones en pasos sucesivos y más sencillos.
Ahora, agreguemos un paso de coloración…
🎨 Colorización
Nuestro paso de restauración respetó los estilos originales de las imágenes de entrada. Los modelos de edición de imágenes recientes se destacan en la transformación de estilos de imágenes, comenzando con los colores. Por lo general, esto se puede realizar directamente con una instrucción sencilla y precisa.
Aquí hay una posible función de coloración usando un mensaje imperativo:
COLORIZATION_PROMPT = """ Colorea esta imagen en un estilo de ilustración de libro moderno, manteniendo todos los detalles originales sin adiciones. """ colorize = object_editing_function( COLORIZATION_PROMPT, WorkflowStep.RESTORED, WorkflowStep.COLORIZED, ) print("✅ Función de colorización definida")
🧪 Modernicemos nuestra ilustración 1485:
colorear (Fuente.incunable)
• Imagen fuente • Página fuente • Vergaderinge der historien van Troy (1485) • Biblioteca del Congreso, División de libros raros y colecciones especiales •
💡 Se conservan todos los detalles, según lo solicitado en el mensaje. Observe cómo la coloración puede reparar naturalmente algunos artefactos restantes (por ejemplo, la decoloración del papel en la espada o la tinta sangrante en la armadura).
🧪 Coloreemos las ilustraciones de nuestra guía del museo:
colorear (Fuente.museum_guidebook)
• Imagen fuente • Página fuente • Museo Americano de Barnum ilustrado (1850) • Biblioteca del Congreso, División de Libros Raros y Colecciones Especiales •
💡 Nuestro mensaje es muy abierto ya que solo especifica "estilo de ilustración de libro moderno". Esto puede generar colorizaciones muy creativas, pero todas parecen tener perfecto sentido.
🧪 ¿Qué pasa con nuestros edificios de Denver?
colorear (Fuente.denver_illustrated)
• Imagen fuente • Página fuente • Denver ilustrado (1893) • Biblioteca del Congreso, Encuentro de Fronteras •
💡 Según lo solicitado, todas parecen ilustraciones modernas, incluidas las primeras imágenes (procedentes de fotografías ruidosas).
Es posible ir más allá no sólo “coloreando” sino también “transformando” la imagen en una imagen significativamente diferente.
🧪 Hagamos de nuestro dibujo “Alicia en el país de las maravillas” una pintura de acuarela:
acuarela_prompt = """ Transforma este objeto visual en una cálida pintura de acuarela. """ colorize(Source.alice_drawing, acuarela_prompt)
• Imagen fuente • Página fuente • Libro abierto que muestra una ilustración y un texto de Las aventuras de Alicia en el país de las maravillas • Foto de Brett Jordan en Unsplash •
🧪 ¿Qué tal si lo convertimos en un cuadro tradicional?
pintura_prompt = """ Transforma este objeto visual en una pintura tradicional. """ colorize(Source.alice_drawing, pintura_prompt)
• Imagen fuente • Página fuente • Libro abierto que muestra una ilustración y un texto de Las aventuras de Alicia en el país de las maravillas • Foto de Brett Jordan en Unsplash •
También podemos cambiar las composiciones de las imágenes. Dependiendo del contexto, algunas composiciones están más o menos implícitas por defecto. Por ejemplo, las ilustraciones suelen tener márgenes, mientras que las fotografías generalmente tienen composiciones de borde a borde (sangrado completo en el mundo de la impresión). Cuando sea posible, es interesante hacer referencia a un tipo de elemento visual (que intrínsecamente aporta mucha semántica al contexto) y ajustar las instrucciones en consecuencia.
🧪 Veamos cómo podemos detectar grabados en este libro de 1847, restaurarlos y transformarlos en gráficos digitales modernos:
detectar_objetos(Fuente.grabados)
• Imagen fuente • Página fuente • Catálogo ilustrado de Harper (1847) • Biblioteca del Congreso •
restaurar_objetos(Fuente.grabados)
• Imagen fuente • Página fuente • Catálogo ilustrado de Harper (1847) • Biblioteca del Congreso •
visual_to_digital_graphic_prompt = """ Transforme este objeto visual en un gráfico digital plano a todo color, ampliando el contenido para lograr un efecto de sangrado completo. """ colorize(Source.engravings, visual_to_digital_graphic_prompt)
• Imagen fuente • Página fuente • Catálogo ilustrado de Harper (1847) • Biblioteca del Congreso •
🧪 También podemos transformar los mismos grabados en fotografías con un mensaje muy sencillo:
visual_to_photo_prompt = """ Transforme esta imagen en una fotografía de cámara moderna y de alta gama. """ colorize(Source.engravings, visual_to_photo_prompt)
• Imagen fuente • Página fuente • Catálogo ilustrado de Harper (1847) • Biblioteca del Congreso •
💡 Como las fotos generalmente están a sangre completa, no es necesario que el mensaje especifique una composición.
Realmente depende de nuestra imaginación, ya que Nano Banana parece captar todos los aspectos de la semántica visual.
Agreguemos un paso final para ver hasta dónde podemos llegar, reinventando imágenes como fotografías cinematográficas…
🎞️ Cinematización
Hasta ahora hemos utilizado indicaciones bastante “cerradas”, elaborando instrucciones y restricciones específicas para controlar las salidas. Es posible ir aún más lejos con indicaciones de "abrir" y generar imágenes en modo totalmente creativo. En particular, puede resultar interesante hacer referencia a la terminología fotográfica o cinematográfica, ya que abarca muchas técnicas visuales.
A continuación se muestra una posible función de cinematización genérica para reimaginar imágenes como fotografías de películas:
CINEMATIZATION_PROMPT = """ Vuelva a imaginar esta imagen como una película cinematográfica de acción en vivo moderna y alegre que aún presenta iluminación y composición profesionales. """ cinematize = object_editing_function( CINEMATIZATION_PROMPT, WorkflowStep.RESTORED, WorkflowStep.CINEMATIZED,)
🧪 Cinematicemos el dibujo “Alicia en el país de las maravillas”:
cinematizar (Fuente.alice_drawing)
• Imagen fuente • Página fuente • Libro abierto que muestra una ilustración y un texto de Las aventuras de Alicia en el país de las maravillas • Foto de Brett Jordan en Unsplash •
💡 Todavía parece una película de alto presupuesto. Hay muchos grados de libertad en el mensaje, pero es probable que obtenga figuras en primer plano nítidamente enfocadas, un fondo borroso gradual, iluminación de "hora dorada" (un ingrediente mágico para muchos directores de fotografía) y texturas detalladas. Estas composiciones realmente evocan atmósferas diferentes en comparación con las fotografías generadas en la prueba anterior.
🧪 Probemos el flujo de trabajo en una página del Maravilloso Mago de Oz que contiene tres dibujos:
detectar_objetos(Fuente.wizard_of_oz_drawings)
• Imagen fuente • Página fuente • El maravilloso Mago de Oz (1899) • Biblioteca del Congreso, División de libros raros y colecciones especiales •
restaurar_objetos (Fuente.wizard_of_oz_drawings)
• Imagen fuente • Página fuente • El maravilloso Mago de Oz (1899) • Biblioteca del Congreso, División de libros raros y colecciones especiales •
cinematizar (Fuente.wizard_of_oz_drawings)
• Imagen fuente • Página fuente • El maravilloso Mago de Oz (1899) • Biblioteca del Congreso, División de libros raros y colecciones especiales •
💡 El elenco de una nueva película está listo 😉
Las imágenes cinematográficas tienen varios casos de uso:
Estas fotografías cinematográficas pueden ser “imágenes de referencia” perfectas para modelos de generación de vídeo como Veo. Consulte Generar vídeos Veo a partir de imágenes de referencia. Al ser representaciones fotorrealistas, también pueden ser una fuente para generar imágenes 2D o 3D, de cualquier estilo, con figuras realistas, proporciones perfectas, iluminación avanzada, composiciones mejoradas… Puedes utilizarlas en multitud de contextos profesionales o para productos de alta gama: presentaciones, revistas, carteles, storyboards, sesiones de lluvia de ideas…
🏁 Conclusión
La comprensión espacial nativa de Gemini permite la detección de objetos visuales específicos basándose en un único mensaje en lenguaje natural. Probamos la detección de ilustraciones en fotografías de libros, algo que los modelos tradicionales de aprendizaje automático (ML) suelen pasar por alto, ya que normalmente están entrenados para detectar personas, animales, vehículos, alimentos y un conjunto finito de clases de objetos físicos. Probamos la detección de ilustraciones rectas, inclinadas e incluso significativamente deformadas, y siempre se identificaron con precisión. La implementación principal fue sencilla y requirió un código mínimo utilizando el SDK de Python y mensajes personalizados. En comparación, ajustar un modelo de detección de objetos tradicional requiere mucho tiempo: implica ensamblar un conjunto de datos de imágenes, etiquetar objetos y administrar trabajos de entrenamiento. Esta solución es muy flexible: podríamos pasar de detectar ilustraciones a componentes electrónicos, adaptando el mensaje, manteniendo el código sin cambios. El uso de resultados estructurados (con un esquema JSON o clases de Pydantic y el SDK de Python) hace que el código sea fácil de implementar y esté listo para implementarse en producción. Luego, Nano Banana permite editar estos objetos visuales prácticamente de cualquier forma imaginable. Probamos un flujo de trabajo con pasos de restauración, coloración e incluso cinematización, utilizando indicaciones imperativas y descriptivas. Las posibilidades parecen realmente infinitas y los principios de esta exploración se pueden reutilizar en diferentes contextos.
➕ ¡Más!
Gracias por leer. ¡Déjame saber si creas algo interesante!