非同期でのコードの実行

タスクキューCloud Vision を使用して、バックグラウンドで実行するタスクにより、ユーザーがアップロードした画像にラベルを付ける方法を学びます。

Vision は、画像内のエンティティに関する情報を検出および抽出し、場所、プロダクト、アクティビティなどの特定の重要な画像属性に信頼レベルを割り当てられるようにします。たとえば、人物写真の場合、Vision はその人が悲しんでいるのか幸せなのか怒っているのかといった情報に関する信頼レベルを提供し、適切なラベルを付けられるようにします。

このページとサンプルは、ユーザーが投稿をアップロードできる簡単なブログ アプリケーションの学習用サンプルを拡張したものの一部です。Go プログラミング言語と基本的なウェブ開発に精通している方を対象に説明します。基礎から始めるには、Go を使用したアプリのビルドをご覧ください。

料金

このチュートリアルの実行に伴う費用はありません。このサンプルアプリを実行するだけであれば、無料の割り当てを超過することはありません。

始める前に

Go を使用したアプリのビルドのガイドを読了している場合は、このセクションをスキップしてください。それ以外の場合は、次の手順を実行します。

  1. プロジェクトとアプリケーションの設定の「始める前に」に記載されている作業を実行します。その後、このページに戻ります。

  2. この例では、gophers-5 サンプル アプリケーションにコードを追加します。

    gophers-5 サンプルとその依存関係をローカルマシンにダウンロードします。

    go get -u -d -v github.com/GoogleCloudPlatform/golang-samples/appengine/gophers/gophers-5/...
    
  3. gophers-5 ディレクトリに移動します。

    cd go/src/github.com/GoogleCloudPlatform/golang-samples/appengine/gophers/gophers-5
    
  4. ユーザーの認証ページから、プロジェクトに Firebase を追加する手順 2~6 を実行します。

  5. Vision を有効にします。

    Vision を有効にする

アプリケーションを構造化する

このサンプル プロジェクトの構造は次のとおりです。

  • go-app/: プロジェクトのルート ディレクトリ。
    • app.yaml: App Engine アプリケーションの構成設定
    • main.go: アプリケーション コード。
    • index.html: ホームページを表示するための HTML テンプレート。
    • static/: 静的ファイルを格納するディレクトリ。
      • style.css: HTML ファイルの外観をフォーマットするスタイルシート。
      • gcp-gopher.svg: Gopher の画像。
      • index.js: Firebase Authentication のユーザー インターフェースを構成し、認証リクエストを処理します。

画像ラベルの設定

ユーザーがアップロードした投稿にラベルや画像を追加するバックエンドを構成します。注: このサンプルでは Vision を使用します。

  1. 次のコマンドを使用して、開発環境に新しいパッケージをダウンロードします。

    go get -u cloud.google.com/go/storage cloud.google.com/go/vision/apiv1 github.com/satori/go.uuid golang.org/x/net/context google.golang.org/appengine/delay
    

    このサンプルでは、次のパッケージを使用します。

  2. main.go ファイル内のインポートのリストに次のパッケージを追加します。

    "context"
    "io"
    "path"
    "strings"
    
    "cloud.google.com/go/storage"
    vision "cloud.google.com/go/vision/apiv1"
    uuid "github.com/satori/go.uuid"
    "google.golang.org/appengine/delay"

  3. 2 つのフィールドを持つ struct として投稿の画像の説明を定義します。Description フィールドは画像のラベルを示し、Score フィールドは画像にラベルがどれだけ正確に当てはまるかを 0(信頼できない)から 1(信頼度が非常に高い)の範囲の値で示します。

    // A Label is a description for a post's image.
    type Label struct {
    	Description string
    	Score       float32
    }
    

    上位 5 つのスコアのラベルが画像の下に表示されます。これらのフィールドには、後で GetDescription() 関数GetScore() 関数でアクセスします。

  4. Post データ構造に ImageURL および Labels フィールドを追加します。

    type Post struct {
    	Author   string
    	UserID   string
    	Message  string
    	Posted   time.Time
    	ImageURL string
    	Labels   []Label
    }
    

    ImageURL フィールドは、アップロードされた画像のパブリック Cloud Storage URL です。

画像にラベルを追加する

