MLKit for Firebase触ってみた(1) -文字認識編-

 MLKit for Firebase触ってみたシリーズ、ここからが本番です。まずは文字認識からやっていきます!

  • 概要/事前準備編: Link
  • 文字認識編: この記事
  • 顔認識編: Link
  • バーコードスキャン編: 執筆中…
  • 物体認識編: 執筆中…

TL;DR

数時間のコーディングでこんな感じのアプリが作れます。

Cloud


On-device


かけた時間の割に結構な精度が出るので、なかなかやばたにえん😊

※On-deviceの場合はアルファベットのみ認識可能です(正確にはLatin characters)

実装

コードを書く前の準備

1. ライブラリの追加

app/build.gradlecom.google.firebase:firebase-ml-vision:15.0.0を追記します。

dependencies {
    ...
    implementation 'com.google.firebase:firebase-ml-vision:16.0.0'
    ...
}

2. AndroidManifestにmeta-dataを追記

AndroidManifest.xml<meta-data.../>の記述を追記します

<application
    ...
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name">
    ...
    <!-- 追記 -->
    <meta-data
        android:name="com.google.firebase.ml.vision.DEPENDENCIES"
        android:value="text" />
    ...
</application>

公式ドキュメントでは「Optional but recommended」となっている箇所です。この記述を追記しておくと、On-Device時に利用するモデルをアプリのダウンロードと同時に落としてくれるようになります。

逆に、この記述無しでは初めてモデルを利用する時にデータのダウンロードが走り、しかもこのダウンロードが終わるまでは文字認識の結果を返さない挙動になる模様。以上の経緯より、記述しておくことをオススメします。

3.Cloud Vision APIの有効化(option)

CloudのAPIを利用する場合、Cloud Vision APIの有効化が必要です。

Cloud Vision APIの利用には、まずFirebaseの利用プランをBlazeプラン(従量課金プラン)に切り替える必要があります。Firebase consoleからプランの変更をすることができます。

次に、Cloud Vision APIの有効化を行います。Google Cloud ConsoleからAPI > ライブラリを選択し、APIの検索窓に「Cloud Vision API」と打ち込みます。

検索結果からAPIの詳細に飛び、「有効にする」をクリックします。以上でCloud Vision APIの有効化は完了です。

コーディング(On-device)

1.FirebaseVisionImageのオブジェクトを取得

FirebaseVisionImageをオブジェクトを取得します。画像の取得形式によって、使用するメソッドやコードが少し異なります。

Bitmapオブジェクトから取得

FirebaseVisionImage.fromBitmap()メソッドを使用します。

val image: FirebaseVisionImage = FirebaseVisionImage.fromBitmap(bitmap)

android.media.Imageオブジェクトから取得

FirebaseVisionImage.fromMediaImage()メソッドを使用します。
android.media.Imageは、Cameraを使った処理で画像を取得すると、出力される形式です。

val image: FirebaseVisionImage = FirebaseVisionImage.fromMediaImage(mediaImage, rotation)

FirebaseVisionImage.fromMediaImage()メソッドには、画像の回転情報を渡すことができます。以下のような、画面の回転角度を返すメソッドを作っておくと便利でしょう。

val ORIENTATIONS = SparseIntArray().also {
    it.append(Surface.ROTATION_0, 0)
    it.append(Surface.ROTATION_90, 90)
    it.append(Surface.ROTATION_180, 180)
    it.append(Surface.ROTATION_270, 270)
}

fun getRotationCompensation(cameraId: String, activity: AppCompatActivity, context: Context): Int {
    val deviceRotation: Int = activity.windowManager.defaultDisplay.rotation
    var rotationCompensation: Int = ORIENTATIONS.get(deviceRotation)
    
    val cameraManager: CameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
    val sensorOrientation: Int = cameraManager
            .getCameraCharacteristics(cameraId)
            .get(CameraCharacteristics.SENSOR_ORIENTATION)
    rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360

    return when (rotationCompensation) {
        0 -> FirebaseVisionImageMetadata.ROTATION_0
        90 -> FirebaseVisionImageMetadata.ROTATION_90
        180 -> FirebaseVisionImageMetadata.ROTATION_180
        270 -> FirebaseVisionImageMetadata.ROTATION_270
        else -> {
            Log.e(TAG, "Bad rotation value: " + rotationCompensation)
            FirebaseVisionImageMetadata.ROTATION_0
        }
    }
}

ByteBufferオブジェクトから取得

FirebaseVisionImage.fromByteBuffer()、またはFirebaseVisionImage.fromByteArray()メソッドを使用します。

val image: FirebaseVisionImage = FirebaseVisionImage.fromByteBuffer(byteBuffer, metadata)
// or FirebaseVisionImage.fromByteArray(byteArray, metadata)

こちらもandroid.media.Imageと同じく、Cameraを使った際のユースケースになります。1つ違うのは、回転情報を含む画像のメタデータが必須となる点です。

FirebaseVisionImage.fromByteBufferに渡すFirebaseVisionImageMetadataオブジェクトは、以下のコードで取得することができます。

val metadata: FirebaseVisionImageMetadata = FirebaseVisionImageMetadata.Builder()
        .setWidth(1280)
        .setHeight(720)
        .setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
        .setRotation(rotation)
        .build()

Uriから取得

FirebaseVisionImage.fromFilePath()メソッドを使用します。

var image: FirebaseVisionImage
try {
    image = FirebaseVisionImage.fromFilePath(context, uri)
} catch (e: IOException) {
    e.printStackTrace()
}

2.FirebaseVisionTextDetectorのインスタンスを取得

