Tutorial sobre o processamento de imagens do Cloud Storage

Este tutorial demonstra a utilização do Cloud Run, da Cloud Vision API e do ImageMagick para detetar e esbater imagens ofensivas carregadas para um contentor do Cloud Storage. Este tutorial baseia-se no tutorial Use o Pub/Sub com o Cloud Run.

Este tutorial explica como modificar uma app de exemplo existente. Também pode transferir o exemplo concluído, se quiser.

Configurar predefinições do gcloud

Para configurar o gcloud com predefinições para o seu serviço do Cloud Run:

  1. Defina o projeto predefinido:

    gcloud config set project PROJECT_ID

    Substitua PROJECT_ID pelo nome do projeto que criou para este tutorial.

  2. Configure o gcloud para a região escolhida:

    gcloud config set run/region REGION

    Substitua REGION pela região do Cloud Run suportada à sua escolha.

Localizações do Cloud Run

O Cloud Run é regional, o que significa que a infraestrutura que executa os seus serviços do Cloud Run está localizada numa região específica e é gerida pela Google para estar disponível de forma redundante em todas as zonas dessa região.

O cumprimento dos seus requisitos de latência, disponibilidade ou durabilidade são fatores principais para selecionar a região onde os seus serviços do Cloud Run são executados. Geralmente, pode selecionar a região mais próxima dos seus utilizadores, mas deve considerar a localização dos outros Google Cloudprodutos usados pelo seu serviço do Cloud Run. A utilização Google Cloud de produtos em conjunto em várias localizações pode afetar a latência do seu serviço, bem como o custo.

O Cloud Run está disponível nas seguintes regiões:

Sujeito aos preços de Nível 1

  • asia-east1 (Taiwan)
  • asia-northeast1 (Tóquio)
  • asia-northeast2 (Osaca)
  • asia-south1 (Mumbai, Índia)
  • europe-north1 (Finlândia) ícone de folha Baixo CO2
  • europe-north2 (Estocolmo) ícone de folha Baixo CO2
  • europe-southwest1 (Madrid) ícone de folha Baixo CO2
  • europe-west1 (Bélgica) ícone de folha Baixo CO2
  • europe-west4 (Países Baixos) ícone de folha Baixo CO2
  • europe-west8 (Milão)
  • europe-west9 (Paris) ícone de folha Baixo CO2
  • me-west1 (Telavive)
  • northamerica-south1 (México)
  • us-central1 (Iowa) ícone de folha Baixo CO2
  • us-east1 (Carolina do Sul)
  • us-east4 (Virgínia do Norte)
  • us-east5 (Columbus)
  • us-south1 (Dallas) ícone de folha Baixo CO2
  • us-west1 (Oregão) ícone de folha Baixo CO2

