En el bucket alpinashop-catalogo hay 60 GB de imágenes. Unas 2.400 fichas de producto, cada una con entre cinco y diez fotos, organizadas en productos/<sku>/original/, productos/<sku>/web/ y productos/<sku>/thumb/.

Sobre esas imágenes, AlpinaShop sabe exactamente una cosa: en qué carpeta están. Nada más.

Y eso se traduce en cuatro problemas concretos que están costando dinero ahora mismo:

La web no es accesible. Ninguna imagen tiene atributo alt. Un lector de pantalla anuncia "imagen" y punto. Además de ser una barrera real para clientes con discapacidad visual, es un problema de posicionamiento y una posible incidencia de cumplimiento.

El filtro por color no existe. Marketing lleva un año pidiendo que se pueda filtrar el catálogo por color. El campo color de la ficha lo rellena el proveedor cuando le apetece, con valores como "azul", "azul marino", "navy", "blau" y en muchos casos vacío.

Nadie modera las fotos de los clientes. Desde hace unos meses, las opiniones admiten fotos. Se publican directamente, sin revisión. Es cuestión de tiempo que alguien suba algo inapropiado y aparezca en una ficha de producto.

El catálogo es visualmente irregular. Unas fotos tienen fondo blanco, otras el suelo del almacén del proveedor. Nadie ha auditado 20.000 imágenes.

Los cuatro se resuelven con la misma herramienta y sin entrenar nada. Es el mismo principio de la lección anterior —no entrenes lo que ya está entrenado— aplicado ahora a píxeles.

Contenido

  1. Qué ofrece la Cloud Vision API
  2. Primera llamada y lectura de la respuesta
  3. Procesamiento masivo con asyncBatchAnnotate
  4. De los JSON de salida a alpinashop_analitica
  5. Coste por millar de imágenes y cómo controlarlo
  6. Aplicación 1: el texto alternativo de accesibilidad
  7. Aplicación 2: colores dominantes y el filtro de la tienda
  8. Aplicación 3: moderar las fotos de los clientes con SafeSearch
  9. Aplicación 4: auditar el estándar visual del catálogo
  10. Arquitectura por eventos con imagenes-subidas
  11. OCR: texto en imágenes y documentos
  12. Límites y sesgos: dónde falla con material de montaña
  13. Cuándo dar el salto a AutoML Vision
  14. Document AI para facturas y albaranes
  15. Rostros, biometría y AI Act: la advertencia seria

  1. Qué ofrece la Cloud Vision API

Una sola API con varias funciones que se piden por separado. Cada función solicitada factura de forma independiente, así que conviene saber cuál sirve para qué.

Función Qué devuelve Uso en AlpinaShop
Detección de etiquetas (LABEL_DETECTION) Conceptos presentes en la imagen, con confianza Base del texto alternativo
Detección de objetos (OBJECT_LOCALIZATION) Objetos con su recuadro y confianza Comprobar encuadre y detectar objetos intrusos
OCR (TEXT_DETECTION) Texto de la imagen con su posición Leer tallas, marcas y códigos en las fotos
OCR de documentos (DOCUMENT_TEXT_DETECTION) Texto estructurado en páginas, bloques y párrafos Documentos escaneados, no fotos de producto
Logos (LOGO_DETECTION) Marcas comerciales reconocidas Verificar que la marca corresponde a la ficha
Puntos de referencia (LANDMARK_DETECTION) Lugares famosos Marginal: fotos de ambiente en montaña
Propiedades de imagen (IMAGE_PROPERTIES) Colores dominantes en RGB con fracción de píxeles El filtro por color
SafeSearch (SAFE_SEARCH_DETECTION) Probabilidad de contenido adulto, violento, médico, etc. Moderación de fotos de clientes
Detección web (WEB_DETECTION) Imágenes similares en la web y entidades asociadas Detectar uso no autorizado de fotos propias
Recorte sugerido (CROP_HINTS) Recortes recomendados por relación de aspecto Generar miniaturas bien encuadradas
Rostros (FACE_DETECTION) Posición y atributos de caras Ver el apartado 15 antes de usarla

Dos precisiones importantes desde el principio.

Detección de etiquetas y detección de objetos no son lo mismo. La primera dice qué conceptos hay ("mochila", "montaña", "azul", "equipamiento"). La segunda dice qué objetos hay y dónde, con coordenadas. Para describir una imagen basta la primera; para comprobar si el producto está centrado y ocupa el encuadre correcto, hace falta la segunda.

La detección web es la menos conocida y tiene un uso muy concreto para una tienda: descubrir si tus fotos de catálogo están siendo usadas por competidores. No es el objetivo de esta lección, pero conviene saber que existe.

  1. Primera llamada y lectura de la respuesta

Tras habilitar la API (gcloud services enable vision.googleapis.com --project=alpinashop-datos):

from google.cloud import vision

cliente = vision.ImageAnnotatorClient()

imagen = vision.Image()
imagen.source.image_uri = "gs://alpinashop-catalogo/productos/MOC-4471/web/01.jpg"

respuesta = cliente.annotate_image({
    "image": imagen,
    "features": [
        {"type_": vision.Feature.Type.LABEL_DETECTION,      "max_results": 10},
        {"type_": vision.Feature.Type.OBJECT_LOCALIZATION,  "max_results": 5},
        {"type_": vision.Feature.Type.IMAGE_PROPERTIES},
        {"type_": vision.Feature.Type.SAFE_SEARCH_DETECTION},
    ],
})

if respuesta.error.message:
    raise RuntimeError(respuesta.error.message)

Nota clave: la imagen se referencia por su URI de Cloud Storage, no se sube en la petición. La API la lee directamente del bucket. Eso significa que no hay que descargar 60 GB a ningún sitio, pero también que la cuenta de servicio necesita permiso de lectura sobre el bucket. Es el fallo de configuración número uno de esta lección.

La respuesta, campo a campo:

# --- Etiquetas ---
for etiqueta in respuesta.label_annotations:
    print(f"{etiqueta.description:25} score={etiqueta.score:.3f}  mid={etiqueta.mid}")
# Mochila          score=0.968  mid=/m/0n1cs
# Bagage           score=0.944
# Equipamiento     score=0.911
# Azul             score=0.887
  • description: el concepto, en el idioma que pidas. Se controla con image_context.language_hints=["es"].
  • score: confianza entre 0 y 1. Por debajo de 0,6 las etiquetas suelen ser ruido.
  • mid: identificador estable del concepto en el grafo de conocimiento de Google. Es más fiable que el texto para agrupar: /m/0n1cs es siempre lo mismo aunque la traducción cambie.

Los objetos llegan en respuesta.localized_object_annotations, cada uno con su name, su score y un bounding_poly.normalized_vertices —por ejemplo, Backpack 0.921 x[0.18-0.83] y[0.09-0.94]—. Las coordenadas son normalizadas: van de 0 a 1 respecto al ancho y alto de la imagen, no en píxeles. Eso las hace independientes de la resolución, lo cual es muy cómodo porque el mismo umbral sirve para la foto original y para la miniatura. Con esos cuatro números se calcula qué fracción del encuadre ocupa el producto y si está centrado, que es justo lo que hace falta en el apartado 9.

