非同期での画像の処理


このチュートリアルでは、Cloud Run for Anthos、Cloud Vision API、ImageMagick を使用して、Cloud Storage バケットにアップロードされた不適切な画像を検出してぼかす方法を説明します。このチュートリアルは、チュートリアル・Cloud Run for Anthos で Pub/Sub を使用するに基づいています。

このチュートリアルでは、既存のサンプルアプリの変更について説明します。完成したサンプルをダウンロードすることもできます。

目標

  • 非同期データ処理サービスを Cloud Run for Anthos に書き込み、ビルドし、デプロイする。
  • Cloud Storage にファイルをアップロードしてサービスを起動し、Pub/Sub メッセージを作成する。
  • Cloud Vision API を使用して、暴力的なコンテンツやアダルト コンテンツを検出する。
  • ImageMagick を使用して、不適切な画像をぼかす。
  • 人食いゾンビの画像をアップロードして、サービスをテストする。

費用

このドキュメントでは、Google Cloud の次の課金対象のコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。 新しい Google Cloud ユーザーは無料トライアルをご利用いただける場合があります。

始める前に

  1. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  2. Make sure that billing is enabled for your Google Cloud project.

  3. Enable the Cloud Run for Anthos and Cloud Vision APIs.

    Enable the APIs

  4. gcloud CLI をインストールして初期化する
  5. kubectl コンポーネントをインストールします。
    gcloud components install kubectl
  6. コンポーネントを更新します。
    gcloud components update
  7. Cloud Run for Anthos を使用して、Cloud Run for Anthos の設定の手順に沿って新しいクラスタを作成します。
  8. Cloud Run for Anthos で Pub/Sub を使用するのチュートリアルに沿って、Pub/Sub トピック、安全な push サブスクリプション、メッセージを処理する最初の Cloud Run for Anthos サービスを設定します。

gcloud のデフォルトを設定する

gcloud に Cloud Run for Anthos サービス用のデフォルトを構成するには、次のようにします。

  1. デフォルト プロジェクトを設定します。

    gcloud config set project PROJECT_ID

    PROJECT_ID は、このチュートリアルで使用するプロジェクトの名前に置き換えます。

  2. クラスタに gcloud を構成します。

    gcloud config set run/platform gke
    gcloud config set run/cluster CLUSTER-NAME
    gcloud config set run/cluster_location REGION

    以下のように置き換えます。

    • CLUSTER-NAME は、クラスタに対して使用した名前に置き換えます。
    • REGION は、選択したサポート対象のクラスタの場所に置き換えます。

一連の操作を理解する

このチュートリアルでは次の流れで手順を説明します。

  1. ユーザーは、Cloud Storage バケットに画像をアップロードします。
  2. Cloud Storage が新しいファイルに関するメッセージを Pub/Sub に公開します。
  3. Pub/Sub が、メッセージを Cloud Run for Anthos サービスにプッシュします。
  4. Cloud Run for Anthos サービスが、Pub/Sub メッセージで参照されているイメージ ファイルを取得します。
  5. Cloud Run for Anthos サービスが Cloud Vision API を使用して画像を分析します。
  6. 暴力的なコンテンツやアダルト コンテンツが検出された場合、Cloud Run for Anthos サービスが ImageMagick を使用して画像をぼかします。
  7. Cloud Run for Anthos サービスは、ぼかした画像を別の Cloud Storage バケットにアップロードして使用します。

ぼかした画像は、読者のためのテスト用に後で使用できます。

Cloud Storage バケットを設定する

  1. 画像をアップロードする Cloud Storage バケットを作成します。INPUT_BUCKET_NAME は、グローバルに固有のバケット名です。

    gsutil mb gs://INPUT_BUCKET_NAME

    Cloud Run for Anthos サービスは、このバケットからのみ読み取りを行います。

  2. ぼかし入りの画像を保存する 2 つ目の Cloud Storage バケットを作成します。BLURRED_BUCKET_NAME はグローバルに固有のバケット名です。

    gsutil mb gs://BLURRED_BUCKET_NAME

    Cloud Run for Anthos サービスは、ぼかし入りの画像をこのバケットにアップロードします。別のバケットを使用すると、処理された画像でサービスを再度トリガーできません。

次の手順で、INPUT_BUCKET_NAME へのファイル アップロードの通知を処理するサービスを作成してデプロイします。新しいサービスの早期呼び出しを回避するため、通知配信はサービスのデプロイとテストをした後にオンにします。

Pub/Sub チュートリアル サンプルコードを変更する

このチュートリアルは、Pub/Sub チュートリアルの使用で作成したコードを基にしています。まだチュートリアルを完了していない場合、今完了してください。クリーンアップ手順をスキップし、ここに戻って画像処理動作を追加します。

画像処理コードを追加する

