Pic-a-daily: Lab 1: Cómo almacenar y analizar fotos (Java)

1. Descripción general

En el primer codelab, subirás fotos en un bucket. Esto generará un evento de creación de archivos que controlará una función. La función llamará a la API de Vision para realizar un análisis de imágenes y guardar los resultados en un almacén de datos.

d650ca5386ea71ad.png

Qué aprenderás

  • Cloud Storage
  • Cloud Functions
  • API de Cloud Vision
  • Cloud Firestore

2. Configuración y requisitos

Configuración del entorno de autoaprendizaje

  1. Accede a Google Cloud Console y crea un proyecto nuevo o reutiliza uno existente. Si aún no tienes una cuenta de Gmail o de Google Workspace, debes crear una.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • El Nombre del proyecto es el nombre visible de los participantes de este proyecto. Es una cadena de caracteres que no se utiliza en las APIs de Google. Puedes actualizarla en cualquier momento.
  • El ID del proyecto debe ser único en todos los proyectos de Google Cloud y es inmutable (no se puede cambiar después de configurarlo). La consola de Cloud genera automáticamente una cadena única. por lo general, no te importa qué es. En la mayoría de los codelabs, deberás hacer referencia al ID del proyecto (por lo general, se identifica como PROJECT_ID). Si no te gusta el ID generado, puedes generar otro aleatorio. También puedes probar el tuyo propio y ver si está disponible. No se puede cambiar después de este paso y se mantendrá mientras dure el proyecto.
  • Para tu información, hay un tercer valor, un número de proyecto que usan algunas APIs. Obtén más información sobre estos tres valores en la documentación.
  1. A continuación, deberás habilitar la facturación en la consola de Cloud para usar las APIs o los recursos de Cloud. Ejecutar este codelab no debería costar mucho, tal vez nada. Para cerrar recursos y evitar que se te facture más allá de este instructivo, puedes borrar los recursos que creaste o borrar todo el proyecto. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de USD 300.

Inicia Cloud Shell

Si bien Google Cloud y Spanner se pueden operar de manera remota desde tu laptop, en este codelab usarás Google Cloud Shell, un entorno de línea de comandos que se ejecuta en la nube.

En Google Cloud Console, haz clic en el ícono de Cloud Shell en la barra de herramientas en la parte superior derecha:

55efc1aaa7a4d3ad.png

El aprovisionamiento y la conexión al entorno deberían tomar solo unos minutos. Cuando termine el proceso, debería ver algo como lo siguiente:

7ffe5cbb04455448.png

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Todo tu trabajo en este codelab se puede hacer en un navegador. No es necesario que instales nada.

3. Habilita las APIs

En este lab, usarás Cloud Functions y la API de Vision, pero primero deberás habilitarlas en la consola de Cloud o con gcloud.

Para habilitar la API de Vision en la consola de Cloud, busca Cloud Vision API en la barra de búsqueda:

cf48b1747ba6a6fb.png

Llegarás a la página de la API de Cloud Vision:

ba4af419e6086fbb.png

Haz clic en el botón ENABLE.

Como alternativa, también puedes habilitarlo en Cloud Shell con la herramienta de línea de comandos de gcloud.

En Cloud Shell, ejecuta el siguiente comando:

gcloud services enable vision.googleapis.com

Deberías ver que la operación finaliza correctamente:

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

También habilita Cloud Functions:

gcloud services enable cloudfunctions.googleapis.com

4. Crea el bucket (consola)

Crear un bucket de almacenamiento para las imágenes Puedes hacerlo desde la consola de Google Cloud ( console.cloud.google.com) o con la herramienta de línea de comandos gsutil de Cloud Shell o tu entorno de desarrollo local.

De la “hamburguesa” (⁕), navega a la página Storage.

1930e055d138150a.png

Asígnale un nombre al bucket

Haz clic en el botón CREATE BUCKET.

34147939358517f8.png

Haz clic en CONTINUE.

Elige la ubicación

197817f20be07678.png

Crea un bucket multirregional en la región que prefieras (aquí Europe).

Haz clic en CONTINUE.

Elige la clase de almacenamiento predeterminada

53cd91441c8caf0e.png

Elige la clase de almacenamiento Standard para tus datos.

Haz clic en CONTINUE.

Configura el control de acceso

