アプリから Google Cloud API を呼び出すには、認可を処理し、API キーなどのシークレット値を保護するための中間 REST API を作成する必要があります。次に、モバイルアプリでこの中間サービスに対する認証と通信を行うためのコードを記述します。
この REST API を作成する方法の一つとして、Firebase Authentication と Functions を使用する方法があります。この方法では、Google Cloud API に対するサーバーレスのマネージド ゲートウェイが提供され、そこで認証が処理されます。このゲートウェイは、事前構築された SDK を使用してモバイルアプリから呼び出すことができます。
このガイドでは、この手法を使用してアプリから Cloud Vision API を呼び出す方法について説明します。この手法では、すべての認証済みユーザーが Cloud のプロジェクトを経由して Cloud Vision の課金サービスにアクセスできます。そのため、続行する前に、目的のユースケースにおいてこの認証メカニズムで十分かどうかを検討してください。
始める前に
プロジェクトを構成する
- まだ Firebase を Android プロジェクトに追加していない場合は追加します。
-
プロジェクトで Cloud ベースの API をまだ有効にしていない場合は、ここで有効にします。
- Firebase コンソールの Firebase ML の [APIs] ページを開きます。
-
まだプロジェクトを Blaze 料金プランにアップグレードしていない場合は、[アップグレード] をクリックしてアップグレードします(プロジェクトをアップグレードするよう求められるのは、プロジェクトが Blaze プランでない場合のみです)。
Blaze レベルのプロジェクトだけが Cloud ベースの API を使用できます。
- Cloud ベースの API がまだ有効になっていない場合は、[Cloud ベースの API を有効化] をクリックします。
- 既存の Firebase API キーを構成して、Cloud Vision API へのアクセスを許可しないようにします。
- Cloud コンソールの [認証情報] ページを開きます。
- リスト内の各 API キーについて、編集ビューを開き、[キーの制限] セクションで Cloud Vision API を除く使用可能なすべての API をリストに追加します。
呼び出し可能関数をデプロイする
次に、アプリと Cloud Vision API の間のブリッジとして使用する Cloud Functions の関数をデプロイします。functions-samples
リポジトリには、使用可能なサンプルが含まれています。
デフォルトでは、この関数を通じて Cloud Vision API にアクセスすると、アプリの認証済みユーザーのみが Cloud Vision API にアクセスできます。関数はさまざまな要件に応じて変更できます。
関数をデプロイするには:
- 関数サンプル リポジトリをダウンロードするか、そのクローンを作成して、
Node-1st-gen/vision-annotate-image
ディレクトリに移動します。git clone https://github.com/firebase/functions-samples
cd Node-1st-gen/vision-annotate-image
- 依存関係をインストールします。
cd functions
npm install
cd ..
- Firebase CLI がインストールされていない場合は、インストールします。
vision-annotate-image
ディレクトリ内で Firebase プロジェクトを初期化します。プロンプトが表示されたら、リストからプロジェクトを選択します。firebase init
- 関数をデプロイします。
firebase deploy --only functions:annotateImage
アプリに Firebase Auth を追加する
上記でデプロイした呼び出し可能関数は、アプリ内で認証されていないユーザーからのリクエストを拒否します。アプリにまだ Firebase Auth を追加していない場合は、Firebase Auth を追加する必要があります。
必要な依存関係をアプリに追加する
implementation("com.google.firebase:firebase-functions:20.4.0") implementation("com.google.code.gson:gson:2.8.6")
これで、画像内のテキストを認識する準備ができました。
1. 入力画像を準備する
Cloud Vision を呼び出すには、画像を base64 でエンコードされた文字列としてフォーマットする必要があります。保存されたファイルの URI から画像を処理する方法は次のとおりです。- 画像を
Bitmap
オブジェクトとして取得します。Kotlin+KTX
var bitmap: Bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
Java
Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
- 必要に応じて、帯域幅を節約するために画像を縮小します。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);
- ビットマップ オブジェクトを 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);
Bitmap
オブジェクトによって表される画像は、さらに回転する必要がないよう、正しい向きになっていなければなりません。
2. 呼び出し可能関数を呼び出し、テキストを認識する
画像内のテキストを認識するには、呼び出し可能関数を呼び出し、JSON Cloud Vision リクエストを渡します。
最初に Cloud Functions のインスタンスを初期化します。
Kotlin+KTX
private lateinit var functions: FirebaseFunctions // ... functions = Firebase.functions
Java
private FirebaseFunctions mFunctions; // ... mFunctions = FirebaseFunctions.getInstance();
関数を呼び出すメソッドを定義します。
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())); } }); }
JSON リクエストを作成します。Cloud Vision API では、
TEXT_DETECTION
とDOCUMENT_TEXT_DETECTION
の 2 つのタイプのテキスト検出がサポートされています。この 2 つのユースケースの違いについては、Cloud Vision OCR のドキュメントをご覧ください。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("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+KTX
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);
関数を呼び出します。
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 レスポンスが返されます。テキスト アノテーションはfullTextAnnotation
オブジェクトにあります。
認識されたテキストは text
フィールド内の文字列として取得できます。例:
Kotlin+KTX
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+KTX
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);
}
}