Tutorial para procesar imágenes de Cloud Storage

En este tutorial se muestra cómo usar Cloud Run, la API Cloud Vision y ImageMagick para detectar y desenfocar imágenes ofensivas subidas a un segmento de Cloud Storage. Este tutorial se basa en el tutorial Usar Pub/Sub con Cloud Run.

En este tutorial se explica cómo modificar una aplicación de ejemplo. También puedes descargar la aplicación de ejemplo completa si quieres.

Configurar los valores predeterminados de gcloud

Para configurar gcloud con los valores predeterminados de tu servicio de Cloud Run, sigue estos pasos:

  1. Configura tu proyecto predeterminado:

    gcloud config set project PROJECT_ID

    Sustituye PROJECT_ID por el nombre del proyecto que has creado para este tutorial.

  2. Configura gcloud para la región que hayas elegido:

    gcloud config set run/region REGION

    Sustituye REGION por la región de Cloud Run compatible que quieras.

Ubicaciones de Cloud Run

Cloud Run es regional, lo que significa que la infraestructura que ejecuta tus servicios de Cloud Run se encuentra en una región específica y Google la gestiona para que esté disponible de forma redundante en todas las zonas de esa región.

Cumplir tus requisitos de latencia, disponibilidad o durabilidad son factores primordiales para seleccionar la región en la que se ejecutan tus servicios de Cloud Run. Por lo general, puedes seleccionar la región más cercana a tus usuarios, pero debes tener en cuenta la ubicación de los otros Google Cloudproductos que utiliza tu servicio de Cloud Run. Usar Google Cloud productos juntos en varias ubicaciones puede afectar a la latencia y al coste de tu servicio.

Cloud Run está disponible en las siguientes regiones:

Con sujeción a los precios del nivel 1

  • asia-east1 (Taiwán)
  • asia-northeast1 (Tokio)
  • asia-northeast2 (Osaka)
  • asia-south1 (Bombay, la India)
  • europe-north1 (Finlandia) icono de una hoja CO2 bajo
  • europe-north2 (Estocolmo) icono de una hoja CO2 bajo
  • europe-southwest1 (Madrid) icono de una hoja CO2 bajo
  • europe-west1 (Bélgica) icono de una hoja CO2 bajo
  • europe-west4 (Países Bajos) icono de una hoja CO2 bajo
  • europe-west8 (Milán)
  • europe-west9 (París) icono de una hoja CO2 bajo
  • me-west1 (Tel Aviv)
  • northamerica-south1 (México)
  • us-central1 (Iowa) icono de una hoja CO2 bajo
  • us-east1 (Carolina del Sur)
  • us-east4 (Norte de Virginia)
  • us-east5 (Columbus)
  • us-south1 (Dallas) icono de una hoja CO2 bajo
  • us-west1 (Oregón) icono de una hoja CO2 bajo

Con sujeción a los precios del nivel 2

  • africa-south1 (Johannesburgo)
  • asia-east2 (Hong Kong)
  • asia-northeast3 (Seúl, Corea del Sur)
  • asia-southeast1 (Singapur)
  • asia-southeast2 (Yakarta)
  • asia-south2 (Delhi, la India)
  • australia-southeast1 (Sídney)
  • australia-southeast2 (Melbourne)
  • europe-central2 Varsovia (Polonia)
  • europe-west10 (Berlín)
  • europe-west12 (Turín)
  • europe-west2 (Londres, Reino Unido) icono de una hoja CO2 bajo
  • europe-west3 (Fráncfort, Alemania)
  • europe-west6 (Zúrich, Suiza) icono de una hoja Bajas emisiones de CO2
  • me-central1 (Doha)
  • me-central2 (Dammam)
  • northamerica-northeast1 (Montreal) icono de una hoja CO2 bajo
  • northamerica-northeast2 (Toronto) icono de una hoja CO2 bajo
  • southamerica-east1 (São Paulo, Brasil) icono de una hoja CO2 bajo
  • southamerica-west1 (Santiago, Chile) icono de una hoja CO2 bajo
  • us-west2 (Los Ángeles)
  • us-west3 (Salt Lake City)
  • us-west4 (Las Vegas)