8c2b3b459d934a51.png

Como trabajarás con imágenes de acceso público, quieres que todas las fotos almacenadas en este bucket tengan el mismo control de acceso uniforme.

Elige la opción de control de acceso Uniform.

Haz clic en CONTINUE.

Configuración de protección/encriptación

d931c24c3e705a68.png

Mantén la configuración predeterminada (Google-managed key), ya que no usarás tus propias claves de encriptación.

Haz clic en CREATE para finalizar la creación del bucket.

Agrega allUsers como visualizador de almacenamiento

Ve a la pestaña Permissions:

d0ecfdcff730ea51.png

Agrega un miembro allUsers al bucket, con una función de Storage > Storage Object Viewer, de la siguiente manera:

e9f25ec1ea0b6cc6.png

Haz clic en SAVE.

5. Crea el bucket (gsutil)

También puedes usar la herramienta de línea de comandos de gsutil en Cloud Shell para crear buckets.

En Cloud Shell, establece una variable para el nombre único del bucket. Cloud Shell ya tiene establecido GOOGLE_CLOUD_PROJECT en el ID único de tu proyecto. Puedes agregarlo al nombre del bucket.

Por ejemplo:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Crea una zona multirregional estándar en Europa:

gsutil mb -l EU gs://${BUCKET_PICTURES}

Garantiza el acceso uniforme a nivel de bucket:

gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}

Configura el bucket como público:

gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}

Si vas a la sección Cloud Storage de la consola, deberías tener un bucket público uploaded-pictures:

a98ed4ba17873e40.png

Prueba que puedes subir fotos al bucket y que estén disponibles públicamente, como se explicó en el paso anterior.

6. Prueba el acceso público al bucket

Si regresas al navegador de Storage, verás tu bucket en la lista, que dice "Público". (incluso una señal de advertencia que te recuerda que cualquier persona tiene acceso al contenido de ese bucket).

89e7a4d2c80a0319.png

Tu bucket ya está listo para recibir fotos.

Si haces clic en el nombre del bucket, verás sus detalles.

131387f12d3eb2d3.png

Allí, puedes probar el botón Upload files para probar que puedes agregar una foto al bucket. Una ventana emergente del selector de archivos te pedirá que selecciones un archivo. Una vez seleccionado, se subirá a tu bucket y verás de nuevo el acceso a public que se atribuyó automáticamente a este archivo nuevo.

e87584471a6e9c6d.png

Junto a la etiqueta de acceso Public, también verás un pequeño ícono de vínculo. Cuando hagas clic en ella, el navegador navegará a la URL pública de esa imagen, que tendrá el siguiente formato:

https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png

BUCKET_NAME es el nombre global único que elegiste para tu bucket y, luego, el nombre de archivo de tu foto.

Si haces clic en la casilla de verificación junto al nombre de la foto, se habilitará el botón DELETE y podrás borrar esta primera imagen.

7. Crea la función

En este paso, crearás una función que reaccione a los eventos de carga de fotos.

Visita la sección Cloud Functions de la consola de Google Cloud. Cuando lo visites, el servicio de Cloud Functions se habilitará automáticamente.

9d29e8c026a7a53f.png

Haz clic en Create function:

Elige un nombre (p. ej., picture-uploaded) y la región (recuerda ser coherente con la región elegida para el bucket):

4bb222633e6f278.png

Existen dos tipos de funciones:

  • Funciones de HTTP que pueden invocarse mediante una URL (es decir, una API web)
  • Funciones en segundo plano que pueden activarse mediante algún evento

Quieres crear una función en segundo plano que se active cuando se suba un archivo nuevo a nuestro bucket Cloud Storage:

d9a12fcf58f4813c.png

Te interesa el tipo de evento Finalize/Create, que es el que se activa cuando se crea o actualiza un archivo en el bucket:

b30c8859b07dc4cb.png

Selecciona el bucket creado anteriormente para indicarle a Cloud Functions que debe recibir una notificación cuando se cree o actualice un archivo en este bucket en particular:

cb15a1f4c7a1ca5f.png

Haz clic en Select para elegir el bucket que creaste anteriormente y, luego, en Save

c1933777fac32c6a.png

Antes de hacer clic en Siguiente, puedes expandir y modificar los valores predeterminados (256 MB de memoria) en Configuración del entorno de ejecución, la compilación, las conexiones y la seguridad, y actualizarlos a 1 GB.