ユーザーがページに画像をアップロードすると、indexHandler 関数は Cloud Storage バケットに画像を追加しつつ、非同期で画像にラベルを追加します。

  1. アップロードされた画像に Cloud Vision でラベルを付けるタスクを作成する labelFunc 変数を作成します。このタスクはタスクキューに追加されるため、バックグラウンドで作業を実行できます。ユーザーが投稿に画像をアップロードすると、indexHandler 関数が labelFunc 関数を呼び出します。

    // labelFunc will be called asynchronously as a Cloud Task. labelFunc can
    // be executed by calling labelFunc.Call(ctx, postID). If an error is returned
    // the function will be retried.
    var labelFunc = delay.Func("label-image", func(ctx context.Context, id int64) error {
    	// Get the post to label.
    	k := datastore.NewKey(ctx, "Post", "", id, nil)
    	post := Post{}
    	if err := datastore.Get(ctx, k, &post); err != nil {
    		log.Errorf(ctx, "getting Post to label: %v", err)
    		return err
    	}
    	if post.ImageURL == "" {
    		// Nothing to label.
    		return nil
    	}
    
    	// Create a new vision client.
    	client, err := vision.NewImageAnnotatorClient(ctx)
    	if err != nil {
    		log.Errorf(ctx, "NewImageAnnotatorClient: %v", err)
    		return err
    	}
    	defer client.Close()
    
    	// Get the image and label it.
    	image := vision.NewImageFromURI(post.ImageURL)
    	labels, err := client.DetectLabels(ctx, image, nil, 5)
    	if err != nil {
    		log.Errorf(ctx, "Failed to detect labels: %v", err)
    		return err
    	}
    
    	for _, l := range labels {
    		post.Labels = append(post.Labels, Label{
    			Description: l.GetDescription(),
    			Score:       l.GetScore(),
    		})
    	}
    
    	// Update the database with the new labels.
    	if _, err := datastore.Put(ctx, k, &post); err != nil {
    		log.Errorf(ctx, "Failed to update image: %v", err)
    		return err
    	}
    	return nil
    })
    

  2. ユーザーがアップロードしたファイルが画像であることを確認する uploadFileFromForm 関数を作成した後、画像のパブリック Cloud Storage URL を作成して返します。

    // uploadFileFromForm uploads a file if it's present in the "image" form field.
    func uploadFileFromForm(ctx context.Context, r *http.Request) (url string, err error) {
    	// Read the file from the form.
    	f, fh, err := r.FormFile("image")
    	if err == http.ErrMissingFile {
    		return "", nil
    	}
    	if err != nil {
    		return "", err
    	}
    
    	// Ensure the file is an image. http.DetectContentType only uses 512 bytes.
    	buf := make([]byte, 512)
    	if _, err := f.Read(buf); err != nil {
    		return "", err
    	}
    	if contentType := http.DetectContentType(buf); !strings.HasPrefix(contentType, "image") {
    		return "", fmt.Errorf("not an image: %s", contentType)
    	}
    	// Reset f so subsequent calls to Read start from the beginning of the file.
    	f.Seek(0, 0)
    
    	// Create a storage client.
    	client, err := storage.NewClient(ctx)
    	if err != nil {
    		return "", err
    	}
    	storageBucket := client.Bucket(firebaseConfig.StorageBucket)
    
    	// Random filename, retaining existing extension.
    	u, err := uuid.NewV4()
    	if err != nil {
    		return "", fmt.Errorf("generating UUID: %v", err)
    	}
    	name := u.String() + path.Ext(fh.Filename)
    
    	w := storageBucket.Object(name).NewWriter(ctx)
    
    	// Warning: storage.AllUsers gives public read access to anyone.
    	w.ACL = []storage.ACLRule{{Entity: storage.AllUsers, Role: storage.RoleReader}}
    	w.ContentType = fh.Header.Get("Content-Type")
    
    	// Entries are immutable, be aggressive about caching (1 day).
    	w.CacheControl = "public, max-age=86400"
    
    	if _, err := io.Copy(w, f); err != nil {
    		w.CloseWithError(err)
    		return "", err
    	}
    	if err := w.Close(); err != nil {
    		return "", err
    	}
    
    	const publicURL = "https://storage.googleapis.com/%s/%s"
    	return fmt.Sprintf(publicURL, firebaseConfig.StorageBucket, name), nil
    }
    

  3. indexHandler 関数で、params.name=post.author を設定した後、imageURL 変数を uploadFileFromForm 関数の出力として設定します。

    // Get the image if there is one.
    imageURL, err := uploadFileFromForm(ctx, r)
    if err != nil {
    	w.WriteHeader(http.StatusBadRequest)
    	params.Notice = "Error saving image: " + err.Error()
    	params.Message = post.Message // Preserve their message so they can try again.
    	indexTemplate.Execute(w, params)
    	return
    }

  4. post データ構造に imageURL 情報を追加します。これで、index.html テンプレートにより、Cloud Storage URL の画像がユーザーの投稿に関連付けられます。

    post.ImageURL = imageURL

  5. 指定された投稿に対して imageURL 値が存在する場合、次のコードは labelFunc 関数を呼び出して、バックグラウンドで画像にラベルを付ける新しいタスクを開始します。

    // Only look for labels if the post has an image.
    if imageURL != "" {
    	// Run labelFunc. This will start a new Task in the background.
    	if err := labelFunc.Call(ctx, key.IntID()); err != nil {
    		log.Errorf(ctx, "delay Call %v", err)
    	}
    }