Sujeito aos preços de Nível 2

  • africa-south1 (Joanesburgo)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seul, Coreia do Sul)
  • asia-southeast1 (Singapura)
  • asia-southeast2 (Jacarta)
  • asia-south2 (Deli, Índia)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsóvia, Polónia)
  • europe-west10 (Berlim)
  • europe-west12 (Turim)
  • europe-west2 (Londres, Reino Unido) ícone de folha Baixo CO2
  • europe-west3 (Frankfurt, Alemanha)
  • europe-west6 (Zurique, Suíça) ícone de folha Baixo CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal) ícone de folha Baixo CO2
  • northamerica-northeast2 (Toronto) ícone de folha Baixo CO2
  • southamerica-east1 (São Paulo, Brasil) ícone de folha Baixo CO2
  • southamerica-west1 (Santiago, Chile) ícone de folha Baixo CO2
  • us-west2 (Los Angeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Se já criou um serviço do Cloud Run, pode ver a região no painel de controlo do Cloud Run na Google Cloud consola.

Compreender a sequência de operações

O fluxo de dados neste tutorial segue estes passos:

  1. Um utilizador carrega uma imagem para um contentor do Cloud Storage.
  2. O Cloud Storage publica uma mensagem sobre o novo ficheiro no Pub/Sub.
  3. O Pub/Sub envia a mensagem para o serviço do Cloud Run.
  4. O serviço do Cloud Run obtém o ficheiro de imagem referenciado na mensagem do Pub/Sub.
  5. O serviço do Cloud Run usa a API Cloud Vision para analisar a imagem.
  6. Se for detetado conteúdo violento ou para adultos, o serviço Cloud Run usa o ImageMagick para esbater a imagem.
  7. O serviço do Cloud Run carrega a imagem esbatida para outro contentor do Cloud Storage para utilização.

A utilização subsequente da imagem esbatida é deixada como um exercício para o leitor.

Crie um repositório padrão do Artifact Registry

Crie um repositório padrão do Artifact Registry para armazenar a sua imagem de contentor:

gcloud artifacts repositories create REPOSITORY \
    --repository-format=docker \
    --location=REGION

Substituição:

  • REPOSITORY com um nome exclusivo para o repositório.
  • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.

Configure contentores do Cloud Storage

gcloud

  1. Crie um contentor do Cloud Storage para carregar imagens, em que INPUT_BUCKET_NAME é um nome de contentor exclusivo a nível global:

    gcloud storage buckets create gs://INPUT_BUCKET_NAME

    O serviço do Cloud Run só lê a partir deste contentor.

  2. Crie um segundo contentor do Cloud Storage para receber imagens esbatidas, em que BLURRED_BUCKET_NAME é um nome de contentor exclusivo a nível global:

    gcloud storage buckets create gs://BLURRED_BUCKET_NAME

    O serviço do Cloud Run carrega imagens esbatidas para este contentor. A utilização de um contentor separado impede que as imagens processadas acionem novamente o serviço.

    Por predefinição, as revisões do Cloud Run são executadas como a conta de serviço predefinida do Compute Engine.

    Em alternativa, se estiver a usar uma conta de serviço gerida pelo utilizador, certifique-se de que atribuiu as funções do IAM necessárias para que tenha autorização storage.objects.get para ler a partir de INPUT_BUCKET_NAME e autorização storage.objects.create para carregar para BLURRED_BUCKET_NAME.

Terraform

Para saber como aplicar ou remover uma configuração do Terraform, consulte os comandos básicos do Terraform.

Crie dois contentores do Cloud Storage: um para carregar imagens originais e outro para o serviço do Cloud Run carregar imagens esbatidas.

Para criar ambos os contentores do Cloud Storage com nomes exclusivos a nível global, adicione o seguinte ao seu ficheiro main.tf existente:

resource "random_id" "bucket_suffix" {
  byte_length = 8
}

resource "google_storage_bucket" "imageproc_input" {
  name     = "input-bucket-${random_id.bucket_suffix.hex}"
  location = "us-central1"
}

output "input_bucket_name" {
  value = google_storage_bucket.imageproc_input.name
}

resource "google_storage_bucket" "imageproc_output" {
  name     = "output-bucket-${random_id.bucket_suffix.hex}"
  location = "us-central1"
}

output "blurred_bucket_name" {
  value = google_storage_bucket.imageproc_output.name
}

Por predefinição, as revisões do Cloud Run são executadas como a conta de serviço predefinida do Compute Engine.

Em alternativa, se estiver a usar uma conta de serviço gerida pelo utilizador, certifique-se de que atribuiu as funções IAM necessárias para que tenha autorização storage.objects.get para ler a partir de google_storage_bucket.imageproc_input e autorização storage.objects.create para carregar para google_storage_bucket.imageproc_output.

Nos passos seguintes, cria e implementa um serviço que processa a notificação de carregamentos de ficheiros para o INPUT_BUCKET_NAME. Ative a entrega de notificações depois de implementar e testar o serviço para evitar a invocação prematura do novo serviço.

Modifique o código de exemplo do tutorial do Pub/Sub

Este tutorial baseia-se no código reunido no tutorial de utilização do Pub/Sub. Se ainda não concluiu esse tutorial, faça-o agora, ignorando os passos de limpeza e, em seguida, volte aqui para adicionar o comportamento de processamento de imagens.

Adicione código de processamento de imagens

O código de processamento de imagens está separado do processamento de pedidos para facilitar a leitura e os testes. Para adicionar código de processamento de imagens:

  1. Altere para o diretório do código de exemplo do tutorial do Pub/Sub.

  2. Adicione código para importar as dependências de processamento de imagens, incluindo bibliotecas para integrar com Google Cloud serviços, o ImageMagick e o sistema de ficheiros.

    Node.js

    Abra um novo ficheiro image.js no 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 ficheiro image.py no 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()

    Ir

    Abra um novo ficheiro imagemagick/imagemagick.go no 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"
    	"cloud.google.com/go/vision/v2/apiv1/visionpb"
    )
    
    // 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 ficheiro src/main/java/com/example/cloudrun/ImageMagick.java no 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 código para receber uma mensagem Pub/Sub como um objeto de evento e controlar o processamento de imagens.

    O evento contém dados sobre a imagem carregada originalmente. Este código determina se a imagem tem de ser esbatida verificando os resultados de uma análise do Cloud Vision para conteúdo violento ou para adultos.

    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

    def blur_offensive_images(data):
        """Blurs uploaded images that are flagged as Adult or Violence.
    
        Args:
            data: Pub/Sub message 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.")
    
    

    Ir

    
    // 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: %w", 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 contentor de entrada do Cloud Storage criado acima, use o ImageMagick para transformar a imagem com um efeito de esbatimento e carregue o resultado para o contentor 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

    def __blur_image(current_blob):
        """Blurs the given file using ImageMagick.
    
        Args:
            current_blob: a Cloud Storage 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)
    
    

    Ir

    
    // 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: %w", 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: %w", 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<>();
        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);
      }
    }