83d757e6c38e10.png

Después de hacer clic en Next, puedes ajustar el Entorno de ejecución, el Código fuente y el punto de entrada.

Mantén el Inline editor para esta función:

b6646ec646082b32.png

Selecciona uno de los entornos de ejecución de Java, por ejemplo, Java 11:

f85b8a6f951f47a7.png

El código fuente consta de un archivo Java y un archivo Maven pom.xml que proporciona varios metadatos y dependencias.

Conserva el fragmento de código predeterminado: registra el nombre de archivo de la imagen subida:

9b7b9801b42f6ca6.png

Por ahora, mantén el nombre de la función que se ejecutará en Example para fines de prueba.

Haz clic en Deploy para crear y, luego, implementar la función. Una vez que la implementación se haya realizado correctamente, deberías ver una marca de verificación en un círculo verde en la lista de funciones:

3732fdf409eefd1a.png

8. Prueba la función

En este paso, prueba que la función responda a los eventos de almacenamiento.

De la “hamburguesa” (⁕), regresa a la página Storage.

Haz clic en el bucket de imágenes y, luego, en Upload files para subir una imagen.

21767ec3cb8b18de.png

Vuelve a navegar en la consola de Cloud para ir a la página Logging > Logs Explorer.

En el selector Log Fields, selecciona Cloud Function para ver los registros dedicados a tus funciones. Desplázate hacia abajo por los campos de registro y hasta puedes seleccionar una función específica para tener una vista más detallada de los registros relacionados con las funciones. Selecciona la función picture-uploaded.

Deberías ver los elementos de registro que mencionan la creación de la función, las horas de inicio y finalización de la función, y nuestra instrucción de registro real:

e8ba7d39c36df36c.png

Nuestra instrucción de registro dice: Processing file: pic-a-daily-architecture-events.png, lo que significa que el evento relacionado con la creación y el almacenamiento de esta imagen se activó según lo esperado.

9. Prepara la base de datos

Almacenarás información sobre la imagen que proporciona la API de Vision en la base de datos de Cloud Firestore, una base de datos NoSQL de documentos nativa de la nube, rápida, completamente administrada y sin servidores. Para preparar tu base de datos, ve a la sección Firestore de la consola de Cloud:

9e4708d2257de058.png

Se ofrecen dos opciones: Native mode o Datastore mode. Usa el modo nativo, que ofrece funciones adicionales, como soporte sin conexión y sincronización en tiempo real.

Haz clic en SELECT NATIVE MODE.

9449ace8cc84de43.png

Elige una multirregión (aquí en Europa, pero idealmente que sea, al menos, la misma región que la función y el bucket de almacenamiento).

Haz clic en el botón CREATE DATABASE.

Una vez creada la base de datos, deberías ver lo siguiente:

56265949a124819e.png

Para crear una colección nueva, haz clic en el botón + START COLLECTION.

Colección de nombres pictures.

75806ee24c4e13a7.png

No es necesario que crees un documento. Las agregarás de manera programática a medida que las fotos nuevas se almacenen en Cloud Storage y se analicen con la API de Vision.

Haz clic en Save.

Firestore crea un primer documento predeterminado en la colección recién creada. Puedes borrarlo de forma segura, ya que no contiene información útil:

5c2f1e17ea47f48f.png

