Pic-a-daily: Laboratório 1: armazenar e analisar imagens (Java)

1. Visão geral

No primeiro codelab, você vai fazer upload de imagens em um bucket. Isso vai gerar um evento de criação de arquivo que será processado por uma função. A função chamará a API Vision para fazer a análise de imagens e salvar os resultados em um repositório de dados.

d650ca5386ea71ad.png

O que você vai aprender

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

2. Configuração e requisitos

Configuração de ambiente autoguiada

  1. Faça login no Console do Google Cloud e crie um novo projeto ou reutilize um existente. Crie uma conta do Gmail ou do Google Workspace, se ainda não tiver uma.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • O Nome do projeto é o nome de exibição para os participantes do projeto. É uma string de caracteres não usada pelas APIs do Google Você pode atualizar a qualquer momento.
  • O ID do projeto precisa ser exclusivo em todos os projetos do Google Cloud e não pode ser alterado após a definição. O console do Cloud gera automaticamente uma string exclusiva. normalmente você não se importa com o que seja. Na maioria dos codelabs, é necessário fazer referência ao ID do projeto, que normalmente é identificado como PROJECT_ID. Se você não gostar do ID gerado, poderá gerar outro ID aleatório. Como alternativa, você pode tentar o seu próprio e ver se ele está disponível. Ela não pode ser alterada após esta etapa e permanecerá durante a duração do projeto.
  • Para sua informação, há um terceiro valor, um Número de projeto, que algumas APIs usam. Saiba mais sobre esses três valores na documentação.
  1. Em seguida, ative o faturamento no console do Cloud para usar os recursos/APIs do Cloud. A execução deste codelab não será muito cara, se tiver algum custo. Para encerrar os recursos e não gerar faturamento além deste tutorial, exclua os recursos criados ou exclua o projeto inteiro. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Inicie o Cloud Shell

Embora o Google Cloud e o Spanner possam ser operados remotamente do seu laptop, neste codelab usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Cloud.

No Console do Google Cloud, clique no ícone do Cloud Shell na barra de ferramentas superior à direita:

55efc1aaa7a4d3ad.png

O provisionamento e a conexão com o ambiente levarão apenas alguns instantes para serem concluídos: Quando o processamento for concluído, você verá algo como:

7ffe5cbb04455448.png

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Neste codelab, todo o trabalho pode ser feito com um navegador. Você não precisa instalar nada.

3. Ativar APIs

Neste laboratório, você usará o Cloud Functions e a API Vision, mas primeiro eles precisam ser ativados no console do Cloud ou com o gcloud.

Para ativar a API Vision no Console do Cloud, procure Cloud Vision API na barra de pesquisa:

cf48b1747ba6a6fb.png

Você será direcionado à página da API Cloud Vision:

ba4af419e6086fbb.png

Clique no botão ENABLE.

Se preferir, ative o Cloud Shell usando a ferramenta de linha de comando gcloud.

No Cloud Shell, execute o seguinte comando:

gcloud services enable vision.googleapis.com

Você verá que a operação será concluída com sucesso:

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

Ative também o Cloud Functions:

gcloud services enable cloudfunctions.googleapis.com

4. Criar o bucket (console)

Criar um bucket de armazenamento para as imagens. Faça isso no console do Google Cloud Platform ( console.cloud.google.com) ou com a ferramenta de linha de comando gsutil do Cloud Shell ou seu ambiente de desenvolvimento local.

No menu "hambúrguer", (☰) menu, navegue até a página Storage.

1930e055d138150a.png

Nomeie seu bucket

Clique no botão CREATE BUCKET.

34147939358517f8.png

Clique em CONTINUE.

Escolher local

197817f20be07678.png

Crie um bucket multirregional na região de sua escolha (aqui Europe).

Clique em CONTINUE.

Escolher a classe de armazenamento padrão

53cd91441c8caf0e.png

Escolha a classe de armazenamento Standard para seus dados.

Clique em CONTINUE.

Definir controle de acesso

8c2b3b459d934a51.png

Como você vai trabalhar com imagens acessíveis publicamente, o ideal é que todas as nossas imagens armazenadas no bucket tenham o mesmo controle de acesso uniforme.

Escolha a opção de controle de acesso Uniform.

Clique em CONTINUE.

Definir proteção/criptografia

d931c24c3e705a68.png

Mantenha o padrão (Google-managed key), já que você não vai usar suas próprias chaves de criptografia.

Clique em CREATE para finalizar a criação do bucket.

