Processamento de imagens

Neste tutorial, demonstramos como usar o Cloud Run for Anthos no Google Cloud, a API Cloud Vision e o ImageMagick para detectar e desfocar imagens ofensivas enviadas para um bucket do Cloud Storage. Este tutorial se baseia no tutorial Como usar o Pub/Sub com o Cloud Run for Anthos no Google Cloud.

Neste tutorial, explicamos como modificar um aplicativo de amostra atual. Também é possível fazer o download da amostra concluída.

Objetivos

  • Gravar, criar e implantar um serviço de processamento de dados assíncrono no Cloud Run for Anthos no Google Cloud
  • Invocar o serviço fazendo upload de um arquivo para o Cloud Storage, criando uma mensagem do Pub/Sub.
  • Usar a API Cloud Vision para detectar conteúdo violento ou adulto.
  • Usar o ImageMagick para desfocar imagens ofensivas.
  • Testar o serviço fazendo upload de uma imagem de um zumbi comedor de carne.

Custos

Neste tutorial, usamos componentes do Cloud Platform que podem ser cobrados, incluindo estes:

Use a calculadora de preços para gerar uma estimativa de custo com base no uso previsto.

Usuários novos do Cloud Platform podem se qualificar para um teste gratuito.

Antes de começar

  1. Faça login na sua conta do Google.

    Se você ainda não tiver uma, inscreva-se.

  2. No Console do Google Cloud, na página do seletor de projetos, selecione ou crie um projeto do Google Cloud.

    Acessar a página do seletor de projetos

  3. Verifique se o faturamento está ativado para seu projeto na nuvem. Saiba como confirmar se o faturamento está ativado para o projeto.

  4. Ative as APIs Cloud Run for Anthos on Google Cloud and Cloud Vision.

    Ative as APIs

  5. Instale e inicie o SDK do Cloud.
  6. Instale o componente kubectl:
    gcloud components install kubectl
  7. Instale o componente beta:
    gcloud components install beta
  8. Atualize os componentes:
    gcloud components update
  9. Usando o Cloud Run for Anthos no Google Cloud, crie um novo cluster usando as instruções em Como configurar o Cloud Run for Anthos no Google Cloud.
  10. Configure um tópico do Pub/Sub, uma assinatura de push segura e um serviço inicial do Cloud Run for Anthos no Google Cloud para processar mensagens, seguindo as instruções em Como usar o Pub/Sub com o Cloud Run for Anthos no Google Cloud.

Como configurar padrões do gcloud

Para configurar a gcloud com os padrões do serviço do Cloud Run for Anthos no Google Cloud:

  1. Defina o projeto padrão:

    gcloud config set project PROJECT_ID

    Substitua PROJECT_ID pelo nome do projeto que você usou neste tutorial.

  2. Configure a gcloud para o cluster:

    gcloud config set kuberun/cluster CLUSTER-NAME
    gcloud config set kuberun/cluster_location REGION

    Substitua:

    • CLUSTER-NAME pelo nome que você usou para o cluster;
    • REGION pelo local do cluster compatível de sua escolha.

Como entender a sequência de operações

O fluxo de dados neste tutorial segue estas etapas:

  1. Um usuário faz o upload de uma imagem para um bucket do Cloud Storage.
  2. O Cloud Storage publica uma mensagem sobre o novo arquivo no Pub/Sub.
  3. O Pub/Sub envia a mensagem ao serviço Cloud Run for Anthos no Google Cloud.
  4. O serviço do Cloud Run for Anthos no Google Cloud recupera o arquivo de imagem referenciado na mensagem do Pub/Sub.
  5. O serviço do Cloud Run for Anthos no Google Cloud usa a API Cloud Vision para analisar a imagem.
  6. Se for detectado conteúdo adulto ou violento, o serviço do Cloud Run for Anthos no Google Cloud usará o ImageMagick para desfocar a imagem.
  7. O serviço do Cloud Run for Anthos no Google Cloud faz upload da imagem desfocada em outro bucket do Cloud Storage para uso.

O próximo uso da imagem desfocada é deixado como um exercício para o leitor.

Como configurar buckets do Cloud Storage

  1. Crie um bucket do Cloud Storage para fazer upload de imagens, em que INPUT_BUCKET_NAME é um nome de bucket globalmente exclusivo:

    gsutil mb gs://INPUT_BUCKET_NAME

    O serviço do Cloud Run for Anthos no Google Cloud lê apenas nesse bucket.

  2. Crie um segundo bucket do Cloud Storage para receber imagens desfocadas, em que BLURRED_BUCKET_NAME é um nome de bucket globalmente exclusivo:

    gsutil mb gs://BLURRED_BUCKET_NAME

    O serviço do Cloud Run for Anthos no Google Cloud faz upload de imagens desfocadas para esse bucket. Usar um bucket separado evita que imagens processadas acionem novamente o serviço.

