Cloud Storage の画像を加工するチュートリアル


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

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

目標

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

費用

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

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

始める前に

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

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

    Go to project selector

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

  6. Enable the Artifact Registry, Cloud Build, Pub/Sub, Cloud Run, Cloud Storage and Cloud Vision APIs.

    Enable the APIs

  7. gcloud CLI をインストールして初期化します
  8. コンポーネントを更新します。
    gcloud components update
  9. Pub/Sub を使用するチュートリアルに従って、Pub/Sub トピック、安全な push サブスクリプション、メッセージを処理する最初の Cloud Run サービスを設定します。

必要なロール

チュートリアルを完了するために必要な権限を取得するには、プロジェクトに対して次の IAM ロールを付与するよう管理者に依頼してください。

ロールの付与の詳細については、アクセス権の管理をご覧ください。

必要な権限は、カスタムロールや他の事前定義ロールから取得することもできます。

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

Cloud Run サービスを gcloud のデフォルトに構成するには:

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

    gcloud config set project PROJECT_ID

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

  2. 選択したリージョン向けに gcloud を構成します。

    gcloud config set run/region REGION

    REGION は、任意のサポートされている Cloud Run のリージョンに置き換えます。

Cloud Run のロケーション

Cloud Run はリージョナルです。つまり、Cloud Run サービスを実行するインフラストラクチャは特定のリージョンに配置され、そのリージョン内のすべてのゾーンで冗長的に利用できるように Google によって管理されます。

レイテンシ、可用性、耐久性の要件を満たしていることが、Cloud Run サービスを実行するリージョンを選択する際の主な判断材料になります。一般的には、ユーザーに最も近いリージョンを選択できますが、Cloud Run サービスで使用されている他の Google Cloud サービスのロケーションも考慮する必要があります。使用する Google Cloud サービスが複数のロケーションにまたがっていると、サービスの料金だけでなくレイテンシにも影響することがあります。

Cloud Run は、次のリージョンで利用できます。

ティア 1 料金を適用

  • asia-east1(台湾)
  • asia-northeast1(東京)
  • asia-northeast2(大阪)
  • europe-north1(フィンランド) リーフアイコン 低 CO2
  • europe-southwest1(マドリッド) リーフアイコン 低 CO2
  • europe-west1(ベルギー) リーフアイコン 低 CO2
  • europe-west4(オランダ) リーフアイコン 低 CO2
  • europe-west8(ミラノ)
  • europe-west9(パリ) リーフアイコン 低 CO2
  • me-west1(テルアビブ)
  • us-central1(アイオワ) リーフアイコン 低 CO2
  • us-east1(サウスカロライナ)
  • us-east4(北バージニア)
  • us-east5(コロンバス)
  • us-south1(ダラス) リーフアイコン 低 CO2
  • us-west1(オレゴン) リーフアイコン 低 CO2

ティア 2 料金を適用

  • africa-south1(ヨハネスブルグ)
  • asia-east2(香港)
  • asia-northeast3(ソウル、韓国)
  • asia-southeast1(シンガポール)
  • asia-southeast2 (ジャカルタ)
  • asia-south1(ムンバイ、インド)
  • asia-south2(デリー、インド)
  • australia-southeast1(シドニー)
  • australia-southeast2(メルボルン)
  • europe-central2(ワルシャワ、ポーランド)
  • europe-west10(ベルリン) リーフアイコン 低 CO2
  • europe-west12(トリノ)
  • europe-west2(ロンドン、イギリス) リーフアイコン 低 CO2
  • europe-west3(フランクフルト、ドイツ) リーフアイコン 低 CO2
  • europe-west6(チューリッヒ、スイス) リーフアイコン 低 CO2
  • me-central1(ドーハ)
  • me-central2(ダンマーム)
  • northamerica-northeast1(モントリオール) リーフアイコン 低 CO2
  • northamerica-northeast2(トロント) リーフアイコン 低 CO2
  • southamerica-east1(サンパウロ、ブラジル) リーフアイコン 低 CO2
  • southamerica-west1(サンティアゴ、チリ) リーフアイコン 低 CO2
  • us-west2(ロサンゼルス)
  • us-west3(ソルトレイクシティ)
  • us-west4(ラスベガス)

Cloud Run サービスをすでに作成している場合は、Google Cloud コンソールの Cloud Run ダッシュボードにリージョンが表示されます。

一連の操作を理解する

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

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

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

Artifact Registry 標準リポジトリを作成する

コンテナ イメージを保存する Artifact Registry 標準リポジトリを作成します。

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

次のように置き換えます。

  • REPOSITORY は、リポジトリの一意の名前に置き換えます。
  • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。

Cloud Storage バケットを設定する

gcloud

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

    gcloud storage buckets create gs://INPUT_BUCKET_NAME

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

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

    gcloud storage buckets create gs://BLURRED_BUCKET_NAME

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

    デフォルトでは、Cloud Run のリビジョンは Compute Engine のデフォルトのサービス アカウントとして実行されます。

    代わりにユーザー管理サービス アカウントを使用している場合、必要な IAM ロールを割り当て、INPUT_BUCKET_NAME からの storage.objects.get 読み取り権限と BLURRED_BUCKET_NAME への storage.objects.create アップロード権限を付与していることを確認します。

Terraform

Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

2 つの Cloud Storage バケットを作成します。一つは元の画像をアップロードするためのもので、もう一つは Cloud Run サービスがぼかしを入れた画像をアップロードするためのものです。

両方の Cloud Storage バケットをグローバルに一意の名前で作成するには、既存の 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
}

デフォルトでは、Cloud Run のリビジョンは Compute Engine のデフォルトのサービス アカウントとして実行されます。