Integre o processamento de imagens no código de exemplo do Pub/Sub

Para modificar o serviço existente de forma a incorporar o código de processamento de imagens:

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

    Node.js

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

    Python

    Adicione as bibliotecas cliente necessárias para que o seu requirements.txt tenha um aspeto semelhante ao seguinte:
    Flask==3.0.3
    google-cloud-storage==2.12.0
    google-cloud-vision==3.8.1
    gunicorn==23.0.0
    Wand==0.6.13
    Werkzeug==3.0.3
    

    Ir

    A aplicação de exemplo go usa módulos go. As novas dependências adicionadas acima na declaração imagemagick/imagemagick.go import são transferidas automaticamente pelo comando seguinte que as necessita.

    Java

    Adicione a seguinte dependência em <dependencyManagement> no pom.xml:
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>4.9.2</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    Adicione as seguintes dependências em <dependencies> no pom.xml:
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    </dependency>
    

  2. Adicione o pacote do sistema ImageMagick ao seu contentor modificando o Dockerfile abaixo da declaração FROM. Se usar um Dockerfile "de várias fases", coloque isto na fase final.

    Debian/Ubuntu
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/run/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/run/docs/tutorials/system-packages#dockerfile
    RUN apk add --no-cache imagemagick
    

    Leia mais sobre como trabalhar com pacotes do sistema no seu serviço do Cloud Run no tutorial de utilização de pacotes do sistema.

  3. Substitua o código de processamento de mensagens Pub/Sub existente por uma chamada de função à nossa nova lógica de esbatimento.

    Node.js

    O ficheiro app.js define a app Express.js e prepara as mensagens do Pub/Sub recebidas para utilização. Faça as seguintes alterações:

    • Adicione código para importar o novo ficheiro image.js
    • Remova o código "Hello World" existente do caminho
    • 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 tem o seguinte aspeto:

    
    const express = require('express');
    const app = express();
    
    // This middleware is available in Express v4.16.0 onwards
    app.use(express.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 ficheiro main.py define a app Flask e prepara as mensagens do Pub/Sub recebidas para utilização. Faça as seguintes alterações:

    • Adicione código para importar o novo ficheiro image.py
    • Remova o código "Hello World" existente do caminho
    • 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 tem o seguinte aspeto:

    import base64
    import json
    import os
    
    from flask import Flask, request
    
    import image
    
    
    app = Flask(__name__)
    
    
    @app.route("/", methods=["POST"])
    def index():
        """Receive and parse Pub/Sub messages containing Cloud Storage event data."""
        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)
    

    Ir

    O ficheiro main.go define o serviço HTTP e prepara as mensagens do Pub/Sub recebidas para utilização. Faça as seguintes alterações:

    • Adicione código para importar o novo ficheiro imagemagick.go
    • Remova o código "Hello World" existente 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

    
    // Sample image-processing is a Cloud Run service which performs asynchronous processing on images.
    package main
    
    import (
    	"encoding/json"
    	"io"
    	"log"
    	"net/http"
    	"os"
    
    	"github.com/GoogleCloudPlatform/golang-samples/run/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.
    // See the documentation for more details:
    // https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
    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 := io.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 ficheiro PubSubController.java define o controlador que processa os pedidos HTTP e prepara as mensagens Pub/Sub recebidas para utilização. Faça as seguintes alterações:

    • Adicione as novas importações
    • Remova o código "Hello World" existente 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<String> 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 = JsonParser.parseString(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);
      }
    }

