Tutoriel sur le traitement des images à partir de Cloud Storage

Ce tutoriel explique comment utiliser Cloud Run, l'API Cloud Vision et ImageMagick pour détecter et flouter les images choquantes importées dans un bucket Cloud Storage. Il s'appuie sur le tutoriel Utiliser Pub/Sub avec Cloud Run.

Ce tutoriel explique comment modifier un exemple d'application existant. Vous pouvez également télécharger l'exemple complet si vous le souhaitez.

Objectifs

  • Écrire, créer et déployer un service de traitement de données asynchrone dans Cloud Run.
  • Appeler le service en important un fichier dans Cloud Storage, en créant un message Pub/Sub
  • Utiliser l'API Cloud Vision pour détecter les contenus violents ou réservés aux adultes
  • Utiliser ImageMagick pour flouter les images choquantes
  • Tester le service en important une image représentant un zombie mangeur de chair

Coûts

Ce tutoriel utilise des composants facturables de Cloud Platform, dont :

Obtenez une estimation des coûts en fonction de votre utilisation prévue à l'aide du simulateur de coût.

Les nouveaux utilisateurs de Cloud Platform peuvent bénéficier d'un essai gratuit.

Avant de commencer

  1. Connectez-vous à votre compte Google Cloud. Si vous débutez sur Google Cloud, créez un compte pour évaluer les performances de nos produits en conditions réelles. Les nouveaux clients bénéficient également de 300 $ de crédits gratuits pour exécuter, tester et déployer des charges de travail.
  2. Dans Google Cloud Console, sur la page de sélection du projet, sélectionnez ou créez un projet Google Cloud.

    Accéder au sélecteur de projet

  3. Assurez-vous que la facturation est activée pour votre projet Cloud. Découvrez comment vérifier que la facturation est activée pour votre projet.

  4. Activer les API Cloud Run and Cloud Vision.

    Activer les API

  5. Installez et initialisez le SDK Cloud.
  6. Mettez à jour les composants :
    gcloud components update
  7. Configurez un sujet Pub/Sub, un abonnement push sécurisé et un service Cloud Run initial pour gérer les messages en suivant le tutoriel Utiliser Pub/Sub.

Configurer les paramètres par défaut de gcloud

Pour configurer gcloud avec les valeurs par défaut pour votre service Cloud Run, procédez comme suit :

  1. Définissez le projet par défaut :

    gcloud config set project PROJECT_ID

    Remplacez PROJECT_ID par le nom du projet que vous avez créé pour ce tutoriel.

  2. Configurez gcloud pour la région choisie :

    gcloud config set run/region REGION

    Remplacez REGION par la région Cloud Run compatible de votre choix.

Emplacements Cloud Run

Cloud Run est régional, ce qui signifie que l’infrastructure qui exécute vos services Cloud Run est située dans une région spécifique et gérée par Google pour être disponible de manière redondante dans toutes les zones de cette région.

Lors de la sélection de la région dans laquelle exécuter vos services Cloud Run, vous devez tout d'abord considérer vos exigences en matière de latence, de disponibilité et de durabilité. Vous pouvez généralement sélectionner la région la plus proche de vos utilisateurs, mais vous devez tenir compte de l'emplacement des autres produits Google Cloud utilisés par votre service Cloud Run. L'utilisation conjointe de produits Google Cloud dans plusieurs emplacements peut avoir une incidence sur la latence et le coût de votre service.

Cloud Run est disponible dans les régions suivantes :

Soumis aux tarifs de niveau 1

Soumis aux tarifs de niveau 2

  • asia-east2 (Hong Kong)
  • asia-northeast3 (Séoul, Corée du Sud)
  • asia-southeast1 (Singapour)
  • asia-southeast2 (Jakarta)
  • asia-south1 (Mumbai, Inde)
  • asia-south2 (Delhi, Inde)
  • australia-southeast1 (Sydney)
  • australia-southeast2 (Melbourne)
  • europe-central2 (Varsovie, Pologne)
  • europe-west2 (Londres, Royaume-Uni)
  • europe-west3 (Francfort, Allemagne)
  • europe-west6 (Zurich, Suisse) Icône Feuille Plus faibles émissions de CO2
  • northamerica-northeast1 (Montréal) Icône Feuille Plus faibles émissions de CO2
  • southamerica-east1 (Sao Paulo, Brésil) Icône Feuille Plus faibles émissions de CO2
  • us-west2 (Los Angeles)
  • us-west3 (Las Vegas)
  • us-west4 (Salt Lake City)