Los documentos que se crearán de manera programática en nuestra colección contendrán 4 campos:

  • name (cadena): el nombre de archivo de la imagen subida, que también es la clave del documento
  • labels (array de cadenas): las etiquetas de los elementos reconocidos por la API de Vision
  • color (cadena): El código de color hexadecimal del color dominante (p. ej., #ab12ef)
  • created (fecha): la marca de tiempo del momento en que se almacenaron los metadatos de esta imagen
  • miniatura (booleano): Es un campo opcional que estará presente y será verdadero si se generó una imagen en miniatura para esta imagen.

Debido a que buscaremos en Firestore para encontrar imágenes que tengan miniaturas disponibles y ordenaremos según la fecha de creación, necesitaremos crear un índice de búsqueda.

Puedes crear el índice con el siguiente comando en Cloud Shell:

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

También puedes hacerlo desde la consola de Cloud. Para ello, haz clic en Indexes en la columna de navegación de la izquierda y, luego, crea un índice compuesto como se muestra a continuación:

ecb8b95e3c791272.png

Haz clic en Create. La creación del índice puede tardar unos minutos.

10. Actualiza la función

Regresa a la página Functions para actualizar la función y invocar la API de Vision con el objetivo de analizar nuestras fotos y almacenar los metadatos en Firestore.

De la “hamburguesa” (⁕), navega a la sección Cloud Functions, haz clic en el nombre de la función, selecciona la pestaña Source y, luego, haz clic en el botón EDIT.

Primero, edita el archivo pom.xml, que enumera las dependencias de nuestra función de Java. Actualiza el código para agregar la dependencia de Maven de la API de Cloud Vision:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cloudfunctions</groupId>
  <artifactId>gcs-function</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud.functions</groupId>
      <artifactId>functions-framework-api</artifactId>
      <version>1.0.4</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
  </dependencies>

  <!-- Required for Java 11 functions in the inline editor -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <excludes>
            <exclude>.google/</exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Ahora que las dependencias están actualizadas, trabajarás en el código de nuestra función. Para ello, actualizarás el archivo Example.java con nuestro código personalizado.

Mueve el mouse sobre el archivo Example.java y haz clic en el lápiz. Reemplaza el nombre del paquete y el nombre del archivo por src/main/java/fn/ImageAnalysis.java.

Reemplaza el código de ImageAnalysis.java con el siguiente código. Se explicará en el siguiente paso.

package fn;

import com.google.cloud.functions.*;
import com.google.cloud.vision.v1.*;
import com.google.cloud.vision.v1.Feature.Type;
import com.google.cloud.firestore.*;
import com.google.api.core.ApiFuture;

import java.io.*;
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
import java.util.logging.Logger;

import fn.ImageAnalysis.GCSEvent;

public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    private static final Logger logger = Logger.getLogger(ImageAnalysis.class.getName());

    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException, ExecutionException {
        String fileName = event.name;
        String bucketName = event.bucket;

        logger.info("New picture uploaded " + fileName);

        try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
            List<AnnotateImageRequest> requests = new ArrayList<>();
            
            ImageSource imageSource = ImageSource.newBuilder()
                .setGcsImageUri("gs://" + bucketName + "/" + fileName)
                .build();

            Image image = Image.newBuilder()
                .setSource(imageSource)
                .build();

            Feature featureLabel = Feature.newBuilder()
                .setType(Type.LABEL_DETECTION)
                .build();
            Feature featureImageProps = Feature.newBuilder()
                .setType(Type.IMAGE_PROPERTIES)
                .build();
            Feature featureSafeSearch = Feature.newBuilder()
                .setType(Type.SAFE_SEARCH_DETECTION)
                .build();
                
            AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
                .addFeatures(featureLabel)
                .addFeatures(featureImageProps)
                .addFeatures(featureSafeSearch)
                .setImage(image)
                .build();
            
            requests.add(request);

            logger.info("Calling the Vision API...");
            BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
            List<AnnotateImageResponse> responses = result.getResponsesList();

            if (responses.size() == 0) {
                logger.info("No response received from Vision API.");
                return;
            }

            AnnotateImageResponse response = responses.get(0);
            if (response.hasError()) {
                logger.info("Error: " + response.getError().getMessage());
                return;
            }

            List<String> labels = response.getLabelAnnotationsList().stream()
                .map(annotation -> annotation.getDescription())
                .collect(Collectors.toList());
            logger.info("Annotations found:");
            for (String label: labels) {
                logger.info("- " + label);
            }

            String mainColor = "#FFFFFF";
            ImageProperties imgProps = response.getImagePropertiesAnnotation();
            if (imgProps.hasDominantColors()) {
                DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
                ColorInfo colorInfo = colorsAnn.getColors(0);

                mainColor = rgbHex(
                    colorInfo.getColor().getRed(), 
                    colorInfo.getColor().getGreen(), 
                    colorInfo.getColor().getBlue());

                logger.info("Color: " + mainColor);
            }

            boolean isSafe = false;
            if (response.hasSafeSearchAnnotation()) {
                SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();

                isSafe = Stream.of(
                    safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
                    safeSearch.getSpoof(), safeSearch.getViolence())
                .allMatch( likelihood -> 
                    likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
                );

                logger.info("Safe? " + isSafe);
            }

            // Saving result to Firestore
            if (isSafe) {
                FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
                Firestore pictureStore = firestoreOptions.getService();

                DocumentReference doc = pictureStore.collection("pictures").document(fileName);

                Map<String, Object> data = new HashMap<>();
                data.put("labels", labels);
                data.put("color", mainColor);
                data.put("created", new Date());

                ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

                logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
            }
        }
    }

    private static String rgbHex(float red, float green, float blue) {
        return String.format("#%02x%02x%02x", (int)red, (int)green, (int)blue);
    }

    public static class GCSEvent {
        String bucket;
        String name;
    }
}