Los colores dominantes llegan en respuesta.image_properties_annotation.dominant_colors.colors, cada uno con su RGB, su pixel_fraction y su score. Son dos campos que se confunden: pixel_fraction es la proporción de píxeles de ese color y score la relevancia visual estimada. En una foto de estudio, el primer resultado suele ser RGB(245,245,244) con fracción 0,61 —el fondo blanco— y el color real del producto viene detrás. Ese es exactamente el problema del apartado 7.

Y el SafeSearch se lee en respuesta.safe_search_annotation, con los campos adult, violence, racy, medical y spoof:

s = respuesta.safe_search_annotation
print(s.adult.name, s.violence.name, s.racy.name, s.medical.name, s.spoof.name)
# VERY_UNLIKELY VERY_UNLIKELY UNLIKELY VERY_UNLIKELY VERY_UNLIKELY

SafeSearch no devuelve un booleano: devuelve uno de seis niveles (UNKNOWN, VERY_UNLIKELY, UNLIKELY, POSSIBLE, LIKELY, VERY_LIKELY) para cada una de cinco categorías. Dónde se pone el corte es una decisión de política editorial, no técnica —apartado 8.

  1. Procesamiento masivo con asyncBatchAnnotate

Llamar 20.000 veces a annotate_image funciona, pero es lento, gestiona mal las cuotas y satura el proceso local. Para volumen existe asyncBatchAnnotate: un trabajo asíncrono que procesa un lote de imágenes y escribe los resultados directamente en Cloud Storage, sin que tu proceso tenga que esperar.

from google.cloud import vision

cliente = vision.ImageAnnotatorClient()

def peticion(uri):
    return vision.AnnotateImageRequest(
        image=vision.Image(source=vision.ImageSource(image_uri=uri)),
        features=[
            vision.Feature(type_=vision.Feature.Type.LABEL_DETECTION, max_results=10),
            vision.Feature(type_=vision.Feature.Type.IMAGE_PROPERTIES),
            vision.Feature(type_=vision.Feature.Type.OBJECT_LOCALIZATION, max_results=5),
        ],
        image_context=vision.ImageContext(language_hints=["es"]),
    )

salida = vision.OutputConfig(
    gcs_destination=vision.GcsDestination(
        uri="gs://alpinashop-datalake/vision/catalogo/"),
    batch_size=100,           # imagenes por fichero JSON de salida
)

operacion = cliente.async_batch_annotate_images(
    requests=[peticion(u) for u in uris_lote],   # hasta 2000 por operacion
    output_config=salida,
)

resultado = operacion.result(timeout=3600)

Los cuatro puntos que hay que entender:

Es asíncrono de verdad. async_batch_annotate_images devuelve una operación de larga duración. Puedes esperarla con operacion.result() o guardarte el nombre de la operación y consultarla después. Para 20.000 imágenes, lo sensato es lo segundo y dejar que un Workflow lo supervise.

Los resultados van a Cloud Storage, no a tu proceso. Se escriben como ficheros JSON en el prefijo indicado. Tu memoria no ve 20.000 respuestas.

batch_size agrupa las respuestas en ficheros. Con 100, cada JSON contiene 100 anotaciones. Ficheros muy pequeños generan miles de objetos que luego hay que listar; muy grandes son incómodos de procesar. Entre 50 y 200 es razonable.

Hay un tope de peticiones por operación (del orden de 2.000). Para las 20.000 imágenes de AlpinaShop hay que trocear en varias operaciones, lanzándolas de forma escalonada.

Y la preparación del listado de imágenes se hace con un gcloud storage ls --recursive "gs://alpinashop-catalogo/productos/**/web/*.jpg", aplicando dos filtros que ahorran dinero:

Solo web/. Las carpetas original/, web/ y thumb/ contienen la misma imagen en tres resoluciones. Analizar las tres multiplica el coste por tres sin aportar absolutamente nada. Este único filtro divide la factura entre tres.

Solo una o dos fotos por SKU para el análisis inicial. Si el objetivo es describir el producto y extraer su color, la foto principal basta. Las demás se procesan solo si hacen falta para la auditoría de encuadre.

  1. De los JSON de salida a alpinashop_analitica

Los ficheros JSON en Cloud Storage no sirven de nada mientras no sean consultables. El puente es un bq load --source_format=NEWLINE_DELIMITED_JSON --autodetect sobre el prefijo de salida. En la práctica, la salida de asyncBatchAnnotate viene anidada como un array responses, así que suele hacer falta un paso de aplanado previo: para volúmenes moderados, un script Python que lea los JSON y escriba filas planas; para volúmenes grandes, un trabajo de Dataflow (04-02).

La tabla normalizada de destino:

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.imagenes_vision`
PARTITION BY fecha_analisis
CLUSTER BY sku AS
SELECT
  REGEXP_EXTRACT(uri, r'productos/([^/]+)/')            AS sku,
  uri,
  ARRAY(SELECT AS STRUCT descripcion, score, mid
        FROM UNNEST(etiquetas) WHERE score >= 0.60)     AS etiquetas,
  colores,
  objetos,
  safesearch_adult, safesearch_violence, safesearch_racy,
  CURRENT_DATE()                                        AS fecha_analisis,
  'vision-api-v1'                                       AS modelo_version
FROM `alpinashop-datos.alpinashop_analitica.raw_vision_catalogo`;

El filtro score >= 0.60 dentro del ARRAY es deliberado: las etiquetas por debajo de esa confianza son casi siempre ruido genérico ("producto", "objeto", "fotografía") que ensucia cualquier análisis posterior.

Y de nuevo modelo_version y fecha_analisis, por la misma razón de 05-04: las APIs preentrenadas se actualizan solas, y sin esas columnas es imposible saber si un cambio en los resultados viene del modelo o de las imágenes.

La comprobación inmediata es un GROUP BY sobre UNNEST(etiquetas) contando imágenes por descripción. Ese listado responde de un vistazo a "¿qué ve la máquina en mi catálogo?" y suele deparar sorpresas: etiquetas genéricas dominando el ranking, o conceptos inesperados que revelan fotos mal clasificadas.

  1. Coste por millar de imágenes y cómo controlarlo

La Cloud Vision API factura por unidad, donde una unidad es una imagen por cada función solicitada. Es la misma lógica de 05-04 y tiene la misma consecuencia: pedir cuatro funciones sobre una imagen son cuatro unidades.

Las tarifas se estructuran por tramos —hay un tramo mensual gratuito y el precio por millar baja al aumentar el volumen— y cambian con el tiempo: consulta siempre el tarificador oficial vigente. Como orden de magnitud, el coste por millar de unidades se mueve en el entorno de unos pocos euros para las funciones habituales.

La estimación para AlpinaShop, hecha antes de gastar:

Escenario Imágenes Funciones Unidades
Todo el bucket, todas las funciones 20.000 5 100.000
Solo web/, todas las funciones ~7.000 5 35.000
Solo web/, foto principal, 3 funciones 2.400 3 7.200
Fotos nuevas al mes ~200 3 600

La diferencia entre la primera y la tercera fila es un factor de casi 14. Y la tercera opción cubre los cuatro problemas del inicio de la lección. Ese es el trabajo real de esta sección: no negociar el precio, sino no pedir lo que no se necesita.

Las cuatro medidas de control:

  1. Filtrar por carpeta. Solo web/. Divide por tres.
  2. Una foto por SKU para etiquetas y color. Divide por otro tanto.
  3. Pedir solo las funciones necesarias. SafeSearch no tiene sentido sobre fotos de catálogo del proveedor: se reserva para las fotos que suben los clientes.
  4. No reprocesar. Guardar el resultado y analizar solo lo nuevo, con la misma consulta incremental de 05-04.

La consulta incremental es la misma idea de 05-04: LEFT JOIN del inventario de imágenes contra imagenes_vision, filtrando v.uri IS NULL y carpeta = 'web', con un LIMIT 2000 como tope de seguridad.

Y las etiquetas de facturación (centro-coste:analitica) con su alerta de presupuesto, igual que en todo el módulo.

  1. Aplicación 1: el texto alternativo de accesibilidad

El problema: 20.000 imágenes sin alt. La solución con etiquetas es directa, pero hay que hacerla bien.

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.imagenes_alt` AS
WITH etiquetas_utiles AS (
  SELECT
    v.sku, v.uri,
    ARRAY_AGG(e.descripcion ORDER BY e.score DESC LIMIT 3) AS conceptos
  FROM `alpinashop-datos.alpinashop_analitica.imagenes_vision` v,
       UNNEST(v.etiquetas) e
  WHERE e.score >= 0.75
    AND LOWER(e.descripcion) NOT IN
        ('producto','objeto','fotografia','imagen','fondo','blanco')
  GROUP BY v.sku, v.uri
)
SELECT
  t.sku, t.uri,
  CONCAT(p.nombre, '. ',
         ARRAY_TO_STRING(t.conceptos, ', '),
         '. Marca ', p.marca, '.')            AS alt_propuesto,
  ARRAY_LENGTH(t.conceptos)                   AS n_conceptos,
  FALSE                                       AS revisado