画像処理コードは、読みやすさとテストを容易にするためリクエスト処理から分離されています。画像処理コードを追加するには:

  1. Pub/Sub チュートリアル サンプルコードのディレクトリに移動します。

  2. Google Cloud サービス、ImageMagick、ファイル システムと統合するためのライブラリなど、画像処理の依存関係をインポートするコードを追加します。

    Node.js

    エディタで新しい image.js ファイルを開き、次のコードをコピーします。
    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

    エディタで新しい image.py ファイルを開き、次のコードをコピーします。
    import os
    import tempfile
    
    from google.cloud import storage, vision
    from wand.image import Image
    
    storage_client = storage.Client()
    vision_client = vision.ImageAnnotatorClient()

    Go

    エディタで新しい imagemagick/imagemagick.go ファイルを開き、次のコードをコピーします。
    
    // 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

    エディタで新しい src/main/java/com/example/cloudrun/ImageMagick.java ファイルを開き、次のコードをコピーします。
    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. Pub/Sub メッセージをイベント オブジェクトとして受信し、画像処理を制御するコードを追加します。

    このイベントには、最初にアップロードされた画像に関するデータが含まれます。このコードは、Cloud Vision による暴力的なコンテンツやアダルト コンテンツの分析結果を確認することで、画像をぼかす必要があるかどうかを判断します。

    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. Cloud Storage 入力バケットから上記で作成した参照画像を取得し、ImageMagick を使って画像をぼかし効果で変換してから、結果を出力バケットにアップロードします。

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

Pub/Sub サンプルコードへ画像処理を統合する

画像処理コードを組み込むために既存のサービスを変更するには:

  1. Cloud Vision や Cloud Storage のクライアント ライブラリなど、サービスの新しい依存関係を追加します。

    Node.js

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

    Python

    必要なクライアント ライブラリを追加すると、requirements.txt は次のようになります。
    Flask==2.1.0
    pytest==7.0.1; python_version > "3.0"
    # pin pytest to 4.6.11 for Python2.
    pytest==7.0.1; python_version < "3.0"
    gunicorn==20.1.0
    google-cloud-vision==3.4.2
    google-cloud-storage==2.9.0
    Wand==0.6.11
    

    Go

    go サンプルアプリケーションは go モジュールを使用します。imagemagick/imagemagick.go インポート ステートメントで追加された上記の新しい依存関係は、それを必要とする次のコマンドで自動的にダウンロードされます。

    Java

    pom.xml<dependencyManagement> に次の依存関係を追加します。
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>4.5.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    pom.xml<dependencies> に次の依存関係を追加します。
    <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. FROM ステートメントの下の Dockerfile を変更して、ImageMagick システム パッケージをコンテナに追加します。「マルチステージ」の Dockerfile を使用する場合は、これを最終ステージに配置します。

    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
    

    Cloud Run for Anthos サービスでのシステム パッケージの操作について詳しくは、システム パッケージ・チュートリアルを使用するをご覧ください。

  3. 既存の関数呼び出し付きの Pub/Sub メッセージ処理コードを新しいぼかしロジックに置き換えます。

    Node.js

    app.js ファイルは Express.js アプリを定義し、受信 Pub/Sub メッセージを使用できるよう準備します。次のように変更します。

    • 新しい image.js ファイルをインポートするコードを追加します。
    • ルートから既存の「Hello World」コードを削除します。
    • Pub/Sub メッセージをさらに検証するコードを追加します。
    • 新しい画像処理関数を呼び出すコードを追加します。

      終了すると、コードは次のようになります。

    
    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

    main.py ファイルは、Flask アプリを定義し、受信した Pub/Sub メッセージを使用できるように準備します。次のように変更します。

    • 新しい image.py ファイルをインポートするコードを追加します。
    • ルートから既存の「Hello World」コードを削除します。
    • Pub/Sub メッセージをさらに検証するコードを追加します。
    • 新しい画像処理関数を呼び出すコードを追加します。

      終了すると、コードは次のようになります。

    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

    main.go ファイルは、HTTP サービスを定義し、受信 Pub/Sub メッセージを使用できるように準備します。次のように変更します。

    • 新しい imagemagick.go ファイルをインポートするコードを追加します。
    • ハンドラから既存の「Hello World」コードを削除します。
    • Pub/Sub メッセージをさらに検証するコードを追加します。
    • 新しい画像処理関数を呼び出すコードを追加します。

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

    PubSubController.java ファイルは、HTTP リクエストを処理し、受信した Pub/Sub メッセージを使用できるように準備するコントローラを定義します。次のように変更します。

    • 新しいインポートを追加します。
    • コントローラから既存の「Hello World」コードを削除します。
    • Pub/Sub メッセージをさらに検証するコードを追加します。
    • 新しい画像処理関数を呼び出すコードを追加します。

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

サンプル全体をダウンロードする

使用する完全な画像処理コードのサンプルを取得するには:

  1. ローカルマシンにサンプルアプリのリポジトリのクローンを作成します。

    Node.js

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

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Python

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

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Go

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

    または、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

    Java

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

    また、zip 形式のサンプルをダウンロードし、ファイルを抽出してもかまいません。

  2. Cloud Run for Anthos のサンプルコードが含まれているディレクトリに移動します。

    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/