968749236c3f01da.png

11. Explora la función

Veamos con más detalle las diversas partes interesantes.

En primer lugar, incluiremos las dependencias específicas en el archivo pom.xml de Maven. Las bibliotecas cliente de Java de Google publican un Bill-of-Materials(BOM) para eliminar cualquier conflicto de dependencias. Al usarlo, no tienes que especificar ninguna versión para las bibliotecas cliente de Google individuales.

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

Luego, preparamos un cliente para la API de Vision:

...
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
...

Ahora viene la estructura de nuestra función. Capturamos a partir del evento entrante los campos que nos interesan y los asignamos a la estructura de GCSEvent que definimos:

...
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException,     
    ExecutionException {
...

    public static class GCSEvent {
        String bucket;
        String name;
    }

Observa la firma y también cómo recuperamos el nombre del archivo y del bucket que activó la Cloud Function.

A modo de referencia, la carga útil del evento se ve de la siguiente manera:

{
  "bucket":"uploaded-pictures",
  "contentType":"image/png",
  "crc32c":"efhgyA==",
  "etag":"CKqB956MmucCEAE=",
  "generation":"1579795336773802",
  "id":"uploaded-pictures/Screenshot.png/1579795336773802",
  "kind":"storage#object",
  "md5Hash":"PN8Hukfrt6C7IyhZ8d3gfQ==",
  "mediaLink":"https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/Screenshot.png?generation=1579795336773802&alt=media",
  "metageneration":"1",
  "name":"Screenshot.png",
  "selfLink":"https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/Screenshot.png",
  "size":"173557",
  "storageClass":"STANDARD",
  "timeCreated":"2020-01-23T16:02:16.773Z",
  "timeStorageClassUpdated":"2020-01-23T16:02:16.773Z",
  "updated":"2020-01-23T16:02:16.773Z"
}

Preparamos una solicitud para enviarla a través del cliente de Vision:

ImageSource imageSource = ImageSource.newBuilder()
    .setGcsImageUri("gs://" + bucketName + "/" + fileName)
    .build();

Image image = Image.newBuilder()
    .setSource(imageSource)
    .build();

Feature featureLabel = Feature.newBuilder()
    .setType(Type.LABEL_DETECTION)
    .build();
Feature featureImageProps = Feature.newBuilder()
    .setType(Type.IMAGE_PROPERTIES)
    .build();
Feature featureSafeSearch = Feature.newBuilder()
    .setType(Type.SAFE_SEARCH_DETECTION)
    .build();
    
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
    .addFeatures(featureLabel)
    .addFeatures(featureImageProps)
    .addFeatures(featureSafeSearch)
    .setImage(image)
    .build();

Solicitamos 3 capacidades clave de la API de Vision:

  • Detección de etiquetas: para comprender qué hay en esas fotos
  • Propiedades de imágenes: Para proporcionar atributos interesantes de la imagen (nos interesa el color dominante de la imagen)
  • Búsqueda segura: para saber si una imagen es segura (no debe incluir contenido para adultos, médico, subido de tono ni violento)

En este punto, podemos realizar la siguiente llamada a la API de Vision:

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = 
                            vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

A modo de referencia, así es como se ve la respuesta de la API de Vision:

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
    ✄ - - - ✄
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
        ✄ - - - ✄
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

Si no se devuelve un error, podemos continuar. Por eso tenemos este bloque if:

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
     logger.info("Error: " + response.getError().getMessage());
     return;
}

Vamos a obtener las etiquetas de las cosas, categorías o temas reconocidos en la imagen:

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());

logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

Nos interesa conocer el color dominante de la imagen:

String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
    DominantColorsAnnotation colorsAnn = 
                               imgProps.getDominantColors();
    ColorInfo colorInfo = colorsAnn.getColors(0);

    mainColor = rgbHex(
        colorInfo.getColor().getRed(), 
        colorInfo.getColor().getGreen(), 
        colorInfo.getColor().getBlue());

    logger.info("Color: " + mainColor);
}

También usamos una función de utilidad para transformar los valores rojo / verde / azul en un código de color hexadecimal que podamos usar en las hojas de estilo CSS.

Comprobemos si es seguro mostrar la imagen:

boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
    SafeSearchAnnotation safeSearch = 
                      response.getSafeSearchAnnotation();

    isSafe = Stream.of(
        safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
        safeSearch.getSpoof(), safeSearch.getViolence())
    .allMatch( likelihood -> 
        likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
    );

    logger.info("Safe? " + isSafe);
}

Estamos verificando los atributos de adultos / falsificación de identidad / médico / violencia / subido de tono para determinar si no son probables o muy probables.

Si el resultado de la búsqueda segura es aceptable, podemos almacenar metadatos en Firestore:

if (isSafe) {
    FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
    Firestore pictureStore = firestoreOptions.getService();

    DocumentReference doc = pictureStore.collection("pictures").document(fileName);

    Map<String, Object> data = new HashMap<>();
    data.put("labels", labels);
    data.put("color", mainColor);
    data.put("created", new Date());

    ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

    logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}

12. Implementa la función

Es hora de implementar la función.

604f47aa11fbf8e.png

Presiona el botón DEPLOY y se implementará la nueva versión. Puedes ver el progreso:

13da63f23e4dbbdd.png

13. Vuelve a probar la función

Una vez que la función se haya implementado correctamente, publicarás una foto en Cloud Storage para ver si se invoca la función, qué muestra la API de Vision y si los metadatos están almacenados en Firestore.

Regresa a Cloud Storage y haz clic en el bucket que creamos al comienzo del lab:

d44c1584122311c7.png

Cuando estés en la página de detalles del bucket, haz clic en el botón Upload files para subir una foto.

26bb31d35fb6aa3d.png

De la “hamburguesa” (⁕), navega al Explorador de Logging > Logs.

En el selector Log Fields, selecciona Cloud Function para ver los registros dedicados a tus funciones. Desplázate hacia abajo por los campos de registro y hasta puedes seleccionar una función específica para tener una vista más detallada de los registros relacionados con las funciones. Selecciona la función picture-uploaded.

b651dca7e25d5b11.png

De hecho, en la lista de registros, puedo ver que se invocó nuestra función:

d22a7f24954e4f63.png

Los registros indican el inicio y el final de la ejecución de la función. Y en el medio, podemos ver los registros que pusimos en nuestra función con las sentencias console.log(). Vemos lo siguiente:

  • Los detalles del evento que activa la función
  • Los resultados sin procesar de la llamada a la API de Vision
  • Las etiquetas que se encontraron en la foto que subimos
  • La información de los colores dominantes,
  • Si es seguro mostrar la imagen,
  • Finalmente, esos metadatos sobre la foto se almacenaron en Firestore.

9ff7956a215c15da.png

Una vez más, desde el punto de vista de “hamburguesa”. (⁕), ve a la sección Firestore. En la subsección Data (que se muestra de forma predeterminada), deberías ver la colección pictures con un documento nuevo agregado, que corresponde a la imagen que acabas de subir:

a6137ab9687da370.png

14. Limpieza (opcional)

Si no pretendes continuar con los otros labs de la serie, puedes liberar recursos para ahorrar costos y ser un buen ciudadano de la nube en general. Puedes limpiar los recursos de forma individual de la siguiente manera.

Borra el bucket:

gsutil rb gs://${BUCKET_PICTURES}

Borra la función:

gcloud functions delete picture-uploaded --region europe-west1 -q

Para borrar la colección de Firestore, selecciona Borrar colección de la colección:

410b551c3264f70a.png

También puedes borrar todo el proyecto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. ¡Felicitaciones!

¡Felicitaciones! Implementaste con éxito el primer servicio de claves del proyecto.

Temas abordados

  • Cloud Storage
  • Cloud Functions
  • API de Cloud Vision
  • Cloud Firestore

Próximos pasos