Si ya has creado un servicio de Cloud Run, puedes ver la región en el panel de control de Cloud Run de la Google Cloud consola.

Información sobre la secuencia de operaciones

El flujo de datos de este tutorial sigue estos pasos:

  1. Un usuario sube una imagen a un segmento de Cloud Storage.
  2. Cloud Storage publica un mensaje sobre el nuevo archivo en Pub/Sub.
  3. Pub/Sub envía el mensaje al servicio de Cloud Run.
  4. El servicio de Cloud Run recupera el archivo de imagen al que se hace referencia en el mensaje de Pub/Sub.
  5. El servicio de Cloud Run usa la API Cloud Vision para analizar la imagen.
  6. Si se detecta contenido violento o para adultos, el servicio Cloud Run usa ImageMagick para difuminar la imagen.
  7. El servicio de Cloud Run sube la imagen desenfocada a otro segmento de Cloud Storage para usarla.

El uso posterior de la imagen desenfocada se deja como ejercicio para el lector.

Crear un repositorio estándar de Artifact Registry

Crea un repositorio estándar de Artifact Registry para almacenar tu imagen de contenedor:

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

Sustituye:

  • REPOSITORY con un nombre único para el repositorio.
  • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.

Configurar segmentos de Cloud Storage

gcloud

  1. Crea un segmento de Cloud Storage para subir imágenes, donde INPUT_BUCKET_NAME es un nombre de segmento único a nivel global:

    gcloud storage buckets create gs://INPUT_BUCKET_NAME

    El servicio de Cloud Run solo lee de este segmento.

  2. Crea un segundo segmento de Cloud Storage para recibir las imágenes desenfocadas. Sustituye BLURRED_BUCKET_NAME por un nombre de segmento único a nivel global:

    gcloud storage buckets create gs://BLURRED_BUCKET_NAME

    El servicio de Cloud Run sube las imágenes desenfocadas a este segmento. Si usas un contenedor independiente, evitarás que las imágenes procesadas vuelvan a activar el servicio.

    De forma predeterminada, las revisiones de Cloud Run se ejecutan como la cuenta de servicio predeterminada de Compute Engine.

    Si, por el contrario, usas una cuenta de servicio gestionada por el usuario, asegúrate de haber asignado los roles de gestión de identidades y accesos necesarios para que tenga el permiso storage.objects.get para leer de INPUT_BUCKET_NAME y el permiso storage.objects.create para subir contenido a BLURRED_BUCKET_NAME.

Terraform

Para saber cómo aplicar o quitar una configuración de Terraform, consulta Comandos básicos de Terraform.

Crea dos segmentos de Cloud Storage: uno para subir las imágenes originales y otro para que el servicio de Cloud Run suba las imágenes desenfocadas.

Para crear ambos segmentos de Cloud Storage con nombres únicos a nivel global, añade lo siguiente al archivo main.tf:

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
}

De forma predeterminada, las revisiones de Cloud Run se ejecutan como la cuenta de servicio predeterminada de Compute Engine.

Si, por el contrario, usas una cuenta de servicio gestionada por el usuario, asegúrate de haber asignado los roles de gestión de identidades y accesos necesarios para que tenga el permiso storage.objects.get para leer de google_storage_bucket.imageproc_input y el permiso storage.objects.create para subir a google_storage_bucket.imageproc_output.

En los pasos siguientes, creará e implementará un servicio que procese las notificaciones de subidas de archivos a INPUT_BUCKET_NAME. Activa el envío de notificaciones después de implementar y probar el servicio para evitar que se invoque prematuramente.