コードの配布

コードの配布は、Cloud Build でコンテナ イメージをビルドする、Container Registry にコンテナ イメージをアップロードする、Cloud Run にコンテナ イメージをデプロイするという 3 ステップで構成されます。

コードを配布するには:

  1. コンテナをビルドして、Container Registry に公開します。

    Node.js

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

    ここで、PROJECT_ID は GCP プロジェクト ID、pubsub はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Python

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

    ここで、PROJECT_ID は GCP プロジェクト ID、pubsub はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Go

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

    ここで、PROJECT_ID は GCP プロジェクト ID、pubsub はサービスに付ける名前です。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Container Registry に保存されます。このイメージは必要に応じて再利用できます。

    Java

    このサンプルでは、Jib を使用して一般的な Java ツールにより Docker イメージをビルドします。Jib は、Dockerfile や Docker をインストールせずにコンテナのビルドを最適化します。Jib を使用して Java コンテナを構築する方法の詳細を確認します。

    1. Dockerfile を使用して、インストールしたシステム パッケージでベースイメージの構成とビルドを行い、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.8_7-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 gcr.io/PROJECT_ID/imagemagick

      PROJECT_ID は、GCP のプロジェクト ID です。

    2. Jib で最終的なコンテナを作成し、Container Registry で公開します。

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.3.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 は、GCP のプロジェクト ID です。

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

    PROJECT_ID は実際の GCP プロジェクト ID に置き換えます。pubsub はコンテナ名、pubsub-tutorial はサービスの名前です。コンテナ イメージは、gcloud のデフォルトの設定で構成したサービスとクラスタにデプロイされることに注意してください。

    BLURRED_BUCKET_NAME を先ほど作成した Cloud Storage バケットに置き換え、ぼかした画像を受け取って環境変数を設定します。

    デプロイが完了するまで待ちます。30 秒ほどかかる場合があります。成功すると、コマンドラインにサービス URL が表示されます。

Cloud Storage からの通知を有効化する

ファイル(オブジェクトと呼ばれます)がアップロードまたは変更されるたびに、Pub/Sub トピックにメッセージを公開するよう Cloud Storage を構成します。以前に作成したトピックに通知を送信して、新しいファイルのアップロードでサービスが起動するようにします。

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

gsutil コマンドは、Google Cloud CLI の一部としてインストールされます。myRunTopic は、前のチュートリアルで作成したトピックです。

INPUT_BUCKET_NAME は、バケットの作成時に使用した名前に置き換えます。

ストレージ バケット通知の詳細については、オブジェクトの変更通知をご覧ください。

試してみる

  1. 肉食ゾンビの画像など、不適切な画像をアップロードします。

    gsutil cp zombie.jpg gs://INPUT_BUCKET_NAME

    INPUT_BUCKET_NAME は、以前に画像アップロード用に作成した Cloud Storage バケットです。

  2. サービスログに移動します。

    1. Google Cloud コンソールで Cloud Run for Anthos ページに移動します。

      Cloud Run for Anthos に移動

    2. pubsub-tutorial サービスをクリックします。

    3. [ログ] タブを選択します。ログが表示されるまで少し時間がかかることがあります。すぐに表示されない場合は、しばらくしてからもう一度確認してください。

  3. Blurred image: zombie.png メッセージを探します。

  4. 先に作成した BLURRED_BUCKET_NAME Cloud Storage バケット内のぼかし画像を表示できます。バケットは Google Cloud Console の [Cloud Storage] ページにあります。

クリーンアップ

このチュートリアル用に新規プロジェクトを作成した場合は、そのプロジェクトを削除します。既存のプロジェクトを使用し、このチュートリアルで変更を加えずに残す場合は、チュートリアル用に作成したリソースを削除します。

プロジェクトを削除する

課金をなくす最も簡単な方法は、チュートリアル用に作成したプロジェクトを削除することです。

プロジェクトを削除するには:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

チュートリアル リソースを削除する

  1. このチュートリアルでデプロイした Cloud Run for Anthos サービスを削除します。

    gcloud run services delete SERVICE-NAME

    SERVICE-NAME は、選択したサービス名です。

    Google Cloud コンソールから Cloud Run for Anthos のサービスを削除することもできます。

    Cloud Run for Anthos に移動

  2. チュートリアルを設定したときに追加した gcloud のデフォルト構成を削除します。

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. プロジェクト構成を削除します。

     gcloud config unset project
    
  4. このチュートリアルで作成した他の Google Cloud リソースを削除します。

次のステップ

  • Cloud Storage で、Cloud Run for Anthos を使用したデータの永続化の詳細を確認する。
  • Cloud Vision API を使用して不適切なコンテンツ以外を検出する方法を学習する。
  • Google Cloud に関するリファレンス アーキテクチャ、図、ベスト プラクティスを確認する。Cloud Architecture Center をご覧ください。