Si vous avez déjà créé un service Cloud Run, vous pouvez afficher la région dans le tableau de bord Cloud Run de Cloud Console.

Comprendre la séquence des opérations

Le flux de données de ce tutoriel comprend les étapes suivantes :

  1. Un utilisateur importe une image dans un bucket Cloud Storage.
  2. Cloud Storage publie un message sur le nouveau fichier dans Pub/Sub.
  3. Pub/Sub envoie le message au service Cloud Run.
  4. Le service Cloud Run récupère le fichier image référencé dans le message Pub/Sub.
  5. Le service Cloud Run utilise l'API Cloud Vision pour analyser l'image.
  6. Si du contenu violent ou réservé aux adultes est détecté, le service Cloud Run utilise ImageMagick pour flouter l'image.
  7. Le service Cloud Run importe l'image floutée dans un autre bucket Cloud Storage en vue de son utilisation.

L'utilisation ultérieure de l'image floutée est laissée en exercice au lecteur.

Configurer des buckets Cloud Storage

  1. Créez un bucket Cloud Storage pour importer des images, où INPUT_BUCKET_NAME est un nom de bucket unique :

    gsutil mb gs://INPUT_BUCKET_NAME

    Le service Cloud Run lit uniquement à partir de ce bucket.

  2. Créez un second bucket Cloud Storage destiné à recevoir les images floutées, où BLURRED_BUCKET_NAME est un nom de bucket unique :

    gsutil mb gs://BLURRED_BUCKET_NAME

    Le service Cloud Run importe les images floutées dans ce bucket. L'utilisation d'un bucket distinct empêche les images traitées de déclencher de nouveau le service.

Dans les étapes suivantes, vous allez créer et déployer un service qui traite les notifications sur les importations de fichiers dans le bucket INPUT_BUCKET_NAME. Vous activez l'envoi de notifications après avoir déployé et testé le service, afin d'éviter tout appel prématuré du nouveau service.

Modifier l'exemple de code du tutoriel Pub/Sub

Ce tutoriel s'appuie sur le code assemblé dans le tutoriel Utiliser Pub/Sub. Si vous n'avez pas encore terminé ce tutoriel, faites-le maintenant en ignorant les étapes de nettoyage, puis revenez sur cette page pour ajouter le comportement de traitement des images.

Ajouter du code de traitement des images

Le code de traitement des images est séparé de la gestion des requêtes pour des raisons de lisibilité et de facilité de test. Pour ajouter du code de traitement des images, procédez comme suit :

  1. Accédez au répertoire de l'exemple de code du tutoriel Pub/Sub.

  2. Ajoutez du code pour importer les dépendances de traitement des images, y compris les bibliothèques à intégrer aux services Google Cloud, à ImageMagick et au système de fichiers.

    Node.js

    Ouvrez un nouveau fichier image.js dans votre éditeur et copiez-y ce qui suit :
    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

    Ouvrez un nouveau fichier image.py dans votre éditeur et copiez-y ce qui suit :
    import os
    import tempfile
    
    from google.cloud import storage, vision
    from wand.image import Image
    
    storage_client = storage.Client()
    vision_client = vision.ImageAnnotatorClient()

    Go

    Ouvrez un nouveau fichier imagemagick/imagemagick.go dans votre éditeur et copiez-y ce qui suit :
    
    // Package imagemagick contains an example of using ImageMagick to process a
    // file uploaded to Cloud Storage.
    package imagemagick
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"log"
    	"os"
    	"os/exec"
    
    	"cloud.google.com/go/storage"
    	vision "cloud.google.com/go/vision/apiv1"
    	visionpb "google.golang.org/genproto/googleapis/cloud/vision/v1"
    )
    
    // Global API clients used across function invocations.
    var (
    	storageClient *storage.Client
    	visionClient  *vision.ImageAnnotatorClient
    )
    
    func init() {
    	// Declare a separate err variable to avoid shadowing the client variables.
    	var err error
    
    	storageClient, err = storage.NewClient(context.Background())
    	if err != nil {
    		log.Fatalf("storage.NewClient: %v", err)
    	}
    
    	visionClient, err = vision.NewImageAnnotatorClient(context.Background())
    	if err != nil {
    		log.Fatalf("vision.NewAnnotatorClient: %v", err)
    	}
    }
    

    Java

    Ouvrez un nouveau fichier src/main/java/com/example/cloudrun/ImageMagick.java dans votre éditeur et copiez-y ce qui suit :
    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. Ajoutez le code pour recevoir un message Pub/Sub en tant qu'objet d'événement et contrôler le traitement des images.

    L'événement contient des données sur l'image importée à l'origine. Ce code détermine si l'image doit être floutée en vérifiant les résultats d'une analyse Cloud Vision pour le contenu violent ou réservé aux adultes.

    Node.js

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

    Python

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

    Go

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

    Java

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

  4. Récupérez l'image référencée dans le bucket d'entrée Cloud Storage créé ci-dessus, utilisez ImageMagick pour transformer l'image avec un effet de flou et importez le résultat dans le bucket de sortie.

    Node.js

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

    Python

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

    Go

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

    Java

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