Modificar el código de ejemplo del tutorial de Pub/Sub

Este tutorial se basa en el código ensamblado en el tutorial sobre el uso de Pub/Sub. Si aún no has completado ese tutorial, hazlo ahora, saltándote los pasos de limpieza y vuelve aquí para añadir el comportamiento de procesamiento de imágenes.

Añadir código de procesamiento de imágenes

El código de procesamiento de imágenes está separado del código de gestión de solicitudes para que sea más fácil de leer y probar. Para añadir código de procesamiento de imágenes, sigue estos pasos:

  1. Cambia al directorio del código de ejemplo del tutorial de Pub/Sub.

  2. Añade código para importar las dependencias de procesamiento de imágenes, incluidas las bibliotecas para integrarlas con los servicios de Google Cloud , ImageMagick y el sistema de archivos.

    Node.js

    Abre un archivo image.js en tu editor y copia lo siguiente:
    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

    Abre un archivo image.py en tu editor y copia lo siguiente:
    import os
    import tempfile
    
    from google.cloud import storage, vision
    from wand.image import Image
    
    storage_client = storage.Client()
    vision_client = vision.ImageAnnotatorClient()

    Go

    Abre un nuevo archivo imagemagick/imagemagick.go en tu editor y copia lo siguiente:
    
    // 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

    Abre un archivo src/main/java/com/example/cloudrun/ImageMagick.java en tu editor y copia lo siguiente:
    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. Añade código para recibir un mensaje de Pub/Sub como objeto de evento y controlar el procesamiento de imágenes.

    El evento contiene datos sobre la imagen que se subió originalmente. Este código determina si la imagen debe desenfocarse comprobando los resultados de un análisis de Cloud Vision para detectar contenido violento o 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.")
    
    

    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: %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. Obtén la imagen a la que se hace referencia del segmento de entrada de Cloud Storage que has creado anteriormente, usa ImageMagick para transformar la imagen con un efecto de desenfoque y sube el resultado al segmento de salida.

    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)
    
    

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

Integrar el procesamiento de imágenes en el código de ejemplo de Pub/Sub

Para modificar el servicio actual e incorporar el código de procesamiento de imágenes, sigue estos pasos:

  1. Añade nuevas dependencias a tu servicio, incluidas las bibliotecas de cliente de Cloud Vision y Cloud Storage:

    Node.js

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

    Python

    Añade las bibliotecas de cliente necesarias para que tu requirements.txt tenga un aspecto similar al siguiente:
    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
    

    Go

    La aplicación de ejemplo de Go usa módulos de Go. Las nuevas dependencias añadidas arriba en la instrucción imagemagick/imagemagick.go import se descargarán automáticamente con el siguiente comando que las necesite.

    Java

    Añade la siguiente dependencia en <dependencyManagement> en 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>
    
    Añade las siguientes dependencias en <dependencies> en el archivo 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. Añade el paquete del sistema ImageMagick a tu contenedor modificando el Dockerfile situado debajo de la instrucción FROM. Si usas un Dockerfile de varias fases, coloca esto en la 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
    

    Consulta más información sobre cómo trabajar con paquetes del sistema en tu servicio de Cloud Run en el tutorial sobre cómo usar paquetes del sistema.

  3. Sustituye el código de gestión de mensajes de Pub/Sub por una llamada de función a nuestra nueva lógica de desenfoque.

    Node.js

    El archivo app.js define la aplicación Express.js y prepara los mensajes de Pub/Sub recibidos para su uso. Haz los siguientes cambios:

    • Añadir código para importar el nuevo archivo image.js
    • Elimina el código "Hello World" de la ruta
    • Añadir código para validar aún más el mensaje de Pub/Sub
    • Añadir código para llamar a la nueva función de procesamiento de imágenes

      Cuando haya terminado, el código tendrá este aspecto:

    
    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

    El archivo main.py define la aplicación Flask y prepara los mensajes de Pub/Sub recibidos para su uso. Haz los siguientes cambios:

    • Añadir código para importar el nuevo archivo image.py
    • Elimina el código "Hello World" de la ruta
    • Añadir código para validar aún más el mensaje de Pub/Sub
    • Añadir código para llamar a la nueva función de procesamiento de imágenes

      Cuando haya terminado, el código tendrá este aspecto:

    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)
    

    Go

    El archivo main.go define el servicio HTTP y prepara los mensajes de Pub/Sub recibidos para su uso. Haz los siguientes cambios:

    • Añadir código para importar el nuevo archivo imagemagick.go
    • Elimina el código "Hello World" del controlador.
    • Añadir código para validar aún más el mensaje de Pub/Sub
    • Añadir código para llamar a la nueva función de procesamiento de imágenes

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

    El archivo PubSubController.java define el controlador que gestiona las solicitudes HTTP y prepara los mensajes de Pub/Sub recibidos para su uso. Haz los siguientes cambios:

    • Añade las nuevas importaciones
    • Elimina el código "Hello World" del controlador
    • Añadir código para validar aún más el mensaje de Pub/Sub
    • Añadir código para llamar a la nueva función de procesamiento de imágenes

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