Adicionar allUsers como leitor do armazenamento

Acesse a guia Permissions:

d0ecfdcff730ea51.png

Adicione um membro allUsers ao bucket, com um papel de Storage > Storage Object Viewer, da seguinte maneira:

e9f25ec1ea0b6cc6.png

Clique em SAVE.

5. Criar o bucket (gsutil)

Também é possível usar a ferramenta de linha de comando gsutil no Cloud Shell para criar buckets.

No Cloud Shell, defina uma variável para o nome exclusivo do bucket. O Cloud Shell já tem GOOGLE_CLOUD_PROJECT definido como seu ID do projeto exclusivo. É possível anexar isso ao nome do bucket.

Exemplo:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

Crie uma zona multirregional padrão na Europa:

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

Garanta acesso uniforme no nível do bucket:

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

Torne o bucket público:

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

Se você acessar a seção Cloud Storage do console, terá um bucket uploaded-pictures público:

a98ed4ba17873e40.png

Verifique se é possível fazer upload das imagens para o bucket e se elas estão disponíveis publicamente, conforme explicado na etapa anterior.

6. Testar o acesso público ao bucket

De volta ao navegador do Storage, você verá seu bucket na lista, com "Público" acesso (incluindo um sinal de aviso para lembrar que qualquer pessoa tem acesso ao conteúdo desse bucket).

89e7a4d2c80a0319.png

Seu bucket está pronto para receber imagens.

Se clicar no nome do bucket, você verá os detalhes dele.

131387f12d3eb2d3.png

Lá, você pode usar o botão Upload files para testar se é possível adicionar uma imagem ao bucket. Um pop-up do seletor de arquivos solicitará que você escolha um arquivo. Depois de selecionado, ele será enviado para seu bucket, e você verá novamente o acesso public que foi atribuído automaticamente a esse novo arquivo.

e87584471a6e9c6d.png

Junto ao marcador de acesso Public, você também verá um pequeno ícone de link. Ao clicar nela, o navegador navegará para o URL público dessa imagem, que terá o seguinte formato:

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

No qual BUCKET_NAME é o nome globalmente exclusivo escolhido para o bucket e depois o nome do arquivo da imagem.

Ao clicar na caixa de seleção ao lado do nome da imagem, o botão DELETE será ativado e você poderá excluir esta primeira imagem.

7. Criar a função

Nesta etapa, você vai criar uma função que reage a eventos de upload de imagens.

Acesse a seção Cloud Functions do console do Google Cloud. Ao acessá-lo, o serviço do Cloud Functions será ativado automaticamente.

9d29e8c026a7a53f.png

Clique em Create function.

Escolha um nome (por exemplo, picture-uploaded) e a região (lembre-se de ser consistente com a escolha da região para o bucket):

4bb222633e6f278.png

Há dois tipos de funções:

  • funções HTTP que podem ser invocadas por um URL (ou seja, uma API da Web),
  • Funções em segundo plano que podem ser acionadas por algum evento.

Crie uma função em segundo plano que seja acionada quando um novo arquivo for enviado para o bucket Cloud Storage:

d9a12fcf58f4813c.png

Você tem interesse no tipo de evento Finalize/Create, que é acionado quando um arquivo é criado ou atualizado no bucket:

b30c8859b07dc4cb.png

Selecione o bucket criado anteriormente para informar ao Cloud Functions que ele será notificado quando um arquivo for criado / atualizado neste bucket específico:

cb15a1f4c7a1ca5f.png

Clique em Select para escolher o bucket criado anteriormente e em Save

c1933777fac32c6a.png

Antes de clicar em "Próxima", você pode expandir e modificar os padrões (256 MB de memória) em Ambiente de execução, build, conexões e configurações de segurança e atualizar para 1 GB.

83d757e6c38e10.png

Depois de clicar em Next, é possível ajustar o ambiente de execução, o código-fonte e o ponto de entrada.

Mantenha a Inline editor para esta função:

b6646ec646082b32.png

Selecione um dos ambientes de execução do Java, por exemplo, Java 11:

f85b8a6f951f47a7.png

O código-fonte consiste em um arquivo Java e um arquivo Maven pom.xml que fornece vários metadados e dependências.

Mantenha o snippet de código padrão, pois ele registra o nome do arquivo da imagem enviada:

9b7b9801b42f6ca6.png

Por enquanto, mantenha o nome da função a ser executada em Example, para fins de teste.