val detector: FirebaseVisionTextDetector = FirebaseVision.getInstance()
        .visionTextDetector

3.各種リスナーを追加

detectImage()メソッドに、手順1で取得したFirebaseVisionImageオブジェクトを渡し、文字認識成功時/失敗時のリスナーをセットします。

val result: Task = detector.detectInImage(image)
        .addOnSuccessListener { firebaseVisionText: FirebaseVisionText
            // 成功時
        }
        .addOnFailureListener { e: Exception ->
            // 失敗時
        }
        .addOnCompleteListener { task: Task ->
            
        }

4.認識結果の展開

文字認識に成功すると、その結果としてFirebaseVisionTextオブジェクトが返ってきます。On-deviceの場合、画像中の文字情報がFirebaseVisionText.Blockという単位のオブジェクトに格納されているため、これを展開するコードを書きます。

firebaseVisionText.blocks.forEach { block: FirebaseVisionText.Block ->
    val boundingBox: Rect = block.boundingBox
    val cornerPoints: Array<Point> = block.cornerPoints
    val text: String = block.text
        // 煮るなり焼くなり...

    block.lines.forEach { line: FirebaseVisionText.Line -> 
        // 煮るなり焼くなり...

        line.elements.forEach { element: FirebaseVisionText.Element ->
            // 煮るなり焼くなり...
        }
    }
}

boundingBoxcornerPointsからは画像中の文字の位置、textからは実際に認識できた文字をそれぞれ取得することができます。

コーディング(Cloud)

1.FirebaseVisionImageのオブジェクトを取得

FirebaseVisionImageをオブジェクトを取得します(On-deviceの時と同じなので省略)

2.FirebaseVisionCloudTextDetectorのインスタンスを取得

On-deviceとほぼ同じですが、呼び出すメソッドが異なります。

val detector: FirebaseVisionCloudTextDetector = FirebaseVision.getInstance()
        .visionCloudTextDetector

また、CloudのAPIでは、この段階で文字認識の設定をオブジェクトで渡すことができます。

val options: FirebaseVisionCloudDetectorOptions = FirebaseVisionCloudDetectorOptions.Builder()
        .setModelType(FirebaseVisionCloudDetectorOptions.LATEST_MODEL)
        .setMaxResults(15)
        .build()

val detector: FirebaseVisionCloudTextDetector = FirebaseVision.getInstance()
        .getVisionCloudTextDetector(options)

3.各種リスナーを追加

On-deviceとほぼ同じです。違いはコールバックで返ってくる値が変わる程度です。

val result: Task = detector.detectInImage(image)
        .addOnSuccessListener { firebaseVisionCloudText: FirebaseVisionCloudText
            // 成功時
        }
        .addOnFailureListener { e: Exception ->
            // 失敗時
        }
        .addOnCompleteListener { task: Task ->
            
        }

4.認識結果の展開

Cloudの場合、認識結果はFirebaseVisionCloudTextオブジェクトとして返ってきます。文字情報の格納形式がOn-deviceの時と大きく異なるため、注意が必要です。

val recognizeText: String = firebaseVisionCloudText.text
firebaseVisionCloudText.pages.forEach { page: FirebaseVisionCloudText.Page ->
    val languages: List<FirebaseVisionCloudText.DetectedLanguage> = page.textProperty?.detectedLanguages
    val height: Int = page.height
    val width: Int = page.width
    val confidence: Float = page.confidence
    // 煮るなり焼くなり...

    page.blocks.forEach { block: FirebaseVisionCloudText.Block ->
        val blockLanguage: List<FirebaseVisionCloudText.DetectedLanguage> = block.textProperty.detectedLanguages
        val paragraphs: List<FirebaseVisionCloudText.Paragraph> = block.textProperty.detectedLanguages
        val boundingBox: Rect = block.boundingBox
        // 煮るなり焼くなり...

        paragraphs.forEach { paragraph: FirebaseVisionCloudText.Paragraph ->
            val words: List<FirebaseVisionCloudText.Word> = paragraph.words
            // 煮るなり焼くなり...

            words.forEach { word: FirebaseVisionCloudText.Word ->
                val symbols: List<FirebaseVisionCloudText.Symbol> = word.symbols
                // 煮るなり焼くなり...
            }
        }
    }
}

On-deviceと比べると、かなり多くの情報を取得できます。
認識された文字は、文章として意味のあるまとまり(paragraph)ごとに取得でき、言語も自動で判別してくれます。

実装したサンプルアプリの動作

 Googleのサンプルコードを参考に、CloudとOn-deviceのAPIを両方使用したサンプルアプリを実装しました。以下、動作の様子と実際のコードになります。

Cloud

On-device

コードはこちら→MLKitTest on GIthub
メインのFragmentの実装はこちら→TextRecognitionFragment.kt

感想

 Cloud Vision APIが初めて登場した時も驚きましたが、このAPIを数時間でアプリに組み込めてしまった今回もめちゃ驚きました。「機械学習なにそれおいしいの???」なアプリケーションエンジニアの僕でもこれくらいのモノは作れてしまうので、みなさんじゃんじゃん使うべきだと思います。

 On-deviceでの認識は、まだまだ発展途上という印象です。というのも、Cloudでの認識が文章を意味のあるまとまりごとに返してくれる上、多言語にきっちり対応しているヤバイ性能なので、それと比較するとどうしても精度や情報量の面で見劣りしてしまいます。
現状では、「ネットワーク接続無しで動作する」ことが最大の利点なので、これを上手く活用できるといいんじゃないかなぁと思いました。

参考リンク

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です