Nas etapas a seguir, você cria e implanta um serviço que processa notificações de uploads de arquivos para o INPUT_BUCKET_NAME. Você ativa a entrega de notificações depois de implantar e testar o serviço, para evitar a invocação prematura do novo serviço.

Como modificar o código de amostra do tutorial do Pub/Sub

Este documento se baseia no código criado no tutorial Como usar o Pub/Sub. Se você ainda não concluiu esse tutorial, faça isso agora, ignore as etapas de limpeza e retorne a este documento para adicionar um comportamento de processamento de imagem.

Como adicionar código de processamento de imagem

O código de processamento de imagem é separado da manipulação da solicitação para facilitar a leitura e o teste. Para adicionar código de processamento de imagem, siga estas etapas:

  1. Mude para o diretório do código de amostra do tutorial do Pub/Sub.

  2. Adicione o código para importar as dependências de processamento de imagem, incluindo as bibliotecas a serem integradas aos serviços do Google Cloud, ao ImageMagick e ao sistema de arquivos.

    Node.js

    Abra um novo arquivo image.js no seu editor e copie o seguinte:
    const gm = require('gm').subClass({imageMagick: true});
    const fs = require('fs');
    const {promisify} = require('util');
    const path = require('path');
    const vision = require('@google-cloud/vision');
    
    const {Storage} = require('@google-cloud/storage');
    const storage = new Storage();
    const client = new vision.ImageAnnotatorClient();
    
    const {BLURRED_BUCKET_NAME} = process.env;

    Python

    Abra um novo arquivo image.py no seu editor e copie o seguinte:
    import os
    import tempfile
    
    from google.cloud import storage, vision
    from wand.image import Image
    
    storage_client = storage.Client()
    vision_client = vision.ImageAnnotatorClient()

    Go

    Abra um novo arquivo imagemagick/imagemagick.go no seu editor e copie o seguinte:
    
    // Package imagemagick contains an example of using ImageMagick to process a
    // file uploaded to Cloud Storage.
    package imagemagick
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"log"
    	"os"
    	"os/exec"
    
    	"cloud.google.com/go/storage"
    	vision "cloud.google.com/go/vision/apiv1"
    	visionpb "google.golang.org/genproto/googleapis/cloud/vision/v1"
    )
    
    // Global API clients used across function invocations.
    var (
    	storageClient *storage.Client
    	visionClient  *vision.ImageAnnotatorClient
    )
    
    func init() {
    	// Declare a separate err variable to avoid shadowing the client variables.
    	var err error
    
    	storageClient, err = storage.NewClient(context.Background())
    	if err != nil {
    		log.Fatalf("storage.NewClient: %v", err)
    	}
    
    	visionClient, err = vision.NewImageAnnotatorClient(context.Background())
    	if err != nil {
    		log.Fatalf("vision.NewAnnotatorClient: %v", err)
    	}
    }
    

    Java

    Abra um novo arquivo src/main/java/com/example/kuberun/ImageMagick.java no seu editor e copie o seguinte:
    import com.google.cloud.storage.Blob;
    import com.google.cloud.storage.BlobId;
    import com.google.cloud.storage.BlobInfo;
    import com.google.cloud.storage.Storage;
    import com.google.cloud.storage.StorageOptions;
    import com.google.cloud.vision.v1.AnnotateImageRequest;
    import com.google.cloud.vision.v1.AnnotateImageResponse;
    import com.google.cloud.vision.v1.BatchAnnotateImagesResponse;
    import com.google.cloud.vision.v1.Feature;
    import com.google.cloud.vision.v1.Feature.Type;
    import com.google.cloud.vision.v1.Image;
    import com.google.cloud.vision.v1.ImageAnnotatorClient;
    import com.google.cloud.vision.v1.ImageSource;
    import com.google.cloud.vision.v1.SafeSearchAnnotation;
    import com.google.gson.JsonObject;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    
    public class ImageMagick {
    
      private static final String BLURRED_BUCKET_NAME = System.getenv("BLURRED_BUCKET_NAME");
      private static Storage storage = StorageOptions.getDefaultInstance().getService();

  3. Adicione o código para receber uma mensagem do Pub/Sub como um objeto de evento e controlar o processamento da imagem.

    O evento contém dados sobre a imagem enviada originalmente. Esse código determina se a imagem precisa ser desfocada verificando os resultados de uma análise do Cloud Vision em busca de conteúdo violento ou adulto.

    Node.js

    // Blurs uploaded images that are flagged as Adult or Violence.
    exports.blurOffensiveImages = async event => {
      // This event represents the triggering Cloud Storage object.
      const object = event;
    
      const file = storage.bucket(object.bucket).file(object.name);
      const filePath = `gs://${object.bucket}/${object.name}`;
    
      console.log(`Analyzing ${file.name}.`);
    
      try {
        const [result] = await client.safeSearchDetection(filePath);
        const detections = result.safeSearchAnnotation || {};
    
        if (
          // Levels are defined in https://cloud.google.com/vision/docs/reference/rest/v1/AnnotateImageResponse#likelihood
          detections.adult === 'VERY_LIKELY' ||
          detections.violence === 'VERY_LIKELY'
        ) {
          console.log(`Detected ${file.name} as inappropriate.`);
          return blurImage(file, BLURRED_BUCKET_NAME);
        } else {
          console.log(`Detected ${file.name} as OK.`);
        }
      } catch (err) {
        console.error(`Failed to analyze ${file.name}.`, err);
        throw err;
      }
    };

    Python

    # Blurs uploaded images that are flagged as Adult or Violence.
    def blur_offensive_images(data):
        file_data = data
    
        file_name = file_data["name"]
        bucket_name = file_data["bucket"]
    
        blob = storage_client.bucket(bucket_name).get_blob(file_name)
        blob_uri = f"gs://{bucket_name}/{file_name}"
        blob_source = vision.Image(source=vision.ImageSource(image_uri=blob_uri))
    
        # Ignore already-blurred files
        if file_name.startswith("blurred-"):
            print(f"The image {file_name} is already blurred.")
            return
    
        print(f"Analyzing {file_name}.")
    
        result = vision_client.safe_search_detection(image=blob_source)
        detected = result.safe_search_annotation
    
        # Process image
        if detected.adult == 5 or detected.violence == 5:
            print(f"The image {file_name} was detected as inappropriate.")
            return __blur_image(blob)
        else:
            print(f"The image {file_name} was detected as OK.")

    Go

    
    // GCSEvent is the payload of a GCS event.
    type GCSEvent struct {
    	Bucket string `json:"bucket"`
    	Name   string `json:"name"`
    }
    
    // BlurOffensiveImages blurs offensive images uploaded to GCS.
    func BlurOffensiveImages(ctx context.Context, e GCSEvent) error {
    	outputBucket := os.Getenv("BLURRED_BUCKET_NAME")
    	if outputBucket == "" {
    		return errors.New("BLURRED_BUCKET_NAME must be set")
    	}
    
    	img := vision.NewImageFromURI(fmt.Sprintf("gs://%s/%s", e.Bucket, e.Name))
    
    	resp, err := visionClient.DetectSafeSearch(ctx, img, nil)
    	if err != nil {
    		return fmt.Errorf("AnnotateImage: %v", err)
    	}
    
    	if resp.GetAdult() == visionpb.Likelihood_VERY_LIKELY ||
    		resp.GetViolence() == visionpb.Likelihood_VERY_LIKELY {
    		return blur(ctx, e.Bucket, outputBucket, e.Name)
    	}
    	log.Printf("The image %q was detected as OK.", e.Name)
    	return nil
    }
    

    Java

    // Blurs uploaded images that are flagged as Adult or Violence.
    public static void blurOffensiveImages(JsonObject data) {
      String fileName = data.get("name").getAsString();
      String bucketName = data.get("bucket").getAsString();
      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, fileName).build();
      // Construct URI to GCS bucket and file.
      String gcsPath = String.format("gs://%s/%s", bucketName, fileName);
      System.out.println(String.format("Analyzing %s", fileName));
    
      // Construct request.
      List<AnnotateImageRequest> requests = new ArrayList<>();
      ImageSource imgSource = ImageSource.newBuilder().setImageUri(gcsPath).build();
      Image img = Image.newBuilder().setSource(imgSource).build();
      Feature feature = Feature.newBuilder().setType(Type.SAFE_SEARCH_DETECTION).build();
      AnnotateImageRequest request =
          AnnotateImageRequest.newBuilder().addFeatures(feature).setImage(img).build();
      requests.add(request);
    
      // Send request to the Vision API.
      try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
        BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests);
        List<AnnotateImageResponse> responses = response.getResponsesList();
        for (AnnotateImageResponse res : responses) {
          if (res.hasError()) {
            System.out.println(String.format("Error: %s\n", res.getError().getMessage()));
            return;
          }
          // Get Safe Search Annotations
          SafeSearchAnnotation annotation = res.getSafeSearchAnnotation();
          if (annotation.getAdultValue() == 5 || annotation.getViolenceValue() == 5) {
            System.out.println(String.format("Detected %s as inappropriate.", fileName));
            blur(blobInfo);
          } else {
            System.out.println(String.format("Detected %s as OK.", fileName));
          }
        }
      } catch (Exception e) {
        System.out.println(String.format("Error with Vision API: %s", e.getMessage()));
      }
    }

  4. Recupere a imagem referenciada do bucket de entrada do Cloud Storage criado acima, use o ImageMagick para transformar a imagem com um efeito de desfoque e faça upload do resultado para o bucket de saída.

    Node.js

    // Blurs the given file using ImageMagick, and uploads it to another bucket.
    const blurImage = async (file, blurredBucketName) => {
      const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
    
      // Download file from bucket.
      try {
        await file.download({destination: tempLocalPath});
    
        console.log(`Downloaded ${file.name} to ${tempLocalPath}.`);
      } catch (err) {
        throw new Error(`File download failed: ${err}`);
      }
    
      await new Promise((resolve, reject) => {
        gm(tempLocalPath)
          .blur(0, 16)
          .write(tempLocalPath, (err, stdout) => {
            if (err) {
              console.error('Failed to blur image.', err);
              reject(err);
            } else {
              console.log(`Blurred image: ${file.name}`);
              resolve(stdout);
            }
          });
      });
    
      // Upload result to a different bucket, to avoid re-triggering this function.
      const blurredBucket = storage.bucket(blurredBucketName);
    
      // Upload the Blurred image back into the bucket.
      const gcsPath = `gs://${blurredBucketName}/${file.name}`;
      try {
        await blurredBucket.upload(tempLocalPath, {destination: file.name});
        console.log(`Uploaded blurred image to: ${gcsPath}`);
      } catch (err) {
        throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
      }
    
      // Delete the temporary file.
      const unlink = promisify(fs.unlink);
      return unlink(tempLocalPath);
    };

    Python

    # Blurs the given file using ImageMagick.
    def __blur_image(current_blob):
        file_name = current_blob.name
        _, temp_local_filename = tempfile.mkstemp()
    
        # Download file from bucket.
        current_blob.download_to_filename(temp_local_filename)
        print(f"Image {file_name} was downloaded to {temp_local_filename}.")
    
        # Blur the image using ImageMagick.
        with Image(filename=temp_local_filename) as image:
            image.resize(*image.size, blur=16, filter="hamming")
            image.save(filename=temp_local_filename)
    
        print(f"Image {file_name} was blurred.")
    
        # Upload result to a second bucket, to avoid re-triggering the function.
        # You could instead re-upload it to the same bucket + tell your function
        # to ignore files marked as blurred (e.g. those with a "blurred" prefix)
        blur_bucket_name = os.getenv("BLURRED_BUCKET_NAME")
        blur_bucket = storage_client.bucket(blur_bucket_name)
        new_blob = blur_bucket.blob(file_name)
        new_blob.upload_from_filename(temp_local_filename)
        print(f"Blurred image uploaded to: gs://{blur_bucket_name}/{file_name}")
    
        # Delete the temporary file.
        os.remove(temp_local_filename)

    Go

    
    // blur blurs the image stored at gs://inputBucket/name and stores the result in
    // gs://outputBucket/name.
    func blur(ctx context.Context, inputBucket, outputBucket, name string) error {
    	inputBlob := storageClient.Bucket(inputBucket).Object(name)
    	r, err := inputBlob.NewReader(ctx)
    	if err != nil {
    		return fmt.Errorf("NewReader: %v", err)
    	}
    
    	outputBlob := storageClient.Bucket(outputBucket).Object(name)
    	w := outputBlob.NewWriter(ctx)
    	defer w.Close()
    
    	// Use - as input and output to use stdin and stdout.
    	cmd := exec.Command("convert", "-", "-blur", "0x8", "-")
    	cmd.Stdin = r
    	cmd.Stdout = w
    
    	if err := cmd.Run(); err != nil {
    		return fmt.Errorf("cmd.Run: %v", err)
    	}
    
    	log.Printf("Blurred image uploaded to gs://%s/%s", outputBlob.BucketName(), outputBlob.ObjectName())
    
    	return nil
    }
    

    Java

      // Blurs the file described by blobInfo using ImageMagick,
      // and uploads it to the blurred bucket.
      public static void blur(BlobInfo blobInfo) throws IOException {
        String bucketName = blobInfo.getBucket();
        String fileName = blobInfo.getName();
        // Download image
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        Path download = Paths.get("/tmp/", fileName);
        blob.downloadTo(download);
    
        // Construct the command.
        List<String> args = new ArrayList<String>();
        args.add("convert");
        args.add(download.toString());
        args.add("-blur");
        args.add("0x8");
        Path upload = Paths.get("/tmp/", "blurred-" + fileName);
        args.add(upload.toString());
        try {
          ProcessBuilder pb = new ProcessBuilder(args);
          Process process = pb.start();
          process.waitFor();
        } catch (Exception e) {
          System.out.println(String.format("Error: %s", e.getMessage()));
        }
    
        // Upload image to blurred bucket.
        BlobId blurredBlobId = BlobId.of(BLURRED_BUCKET_NAME, fileName);
        BlobInfo blurredBlobInfo =
            BlobInfo.newBuilder(blurredBlobId).setContentType(blob.getContentType()).build();
        try {
          byte[] blurredFile = Files.readAllBytes(upload);
          Blob blurredBlob = storage.create(blurredBlobInfo, blurredFile);
          System.out.println(
              String.format("Blurred image uploaded to: gs://%s/%s", BLURRED_BUCKET_NAME, fileName));
        } catch (Exception e) {
          System.out.println(String.format("Error in upload: %s", e.getMessage()));
        }
    
        // Remove images from fileSystem
        Files.delete(download);
        Files.delete(upload);
      }
    }

