使用 Firebase Auth、Firebase Functions 和 Cloud Vision 安全地为图片加标签 (Android)

如需从应用中调用 Google Cloud API,您需要创建一个中间 REST API 来处理授权并保护 API 密钥等密钥值。然后,您需要在移动应用中编写代码,用于向此中间服务进行身份验证并与其通信。

您可以使用 Firebase Authentication 和 Firebase Functions 来创建此 REST API,这样您便有了一个连接到 Google Cloud API 的代管式无服务器网关来处理身份验证,而且您可以通过预构建的 SDK 从自己的移动应用中调用此网关。

本指南演示了如何使用此方法从应用中调用 Cloud Vision API。此方法将允许所有经过身份验证的用户通过您的 Cloud 项目访问 Cloud Vision 收费服务,因此请考虑这种身份验证机制是否满足您的使用场景,然后再继续操作。

准备工作

配置您的项目

  1. 将 Firebase 添加到您的 Android 项目(如果尚未添加)。
  2. 如果您尚未为项目启用基于 Cloud 的 API,请立即完成以下操作:

    1. 打开 Firebase 控制台的 Firebase ML API 页面
    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

部署 Callable 函数

接下来,部署将用于衔接您的应用与 Cloud Vision API 的 Cloud Functions 函数。functions-samples 代码库包含一个您可以使用的示例。

默认情况下,此函数将仅允许通过身份验证的应用用户访问 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 添加到您的应用

上面部署的 Callable 函数将拒绝未经身份验证的应用用户的任何请求。如果您尚未将 Firebase Auth 添加到您的应用,则需要执行此操作。

为应用添加必要的依赖项

  • 将 Android 版 Firebase Functions 库和 Gson 库的依赖项添加到您的 **模块(应用级)** Gradle 文件(通常为“//build.gradle.kts”或“//build.gradle”):
        implementation("com.google.firebase:firebase-functions:20.4.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. 调用 Callable 函数来给图片加标签

    如需给图片中的对象加标签,调用 Callable 函数,并向其传递 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. 创建 JSON 请求,并将 Type 设置为 LABEL_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("LABEL_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("LABEL_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 响应。labelAnnotations 数组中的每个对象表示图片中加了标签的某个事物。对于每个标签,您可以获取标签的文本说明、其知识图谱实体 ID(如果有),以及匹配的置信度分数。例如:

    Kotlin+KTX

    for (label in task.result!!.asJsonArray[0].asJsonObject["labelAnnotations"].asJsonArray) {
        val labelObj = label.asJsonObject
        val text = labelObj["description"]
        val entityId = labelObj["mid"]
        val confidence = labelObj["score"]
    }
    

    Java

    for (JsonElement label : task.getResult().getAsJsonArray().get(0).getAsJsonObject().get("labelAnnotations").getAsJsonArray()) {
        JsonObject labelObj = label.getAsJsonObject();
        String text = labelObj.get("description").getAsString();
        String entityId = labelObj.get("mid").getAsString();
        float score = labelObj.get("score").getAsFloat();
    }