Descargar la muestra completa

Para obtener el código de muestra completo de procesamiento de imágenes, sigue estos pasos:

  1. Clona el repositorio de aplicaciones de muestra en la máquina local:

    Node.js

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

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

    Python

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

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

    Go

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

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

    Java

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

    También puedes descargar el ejemplo como un archivo ZIP y extraerlo.

  2. Cambia al directorio que contiene el código de ejemplo de Cloud Run:

    Node.js

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

    Python

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

    Go

    cd golang-samples/run/image-processing/

    Java

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

Lanzar el código

El envío de código consta de tres pasos: crear una imagen de contenedor con Cloud Build, subir la imagen de contenedor a Artifact Registry y desplegar la imagen de contenedor en Cloud Run.

Para enviar tu código, sigue estos pasos:

  1. Crea el contenedor y publícalo en Artifact Registry:

    Node.js

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

    Donde pubsub es el nombre de tu servicio.

    Sustituye:

    • PROJECT_ID con el ID de tu proyecto Google Cloud
    • REPOSITORY con el nombre del repositorio de Artifact Registry.
    • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.

    Si la operación se realiza correctamente, verás un mensaje de ÉXITO que contiene el ID, la hora de creación y el nombre de la imagen. La imagen se almacena en Artifact Registry y se puede volver a usar si es necesario.

    Python

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

    Donde pubsub es el nombre de tu servicio.

    Sustituye:

    • PROJECT_ID con el ID de tu proyecto Google Cloud
    • REPOSITORY con el nombre del repositorio de Artifact Registry.
    • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.

    Si la operación se realiza correctamente, verás un mensaje de ÉXITO que contiene el ID, la hora de creación y el nombre de la imagen. La imagen se almacena en Artifact Registry y se puede volver a usar si es necesario.

    Go

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

    Donde pubsub es el nombre de tu servicio.

    Sustituye:

    • PROJECT_ID con el ID de tu proyecto Google Cloud
    • REPOSITORY con el nombre del repositorio de Artifact Registry.
    • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.

    Si la operación se realiza correctamente, verás un mensaje de ÉXITO que contiene el ID, la hora de creación y el nombre de la imagen. La imagen se almacena en Artifact Registry y se puede volver a usar si es necesario.

    Java

    En este ejemplo se usa Jib para crear imágenes de Docker con herramientas comunes de Java. Jib optimiza las compilaciones de contenedores sin necesidad de usar un Dockerfile ni de tener Docker instalado. Más información sobre cómo crear contenedores Java con Jib

    1. Con el Dockerfile, configura y compila una imagen base con los paquetes del sistema instalados para anular la imagen base predeterminada de 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

      Sustituye:

      • PROJECT_ID con el ID de tu proyecto Google Cloud
      • REPOSITORY con el nombre del repositorio de Artifact Registry.
      • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.
    2. Usa el asistente de credenciales de gcloud para autorizar a Docker a enviar contenido a tu Artifact Registry.

      gcloud auth configure-docker

    3. Crea el contenedor final con Jib y publícalo en 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

      Sustituye:

      • PROJECT_ID con el ID de tu proyecto Google Cloud
      • REPOSITORY con el nombre del repositorio de Artifact Registry.
      • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.

  2. Ejecuta el siguiente comando para implementar tu servicio. Usa el mismo nombre de servicio que usaste en el tutorial sobre cómo usar 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

    Go

    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

    Donde pubsub es el nombre del contenedor y pubsub-tutorial es el nombre del servicio. Ten en cuenta que la imagen de contenedor se despliega en el servicio y la región (Cloud Run) que configuraste anteriormente en Configurar valores predeterminados de gcloud. Sustituye:

    • PROJECT_ID con el ID de tu proyecto Google Cloud
    • REPOSITORY con el nombre del repositorio de Artifact Registry.
    • REGION con la Google Cloud región que se va a usar en el repositorio de Artifact Registry.
    • BLURRED_BUCKET_NAME con el segmento de Cloud Storage que has creado anteriormente para recibir imágenes borrosas y definir la variable de entorno.

    La marca --no-allow-unauthenticated restringe el acceso no autenticado al servicio. Si mantienes el servicio privado, puedes usar la integración automática de Pub/Sub de Cloud Run para autenticar las solicitudes. Consulta Integración con Pub/Sub para obtener más información sobre cómo se configura. Consulta Gestionar el acceso para obtener más información sobre la autenticación basada en IAM.

    Espera a que se complete el despliegue, que puede tardar aproximadamente medio minuto. Si la acción se realiza correctamente, la línea de comandos mostrará la URL del servicio.