Clique em Deploy para criar e implantar a função. Quando a implantação for concluída, uma marca de seleção com um círculo verde vai aparecer na lista de funções:

3732fdf409eefd1a.png

8. Testar a função

Nesta etapa, teste se a função responde a eventos de armazenamento.

No menu "hambúrguer", (☰) menu, volte para a página Storage.

Clique no bucket de imagens e, em seguida, em Upload files para fazer upload de uma imagem.

21767ec3cb8b18de.png

Navegue novamente no console do Cloud para acessar a página Logging > Logs Explorer.

No seletor Log Fields, selecione Cloud Function para conferir os registros dedicados às suas funções. Role para baixo pelos campos de registro e até selecione uma função específica para ter uma visualização mais detalhada dos registros relacionados às funções. Selecione a função picture-uploaded.

Você vai encontrar os itens de registro mencionando a criação da função, os horários de início e término e o nosso log statement:

e8ba7d39c36df36c.png

Nosso log statement diz: Processing file: pic-a-daily-architecture-events.png, o que significa que o evento relacionado à criação e ao armazenamento dessa imagem foi realmente acionado conforme esperado.

9. Preparar o banco de dados

Você vai armazenar informações sobre a imagem fornecida pela API Vision no banco de dados do Cloud Firestore, um banco de dados de documentos NoSQL rápido, totalmente gerenciado, sem servidor e nativo da nuvem. Prepare o banco de dados acessando a seção Firestore do Console do Cloud:

9e4708d2257de058.png

Há duas opções disponíveis: Native mode ou Datastore mode. Use o modo nativo, que oferece recursos extras como suporte off-line e sincronização em tempo real.

Clique em SELECT NATIVE MODE.

9449ace8cc84de43.png

Escolha um local multirregional (aqui na Europa, mas idealmente pelo menos a mesma região da função e do bucket de armazenamento).

Clique no botão CREATE DATABASE.

Depois que o banco de dados for criado, você verá o seguinte:

56265949a124819e.png

Crie uma nova coleção clicando no botão + START COLLECTION.

Conjunto de nomes pictures.

75806ee24c4e13a7.png

Não é necessário criar um documento. Você as adicionará programaticamente à medida que novas imagens forem armazenadas no Cloud Storage e analisadas pela API Vision.

Clique em Save.

O Firestore cria um primeiro documento padrão na coleção recém-criada. É possível excluir esse documento com segurança, já que ele não contém informações úteis:

5c2f1e17ea47f48f.png

Os documentos que serão criados programaticamente na nossa coleção terão quatro campos:

  • name (string): o nome do arquivo da imagem enviada, que também é a chave do documento.
  • labels (matriz de strings): os rótulos dos itens reconhecidos pela API Vision
  • color (string): o código de cor hexadecimal da cor dominante (ou seja, #ab12ef).
  • created (data): o carimbo de data/hora de quando os metadados da imagem foram armazenados
  • miniatura (booleano): um campo opcional que vai estar presente e ser verdadeiro se uma imagem em miniatura tiver sido gerada para a imagem

Como vamos pesquisar no Firestore para encontrar imagens que tenham miniaturas disponíveis e classificar ao longo da data de criação, precisaremos criar um índice de pesquisa.

É possível criar o índice com o seguinte comando no Cloud Shell:

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

Também é possível fazer isso no Console do Cloud, clicando em Indexes na coluna de navegação à esquerda e criando um índice composto, conforme mostrado abaixo:

ecb8b95e3c791272.png

Clique em Create. A criação do índice pode levar alguns minutos.

10. Atualizar a função

Volte à página Functions para atualizar a função a fim de invocar a API Vision para analisar nossas imagens e armazenar os metadados no Firestore.

No menu "hambúrguer", (☰), navegue até a seção Cloud Functions, clique no nome da função, selecione a guia Source e clique no botão EDIT.

Primeiro, edite o arquivo pom.xml, que lista as dependências da função Java. Atualize o código para adicionar a dependência Maven da API 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>

Agora que as dependências estão atualizadas, você trabalhará no código da nossa função atualizando o arquivo Example.java com nosso código personalizado.

Mova o mouse sobre o arquivo Example.java e clique no lápis. Substitua o nome do pacote e o nome do arquivo por src/main/java/fn/ImageAnalysis.java.

Substitua o código em ImageAnalysis.java pelo abaixo. Isso será explicado na próxima etapa.

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. Conhecer a função

Vamos analisar melhor as várias partes interessantes.

Primeiro, estamos incluindo as dependências específicas no arquivo pom.xml do Maven. As bibliotecas de cliente do Google para Java publicam um Bill-of-Materials(BOM) para eliminar conflitos de dependência. Ao usá-lo, você não precisa especificar nenhuma versão para as bibliotecas de cliente individuais do Google.

  <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>

Em seguida, preparamos um cliente para a API Vision:

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

Agora vem a estrutura da nossa função. Capturamos os campos de interesse do evento de entrada e os mapeamos para a estrutura do GCSEvent definida:

...
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;
    }