HTML ページにラベル付きの画像を追加する

index.html ファイルでフォームを更新し、画像送信を受け入れて画像とそのラベルをユーザーがアップロードした投稿に表示するようにします。

  1. multipart/form-data 値とともに enctype 属性を index.html の form タグに追加します。

    これは、ファイル アップロード制御でフォームを使用する場合に必要です。

  2. HTML フォームに画像を受け入れる input タグを作成します。

    <form id="post-form" enctype="multipart/form-data" action="/" method="post" hidden=true>
      <div>Message: <input name="message" value="{{.Message}}"></div>
      <input name="image" id="image" type="file" accept="image/*">
      <input type="hidden" name="token" id="token">
      <input type="submit">
    </form>

  3. 画像とそのラベルを投稿に表示するため、Posts テンプレート変数の Author および Message テンプレート セクションに次の行を追加します。

    {{ if .ImageURL }}
    <img src="{{.ImageURL}}">
    {{ if .Labels }}
    <p>Labels:
      {{ range $i, $l := .Labels }}
      {{- if $i }}, {{end}}
      {{ printf "%s (%.3f)" $l.Description $l.Score -}}
      {{end}}
    </p>
    {{end}}
    {{end}}

アプリケーションをローカルで実行する

Cloud SDK に含まれているローカル開発用サーバー(dev_appserver.py)でアプリケーションを実行し、テストします。

  1. 次のコマンドを使用して、アプリケーションの app.yaml が配置されているプロジェクトのルート ディレクトリから、ローカルの開発用サーバーを起動します。

    dev_appserver.py app.yaml
    

    ローカルの開発用サーバーが起動し、ポート 8080 でリクエストをリッスンします。問題が発生する場合は、ここをクリックしてください。

  2. ウェブブラウザで http://localhost:8080/ にアクセスしてアプリを表示します。

    完成版

ローカル開発サーバー(dev_appserver.py)の実行

ローカルの開発用サーバーを実行するには、ディレクトリをフルパスで指定して dev_appserver.py を実行するか、PATH 環境変数に dev_appserver.py を追加します。

  • 元の App Engine SDK をインストールした場合、ツールは次の場所に配置されています。

    [PATH_TO_APP_ENGINE_SDK]/dev_appserver.py
    
  • Google Cloud SDK をインストールした場合、ツールは次の場所に配置されています。

    [PATH_TO_CLOUD_SDK]/google-cloud-sdk/bin/dev_appserver.py
    

    ヒント: Google Cloud SDK ツールを PATH 環境変数に追加し、シェルでコマンドの補完を有効にするには、次のコマンドを実行します。

    [PATH_TO_CLOUD_SDK]/google-cloud-sdk/install.sh
    

ポート番号の変更方法など、ローカルの開発用サーバーの実行について詳しくは、ローカルの開発用サーバーのリファレンスをご覧ください。

コードを変更する

ローカルの開発用サーバーはプロジェクト ファイルの変更を監視しており、コードに変更を加えるとアプリケーションを再コンパイルして再起動します。

  1. 試してみるには、ローカルの開発用サーバーを実行したまま、index.html を編集して、「The Gopher Network」を別の語句に変更します。

  2. http://localhost:8080/ を再読み込みして変更を確認します。

アプリケーションのデプロイ

app.yaml ファイルが配置されているプロジェクトのルート ディレクトリから次のコマンドを実行して、アプリケーションを App Engine にデプロイします。

gcloud app deploy

アプリケーションの表示

ブラウザを起動して http://[YOUR_PROJECT_ID].appspot.com でアプリケーションを表示するには、次のコマンドを実行します。

gcloud app browse

次のステップ

これで、アップロードされた画像を保存および分類できるアプリケーションを作成できました。次は、以下のページを参照して、アプリケーションに他の機能を追加する方法を学びましょう。

このページは役立ちましたか?評価をお願いいたします。

フィードバックを送信...

Go の App Engine スタンダード環境