Activar las notificaciones de Cloud Storage

Configura Cloud Storage para que publique un mensaje en un tema de Pub/Sub cada vez que se suba o se modifique un archivo (conocido como objeto). Envía la notificación al tema creado anteriormente para que cualquier archivo nuevo que se suba invoque el servicio.

gcloud

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

myRunTopic es el tema que creaste en el tutorial anterior.

Sustituye INPUT_BUCKET_NAME por el nombre que usaste al crear los contenedores.

Para obtener más información sobre las notificaciones de segmentos de almacenamiento, consulta las notificaciones de cambios en objetos.

Terraform

Para saber cómo aplicar o quitar una configuración de Terraform, consulta Comandos básicos de Terraform.

Para habilitar las notificaciones, la cuenta de servicio de Cloud Storage, que es única para el proyecto, debe existir y tener el permiso de gestión de identidades y accesos pubsub.publisher en el tema de Pub/Sub. Para conceder este permiso y crear una notificación de Cloud Storage, añade lo siguiente al archivo main.tf:

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

Pruébalo

  1. Sube una imagen ofensiva, como esta de un zombi 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

    donde INPUT_BUCKET_NAME es el segmento de Cloud Storage que has creado anteriormente para subir imágenes.

  2. Ve a los registros del servicio:

    1. Ve a la página Cloud Run de la Google Cloud consola.
    2. Haz clic en el servicio pubsub-tutorial.
    3. Selecciona la pestaña Registros. Puede que los registros tarden unos instantes en aparecer. Si no los ves inmediatamente, vuelve a comprobarlo al cabo de unos instantes.
  3. Busca el mensaje Blurred image: zombie.png.

  4. Puedes ver las imágenes desenfocadas en el BLURRED_BUCKET_NAME segmento de Cloud Storage que has creado anteriormente. Para ello, busca el segmento en la página Cloud Storage de la Google Cloud consola.