Observe a assinatura e também como recuperamos o nome do arquivo e do bucket que acionou a função do Cloud.

Para referência, veja a aparência do payload do evento:

{
  "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 uma solicitação para enviar pelo cliente 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();

Estamos solicitando três recursos principais da API Vision:

  • Detecção de rótulos: para entender o que aparece nessas fotos.
  • Propriedades de imagem: para fornecer atributos interessantes da imagem (estamos interessados na cor dominante da imagem)
  • Pesquisa segura: para saber se a imagem é segura para exibição (ela não deve conter conteúdo adulto / médico / potencialmente ofensivo / violento)

Agora, podemos fazer a chamada para a API Vision:

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

Como referência, confira a resposta da API 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
}

Se nenhum erro for retornado, podemos prosseguir. É por isso que temos este bloco "if":

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

Vamos obter os rótulos das coisas, categorias ou temas reconhecidos na imagem:

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

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

Estamos interessados em saber a cor dominante da imagem:

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);
}

Também estamos usando uma função utilitária para transformar os valores vermelho / verde / azul em um código de cor hexadecimal que pode ser usado em folhas de estilo CSS.

Vamos verificar se a imagem pode ser exibida:

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 os atributos de adulto / paródia / medicina / violência / potencialmente ofensivo para ver se eles são prováveis ou muito prováveis.

Se o resultado da pesquisa segura estiver correto, poderemos armazenar os metadados no 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. implantar a função

É hora de implantar a função.

604f47aa11fbf8e.png

Clique no botão DEPLOY para implantar a nova versão. Confira o progresso:

13da63f23e4dbbdd.png

13. Testar a função novamente

Depois que a função for implantada, você publicará uma imagem no Cloud Storage, verá se a função é invocada, o que a API Vision retorna e se os metadados são armazenados no Firestore.

Navegue de volta para Cloud Storage e clique no bucket que criamos no início do laboratório:

d44c1584122311c7.png

Na página de detalhes do bucket, clique no botão Upload files para fazer upload de uma imagem.

26bb31d35fb6aa3d.png

No menu "hambúrguer", (☰) no menu, navegue até o explorador "Logging > Logs".

No seletor Log Fields, selecione Cloud Function para conferir os registros dedicados às suas funções. Role para baixo pelos campos de registro e até selecione uma função específica para ter uma visualização mais detalhada dos registros relacionados às funções. Selecione a função picture-uploaded.

b651dca7e25d5b11.png

De fato, na lista de registros, vejo que nossa função foi invocada:

d22a7f24954e4f63.png

Os registros indicam o início e o fim da execução da função. E, entre elas, podemos ver os registros que colocamos em nossa função com as instruções console.log(). Vemos:

  • Os detalhes do evento que aciona a função,
  • Os resultados brutos da chamada da API Vision
  • Os marcadores que foram encontrados na imagem que carregamos,
  • As informações sobre cores dominantes,
  • Se a imagem pode ser exibida,
  • Por fim, os metadados sobre a imagem foram armazenados no Firestore.

9ff7956a215c15da.png

Mais uma vez, no "hambúrguer", (☰) no menu, acesse a seção Firestore. Na subseção Data (mostrada por padrão), você vai encontrar a coleção pictures com um novo documento adicionado, correspondente à imagem que você acabou de enviar:

a6137ab9687da370.png

14. Limpeza (opcional)

Se você não pretende continuar com os outros laboratórios da série, limpe os recursos para economizar custos e ser um bom cidadão da nuvem. É possível limpar recursos individualmente da seguinte maneira.

Excluir o bucket:

gsutil rb gs://${BUCKET_PICTURES}

Exclua a função:

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

Para excluir a coleção do Firestore, selecione "Excluir coleção da coleção":

410b551c3264f70a.png

Se preferir, exclua todo o projeto:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. Parabéns!

Parabéns! Você implementou o primeiro serviço de chaves do projeto.

O que vimos

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

Próximas etapas