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

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

建立此 REST API 的其中一種方法是使用 Firebase 驗證和函式。這類閘道會提供一個代管的無伺服器閘道,讓您 處理驗證作業的 Google Cloud API,可透過行動應用程式呼叫 預先建構的 SDK

本指南示範如何使用這項技術從應用程式呼叫 Cloud Vision API。 這個方法將允許所有通過驗證的使用者透過您的 Cloud 專案存取 Cloud Vision 計費服務。 請先思考這項驗證機制是否足以滿足您的用途需求,再繼續操作。

事前準備

設定專案

  1. 如果還沒試過 將 Firebase 新增至您的 Android 專案
  2. 如果您尚未為專案啟用雲端式 API,請先啟用 現在:

    1. 開啟 Firebase ML Firebase 控制台的 API 頁面
    2. 如果您尚未將專案升級至 Blaze 定價方案,請按一下 如要這麼做,請升級。(只有在您的 專案並未採用 Blaze 方案)。

      只有 Blaze 層級的專案可以使用以雲端為基礎的 API。

    3. 如果尚未啟用雲端式 API,請按一下「Enable Cloud-based API」(啟用雲端式 API) API
    ,瞭解如何調查及移除這項存取權。
  3. 設定現有的 Firebase API 金鑰來禁止存取 Cloud Vision API:
    1. 開啟 Cloud 控制台的「Credentials」(憑證) 頁面。
    2. 針對清單中的每個 API 金鑰,開啟編輯檢視畫面,然後 限制專區,請新增所有可用的 API (「Cloud Vision」除外) 新增至清單

部署可呼叫函式

接下來,請部署您將用來連結應用程式和 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 驗證新增至應用程式

上方部署的可呼叫函式會拒絕所有未經驗證的要求 對於應用程式使用者而言如果尚未新增 Firebase 作業,請先完成這項操作。 向應用程式進行驗證。

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

  • 為 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+KTX

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

      Java

      Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
    2. 或者,您也可以選擇縮小圖片來節省頻寬。請參閱 Cloud Vision 建議的圖片大小。

      Kotlin+KTX

      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+KTX

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

      Java

      // Scale down bitmap size
      bitmap = scaleBitmapDown(bitmap, 640);
    3. 將點陣圖物件轉換為採用 Base64 編碼的字串:

      Kotlin+KTX

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

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

      Java

      private FirebaseFunctions mFunctions;
      // ...
      mFunctions = FirebaseFunctions.getInstance();
      
    2. 定義叫用函式的方法:

      Kotlin+KTX

      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」建立 JSON 要求 LANDMARK_DETECTION:

      Kotlin+KTX

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

      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. 取得可辨識的地標相關資訊

    如果地標辨識作業成功,系統會傳回 批次註解圖片回應 會在工作的結果中傳回landmarkAnnotations 中的每個物件 陣列代表圖片中可辨識的地標。針對每個地標 您可以在輸入圖像、地標名稱 經緯度、知識圖譜實體 ID (如有),以及 比對的可信度分數例如:

    Kotlin+KTX

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