FROM etiquetas_utiles t
JOIN `alpinashop-datos.alpinashop_analitica.productos` p USING (sku);

Las tres decisiones de esta consulta:

Empezar por el nombre del producto, no por las etiquetas. El alt más útil para un lector de pantalla es "Mochila Trek 40L Azul. Mochila, equipamiento de montaña, azul. Marca Alpina." La información que la tienda ya tiene es más precisa que la que adivina el modelo; la API la complementa, no la sustituye.

Lista negra de etiquetas genéricas. "Producto", "objeto" o "fotografía" no aportan nada a nadie y aparecen constantemente.

revisado = FALSE por defecto. Es una propuesta, no un texto publicado. Y aquí viene lo importante.

Un alt incorrecto es peor que no tener alt. Una persona ciega que oye "chaqueta, textil, rojo" sobre una foto de una mochila azul recibe información falsa, sin ninguna forma de detectar el error. La ausencia de alt es una carencia; un alt erróneo es engaño.

El flujo correcto es escalonado: publicar automáticamente solo las propuestas de alta confianza —aquellas donde la etiqueta principal coincide con la categoría de la ficha— y enviar el resto a revisión humana. Con 2.400 fichas, si el 80 % coincide, quedan unas 480 para revisar: una tarde de trabajo frente a un proyecto de semanas, y el resultado es correcto.

Y una nota de alcance: el alt que genera esta técnica es descriptivo y correcto, pero plano. Redacciones más naturales y ricas se consiguen combinando las etiquetas con Gemini, que ve la imagen y escribe la frase. Eso es 05-06.

  1. Aplicación 2: colores dominantes y el filtro de la tienda

El campo color de las fichas es un desastre de valores libres. Los colores dominantes de la API son objetivos y consistentes, pero tienen un problema evidente: el fondo suele ser el color dominante.

CREATE OR REPLACE TABLE `alpinashop-datos.alpinashop_analitica.productos_color` AS
WITH dominante AS (
  SELECT
    v.sku, c.red, c.green, c.blue, c.pixel_fraction,
    ROW_NUMBER() OVER (PARTITION BY v.sku ORDER BY c.score DESC) AS pos
  FROM `alpinashop-datos.alpinashop_analitica.imagenes_vision` v,
       UNNEST(v.colores) c
  WHERE NOT (c.red > 225 AND c.green > 225 AND c.blue > 225)   -- fondo blanco
    AND NOT (c.red < 30  AND c.green < 30  AND c.blue < 30)    -- fondo negro
    AND c.pixel_fraction >= 0.05
)
SELECT
  sku, red, green, blue, ROUND(pixel_fraction, 3) AS fraccion,
  CASE
    WHEN red > 150 AND green < 90  AND blue < 90        THEN 'rojo'
    WHEN blue > 130 AND red < 110 AND green < 130       THEN 'azul'
    WHEN green > 120 AND red < 120 AND blue < 120       THEN 'verde'
    WHEN red > 190 AND green > 140 AND blue < 90        THEN 'naranja'
    WHEN red > 190 AND green > 190 AND blue < 120       THEN 'amarillo'
    WHEN red < 80  AND green < 80  AND blue < 80        THEN 'negro'
    WHEN red > 190 AND green > 190 AND blue > 190       THEN 'blanco'
    WHEN ABS(red-green) < 28 AND ABS(green-blue) < 28   THEN 'gris'
    ELSE 'otro'
  END AS color_comercial
FROM dominante
WHERE pos = 1;

Los dos filtros del principio son la clave de todo el apartado. Sin ellos, el 90 % de los productos del catálogo saldrían clasificados como "blanco", porque el fondo de estudio ocupa más píxeles que el producto. Descartar los extremos de luminosidad y exigir al menos un 5 % de píxeles deja los colores que realmente pertenecen al objeto.

La traducción de RGB a nombre comercial es aproximada y es una decisión de negocio. Los umbrales de arriba son un punto de partida razonable; el color "azul marino" y el "azul eléctrico" caerán ambos en "azul", y eso probablemente está bien para un filtro de tienda. Si hiciera falta más finura, la conversión correcta pasa por el espacio HSV, donde el tono se separa de la saturación y el brillo.

Y la validación imprescindible, que cuesta diez minutos: un GROUP BY p.color, c.color_comercial uniendo productos_color con productos sobre las fichas que sí tienen el color declarado. Comparar contra los colores que sí están declarados en la ficha es la comprobación honesta: si "azul" en la ficha se corresponde con "azul" detectado en la mayoría de los casos, el método funciona. Es exactamente la misma idea que cruzar el sentimiento con las estrellas en 05-04: usar una etiqueta humana que ya existe para validar la salida automática.

  1. Aplicación 3: moderar las fotos de los clientes con SafeSearch

Este es el caso con más riesgo de los cuatro, y el que más se agradece tener resuelto antes de que haga falta.

