在 Android 上使用 Firebase 驗證和函式,透過 Cloud Vision 安全地識別地標

如要從應用程式呼叫 Google Cloud API,您必須建立中介 REST API,以便處理授權並保護 API 金鑰等機密值。接著,您需要在行動應用程式中編寫程式碼,以便驗證並與這項中介服務通訊。

您可以使用 Firebase 驗證和功能,透過受管理的無伺服器閘道建立這個 REST API,以便處理驗證,並透過預先建構的 SDK 從行動應用程式呼叫。

本指南將說明如何使用這項技術,從應用程式呼叫 Cloud Vision API。這項方法可讓所有已驗證的使用者透過您的 Cloud 專案存取 Cloud Vision 收費服務,因此請先考量這項驗證機制是否適用於您的用途,再繼續操作。

事前準備

設定專案

  1. 如果您尚未將 Firebase 新增至 Android 專案,請新增 Firebase
  2. 如果您尚未為專案啟用雲端 API,請立即啟用:

    1. 開啟 Firebase 控制台的 Firebase ML API 頁面
    2. 如果您尚未將專案升級至 Blaze 定價方案,請按一下「Upgrade」進行升級 (只有在專案未採用 Blaze 方案時,系統才會提示您升級)。

      只有 Blaze 級別專案可以使用雲端 API。

    3. 如果您尚未啟用雲端 API,請按一下「啟用雲端 API」
  3. 設定現有的 Firebase API 金鑰,禁止存取 Cloud Vision API:
    1. 開啟 Cloud 控制台的「憑證」頁面。
    2. 針對清單中的每個 API 金鑰,請開啟編輯檢視畫面,然後在「金鑰限制」專區中,將所有可用的 API 除了 Cloud Vision API 以外的 API 新增至清單。

部署可呼叫的函式

接著,部署 Cloud Function,以便用來連結應用程式和 Cloud Vision API。functions-samples 存放區包含您可以使用的範例。

根據預設,透過這個函式存取 Cloud Vision API 時,只有已驗證的應用程式使用者才能存取 Cloud Vision API。您可以視需要修改函式。

如何部署函式:

  1. 複製或下載 functions-samples 存放區,然後變更為 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。

為應用程式新增必要的依附元件

  • 將 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. 使用 Type LANDMARK_DETECTION 建立 JSON 要求:

      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 陣列中的每個物件都代表圖片中辨識到的地標。針對每個地標,您可以取得其在輸入圖像中的邊界座標、地標名稱、經緯度、Knowledge Graph 實體 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();
        }
    }