Firebase Auth と Functions を使用して Cloud Vision でランドマークを安全に認識する(Android)

アプリから Google Cloud API を呼び出すには、認可を処理し、API キーなどのシークレット値を保護するための中間 REST API を作成する必要があります。次に、モバイルアプリでこの中間サービスに対する認証と通信を行うためのコードを記述します。

この REST API を作成する方法の一つとして、Firebase Authentication と Functions を使用する方法があります。この方法では、Google Cloud API に対するサーバーレスのマネージド ゲートウェイが提供され、そこで認証が処理されます。このゲートウェイは、事前構築された SDK を使用してモバイルアプリから呼び出すことができます。

このガイドでは、この手法を使用してアプリから Cloud Vision API を呼び出す方法について説明します。この手法では、すべての認証済みユーザーが Cloud のプロジェクトを経由して Cloud Vision の課金サービスにアクセスできます。そのため、続行する前に、目的のユースケースにおいてこの認証メカニズムで十分かどうかを検討してください。

始める前に

プロジェクトを構成する

  1. まだ Firebase を Android プロジェクトに追加していない場合は追加します。
  2. プロジェクトで Cloud ベースの API をまだ有効にしていない場合は、ここで有効にします。

    1. Firebase コンソールの Firebase ML の [APIs] ページを開きます。
    2. まだプロジェクトを Blaze 料金プランにアップグレードしていない場合は、[アップグレード] をクリックしてアップグレードします(プロジェクトをアップグレードするよう求められるのは、プロジェクトが Blaze プランでない場合のみです)。

      Blaze レベルのプロジェクトだけが Cloud ベースの API を使用できます。

    3. Cloud ベースの API がまだ有効になっていない場合は、[Cloud ベースの API を有効化] をクリックします。
  3. 既存の Firebase API キーを構成して、Cloud Vision API へのアクセスを許可しないようにします。
    1. Cloud コンソールの [認証情報] ページを開きます。
    2. リスト内の各 API キーについて、編集ビューを開き、[キーの制限] セクションで Cloud Vision API を除く使用可能なすべての API をリストに追加します。

呼び出し可能関数をデプロイする

次に、アプリと Cloud Vision API の間のブリッジとして使用する Cloud Functions の関数をデプロイします。functions-samples リポジトリには、使用可能なサンプルが含まれています。

デフォルトでは、この関数を通じて Cloud Vision API にアクセスすると、アプリの認証済みユーザーのみが Cloud Vision API にアクセスできます。関数はさまざまな要件に応じて変更できます。

関数をデプロイするには:

  1. 関数サンプル リポジトリをダウンロードするか、そのクローンを作成して、Node-1st-gen/vision-annotate-image ディレクトリに移動します。
    git clone https://github.com/firebase/functions-samples
    cd Node-1st-gen/vision-annotate-image
    
  2. 依存関係をインストールします。
    cd functions
    npm install
    cd ..
  3. Firebase CLI がインストールされていない場合は、インストールします。
  4. vision-annotate-image ディレクトリ内で Firebase プロジェクトを初期化します。プロンプトが表示されたら、リストからプロジェクトを選択します。
    firebase init
  5. 関数をデプロイします。
    firebase deploy --only functions:annotateImage

アプリに Firebase Auth を追加する

上記でデプロイした呼び出し可能関数は、アプリ内で認証されていないユーザーからのリクエストを拒否します。アプリにまだ Firebase Auth を追加していない場合は、Firebase Auth を追加する必要があります。