Transfira o exemplo completo

Para obter o exemplo de código de processamento de imagens completo para utilização:

  1. Clone o repositório da app de exemplo para a sua máquina local:

    Node.js

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

    Python

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

    Ir

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

    Java

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

    Em alternativa, pode transferir o exemplo como um ficheiro ZIP e extraí-lo.

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

    Node.js

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

    Python

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

    Ir

    cd golang-samples/run/image-processing/

    Java

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

Envie o código

O código de envio consiste em três passos: criar uma imagem de contentor com o Cloud Build, carregar a imagem de contentor para o Artifact Registry e implementar a imagem de contentor no Cloud Run.

Para enviar o código:

  1. Crie o contentor e publique-o no Artifact Registry:

    Node.js

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub

    Em que pubsub é o nome do seu serviço.

    Substituição:

    • PROJECT_ID com o seu Google Cloud ID do projeto
    • REPOSITORY com o nome do repositório do Artifact Registry.
    • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.

    Após a conclusão com êxito, é apresentada uma mensagem SUCCESS com o ID, a hora de criação e o nome da imagem. A imagem é armazenada no Artifact Registry e pode ser reutilizada, se necessário.

    Python

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub

    Em que pubsub é o nome do seu serviço.

    Substituição:

    • PROJECT_ID com o seu Google Cloud ID do projeto
    • REPOSITORY com o nome do repositório do Artifact Registry.
    • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.

    Após a conclusão com êxito, é apresentada uma mensagem SUCCESS com o ID, a hora de criação e o nome da imagem. A imagem é armazenada no Artifact Registry e pode ser reutilizada, se necessário.

    Ir

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub

    Em que pubsub é o nome do seu serviço.

    Substituição:

    • PROJECT_ID com o seu Google Cloud ID do projeto
    • REPOSITORY com o nome do repositório do Artifact Registry.
    • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.

    Após a conclusão com êxito, é apresentada uma mensagem SUCCESS com o ID, a hora de criação e o nome da imagem. A imagem é armazenada no Artifact Registry e pode ser reutilizada, se necessário.

    Java

    Este exemplo usa o Jib para criar imagens do Docker com ferramentas Java comuns. O Jib otimiza as compilações de contentores sem necessidade de um Dockerfile ou de ter o Docker instalado. Saiba mais sobre como criar contentores Java com o Jib.

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

      # Use eclipse-temurin for base image.
      # It's important to use JDK 8u191 or above that has container support enabled.
      # https://hub.docker.com/_/eclipse-temurin/
      # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
      FROM eclipse-temurin:17.0.16_8-jre
      
      # Install Imagemagick into the container image.
      # For more on system packages review the system packages tutorial.
      # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
      RUN set -ex; \
        apt-get -y update; \
        apt-get -y install imagemagick; \
        rm -rf /var/lib/apt/lists/*

      gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/imagemagick

      Substituição:

      • PROJECT_ID com o seu Google Cloud ID do projeto
      • REPOSITORY com o nome do repositório do Artifact Registry.
      • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.
    2. Use o gcloud credential helper para autorizar o Docker a enviar para o seu Artifact Registry.

      gcloud auth configure-docker

    3. Crie o contentor final com o Jib e publique-o no Artifact Registry:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.4.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=REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/pubsub \
        -Djib.from.image=REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/imagemagick

      Substituição:

      • PROJECT_ID com o seu Google Cloud ID do projeto
      • REPOSITORY com o nome do repositório do Artifact Registry.
      • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.

  2. Execute o seguinte comando para implementar o seu serviço, usando o mesmo nome do serviço que usou no tutorial Usar o Pub/Sub:

    Node.js

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Python

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Ir

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Java

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --memory 512M --no-allow-unauthenticated

    Onde pubsub é o nome do contentor e pubsub-tutorial é o nome do serviço. Tenha em atenção que a imagem do contentor é implementada no serviço e na região (Cloud Run) que configurou anteriormente em Configurar predefinições do gcloud. Substituição:

    • PROJECT_ID com o seu Google Cloud ID do projeto
    • REPOSITORY com o nome do repositório do Artifact Registry.
    • REGION com a Google Cloud região a usar para o repositório do Artifact Registry.
    • BLURRED_BUCKET_NAME com o contentor do Cloud Storage que criou anteriormente para receber imagens esbatidas para definir a variável de ambiente.

    A flag --no-allow-unauthenticated restringe o acesso não autenticado ao serviço. Ao manter o serviço privado, pode confiar na integração automática do Pub/Sub do Cloud Run para autenticar pedidos. Consulte o artigo Integração com o Pub/Sub para ver mais detalhes sobre como esta opção é configurada. Consulte o artigo Gerir o acesso para mais detalhes sobre a autenticação baseada na IAM.

    Aguarde até que a implementação esteja concluída. Este processo pode demorar cerca de meio minuto. Se for bem-sucedido, a linha de comandos apresenta o URL do serviço.

Ative as notificações do Cloud Storage

Configurar o Cloud Storage para publicar uma mensagem num tópico do Pub/Sub sempre que um ficheiro (conhecido como um objeto) é carregado ou alterado. Envie a notificação para o tópico criado anteriormente para que qualquer novo carregamento de ficheiro invoque o serviço.

gcloud

gcloud storage service-agent --project=PROJECT_ID
gcloud storage buckets notifications create gs://INPUT_BUCKET_NAME --topic=myRunTopic --payload-format=json

myRunTopic é o tópico que criou no tutorial anterior.

Substitua INPUT_BUCKET_NAME pelo nome que usou quando criou os grupos.

Para mais detalhes sobre as notificações de contentores de armazenamento, leia o artigo sobre as notificações de alterações de objetos.

Terraform

Para saber como aplicar ou remover uma configuração do Terraform, consulte os comandos básicos do Terraform.

Para ativar as notificações, a conta de serviço do Cloud Storage exclusiva do projeto tem de existir e ter a autorização de IAM pubsub.publisher no tópico do Pub/Sub. Para conceder esta autorização e criar uma notificação do Cloud Storage, adicione o seguinte ao seu ficheiro main.tf existente:

data "google_storage_project_service_account" "gcs_account" {}

resource "google_pubsub_topic_iam_binding" "binding" {
  topic   = google_pubsub_topic.default.name
  role    = "roles/pubsub.publisher"
  members = ["serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"]
}

resource "google_storage_notification" "notification" {
  bucket         = google_storage_bucket.imageproc_input.name
  payload_format = "JSON_API_V1"
  topic          = google_pubsub_topic.default.id
  depends_on     = [google_pubsub_topic_iam_binding.binding]
}

Experimentar

  1. Carregar uma imagem ofensiva, como esta imagem de um zombie carnívoro:

    curl -o zombie.jpg https://cdn.pixabay.com/photo/2015/09/21/14/24/zombie-949916_960_720.jpg
    gcloud storage cp zombie.jpg gs://INPUT_BUCKET_NAME

    onde INPUT_BUCKET_NAME é o contentor do Cloud Storage que criou anteriormente para carregar imagens.

  2. Navegue para os registos de serviço:

    1. Navegue para a página do Cloud Run na Google Cloud consola
    2. Clique no serviço pubsub-tutorial.
    3. Selecione o separador Registos. Os registos podem demorar alguns momentos a aparecer. Se não os vir imediatamente, verifique novamente após alguns momentos.
  3. Procure a mensagem Blurred image: zombie.png.

  4. Pode ver as imagens esbatidas no contentor do BLURRED_BUCKET_NAMECloud Storage que criou anteriormente: localize o contentor na página do Cloud Storage na Google Cloud consola