SafeSearch devuelve cinco categorías con seis niveles cada una. La política de moderación de AlpinaShop:

Categoría Qué detecta Umbral de bloqueo Umbral de revisión
adult Contenido sexual explícito LIKELY POSSIBLE
violence Contenido violento o sangriento LIKELY POSSIBLE
racy Contenido sugerente VERY_LIKELY LIKELY
medical Contenido médico o quirúrgico — LIKELY
spoof Imagen manipulada o meme — LIKELY
flowchart TD
    A[Cliente sube foto en opinion] --> B[Almacenar en cuarentena]
    B --> C[SafeSearch]
    C -->|adult o violence LIKELY+| D[Rechazo automatico<br/>+ aviso al cliente]
    C -->|categoria en POSSIBLE| E[Cola de revision humana]
    C -->|todo VERY_UNLIKELY/UNLIKELY| F[Comprobar objeto de producto]
    F -->|producto detectado| G[Publicar]
    F -->|sin producto| E
    E --> H[Decision humana registrada]

Las cinco decisiones de diseño de esta arquitectura:

Cuarentena por defecto. La foto se almacena en un bucket privado y no es accesible públicamente hasta que pasa el filtro. Publicar primero y moderar después significa que el contenido problemático estuvo visible.

Tres salidas, no dos. Rechazo automático, revisión humana y publicación. La franja intermedia es donde vive la mayor parte de la ambigüedad, y forzar una decisión binaria garantiza errores en las dos direcciones.

medical y spoof nunca bloquean automáticamente. En una tienda de montaña, una foto de una rozadura del arnés o de una ampolla es contenido legítimo y útil en una opinión sobre unas botas. Bloquearla sería absurdo. Va a revisión.

Se comprueba también que aparezca un producto. Una foto perfectamente inocua pero que no muestra ningún producto —un paisaje, una captura de pantalla, una foto accidental— tampoco debería publicarse en una ficha. OBJECT_LOCALIZATION resuelve esto y aporta más valor que SafeSearch en el día a día.

Toda decisión queda registrada. Con la versión del modelo, los niveles devueltos y, si hubo revisión, quién decidió y cuándo. Es lo que se enseña si un cliente reclama que su foto fue rechazada injustamente.

NIVEL = {"UNKNOWN": 0, "VERY_UNLIKELY": 1, "UNLIKELY": 2,
         "POSSIBLE": 3, "LIKELY": 4, "VERY_LIKELY": 5}

def decidir(safesearch):
    s = {k: NIVEL[getattr(safesearch, k).name]
         for k in ("adult", "violence", "racy", "medical", "spoof")}
    if s["adult"] >= 4 or s["violence"] >= 4 or s["racy"] >= 5:
        return "RECHAZADA", s
    if max(s.values()) >= 3:
        return "REVISION", s
    return "APTA", s

Y la advertencia final del apartado, que es de gestión y no técnica: ningún filtro automático es infalible en ninguna de las dos direcciones. Habrá contenido problemático que pase y contenido inocuo que se bloquee. Por eso hace falta, además del filtro, un canal de denuncia para los usuarios y un procedimiento de retirada rápida. La moderación automática reduce el volumen de trabajo humano; no lo elimina, y no traslada la responsabilidad a la máquina.

  1. Aplicación 4: auditar el estándar visual del catálogo

El estándar de AlpinaShop dice que la foto principal debe tener fondo claro uniforme, el producto centrado ocupando entre el 50 % y el 85 % del encuadre, y ningún objeto ajeno. Nadie lo ha verificado nunca.

Con OBJECT_LOCALIZATION e IMAGE_PROPERTIES se comprueba solo:

CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_auditoria_catalogo` AS
WITH metricas AS (
  SELECT
    v.sku, v.uri,
    -- Area del objeto principal (coordenadas normalizadas 0-1)
    (o.x_max - o.x_min) * (o.y_max - o.y_min)      AS area_producto,
    ABS(((o.x_min + o.x_max) / 2) - 0.5)           AS desvio_h,
    ABS(((o.y_min + o.y_max) / 2) - 0.5)           AS desvio_v,
    (SELECT MAX(c.pixel_fraction) FROM UNNEST(v.colores) c
      WHERE c.red > 215 AND c.green > 215 AND c.blue > 215) AS fondo_claro,
    ARRAY_LENGTH(v.objetos)                        AS n_objetos
  FROM `alpinashop-datos.alpinashop_analitica.imagenes_vision` v,
       UNNEST(v.objetos) o
  WHERE o.principal
)
SELECT
  sku, uri, ROUND(area_producto, 3) AS area, n_objetos,
  ARRAY_TO_STRING(ARRAY(
    SELECT problema FROM UNNEST([
      IF(area_producto < 0.50,               'producto_pequeno',  NULL),
      IF(area_producto > 0.85,               'producto_recortado',NULL),
      IF(desvio_h > 0.12 OR desvio_v > 0.12, 'descentrado',       NULL),
      IF(IFNULL(fondo_claro, 0) < 0.35,      'fondo_no_estandar', NULL),
      IF(n_objetos > 2,                      'objetos_ajenos',    NULL)
    ]) AS problema WHERE problema IS NOT NULL), ', ') AS problemas
FROM metricas;

La lista de trabajo sale de unir esa vista con productos y con las ventas de los últimos doce meses, filtrando problemas != '' y ordenando por unidades vendidas descendente.

Y aquí está el patrón que se repite en todo el módulo, ya por cuarta vez. El resultado no es un sistema que rechaza fotos automáticamente. Es una lista de trabajo priorizada por impacto de negocio: cincuenta fichas, las más vendidas, con el problema concreto de cada una. Alguien la mira, decide y pide fotos nuevas al proveedor donde toque.

Un sistema automático que rechazara fotos sería más "inteligente" y mucho peor: se equivocaría en casos legítimos, bloquearía altas de producto y acabaría desactivado a las dos semanas.

  1. Arquitectura por eventos con imagenes-subidas

Todo lo anterior es procesamiento del histórico. Para las imágenes nuevas hace falta un flujo automático, y AlpinaShop ya tiene la pieza central: el topic imagenes-subidas de Pub/Sub (04-04).

flowchart LR
    A[Subida a Cloud Storage] -->|notificacion| B[Topic imagenes-subidas]
    B --> C[Suscripcion push]
    C --> D[Cloud Function 06-03]
    D --> E[Vision API]
    E --> D
    D --> F[BigQuery<br/>imagenes_vision]
    D -->|foto de cliente| G{SafeSearch}
    G -->|apta| H[Publicar]
    G -->|dudosa| I[Cola de revision]
    G -->|rechazada| J[Marcar y avisar]
    D -->|error| K[Dead letter topic]

La notificación de Cloud Storage a Pub/Sub se configura con gcloud storage buckets notifications create gs://alpinashop-catalogo --topic=imagenes-subidas --event-types=OBJECT_FINALIZE --object-prefix=productos/. OBJECT_FINALIZE se dispara cuando termina la subida de un objeto, y el prefijo evita generar eventos por ficheros que no interesan.

Cuatro cosas que ya sabes del módulo 4 y que aquí aplican tal cual:

Idempotencia. El mismo evento puede entregarse más de una vez. La clave de negocio es el URI del objeto: si ya existe una fila para ese URI con la misma versión de modelo, no se reprocesa. Sin esto, un reintento cuesta dinero dos veces.

Dead letter topic. Si el procesamiento falla repetidamente —imagen corrupta, formato no soportado—, el mensaje acaba en la cola de mensajes fallidos en lugar de reintentarse eternamente. Es exactamente el patrón de 04-04.

Autenticación OIDC en la suscripción push, para que solo Pub/Sub pueda invocar el endpoint.

Desacoplamiento. La subida de la imagen no espera al análisis. El proveedor sube la foto y termina; el análisis ocurre después.

La implementación de la función llega en 06-03, con Cloud Functions. Aquí queda definida la arquitectura y las garantías que debe cumplir.

  1. OCR: texto en imágenes y documentos

La API distingue dos modos de reconocimiento de texto:

Modo Pensado para Devuelve
TEXT_DETECTION Texto disperso en fotos Texto completo + cada palabra con su posición
DOCUMENT_TEXT_DETECTION Documentos densos escaneados Estructura en páginas, bloques, párrafos y palabras

Para las fotos de producto de AlpinaShop, TEXT_DETECTION tiene tres usos concretos y útiles:

  • Verificar la marca impresa en el producto contra la marca declarada en la ficha. Detecta errores de asignación de fotos.
  • Leer tallas y capacidades visibles en la imagen ("40L", "XL", "8000 mm"), útiles para comprobar que la foto corresponde a la variante correcta.
  • Detectar marcas de agua o texto promocional del proveedor que no debería aparecer en el catálogo ("50% OFF", el logotipo del distribuidor). Es un problema real y difícil de encontrar a mano.

Se pide con features=[{"type_": vision.Feature.Type.TEXT_DETECTION}] y se lee en respuesta.full_text_annotation.text. El primer elemento de text_annotations contiene todo el texto detectado; los siguientes, cada palabra con su recuadro. Y language_hints=["es","en"] importa: en material técnico se mezclan español e inglés constantemente.

  1. Límites y sesgos: dónde falla con material de montaña

Igual que en 05-04, toca la parte honesta.

Las etiquetas son genéricas por diseño. El modelo se entrenó con un vocabulario amplio y general. Para AlpinaShop, eso produce resultados como estos:

Producto real Etiquetas típicas devueltas Problema
Piolet técnico de cascada "Hacha", "Herramienta", "Metal" Categoría equivocada
Crampones de 12 puntas "Metal", "Calzado", "Herramienta" No lo reconoce
Chaqueta con membrana "Ropa exterior", "Chaqueta", "Azul" Correcto pero superficial
Mochila de 60 L "Mochila", "Equipaje", "Bolsa" Correcto
Arnés de escalada "Cinturón", "Correa", "Cuerda" Descripción parcial
Mosquetón HMS "Metal", "Herramienta", "Anillo" Irreconocible

El patrón es claro: cuanto más común es el objeto en la vida cotidiana, mejor lo identifica. Una mochila, perfecto. Un mosquetón HMS, un "anillo de metal". La razón es simple y no tiene remedio dentro de la API preentrenada: el modelo vio millones de mochilas durante su entrenamiento y muy pocos mosquetones.

Sesgos derivados del entrenamiento. El modelo refleja lo que había en sus datos: es más preciso con marcas y estilos de producto habituales en los mercados sobrerrepresentados, y puede asociar etiquetas de género a prendas técnicas de forma discutible. Para AlpinaShop, el efecto es menor —hablamos de objetos, no de personas—, pero merece una comprobación: si el filtro de color o las etiquetas se comportan sistemáticamente peor en alguna categoría, hay que saberlo antes de que un cliente lo note.

Y el límite operativo: las etiquetas no son estables en el tiempo. El modelo se actualiza y las etiquetas pueden cambiar entre ejecuciones. Por eso modelo_version y fecha_analisis no son opcionales.

  1. Cuándo dar el salto a AutoML Vision

La tabla del apartado 12 dibuja la frontera con precisión.

Necesidad Herramienta
Describir una imagen en términos generales Vision API
Extraer colores dominantes Vision API
Moderar contenido Vision API (SafeSearch)
Leer texto Vision API (OCR)
Comprobar encuadre y composición Vision API (objetos)
Clasificar en la taxonomía propia de la tienda AutoML Vision (05-02)
Distinguir mochila de travesía de mochila de ataque AutoML Vision
Detectar un defecto de fabricación concreto AutoML Vision (detección de objetos)
Describir la imagen en lenguaje natural rico Gemini (05-06)

El criterio en una frase: si tu vocabulario es el del mundo, usa Vision API; si tu vocabulario es el tuyo, entrena con AutoML Vision.

Y las dos cosas se complementan bien. En 05-02, AlpinaShop entrenó AutoML Vision con su taxonomía de ocho categorías precisamente porque la API genérica no distinguía sus productos. La API sigue aportando lo que AutoML no da: colores, texto, moderación, encuadre. No es una sustituye a la otra: se piden funciones distintas sobre la misma imagen.

Un flujo combinado razonable para cada foto nueva del catálogo: Vision API para colores, OCR y encuadre; AutoML Vision para la categoría propia; y Gemini, cuando llegue, para redactar la descripción.

  1. Document AI para facturas y albaranes

Hay un problema de AlpinaShop que no es de imágenes de producto: las facturas y albaranes de proveedor llegan en PDF y alguien los teclea a mano en el ERP.

DOCUMENT_TEXT_DETECTION extrae el texto de un PDF escaneado, pero devuelve texto plano: hay que escribir expresiones regulares para encontrar el número de factura, el importe o las líneas de detalle, y esas expresiones se rompen con cada proveedor y con cada cambio de plantilla.

Document AI es un producto distinto y resuelve el problema de otra forma: usa procesadores especializados por tipo de documento que devuelven campos estructurados.

Aspecto Vision OCR Document AI
Salida Texto plano con posiciones Campos con nombre y valor
Facturas Requiere parseo propio Procesador de facturas dedicado
Tablas Hay que reconstruirlas Extracción de tablas nativa
Documentos propios No aplica Procesador personalizado entrenable
Coste por página Menor Mayor

Para AlpinaShop, el caso de uso natural sería un procesador de facturas que extraiga proveedor, número, fecha, base imponible, IVA, total y líneas de detalle, y que vuelque el resultado en BigQuery para conciliarlo con los pedidos de compra. Con las mismas cautelas de siempre: una factura contiene datos personales y financieros, y la extracción automática requiere validación humana antes de contabilizar nada.

Es un producto que merece su propio proyecto y queda fuera del alcance de esta lección. Lo relevante aquí es saber que existe y no intentar hacer con OCR y expresiones regulares lo que tiene una herramienta específica.

  1. Rostros, biometría y AI Act: la advertencia seria

Este apartado es corto y es el más importante de la lección.

La Cloud Vision API ofrece detección de rostros (FACE_DETECTION). Devuelve la posición de cada cara en la imagen, puntos de referencia faciales y probabilidades de expresión (alegría, sorpresa, enfado). No realiza reconocimiento facial: no identifica quién es la persona ni la compara con una base de datos.

Esa distinción es importante técnicamente y no basta legalmente. Conviene entender por qué.

Detección de rostros. Determinar que hay una cara en una imagen y dónde está. En sí mismo no identifica a nadie.

Reconocimiento facial. Determinar quién es esa persona. Implica tratamiento de datos biométricos, que el RGPD clasifica como categoría especial de datos (artículo 9), con un régimen mucho más estricto: prohibición general con excepciones tasadas, entre ellas el consentimiento explícito.

Reconocimiento de emociones. Inferir el estado emocional a partir de la cara. El AI Act europeo contempla restricciones específicas para los sistemas de reconocimiento de emociones, especialmente en los ámbitos laboral y educativo, y establece obligaciones de transparencia en otros contextos. Los atributos de expresión que devuelve la detección de rostros caen en un terreno que hay que valorar con criterio jurídico, no técnico.

Dónde aparece esto en AlpinaShop, sin que nadie lo haya buscado:

  • Fotos que suben los clientes en las opiniones. Una persona se fotografía con la mochila puesta. Su cara está en la imagen.
  • Fotos de catálogo con modelos. Las prendas se fotografían sobre personas.
  • Fotos de ambiente de escalada o travesía enviadas por el proveedor.

En ninguno de los tres casos AlpinaShop necesita analizar caras. Y ahí está la recomendación práctica:

No solicites FACE_DETECTION si no tienes una necesidad de negocio clara, documentada y validada jurídicamente. No la pidas "por si acaso" ni porque venga incluida en un ejemplo copiado. Cada función que se solicita es una decisión sobre qué datos se tratan.

Si en algún momento hiciera falta —por ejemplo, para difuminar caras antes de publicar una foto de cliente, que es un uso protector y razonable—, entonces habría que documentar la finalidad y la base legal, aplicar minimización tratando solo lo imprescindible, no conservar los datos faciales más allá del procesamiento, e informar a los usuarios en la política de privacidad y en el propio formulario de subida.

Recomendación expresa. Antes de habilitar cualquier función de análisis facial, un profesional de compliance o el DPO debe determinar: si el tratamiento implica datos biométricos bajo el artículo 9 del RGPD; la base legal aplicable y si se requiere consentimiento explícito; la clasificación del sistema bajo el AI Act y las obligaciones asociadas, con atención especial a las restricciones sobre reconocimiento de emociones; la necesidad de una evaluación de impacto (EIPD); y los requisitos de información a los interesados. Esta lección describe controles técnicos y no constituye asesoramiento jurídico. Todos los datos son ficticios.

Y para el resto de funciones —etiquetas, colores, OCR, SafeSearch sobre imágenes de producto— las precauciones son las de siempre: la imagen se envía a la API, hay que verificar en la documentación oficial la disponibilidad de endpoints regionales si la residencia del dato en la UE es un requisito, y las fotos de clientes deben tener un plazo de conservación definido y una vía de supresión a petición del interesado.

Errores Comunes y Consejos

Analizar original/, web/ y thumb/. Es la misma imagen tres veces. Multiplica el coste por tres sin aportar nada.

Olvidar el permiso de lectura sobre el bucket. La API lee la imagen desde Cloud Storage con la cuenta de servicio. Sin roles/storage.objectViewer, todas las llamadas fallan.

Confundir pixel_fraction con relevancia. El fondo suele ganar en fracción de píxeles. Sin filtrar los extremos de luminosidad, todo el catálogo sale "blanco".

Publicar el alt generado sin revisión. Un texto alternativo incorrecto es peor que ninguno: engaña a quien no puede verificarlo.

Tratar SafeSearch como un booleano. Son cinco categorías con seis niveles. Forzar una decisión binaria produce falsos bloqueos y falsos pases.

Bloquear automáticamente por medical. En una tienda de montaña, una foto de una rozadura es contenido legítimo en una opinión.

Pedir todas las funciones sobre todas las imágenes. Cada función factura por separado. SafeSearch sobre fotos de catálogo del proveedor es dinero tirado.

Esperar precisión con material técnico. Un mosquetón HMS será "un anillo de metal". Si necesitas tu taxonomía, es AutoML Vision.

Pedir FACE_DETECTION sin necesidad. Ver el apartado 15.

Consejo: guarda el mid de cada etiqueta, no solo el texto. Es un identificador estable que no depende del idioma ni de cambios de traducción.

Consejo: valida siempre contra un dato humano que ya tengas. El color declarado en la ficha, la categoría del producto, la marca. Es la comprobación más barata que existe y la que detecta los fallos silenciosos.

Ejercicios

Ejercicio 1

Marta calcula que analizar las imágenes del catálogo costará unos 500 € y le parece excesivo. Su plan era: todas las imágenes del bucket, con las funciones de etiquetas, objetos, propiedades, SafeSearch, OCR y logos. Reduce el coste al menos un 90 % sin perder ninguna de las cuatro aplicaciones de la lección, y justifica cada recorte.

Ejercicio 2

Diseña la comprobación automática que detecte fichas de producto cuya foto no corresponde al producto descrito. Indica qué señales usarías, cómo las combinarías, y qué harías con los resultados.

Ejercicio 3

Un cliente reclama que su foto fue rechazada injustamente en una opinión sobre unas botas. Describe qué información necesitas para responderle, qué tuvo que haberse registrado en el momento del rechazo, y qué cambiarías en el sistema si resulta que el rechazo fue incorrecto.

Soluciones

Solución 1

El plan original: 20.000 imágenes × 6 funciones = 120.000 unidades.

Los cuatro recortes, en orden de impacto:

Recorte 1 — Solo la carpeta web/. Las carpetas original/, web/ y thumb/ son la misma imagen en tres resoluciones. Analizar las tres es literalmente pagar tres veces por el mismo resultado. De 20.000 a unas 7.000 imágenes. Reducción: 65 %.

Recorte 2 — Solo la foto principal de cada SKU para etiquetas, color y OCR. Para describir el producto, extraer su color y leer su marca, la foto principal es suficiente: las demás son el mismo producto desde otro ángulo. De 7.000 a 2.400 imágenes. Reducción acumulada: 88 %.

Recorte 3 — Eliminar funciones innecesarias.

Función ¿Necesaria? Justificación
Etiquetas Sí Base del alt (aplicación 1)
Propiedades de imagen Sí Filtro por color (aplicación 2)
Objetos Sí Auditoría de encuadre (aplicación 4)
SafeSearch No aquí Son fotos del proveedor, no de clientes. Se reserva para el flujo de opiniones
OCR No masivo Solo sobre la muestra donde se sospeche marca de agua
Logos No La marca ya está en la ficha; no aporta

De 6 funciones a 3. Reducción acumulada: 94 %.

Recorte 4 — No reprocesar. Guardar los resultados en imagenes_vision y analizar solo lo nuevo con la consulta incremental del apartado 5. El coste recurrente pasa a ser de unas 200 imágenes al mes, es decir, prácticamente nada.

Resultado: 2.400 imágenes × 3 funciones = 7.200 unidades frente a 120.000. Una reducción del 94 %, y las cuatro aplicaciones de la lección siguen cubiertas:

Aplicación ¿Cubierta? Con qué
Texto alternativo Sí Etiquetas sobre la foto principal
Filtro por color Sí Propiedades de imagen
Moderación de fotos de clientes Sí SafeSearch en el flujo por eventos, no en el histórico
Auditoría de catálogo Sí Objetos sobre la foto principal

Lo que se pierde y hay que decirlo: la auditoría de encuadre solo cubre la foto principal, no todas las del carrusel. Es una limitación aceptable —la principal es la que ve el 90 % de los clientes—, y siempre se puede ampliar después a las fichas más vendidas.

La lección de fondo: no se ha negociado ningún precio ni se ha renunciado a ninguna funcionalidad. Simplemente se ha dejado de pedir lo que no se necesitaba. Es el mismo razonamiento del SELECT de columnas concretas en BigQuery (04-01).

Solución 2

Señales disponibles, y ninguna es concluyente por sí sola:

Señal Origen Fuerza
Categoría de AutoML Vision vs. categoría de la ficha 05-02 Alta
Etiqueta principal de Vision API vs. categoría Vision API Media
Color detectado vs. color declarado Vision API Media
Marca leída por OCR vs. marca de la ficha Vision API Alta cuando hay texto
Objeto detectado vs. tipo de producto Vision API Media
Ausencia total de objeto reconocible Vision API Alta (foto inservible)

La combinación: un sistema de puntos, no una regla única.

CREATE OR REPLACE VIEW `alpinashop-datos.alpinashop_analitica.v_fotos_sospechosas` AS
WITH senyales AS (
  SELECT
    v.sku, v.uri, p.nombre, p.categoria, p.marca,
    IF(a.categoria_predicha != p.categoria AND a.confianza > 0.85, 3, 0) AS p_automl,
    IF(t.marca_detectada IS NOT NULL
       AND UPPER(t.marca_detectada) != UPPER(p.marca), 3, 0)             AS p_marca,
    IF(ARRAY_LENGTH(v.objetos) = 0, 2, 0)                                AS p_sin_objeto,
    IF(c.color_comercial != LOWER(p.color) AND p.color IS NOT NULL, 1, 0) AS p_color,
    IF(NOT EXISTS(SELECT 1 FROM UNNEST(v.etiquetas) e WHERE e.score > 0.80
         AND LOWER(e.descripcion) LIKE CONCAT('%', LOWER(p.categoria), '%')), 1, 0)
                                                                         AS p_etiqueta
  FROM `alpinashop-datos.alpinashop_analitica.imagenes_vision` v
  JOIN `alpinashop-datos.alpinashop_analitica.productos` p USING (sku)
  LEFT JOIN `alpinashop-datos.alpinashop_analitica.imagenes_clasificadas` a USING (uri)
  LEFT JOIN `alpinashop-datos.alpinashop_analitica.productos_color` c USING (sku)
  LEFT JOIN `alpinashop-datos.alpinashop_analitica.imagenes_texto` t USING (uri)
)
SELECT *, p_automl + p_marca + p_sin_objeto + p_color + p_etiqueta AS puntuacion
FROM senyales
WHERE p_automl + p_marca + p_sin_objeto + p_color + p_etiqueta >= 3;

Por qué los pesos son distintos. La discrepancia de AutoML (3 puntos) y la de marca por OCR (3 puntos) son señales fuertes: AutoML está entrenado con la taxonomía propia y el texto impreso en el producto es difícil de malinterpretar. El color (1 punto) es la señal más débil porque un producto puede tener varias variantes de color compartiendo ficha, y porque la traducción RGB a nombre es aproximada. La ausencia de objeto (2 puntos) indica una foto inservible más que una foto equivocada.

Un solo punto no dispara nada. Un umbral de 3 exige o bien una señal fuerte, o bien la coincidencia de varias débiles. Esto reduce drásticamente los falsos positivos, que es lo que mata este tipo de sistemas: una lista con 800 falsas alarmas no la revisa nadie.

Qué hacer con los resultados, en tres niveles:

  1. Puntuación ≥ 6 (varias señales fuertes): revisión prioritaria, y si la ficha está activa y se vende, avisar a quien gestione el catálogo.
  2. Puntuación 3-5: cola de revisión ordenada por ventas de los últimos doce meses. El impacto de un error en un producto que vende 400 unidades no es el mismo que en uno que vende 2.
  3. Nunca despublicar automáticamente. Un falso positivo dejaría un producto sin foto en la tienda, que es peor que el problema que se intenta resolver.

Y una recomendación operativa: cada revisión humana genera una etiqueta. Al cabo de unos meses hay un conjunto de casos confirmados y descartados que permite calibrar los pesos con datos en lugar de con intuición —o, si el volumen lo justifica, entrenar un clasificador propio.

Solución 3

Información necesaria para responder al cliente:

  1. Identificador de la foto y momento exacto de la subida.
  2. Respuesta completa de SafeSearch: los cinco niveles devueltos, no solo la conclusión.
  3. Versión del modelo y fecha del análisis.
  4. Regla que se aplicó: qué categoría y qué umbral dispararon el rechazo.
  5. Si hubo revisión humana: quién, cuándo y con qué criterio.
  6. La imagen misma, para poder mirarla.

Lo que tuvo que registrarse en el momento del rechazo:

CREATE TABLE IF NOT EXISTS `alpinashop-datos.alpinashop_analitica.moderacion_imagenes` (
  imagen_id      STRING NOT NULL, opinion_id STRING, uri_cuarentena STRING,
  subida_en      TIMESTAMP,       analizada_en TIMESTAMP, modelo_version STRING,
  nivel_adult    STRING, nivel_violence STRING, nivel_racy STRING,
  nivel_medical  STRING, nivel_spoof    STRING,
  decision       STRING,   -- APTA | REVISION | RECHAZADA
  regla_aplicada STRING,   -- p.ej. 'adult >= LIKELY'
  revisor        STRING,   -- NULL si fue automatica
  revisado_en    TIMESTAMP, motivo_revisor STRING,
  reclamacion    BOOL,      resolucion     STRING
)
PARTITION BY DATE(subida_en);

Sin esta tabla, la respuesta al cliente es "el sistema lo rechazó y no sabemos por qué", que es inaceptable y, si la decisión afecta a sus derechos, jurídicamente problemática.

Diagnóstico probable. En una tienda de montaña, el sospechoso habitual es medical o violence disparándose ante una foto de una ampolla, una rozadura o un moratón causado por las botas. Es exactamente el tipo de foto que un cliente sube para justificar una crítica sobre un calzado incómodo: contenido perfectamente legítimo, informativo y relevante. El segundo sospechoso es racy ante una foto donde aparece piel —un pie descalzo mostrando la rozadura— sin ninguna connotación.

Qué cambiaría en el sistema:

1. Nunca rechazo automático por medical. Como ya prevé la política del apartado 8, esta categoría solo debe enviar a revisión. Si el rechazo se produjo por ahí, hay un error de implementación respecto a la política definida.

2. Subir el umbral de racy y bajar el de revisión. En este contexto, es preferible que pasen a revisión humana más fotos de las estrictamente necesarias que rechazar automáticamente contenido legítimo. El coste de un falso positivo —un cliente enfadado y una opinión perdida— es mayor que el de unos minutos de revisión.

3. Contexto en la decisión. Combinar SafeSearch con la detección de objetos: si en la imagen se detecta calzado, mochila o material deportivo, es señal de que la foto es sobre el producto. Una foto con medical en POSSIBLE y una bota detectada es casi con seguridad una rozadura legítima.

4. Canal de reclamación con plazo. El cliente debe poder pedir revisión de un rechazo, y esa petición debe llegar a una persona en un plazo definido. Que exista este ejercicio significa que el cliente encontró la manera de reclamar, lo cual ya es bueno.

5. Mensaje de rechazo honesto. "Tu foto está pendiente de revisión" es correcto y no acusa a nadie. "Tu foto ha sido rechazada por contenido inapropiado" ante una ampolla es ofensivo, y además probablemente falso.

6. Revisión periódica de la tasa de rechazo. Si el porcentaje de fotos rechazadas automáticamente supera un umbral pequeño, algo está mal calibrado. Es la misma disciplina de las reglas de calidad de Dataplex en 04-07: medir el propio proceso, no solo el resultado.

Y la conclusión de gestión: este ejercicio ilustra por qué la moderación automática no traslada la responsabilidad a la máquina. AlpinaShop respondió por esa decisión, no el proveedor de la API. La automatización reduce el trabajo humano; la responsabilidad se queda donde estaba.

Conclusión

Sesenta gigabytes de imágenes que solo tenían una ruta de carpeta son ahora datos consultables en alpinashop_analitica, y han resuelto los cuatro problemas con los que empezaba la lección, sin entrenar nada y por una cifra que cabe en el presupuesto de una tarde.

Conoces las funciones de la Cloud Vision API y, más importante, cuál sirve para qué: etiquetas para describir, objetos con recuadro para comprobar composición, OCR para leer, propiedades de imagen para el color, SafeSearch para moderar, detección web para vigilar el uso de tus fotos. Y sabes leer la respuesta campo a campo: el mid como identificador estable frente al texto traducible, las coordenadas normalizadas que funcionan igual en cualquier resolución, y la diferencia entre pixel_fraction y score en los colores, que es lo que separa un filtro de color útil de uno que clasifica todo el catálogo como blanco.

Has procesado el histórico con asyncBatchAnnotate, entendiendo que es asíncrono de verdad, que los resultados van a Cloud Storage y no a tu memoria, y que hay que trocear por el tope de peticiones. Y los has llevado a BigQuery particionados y agrupados, con modelo_version y fecha_analisis, porque las APIs preentrenadas se actualizan solas y sin esas columnas no se puede distinguir un cambio del modelo de un cambio en las imágenes.

Sobre el coste, has aplicado el razonamiento que vale para todo el módulo: la factura no se negocia, se deja de pedir lo que no se necesita. Analizar solo web/, solo la foto principal y solo tres funciones reduce el gasto un 94 % sin perder ninguna aplicación.

Y las cuatro aplicaciones están construidas. El texto alternativo partiendo del nombre del producto y completándolo con las etiquetas, con publicación automática solo donde hay coincidencia y revisión humana para el resto, porque un alt incorrecto engaña a quien no puede verificarlo. El filtro por color con los dos filtros de luminosidad que impiden que el fondo de estudio se lleve la clasificación, validado contra el color que ya estaba en la ficha. La moderación de fotos de clientes con cuarentena por defecto, tres salidas en lugar de dos, medical y spoof sin bloqueo automático porque una foto de una rozadura es contenido legítimo en una tienda de montaña, y registro completo de cada decisión. Y la auditoría del estándar visual, que produce lo mismo que produjeron AutoML y el análisis de opiniones: una lista de trabajo priorizada por ventas, no un sistema que decide solo.

Tienes la arquitectura por eventos definida sobre el topic imagenes-subidas, con idempotencia por URI, dead letter topic, autenticación OIDC y desacoplamiento —las mismas garantías de 04-04—, lista para que la implementación llegue con Cloud Functions en 06-03.

Y conoces los límites sin adornos: la API reconoce perfectamente una mochila y llama "anillo de metal" a un mosquetón HMS, porque vio millones de las primeras y muy pocos de los segundos. Ahí está la frontera con AutoML Vision: si tu vocabulario es el del mundo, la API; si es el tuyo, entrena. Y no compiten, se complementan sobre la misma imagen.

Por último, la advertencia que más importa. La detección de rostros no es reconocimiento facial, pero esa distinción técnica no basta legalmente: los datos biométricos son categoría especial bajo el artículo 9 del RGPD, y el AI Act restringe específicamente el reconocimiento de emociones. AlpinaShop tiene caras en las fotos de clientes, en las de catálogo con modelos y en las de ambiente, y no necesita analizar ninguna. La recomendación es literal: no pidas FACE_DETECTION sin una necesidad documentada y validada jurídicamente. Cada función que solicitas es una decisión sobre qué datos tratas.

Quedan dos preguntas del módulo 4 sin responder, y son las dos de producir en lugar de clasificar. Las 2.400 fichas de producto que nadie ha escrito, con descripciones que hoy son la ficha técnica del proveedor copiada tal cual. Y el buscador de la tienda, que solo encuentra lo que coincide literalmente: un cliente que escriba "mochila para tres días de travesía" no obtiene nada, porque ninguna ficha contiene esas palabras exactas.

En 05-06, IA generativa en Vertex AI, cambia la naturaleza del problema: de clasificar a producir. Trabajarás con la familia Gemini desde el SDK, entendiendo qué hacen de verdad temperature, top_p y las instrucciones de sistema, y cómo forzar salida en JSON. Generarás las 2.400 fichas por lotes con control de coste por token y revisión humana obligatoria antes de publicar. Resumirás las opiniones de cada producto en tres frases útiles, comparándolo con el sentimiento de 05-04. Entenderás qué son los embeddings de texto y construirás el buscador semántico del catálogo con Vector Search o con VECTOR_SEARCH en BigQuery. Verás el patrón RAG para el asistente de atención al cliente. Y afrontarás lo que la IA generativa trae consigo y las APIs anteriores no tenían: alucinaciones, filtros de seguridad, grounding, evaluación, transparencia del contenido generado, propiedad intelectual y AI Act.

Curso de Google Cloud Platform (GCP)

Módulo 1: Introducción a Google Cloud Platform

Módulo 2: Servicios principales de GCP

Módulo 3: Redes y seguridad

Módulo 4: Datos y análisis

Módulo 5: Aprendizaje automático e IA

Módulo 6: DevOps y monitoreo

Módulo 7: Temas avanzados de GCP

Módulo 8: Proyecto final

© Copyright 2026. Todos los derechos reservados