Как безопасно добавлять ярлыки к изображениям с помощью Cloud Vision, Firebase Auth и функций на устройствах Android

Чтобы вызвать Google Cloud API из приложения, необходимо создать промежуточный REST API, который обрабатывает авторизацию и защищает секретные значения, такие как ключи API. Затем вам нужно будет написать код в мобильном приложении для аутентификации и связи с этим промежуточным сервисом.

Один из способов создать такой REST API – использовать Firebase Authentication и Functions. Это управляемый бессерверный шлюз к Google Cloud API, который обрабатывает аутентификацию и может вызываться из мобильного приложения с помощью готовых SDK.

В этом руководстве показано, как использовать этот метод для вызова Cloud Vision API из приложения. Этот метод позволит всем аутентифицированным пользователям получать доступ к платным сервисам Cloud Vision через ваш облачный проект, поэтому, прежде чем продолжить, подумайте, подходит ли этот механизм аутентификации для вашего варианта использования.

Подготовка

Настройте проект

  1. Если вы ещё этого не сделали, добавьте Firebase в проект для Android.
  2. Если вы ещё не включили облачные API для своего проекта, сделайте это сейчас:

    1. Откройте страницу Firebase ML API в консоли Firebase.
    2. Если вы ещё не перешли на тарифный план Blaze с оплатой по мере использования, нажмите Перейти. Это предложение появится, только если вы используете другой тарифный план.

      Облачные API доступны только в проектах с тарифным планом Blaze.

    3. Если облачные API ещё не включены, нажмите Enable Cloud-based APIs (Включить облачные API).
  3. Настройте существующие ключи Firebase API, чтобы запретить доступ к Cloud Vision API:
    1. Откройте страницу Credentials (Учетные данные) в Cloud Console.
    2. Для каждого ключа API в списке откройте режим редактирования и в разделе Key Restrictions (Ограничения ключа) добавьте в список все доступные API, кроме Cloud Vision API.

Разверните вызываемую функцию

Затем разверните функцию Cloud Functions, которая будет использоваться для связи приложения с Cloud Vision API. Пример можно найти в репозитории 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, установите его.
  4. Инициализируйте проект Firebase в каталоге vision-annotate-image. При появлении запроса выберите проект из списка.
    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:23.0.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, в котором для параметра Type задано значение LABEL_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("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

      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. Как получить информацию об объектах с ярлыками

    Если операция пометки изображений выполнена успешно, в результате задачи будет возвращен ответ JSON BatchAnnotateImagesResponse. Каждый объект в массиве labelAnnotations представляет собой то, что было помечено на изображении. Для каждого ярлыка можно получить текстовое описание, идентификатор объекта в сети знаний (если доступен) и оценку достоверности совпадения. Пример:

    Kotlin

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