代わりにユーザー管理サービス アカウントを使用している場合、必要な IAM ロールを割り当て、google_storage_bucket.imageproc_input からの storage.objects.get 読み取り権限と google_storage_bucket.imageproc_output への storage.objects.create アップロード権限を付与していることを確認します。

次の手順で、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==3.0.3
    google-cloud-storage==2.12.0
    google-cloud-vision==3.4.5
    gunicorn==22.0.0
    Wand==0.6.13
    Werkzeug==3.0.3
    

    Go

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

    Java

    pom.xml<dependencyManagement> に次の依存関係を追加します。
    <dependency>
      g<roupIdc>om.google.cloud/<groupId
    >  ar<tifactIdsp>ring-cloud-gcp-dependencies/a<rtifactId
     > ver<sion4.9>.2/ve<rsion
      >type<pom/>typ<e
      s>cope<impor>t/scop<e
    /dep>en<dency>
    pom.xml<dependencies> に次の依存関係を追加します。
    <dependency>
      g<roupIdc>om.google.code.gson/<groupId
    >  ar<tifactIdgs>on/a<rtifactId
     > sco<pecom>pile/sc<ope
    /d>ep<endency
    dep>en<dency
      gr>oupI<dcom.go>ogle.cloud/group<Id
      art>ifac<tIdspring->cloud-gcp-starter-vision/artifa<ctId
    /depen>de<ncy
    depende>nc<y
      groupI>dcom<.google>.cloud/groupId
     < >artifactIds<pring-clou>d-gcp-starter-storage/artifactId<
    /dependenc>y<>

  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 サービスでのシステム パッケージの操作について詳しくは、システム パッケージ・チュートリアルを使用するをご覧ください。

  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 のサンプルコードが含まれているディレクトリに移動します。

    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 でコンテナ イメージをビルドする、Artifact Registry にコンテナ イメージをアップロードする、Cloud Run にコンテナ イメージをデプロイするという 3 つのステップで構成されます。

コードを配布するには:

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

    Node.js

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

    ここで、pubsub はサービスの名前です。

    次のように置き換えます。

    • PROJECT_ID は、Google Cloud プロジェクト ID に置き換えます。
    • REPOSITORY は、Artifact Registry リポジトリの名前に置き換えます。
    • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。

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

    Python

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

    ここで、pubsub はサービスの名前です。

    次のように置き換えます。

    • PROJECT_ID は、Google Cloud プロジェクト ID に置き換えます。
    • REPOSITORY は、Artifact Registry リポジトリの名前に置き換えます。
    • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。

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

    Go

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

    ここで、pubsub はサービスの名前です。

    次のように置き換えます。

    • PROJECT_ID は、Google Cloud プロジェクト ID に置き換えます。
    • REPOSITORY は、Artifact Registry リポジトリの名前に置き換えます。
    • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。

    ビルドが成功すると、ID、作成時間、画像の名前を含む SUCCESS メッセージが表示されます。イメージが Artifact 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.12_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 REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/imagemagick

      次のように置き換えます。

      • PROJECT_ID は、Google Cloud プロジェクト ID に置き換えます。
      • REPOSITORY は、Artifact Registry リポジトリの名前に置き換えます。
      • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。
    2. Docker を承認して Artifact Registry に push するには、gcloud 認証ヘルパーを使用します。

      gcloud auth configure-docker

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

      <plugin>
        g<roupIdc>om.google.cloud.tools/<groupId
      >  ar<tifactIdji>b-maven-plugin/a<rtifactId
       > ver<sion3.4>.0/ve<rsion
        >conf<iguration
         > from
      <    >  imageg<cr.io>/PROJECT_ID/imagemagick/image<
          />from
       <   to
           < <>/span>imagegcr.<io/PR>OJECT_ID/pubsub/image
        <  /to
        /con<fig>urat<ion
      /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

      次のように置き換えます。

      • PROJECT_ID は、Google Cloud プロジェクト ID に置き換えます。
      • REPOSITORY は、Artifact Registry リポジトリの名前に置き換えます。
      • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。

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

    ここで、pubsub はコンテナ名、pubsub-tutorial はサービスの名前です。コンテナ イメージは、gcloud defaults の設定で構成したサービスとリージョン(Cloud Run)にデプロイされることに注意してください。次のように置き換えます。

    • PROJECT_ID は、Google Cloud プロジェクト ID に置き換えます。
    • REPOSITORY は、Artifact Registry リポジトリの名前に置き換えます。
    • REGION は、Artifact Registry リポジトリに使用する Google Cloud リージョンに置き換えます。
    • BLURRED_BUCKET_NAME は、先ほど作成した Cloud Storage バケットに置き換え、ぼかした画像を受け取って環境変数を設定します。

    --no-allow-unauthenticated フラグは、サービスへの未認証アクセスを制限します。サービスを非公開にすることで、Cloud Run と Pub/Sub の自動統合でリクエストの認証を行うことができます。構成方法の詳細については、Pub/Sub との統合をご覧ください。IAM ベースの認証の詳細については、アクセスの管理をご覧ください。

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

Cloud Storage からの通知をオンにする

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

gcloud

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

myRunTopic は、前のチュートリアルで作成したトピックです。

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

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

Terraform

Terraform 構成を適用または削除する方法については、基本的な Terraform コマンドをご覧ください。

通知を有効にするには、プロジェクトに固有の Cloud Storage サービス アカウントが存在し、Pub/Sub トピックに対する IAM 権限 pubsub.publisher が付与されている必要があります。この権限を付与して Cloud Storage 通知を作成するには、既存の 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]
}

試してみる

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

    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
    

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

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

    1. Google Cloud Console の [Cloud Run] ページに移動します。
    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 サービスを削除します。

    gcloud run services delete SERVICE-NAME

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

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

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

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

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

次のステップ

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