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

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

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

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

Подготовка

Как настроить проект

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

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

      Использовать облачные API могут только проекты с тарифным планом Blaze.

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

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

Затем разверните функцию Cloud Functions, которая будет использоваться для связи вашего приложения и Cloud Vision API. В репозитории functions-samples есть пример, который можно использовать.

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

Чтобы развернуть функцию:

  1. Клонируйте или скачайте репозиторий с примерами функций и перейдите в каталог 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. Вызов функции для распознавания текста

    Чтобы распознать текст на изображении, вызовите функцию, передав ей запрос к Cloud Vision API в формате JSON.

    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. Cloud Vision API поддерживает два типа распознавания текста: TEXT_DETECTION и DOCUMENT_TEXT_DETECTION. Ознакомьтесь с документацией по OCR в Cloud Vision, чтобы узнать больше о различиях между этими двумя вариантами использования.

      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("type", JsonPrimitive("TEXT_DETECTION"))
      // Alternatively, for DOCUMENT_TEXT_DETECTION:
      // feature.add("type", JsonPrimitive("DOCUMENT_TEXT_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("type", new JsonPrimitive("TEXT_DETECTION"));
      // Alternatively, for DOCUMENT_TEXT_DETECTION:
      //feature.add("type", new JsonPrimitive("DOCUMENT_TEXT_DETECTION"));
      JsonArray features = new JsonArray();
      features.add(feature);
      request.add("features", features);
      

      При необходимости укажите подсказки о языке, чтобы помочь с его определением (см. список поддерживаемых языков):

      Kotlin

      val imageContext = JsonObject()
      val languageHints = JsonArray()
      languageHints.add("en")
      imageContext.add("languageHints", languageHints)
      request.add("imageContext", imageContext)
      

      Java

      JsonObject imageContext = new JsonObject();
      JsonArray languageHints = new JsonArray();
      languageHints.add("en");
      imageContext.add("languageHints", languageHints);
      request.add("imageContext", imageContext);
      
    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. Текстовые аннотации можно найти в объекте fullTextAnnotation.

    Распознанный текст можно получить в виде строки в поле text. Пример:

    Kotlin

    val annotation = task.result!!.asJsonArray[0].asJsonObject["fullTextAnnotation"].asJsonObject
    System.out.format("%nComplete annotation:")
    System.out.format("%n%s", annotation["text"].asString)
    

    Java

    JsonObject annotation = task.getResult().getAsJsonArray().get(0).getAsJsonObject().get("fullTextAnnotation").getAsJsonObject();
    System.out.format("%nComplete annotation:%n");
    System.out.format("%s%n", annotation.get("text").getAsString());
    

    Также можно получить информацию об отдельных областях изображения. Для каждого из типов block, paragraph, word и symbol можно получить текст, распознанный в регионе, и координаты границ этого региона. Пример:

    Kotlin

    for (page in annotation["pages"].asJsonArray) {
        var pageText = ""
        for (block in page.asJsonObject["blocks"].asJsonArray) {
            var blockText = ""
            for (para in block.asJsonObject["paragraphs"].asJsonArray) {
                var paraText = ""
                for (word in para.asJsonObject["words"].asJsonArray) {
                    var wordText = ""
                    for (symbol in word.asJsonObject["symbols"].asJsonArray) {
                        wordText += symbol.asJsonObject["text"].asString
                        System.out.format(
                            "Symbol text: %s (confidence: %f)%n",
                            symbol.asJsonObject["text"].asString,
                            symbol.asJsonObject["confidence"].asFloat,
                        )
                    }
                    System.out.format(
                        "Word text: %s (confidence: %f)%n%n",
                        wordText,
                        word.asJsonObject["confidence"].asFloat,
                    )
                    System.out.format("Word bounding box: %s%n", word.asJsonObject["boundingBox"])
                    paraText = String.format("%s%s ", paraText, wordText)
                }
                System.out.format("%nParagraph: %n%s%n", paraText)
                System.out.format("Paragraph bounding box: %s%n", para.asJsonObject["boundingBox"])
                System.out.format("Paragraph Confidence: %f%n", para.asJsonObject["confidence"].asFloat)
                blockText += paraText
            }
            pageText += blockText
        }
    }
    

    Java

    for (JsonElement page : annotation.get("pages").getAsJsonArray()) {
        StringBuilder pageText = new StringBuilder();
        for (JsonElement block : page.getAsJsonObject().get("blocks").getAsJsonArray()) {
            StringBuilder blockText = new StringBuilder();
            for (JsonElement para : block.getAsJsonObject().get("paragraphs").getAsJsonArray()) {
                StringBuilder paraText = new StringBuilder();
                for (JsonElement word : para.getAsJsonObject().get("words").getAsJsonArray()) {
                    StringBuilder wordText = new StringBuilder();
                    for (JsonElement symbol : word.getAsJsonObject().get("symbols").getAsJsonArray()) {
                        wordText.append(symbol.getAsJsonObject().get("text").getAsString());
                        System.out.format("Symbol text: %s (confidence: %f)%n", symbol.getAsJsonObject().get("text").getAsString(), symbol.getAsJsonObject().get("confidence").getAsFloat());
                    }
                    System.out.format("Word text: %s (confidence: %f)%n%n", wordText.toString(), word.getAsJsonObject().get("confidence").getAsFloat());
                    System.out.format("Word bounding box: %s%n", word.getAsJsonObject().get("boundingBox"));
                    paraText.append(wordText.toString()).append(" ");
                }
                System.out.format("%nParagraph:%n%s%n", paraText);
                System.out.format("Paragraph bounding box: %s%n", para.getAsJsonObject().get("boundingBox"));
                System.out.format("Paragraph Confidence: %f%n", para.getAsJsonObject().get("confidence").getAsFloat());
                blockText.append(paraText);
            }
            pageText.append(blockText);
        }
    }