Intégrer le traitement des images dans l'exemple de code Pub/Sub

Pour modifier le service existant afin d'intégrer le code de traitement des images, procédez comme suit :

  1. Ajoutez de nouvelles dépendances pour votre service, y compris les bibliothèques clientes Cloud Vision et Cloud Storage :

    Node.js

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

    Python

    Ajoutez les bibliothèques clientes nécessaires pour que votre fichier requirements.txt se présente comme suit :
    Flask==2.0.1
    pytest==5.3.0; python_version > "3.0"
    pytest==4.6.6; python_version < "3.0"
    gunicorn==20.1.0
    google-cloud-vision==2.4.0
    google-cloud-storage==1.41.0
    Wand==0.6.6
    

    Go

    L'exemple d'application Go utilise des modules Go. Les nouvelles dépendances ajoutées ci-dessus dans l'instruction d'importation imagemagick/imagemagick.go seront automatiquement téléchargées par la prochaine commande qui en a besoin.

    Java

    Ajoutez la dépendance suivante sous <dependencyManagement> dans le fichier pom.xml :
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>1.2.8.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    Ajoutez les dépendances suivantes sous <dependencies> dans le fichier pom.xml :
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.7</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20210307</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.11.0</version>
    </dependency>
    

  2. Ajoutez le package système ImageMagick à votre conteneur en modifiant le fichier Dockerfile sous l'instruction FROM. Si vous utilisez un fichier Dockerfile "multi-étapes", placez-le à l'étape finale.

    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
    

    Pour en savoir plus sur l'utilisation des packages système dans votre service Cloud Run, consultez le tutoriel Utiliser des packages système.

  3. Remplacez le code de traitement des messages Pub/Sub existant par un appel de fonction à notre nouvelle logique de floutage.

    Node.js

    Le fichier app.js définit l'application Express.js et prépare les messages Pub/Sub reçus en vue de leur utilisation. Apportez les modifications suivantes :

    • Ajoutez du code pour importer le nouveau fichier image.js.
    • Supprimez le code "Hello World" existant de la route.
    • Ajoutez du code pour valider le message Pub/Sub.
    • Ajoutez du code pour appeler la nouvelle fonction de traitement des images.

      Lorsque vous avez terminé, le code se présente comme suit :

    
    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

    Le fichier main.py définit l'application Flask et prépare les messages Pub/Sub reçus en vue de leur utilisation. Apportez les modifications suivantes :

    • Ajoutez du code pour importer le nouveau fichier image.py.
    • Supprimez le code "Hello World" existant de la route.
    • Ajoutez du code pour valider le message Pub/Sub.
    • Ajoutez du code pour appeler la nouvelle fonction de traitement des images.

      Lorsque vous avez terminé, le code se présente comme suit :

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

    Go

    Le fichier main.go définit le service HTTP et prépare les messages Pub/Sub reçus en vue de leur utilisation. Apportez les modifications suivantes :

    • Ajoutez du code pour importer le nouveau fichier imagemagick.go.
    • Supprimez le code "Hello World" existant du gestionnaire.
    • Ajoutez du code pour valider le message Pub/Sub.
    • Ajoutez du code pour appeler la nouvelle fonction de traitement des images.

    
    // Sample image-processing is a Cloud Run service which performs asynchronous processing on images.
    package main
    
    import (
    	"encoding/json"
    	"io/ioutil"
    	"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 := ioutil.ReadAll(r.Body)
    	if err != nil {
    		log.Printf("ioutil.ReadAll: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    	if err := json.Unmarshal(body, &m); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	var e imagemagick.GCSEvent
    	if err := json.Unmarshal(m.Message.Data, &e); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if e.Name == "" || e.Bucket == "" {
    		log.Printf("invalid GCSEvent: expected name and bucket")
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if err := imagemagick.BlurOffensiveImages(r.Context(), e); err != nil {
    		log.Printf("imagemagick.BlurOffensiveImages: %v", err)
    		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    	}
    }
    

    Java

    Le fichier PubSubController.java définit le contrôleur qui gère les requêtes HTTP et prépare les messages Pub/Sub reçus en vue de leur utilisation. Apportez les modifications suivantes :

    • Ajoutez les nouvelles importations.
    • Supprimez le code "Hello World" existant du contrôleur.
    • Ajoutez du code pour valider le message Pub/Sub.
    • Ajoutez du code pour appeler la nouvelle fonction de traitement des images.

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

Télécharger l'exemple complet

Pour récupérer l'intégralité de l'exemple de code de traitement des images à utiliser, procédez comme suit :

  1. Clonez le dépôt de l'exemple d'application sur votre machine locale :

    Node.js

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

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    Python

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

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    Go

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

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

    Java

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

    Vous pouvez également télécharger l'exemple en tant que fichier ZIP et l'extraire.

  2. Accédez au répertoire contenant l'exemple de code 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/

Transmettre le code

La transmission du code se déroule en trois étapes : création d'une image de conteneur avec Cloud Build, importation de l'image de conteneur dans Container Registry, puis déploiement de cette image dans Cloud Run.

Pour transmettre votre code, procédez comme suit :

  1. Créez votre conteneur et publiez-le dans Container Registry :

    Node.js

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

    PROJECT_ID est l'ID de votre projet GCP et pubsub le nom que vous souhaitez attribuer à votre service.

    En cas de réussite, un message SUCCESS apparaît contenant l'ID, l'heure de création et le nom de l'image. Celle-ci est stockée dans Container Registry et peut être réutilisée au besoin.

    Python

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

    PROJECT_ID est l'ID de votre projet GCP et pubsub le nom que vous souhaitez attribuer à votre service.

    En cas de réussite, un message SUCCESS apparaît contenant l'ID, l'heure de création et le nom de l'image. Celle-ci est stockée dans Container Registry et peut être réutilisée au besoin.

    Go

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

    PROJECT_ID est l'ID de votre projet GCP et pubsub le nom que vous souhaitez attribuer à votre service.

    En cas de réussite, un message SUCCESS apparaît contenant l'ID, l'heure de création et le nom de l'image. Celle-ci est stockée dans Container Registry et peut être réutilisée au besoin.

    Java

    Cet exemple utilise Jib pour créer des images Docker à l'aide d'outils Java courants. Jib optimise les builds de conteneurs sans requérir de fichier Dockerfile ni d'installation Docker. Découvrez comment créer des conteneurs Java avec Jib.

    1. À l'aide du fichier Dockerfile, configurez et créez une image de base avec les packages système installés pour remplacer l'image de base par défaut de Jib :

      # Use AdoptOpenJDK for base image.
      # It's important to use OpenJDK 8u191 or above that has container support enabled.
      # https://hub.docker.com/r/adoptopenjdk/openjdk11
      FROM adoptopenjdk/openjdk11:alpine-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 apk add --no-cache imagemagick

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

      PROJECT_ID est l'ID de votre projet GCP.

    2. Utilisez l'assistant d'identification gcloud pour autoriser Docker à transférer du contenu vers votre registre de conteneurs.

      gcloud auth configure-docker

    3. Créez votre conteneur final avec Jib et publiez-le sur Container Registry :

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

      PROJECT_ID est l'ID de votre projet GCP.

  2. Exécutez la commande suivante pour déployer votre service, en utilisant le même nom de service que celui utilisé dans le tutoriel Utiliser Cloud Pub/Sub :

    Node.js

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Python

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Go

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Java

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --memory 512M

    Remplacez PROJECT_ID par l'ID de votre projet GCP. pubsub correspond au nom du conteneur et pubsub-tutorial au nom du service. Notez que l'image de conteneur est déployée sur le service et la région (Cloud Run) que vous avez précédemment configurés dans la section Configurer les paramètres par défaut de gcloud.

    Pour définir la variable d'environnement, remplacez BLURRED_BUCKET_NAME par le bucket Cloud Storage destiné à recevoir les images floutées que vous avez créé précédemment.

    Si vous procédez au déploiement sur Cloud Run, répondez n (non) à l'invite "Autoriser l'accès non authentifié". En gardant le service privé, le processus d'intégration automatique à Pub/Sub de Cloud Run assure l'authentification des requêtes. Cette configuration est présentée en détail dans la section Intégrer à Pub/Sub. Pour en savoir plus sur l'authentification basée sur IAM, consultez la page Gérer les accès.

    Patientez jusqu'à la fin du déploiement, soit environ 30 secondes. En cas de réussite, la ligne de commande affiche l'URL du service.

Activer les notifications depuis Cloud Storage

Configurez Cloud Storage pour publier un message dans un sujet Pub/Sub chaque fois qu'un fichier (appelé "objet") est importé ou modifié. Envoyez la notification au sujet créé précédemment pour que toute nouvelle importation de fichier appelle le service.

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

La commande gsutil est installée dans le cadre du SDK Cloud. myRunTopic est le sujet que vous avez créé dans le tutoriel précédent.

Remplacez INPUT_BUCKET_NAME par le nom que vous avez utilisé lors de la création des buckets.

Pour plus d'informations sur les notifications concernant les buckets de stockage, consultez la section Notifications de modification des objets.

Essayez-le !

  1. Importez une image choquante, telle que cette image d'un zombie mangeur de chair :

    gsutil cp zombie.jpg gs://INPUT_BUCKET_NAME

    INPUT_BUCKET_NAME est le bucket Cloud Storage que vous avez créé précédemment pour importer des images.

  2. Accédez aux journaux du service :

    1. Accédez à la page Cloud Run de Google Cloud Console.
    2. Cliquez sur le service pubsub-tutorial.
    3. Sélectionnez l'onglet Journaux. L'affichage des journaux peut nécessiter quelques instants. S'ils n'apparaissent pas immédiatement, patientez et vérifiez de nouveau.
  3. Recherchez le message Blurred image: zombie.png.

  4. Vous pouvez afficher les images floutées dans le bucket Cloud Storage BLURRED_BUCKET_NAME que vous avez créé précédemment : recherchez-le sur la page Cloud Storage de Google Cloud Console.

Nettoyer

Si vous avez créé un projet pour ce tutoriel, supprimez-le. Si vous avez utilisé un projet existant et que vous souhaitez le conserver sans les modifications du présent tutoriel, supprimez les ressources créées pour ce tutoriel.

Supprimer le projet

Le moyen le plus simple d'empêcher la facturation est de supprimer le projet que vous avez créé pour ce tutoriel.

Pour supprimer le projet :

  1. Dans Cloud Console, accédez à la page Gérer les ressources.

    Accéder à la page Gérer les ressources

  2. Dans la liste des projets, sélectionnez le projet que vous souhaitez supprimer, puis cliquez sur Supprimer.
  3. Dans la boîte de dialogue, saisissez l'ID du projet, puis cliquez sur Arrêter pour supprimer le projet.

Supprimer les ressources du tutoriel

  1. Supprimez le service Cloud Run que vous avez déployé dans ce tutoriel :

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME est le nom de service que vous avez choisi.

    Vous pouvez également supprimer des services Cloud Run à partir de Google Cloud Console.

  2. Supprimez la configuration régionale gcloud par défaut que vous avez ajoutée lors de la configuration du tutoriel :

     gcloud config unset run/region
    
  3. Supprimez la configuration du projet :

     gcloud config unset project
    
  4. Supprimez les autres ressources Google Cloud créées dans ce tutoriel :

Étape suivante

  • Apprenez-en plus sur la persistance des données avec Cloud Run via Cloud Storage.
  • Découvrez comment utiliser l'API Cloud Vision pour détecter des éléments autres que du contenu explicite.
  • Explorez des architectures de référence, des schémas, des tutoriels et des bonnes pratiques concernant Google Cloud. Consultez notre Centre d'architecture cloud.