Como integrar o processamento de imagens ao código de amostra do Pub/Sub

Para modificar o serviço atual para incorporar o código de processamento de imagem, siga estas etapas:

  1. Adicione novas dependências ao serviço, incluindo as bibliotecas de cliente do Cloud Vision e do Cloud Storage:

    Node.js

    npm install --save gm @google-cloud/storage @google-cloud/vision

    Python

    Adicione as bibliotecas de cliente necessárias para que o requirements.txt fique assim:
    Flask==1.1.2
    pytest==5.3.0; python_version > "3.0"
    pytest==4.6.6; python_version < "3.0"
    gunicorn==20.0.4
    google-cloud-vision==2.0.0
    google-cloud-storage==1.35.0
    Wand==0.6.5
    

    Go

    A amostra de aplicativo em Go usa módulos do Go. Será feito o download automático das novas dependências adicionadas acima na instrução de importação imagemagick/imagemagick.go pelo próximo comando que precisar delas.

    Java

    Adicione a dependência a seguir em <dependencyManagement> no pom.xml:
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>1.2.6.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    Adicione as dependências a seguir em <dependencies> no pom.xml:
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.6</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20201115</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.8.0</version>
    </dependency>
    

  2. Adicione o pacote do sistema ImageMagick ao seu contêiner modificando o Dockerfile abaixo da instrução FROM. Se estiver usando um Dockerfile "de vários estágios", coloque-o no estágio final.

    Debian/Ubuntu
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/kuberun/docs/tutorials/system-packages#dockerfile
    RUN set -ex; \
      apt-get -y update; \
      apt-get -y install imagemagick; \
      rm -rf /var/lib/apt/lists/*
    
    Alpine
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/kuberun/docs/tutorials/system-packages#dockerfile
    RUN apk add --no-cache imagemagick
    

    Leia mais sobre como trabalhar com pacotes do sistema no Cloud Run for Anthos no Google Cloud no tutorial Como usar pacotes do sistema.

  3. Substitua o código de tratamento da mensagem do Pub/Sub atual por uma chamada de função para nossa nova lógica de desfoque.

    Node.js

    O arquivo app.js define o aplicativo Express.js e prepara mensagens do Pub/Sub recebidas para uso. Faça as mudanças a seguir:

    • Adicione código para importar o novo arquivo image.js.
    • Remova o código "Hello World" atual da rota.
    • Adicione código para validar ainda mais a mensagem do Pub/Sub.
    • Adicione código para chamar a nova função de processamento de imagens.

      Quando terminar, o código ficará assim:

    
    const express = require('express');
    const bodyParser = require('body-parser');
    const app = express();
    
    app.use(bodyParser.json());
    
    const image = require('./image');
    
    app.post('/', async (req, res) => {
      if (!req.body) {
        const msg = 'no Pub/Sub message received';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
      if (!req.body.message || !req.body.message.data) {
        const msg = 'invalid Pub/Sub message format';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      // Decode the Pub/Sub message.
      const pubSubMessage = req.body.message;
      let data;
      try {
        data = Buffer.from(pubSubMessage.data, 'base64').toString().trim();
        data = JSON.parse(data);
      } catch (err) {
        const msg =
          'Invalid Pub/Sub message: data property is not valid base64 encoded JSON';
        console.error(`error: ${msg}: ${err}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      // Validate the message is a Cloud Storage event.
      if (!data.name || !data.bucket) {
        const msg =
          'invalid Cloud Storage notification: expected name and bucket properties';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      try {
        await image.blurOffensiveImages(data);
        res.status(204).send();
      } catch (err) {
        console.error(`error: Blurring image: ${err}`);
        res.status(500).send();
      }
    });

    Python

    O arquivo main.py define o aplicativo Flask e prepara as mensagens do Pub/Sub recebidas para uso. Faça as mudanças a seguir:

    • Adicione código para importar o novo arquivo image.py.
    • Remova o código "Hello World" atual da rota.
    • Adicione código para validar ainda mais a mensagem do Pub/Sub.
    • Adicione código para chamar a nova função de processamento de imagens.

      Quando terminar, o código ficará assim:

    import base64
    import json
    import os
    
    from flask import Flask, request
    
    import image
    
    app = Flask(__name__)
    
    @app.route("/", methods=["POST"])
    def index():
        envelope = request.get_json()
        if not envelope:
            msg = "no Pub/Sub message received"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        if not isinstance(envelope, dict) or "message" not in envelope:
            msg = "invalid Pub/Sub message format"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        # Decode the Pub/Sub message.
        pubsub_message = envelope["message"]
    
        if isinstance(pubsub_message, dict) and "data" in pubsub_message:
            try:
                data = json.loads(base64.b64decode(pubsub_message["data"]).decode())
    
            except Exception as e:
                msg = (
                    "Invalid Pub/Sub message: "
                    "data property is not valid base64 encoded JSON"
                )
                print(f"error: {e}")
                return f"Bad Request: {msg}", 400
    
            # Validate the message is a Cloud Storage event.
            if not data["name"] or not data["bucket"]:
                msg = (
                    "Invalid Cloud Storage notification: "
                    "expected name and bucket properties"
                )
                print(f"error: {msg}")
                return f"Bad Request: {msg}", 400
    
            try:
                image.blur_offensive_images(data)
                return ("", 204)
    
            except Exception as e:
                print(f"error: {e}")
                return ("", 500)
    
        return ("", 500)

    Go

    O arquivo main.go define o serviço HTTP e prepara mensagens do Pub/Sub recebidas para uso. Faça as mudanças a seguir:

    • Adicione código para importar o novo arquivo imagemagick.go.
    • Remova o código "Hello World" atual do gerenciador.
    • Adicione código para validar ainda mais a mensagem do Pub/Sub.
    • Adicione código para chamar a nova função de processamento de imagens.

    
    // Sample image-processing is a KubeRun service which performs asynchronous processing on images.
    package main
    
    import (
    	"encoding/json"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"os"
    
    	"github.com/GoogleCloudPlatform/golang-samples/kuberun/image-processing/imagemagick"
    )
    
    func main() {
    	http.HandleFunc("/", HelloPubSub)
    	// Determine port for HTTP service.
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    	// Start HTTP server.
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // PubSubMessage is the payload of a Pub/Sub event.
    type PubSubMessage struct {
    	Message struct {
    		Data []byte `json:"data,omitempty"`
    		ID   string `json:"id"`
    	} `json:"message"`
    	Subscription string `json:"subscription"`
    }
    
    // HelloPubSub receives and processes a Pub/Sub push message.
    func HelloPubSub(w http.ResponseWriter, r *http.Request) {
    	var m PubSubMessage
    	body, err := ioutil.ReadAll(r.Body)
    	if err != nil {
    		log.Printf("ioutil.ReadAll: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    	if err := json.Unmarshal(body, &m); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	var e imagemagick.GCSEvent
    	if err := json.Unmarshal(m.Message.Data, &e); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if e.Name == "" || e.Bucket == "" {
    		log.Printf("invalid GCSEvent: expected name and bucket")
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if err := imagemagick.BlurOffensiveImages(r.Context(), e); err != nil {
    		log.Printf("imagemagick.BlurOffensiveImages: %v", err)
    		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    	}
    }
    

    Java

    O arquivo PubSubController.java define o controlador que manipula solicitações HTTP e prepara mensagens do Pub/Sub recebidas para uso. Faça as mudanças a seguir:

    • Adicione as novas importações.
    • Remova o código "Hello World" atual do controlador.
    • Adicione código para validar ainda mais a mensagem do Pub/Sub.
    • Adicione código para chamar a nova função de processamento de imagens.

    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import java.util.Base64;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    // PubsubController consumes a Pub/Sub message.
    @RestController
    public class PubSubController {
      @RequestMapping(value = "/", method = RequestMethod.POST)
      public ResponseEntity receiveMessage(@RequestBody Body body) {
        // Get PubSub message from request body.
        Body.Message message = body.getMessage();
        if (message == null) {
          String msg = "Bad Request: invalid Pub/Sub message format";
          System.out.println(msg);
          return new ResponseEntity(msg, HttpStatus.BAD_REQUEST);
        }
    
        // Decode the Pub/Sub message.
        String pubSubMessage = message.getData();
        JsonObject data;
        try {
          String decodedMessage = new String(Base64.getDecoder().decode(pubSubMessage));
          data = new JsonParser().parse(decodedMessage).getAsJsonObject();
        } catch (Exception e) {
          String msg = "Error: Invalid Pub/Sub message: data property is not valid base64 encoded JSON";
          System.out.println(msg);
          return new ResponseEntity(msg, HttpStatus.BAD_REQUEST);
        }
    
        // Validate the message is a Cloud Storage event.
        if (data.get("name") == null || data.get("bucket") == null) {
          String msg = "Error: Invalid Cloud Storage notification: expected name and bucket properties";
          System.out.println(msg);
          return new ResponseEntity(msg, HttpStatus.BAD_REQUEST);
        }
    
        try {
          ImageMagick.blurOffensiveImages(data);
        } catch (Exception e) {
          String msg = String.format("Error: Blurring image: %s", e.getMessage());
          System.out.println(msg);
          return new ResponseEntity(msg, HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return new ResponseEntity(HttpStatus.OK);
      }
    }

Como fazer o download da amostra completa

Para recuperar a amostra de código de processamento de imagem completa para uso, siga estas etapas:

  1. Clone o repositório do aplicativo de amostra na máquina local:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    Outra alternativa é fazer o download da amostra como um arquivo ZIP e extraí-lo.

  2. Altere para o diretório que contém o exemplo de código do Cloud Run for Anthos no Google Cloud:

    Node.js

    cd nodejs-docs-samples/kuberun/image-processing/

    Python

    cd python-docs-samples/kuberun/image-processing/

    Go

    cd golang-samples/kuberun/image-processing/

    Java

    cd java-docs-samples/kuberun/image-processing/

Como enviar o código

O código de envio consiste em três etapas: criar uma imagem de contêiner com o Cloud Build, fazer upload da imagem de contêiner no Container Registry e implantar a imagem de contêiner no Cloud Run for Anthos no Google Cloud.

Para enviar o código, siga estas etapas:

  1. Compile seu contêiner e publique no Container Registry.

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    PROJECT_ID é o ID do projeto do GCP e pubsub é o nome que você quer dar ao serviço.

    Após a conclusão, você verá uma mensagem de SUCESSO contendo o ID, a hora da criação e o nome da imagem. A imagem é armazenada no Container Registry e poderá ser reutilizada, se você quiser.

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    PROJECT_ID é o ID do projeto do GCP e pubsub é o nome que você quer dar ao serviço.

    Após a conclusão, você verá uma mensagem de SUCESSO contendo o ID, a hora da criação e o nome da imagem. A imagem é armazenada no Container Registry e poderá ser reutilizada, se você quiser.

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    PROJECT_ID é o ID do projeto do GCP e pubsub é o nome que você quer dar ao serviço.

    Após a conclusão, você verá uma mensagem de SUCESSO contendo o ID, a hora da criação e o nome da imagem. A imagem é armazenada no Container Registry e poderá ser reutilizada, se você quiser.

    Java

    Esta amostra usa o Jib (em inglês) para criar imagens do Docker usando ferramentas comuns do Java. O Jib otimiza builds de contêiner sem a necessidade de um Dockerfile ou de ter o Docker (em inglês) instalado. Saiba mais sobre como criar contêineres Java com o Jib.

    1. Usando o Dockerfile, configure e crie uma imagem base com os pacotes do sistema instalados para substituir a imagem base padrão do Jib:

      # Use AdoptOpenJDK for base image.
      # It's important to use OpenJDK 8u191 or above that has container support enabled.
      # https://hub.docker.com/r/adoptopenjdk/openjdk11
      FROM adoptopenjdk/openjdk11:alpine-slim
      
      # Install Imagemagick into the container image.
      # For more on system packages review the system packages tutorial.
      # https://cloud.google.com/kuberun/docs/tutorials/system-packages#dockerfile
      RUN apk add --no-cache imagemagick

      gcloud builds submit --tag gcr.io/PROJECT_ID/imagemagick

      PROJECT_ID é o ID do projeto do GCP.

    2. Crie seu contêiner final com o Jib e publique no Container Registry:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.7.0</version>
        <configuration>
          <from>
            <image>gcr.io/PROJECT_ID/imagemagick</image>
          </from>
          <to>
            <image>gcr.io/PROJECT_ID/pubsub</image>
          </to>
        </configuration>
      </plugin>
      
      mvn compile jib:build \
        -Dimage=gcr.io/PROJECT_ID/pubsub \
        -Djib.from.image=gcr.io/PROJECT_ID/imagemagick

      PROJECT_ID é o ID do projeto do GCP.

  2. Execute o comando a seguir para implantar seu serviço usando o mesmo nome de serviço usado no tutorial do Pub/Sub:

    Node.js

    gcloud kuberun core services create pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Python

    gcloud kuberun core services create pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Go

    gcloud kuberun core services create pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Java

    gcloud kuberun core services create pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --memory 512M

    Substitua PROJECT_ID pelo ID do projeto do GCP. pubsub é o nome do contêiner e pubsub-tutorial é o nome do serviço. Observe que a imagem do contêiner é implantada no serviço e no cluster que você configurou anteriormente em Como configurar os padrões do gcloud.

    Substitua BLURRED_BUCKET_NAME pelo bucket do Cloud Storage criado anteriormente para receber imagens desfocadas e definir a variável de ambiente.

    Aguarde até que a implantação esteja concluída. Isso pode levar cerca de 30 segundos. Em caso de sucesso, a linha de comando exibe o URL de serviço.

Como ativar notificações do Cloud Storage

Configure o Cloud Storage para publicar uma mensagem em um tópico do Pub/Sub sempre que um arquivo (conhecido como objeto) for carregado ou alterado. Envie a notificação para o tópico criado anteriormente para que o novo upload do arquivo invoque o serviço.

gsutil notification create -t myRunTopic -f json gs://INPUT_BUCKET_NAME

O comando gsutil é instalado como parte do SDK do Cloud. myRunTopic é o tópico que você criou no tutorial anterior.

Substitua INPUT_BUCKET_NAME pelo nome usado quando você criou os buckets.

Para mais detalhes sobre notificações de bucket de armazenamento, leia Notificações de alteração de objeto.

Como testar

  1. Faça upload de uma imagem ofensiva, como esta imagem de um zumbi comedor de carne:

    gsutil cp zombie.jpg gs://INPUT_BUCKET_NAME

    em que INPUT_BUCKET_NAME é o bucket do Cloud Storage criado anteriormente para o upload de imagens.

  2. Navegue até os registros de serviço:

    1. Navegue até a página do Cloud Run para Anthos no Google Cloud no Console do Cloud
    2. Clique no serviço pubsub-tutorial.
    3. Selecione a guia Registros. Os registros podem demorar alguns instantes para aparecer. Se você não os vir imediatamente, verifique outra vez após alguns instantes.
  3. Procure a mensagem Blurred image: zombie.png.

  4. Para ver as imagens desfocadas no bucket BLURRED_BUCKET_NAME do Cloud Storage criado anteriormente, localize o bucket na página do Cloud Storage no Console do Cloud

Limpeza

Se você criou um novo projeto para este tutorial, exclua o projeto. Se você usou um projeto atual e quer mantê-lo sem as alterações incluídas neste tutorial, exclua os recursos criados para o tutorial.

Como excluir o projeto

O jeito mais fácil de evitar cobranças é excluindo o projeto que você criou para o tutorial.

Para excluir o projeto:

  1. No Console do Cloud, acesse a página Gerenciar recursos:

    Acessar "Gerenciar recursos"

  2. Na lista de projetos, selecione o projeto que você quer excluir e clique em Excluir .
  3. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.

Como excluir recursos do tutorial

  1. Exclua o serviço do Cloud Run para Anthos no Google Cloud que você implantou neste tutorial:

    gcloud kuberun core services delete SERVICE-NAME

    SERVICE-NAME é o nome escolhido do serviço.

    Também é possível excluir o Cloud Run para Anthos nos serviços do Google Cloud no Console do Google Cloud.

  2. Remova as configurações padrão da gcloud que você adicionou durante a configuração do tutorial:

     gcloud config unset kuberun/cluster
     gcloud config unset kuberun/cluster_location
    
  3. Remova a configuração do projeto:

     gcloud config unset project
    
  4. Exclua outros recursos do Google Cloud criados neste tutorial:

A seguir

  • Saiba mais sobre a permanência de dados com o Cloud Run for Anthos no Google Cloud por meio do Cloud Storage
  • Saiba como usar a API Cloud Vision para detectar elementos além do conteúdo explícito.
  • Teste outros recursos do Google Cloud. Veja nossos tutoriais.