必要な依存関係をアプリに追加する

  • Cloud Functions for Firebase(クライアント)と Gson Android ライブラリの依存関係をモジュール(アプリレベル)の Gradle ファイル(通常は <project>/<app-module>/build.gradle.kts または <project>/<app-module>/build.gradle)に追加します。
    implementation("com.google.firebase:firebase-functions:21.1.0")
    implementation("com.google.code.gson:gson:2.8.6")
  • 1. 入力画像を準備する

    Cloud Vision を呼び出すには、画像を base64 でエンコードされた文字列としてフォーマットする必要があります。保存されたファイルの URI から画像を処理する方法は次のとおりです。
    1. 画像を Bitmap オブジェクトとして取得します。

      Kotlin

      var bitmap: Bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)

      Java

      Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
    2. 必要に応じて、帯域幅を節約するために画像を縮小します。Cloud Vision の推奨画像サイズをご覧ください。

      Kotlin

      private fun scaleBitmapDown(bitmap: Bitmap, maxDimension: Int): Bitmap {
          val originalWidth = bitmap.width
          val originalHeight = bitmap.height
          var resizedWidth = maxDimension
          var resizedHeight = maxDimension
          if (originalHeight > originalWidth) {
              resizedHeight = maxDimension
              resizedWidth =
                  (resizedHeight * originalWidth.toFloat() / originalHeight.toFloat()).toInt()
          } else if (originalWidth > originalHeight) {
              resizedWidth = maxDimension
              resizedHeight =
                  (resizedWidth * originalHeight.toFloat() / originalWidth.toFloat()).toInt()
          } else if (originalHeight == originalWidth) {
              resizedHeight = maxDimension
              resizedWidth = maxDimension
          }
          return Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false)
      }

      Java

      private Bitmap scaleBitmapDown(Bitmap bitmap, int maxDimension) {
          int originalWidth = bitmap.getWidth();
          int originalHeight = bitmap.getHeight();
          int resizedWidth = maxDimension;
          int resizedHeight = maxDimension;
      
          if (originalHeight > originalWidth) {
              resizedHeight = maxDimension;
              resizedWidth = (int) (resizedHeight * (float) originalWidth / (float) originalHeight);
          } else if (originalWidth > originalHeight) {
              resizedWidth = maxDimension;
              resizedHeight = (int) (resizedWidth * (float) originalHeight / (float) originalWidth);
          } else if (originalHeight == originalWidth) {
              resizedHeight = maxDimension;
              resizedWidth = maxDimension;
          }
          return Bitmap.createScaledBitmap(bitmap, resizedWidth, resizedHeight, false);
      }

      Kotlin

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640)

      Java

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640);
    3. ビットマップ オブジェクトを base64 でエンコードされた文字列に変換します。

      Kotlin

      // Convert bitmap to base64 encoded string
      val byteArrayOutputStream = ByteArrayOutputStream()
      bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
      val imageBytes: ByteArray = byteArrayOutputStream.toByteArray()
      val base64encoded = Base64.encodeToString(imageBytes, Base64.NO_WRAP)

      Java

      // Convert bitmap to base64 encoded string
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
      bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
      byte[] imageBytes = byteArrayOutputStream.toByteArray();
      String base64encoded = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
    4. Bitmap オブジェクトによって表される画像は、さらに回転する必要がないよう、正しい向きになっていなければなりません。

    2. 呼び出し可能関数を呼び出し、ランドマークを認識する

    画像内のランドマークを認識するには、呼び出し可能関数を呼び出し、JSON Cloud Vision リクエストを渡します。

    1. 最初に Cloud Functions のインスタンスを初期化します。

      Kotlin

      private lateinit var functions: FirebaseFunctions
      // ...
      functions = Firebase.functions
      

      Java

      private FirebaseFunctions mFunctions;
      // ...
      mFunctions = FirebaseFunctions.getInstance();
      
    2. 関数を呼び出すメソッドを定義します。

      Kotlin

      private fun annotateImage(requestJson: String): Task<JsonElement> {
          return functions
              .getHttpsCallable("annotateImage")
              .call(requestJson)
              .continueWith { task ->
                  // This continuation runs on either success or failure, but if the task
                  // has failed then result will throw an Exception which will be
                  // propagated down.
                  val result = task.result?.data
                  JsonParser.parseString(Gson().toJson(result))
              }
      }
      

      Java

      private Task<JsonElement> annotateImage(String requestJson) {
          return mFunctions
                  .getHttpsCallable("annotateImage")
                  .call(requestJson)
                  .continueWith(new Continuation<HttpsCallableResult, JsonElement>() {
                      @Override
                      public JsonElement then(@NonNull Task<HttpsCallableResult> task) {
                          // This continuation runs on either success or failure, but if the task
                          // has failed then getResult() will throw an Exception which will be
                          // propagated down.
                          return JsonParser.parseString(new Gson().toJson(task.getResult().getData()));
                      }
                  });
      }
      
    3. JSON リクエストを作成し、タイプとして LANDMARK_DETECTION を設定します。

      Kotlin

      // Create json request to cloud vision
      val request = JsonObject()
      // Add image to request
      val image = JsonObject()
      image.add("content", JsonPrimitive(base64encoded))
      request.add("image", image)
      // Add features to the request
      val feature = JsonObject()
      feature.add("maxResults", JsonPrimitive(5))
      feature.add("type", JsonPrimitive("LANDMARK_DETECTION"))
      val features = JsonArray()
      features.add(feature)
      request.add("features", features)
      

      Java

      // Create json request to cloud vision
      JsonObject request = new JsonObject();
      // Add image to request
      JsonObject image = new JsonObject();
      image.add("content", new JsonPrimitive(base64encoded));
      request.add("image", image);
      //Add features to the request
      JsonObject feature = new JsonObject();
      feature.add("maxResults", new JsonPrimitive(5));
      feature.add("type", new JsonPrimitive("LANDMARK_DETECTION"));
      JsonArray features = new JsonArray();
      features.add(feature);
      request.add("features", features);
      
    4. 関数を呼び出します。

      Kotlin

      annotateImage(request.toString())
          .addOnCompleteListener { task ->
              if (!task.isSuccessful) {
                  // Task failed with an exception
                  // ...
              } else {
                  // Task completed successfully
                  // ...
              }
          }
      

      Java

      annotateImage(request.toString())
              .addOnCompleteListener(new OnCompleteListener<JsonElement>() {
                  @Override
                  public void onComplete(@NonNull Task<JsonElement> task) {
                      if (!task.isSuccessful()) {
                          // Task failed with an exception
                          // ...
                      } else {
                          // Task completed successfully
                          // ...
                      }
                  }
              });
      

    3. 認識されたランドマークに関する情報を取得する

    ランドマーク認識処理が成功すると、その結果として BatchAnnotateImagesResponse の JSON レスポンスが返されます。landmarkAnnotations 配列内の各オブジェクトは、画像内で認識されたランドマークを表します。ランドマークごとに、入力画像の境界座標、ランドマーク名、緯度と経度、ナレッジグラフ エンティティ ID(使用できる場合)、一致の信頼スコアを取得できます。次に例を示します。

    Kotlin

    for (label in task.result!!.asJsonArray[0].asJsonObject["landmarkAnnotations"].asJsonArray) {
        val labelObj = label.asJsonObject
        val landmarkName = labelObj["description"]
        val entityId = labelObj["mid"]
        val score = labelObj["score"]
        val bounds = labelObj["boundingPoly"]
        // Multiple locations are possible, e.g., the location of the depicted
        // landmark and the location the picture was taken.
        for (loc in labelObj["locations"].asJsonArray) {
            val latitude = loc.asJsonObject["latLng"].asJsonObject["latitude"]
            val longitude = loc.asJsonObject["latLng"].asJsonObject["longitude"]
        }
    }
    

    Java

    for (JsonElement label : task.getResult().getAsJsonArray().get(0).getAsJsonObject().get("landmarkAnnotations").getAsJsonArray()) {
        JsonObject labelObj = label.getAsJsonObject();
        String landmarkName = labelObj.get("description").getAsString();
        String entityId = labelObj.get("mid").getAsString();
        float score = labelObj.get("score").getAsFloat();
        JsonObject bounds = labelObj.get("boundingPoly").getAsJsonObject();
        // Multiple locations are possible, e.g., the location of the depicted
        // landmark and the location the picture was taken.
        for (JsonElement loc : labelObj.get("locations").getAsJsonArray()) {
            JsonObject latLng = loc.getAsJsonObject().get("latLng").getAsJsonObject();
            double latitude = latLng.get("latitude").getAsDouble();
            double longitude = latLng.get("longitude").getAsDouble();
        }
    }