Catch up on everything announced at Firebase Summit, and learn how Firebase can help you accelerate app development and run your app with confidence. Learn More

在 Android 上使用 Firebase 身份驗證和函數通過 Cloud Vision 安全識別地標

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

為了從您的應用調用 Google Cloud API,您需要創建一個中間 REST API 來處理授權並保護秘密值(例如 API 密鑰)。然後,您需要在您的移動應用程序中編寫代碼以驗證此中間服務並與之通信。

創建此 REST API 的一種方法是使用 Firebase Authentication and Functions,它為您提供了一個託管的無服務器網關,用於處理身份驗證的 Google Cloud API,並且可以使用預構建的 SDK 從您的移動應用程序調用。

本指南演示瞭如何使用此技術從您的應用程序調用 Cloud Vision API。此方法將允許所有經過身份驗證的用戶通過您的 Cloud 項目訪問 Cloud Vision 計費服務,因此在繼續之前考慮此身份驗證機制是否足以滿足您的用例。

在你開始之前

配置您的項目

  1. 如果您還沒有,請將 Firebase 添加到您的 Android 項目中。
  2. 如果您尚未為您的項目啟用基於雲的 API,請立即啟用:

    1. 打開 Firebase 控制台的Firebase ML API 頁面
    2. 如果您尚未將項目升級到 Blaze 定價計劃,請單擊升級進行升級。 (僅當您的項目不在 Blaze 計劃中時,系統才會提示您升級。)

      只有 Blaze 級項目可以使用基於雲的 API。

    3. 如果尚未啟用基於雲的 API,請單擊啟用基於雲的 API
  3. 配置您現有的 Firebase API 密鑰以禁止訪問 Cloud Vision API:
    1. 打開雲控制台的憑據頁面。
    2. 對於列表中的每個 API 密鑰,打開編輯視圖,然後在密鑰限制部分中,將Cloud Vision API 之外的所有可用 API 添加到列表中。

部署可調用函數

接下來,部署將用於橋接應用程序和 Cloud Vision API 的 Cloud Function。 functions-samples存儲庫包含一個您可以使用的示例。

默認情況下,通過此函數訪問 Cloud Vision API 將僅允許您的應用程序的經過身份驗證的用戶訪問 Cloud Vision API。您可以根據不同的要求修改該功能。

要部署函數:

  1. 克隆或下載functions-samples 存儲庫並切換到vision-annotate-image目錄:
    git clone https://github.com/firebase/functions-samples
    cd 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 函數和 gson Android 庫的依賴項添加到您的模塊(應用程序級)Gradle 文件(通常是 app/build.gradle):
    implementation 'com.google.firebase:firebase-functions:20.2.2'
    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. 創建類型為LANDMARK_DETECTION的 JSON 請求:

      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.獲取有關已識別地標的信息

    如果地標識別操作成功,將在任務結果中返回BatchAnnotateImagesResponse的 JSON 響應。 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();
        }
    }