• t.t

Google ML Kitを利用したオンデバイス日本語テキスト認識

最近、Googleが提供するモバイルデバイス向け機械学習SDK(ML Kit)に、オンデバイスの日本語テキスト認識機能が追加されました。現時点(2022/4)では、まだベータ版なのですが、まずはどれぐらい認識できるのかの検証のため、カメラ撮影した写真をテキスト認識する簡単なアプリを作成し実験してみました。


1. ML Kitとは

ML Kitとは、Googleが提供するモバイル向けの機械学習ソフトウェア開発キット(SDK)です。

このSDKを使うことで、Googleの機械学習技術を利用したモバイルアプリを簡単に開発することができます。テキスト認識のほかには、顔検出や画像ラベル付け、物体検出、テキスト翻訳などの機能が提供されています。また、それに加え、独自に機械学習モデル開発してML Kit上で実行することもできます。


2. オンデバイス日本語テキスト認識

今回試してみたのは、そのML Kitに含まれるテキスト認識機能です。

特徴は次の通りです。

  • 画像データからテキストを認識

  • デバイス上ですべての認識処理を実行

  • 日本語を含む多くの言語の認識に対応

  • 行や段落といったテキスト構造を解析可能

  • 認識したテキストがどこの言語であるか特定可能

  • ラテン語系の言語であれば、ほぼリアルタイムに認識可能

  • 無償で利用可能

  • ベータ版(2022/4時点)

デバイス上で認識処理が実行されるので、デバイスの外部に送信したくないセキュアなデータを認識する場合も気になりませんし、ネットワーク接続できない環境で利用できるのも嬉しいですね。


以降、3. 検証用アプリの作成では、このテキスト認識機能をアプリに組み込む方法をご説明し、4.テキスト認識の実験では、検証用アプリを使っていろいろ認識してみた結果をご紹介します。


3. 検証用アプリの作成

検証のため、カメラで撮影した写真をテキスト認識する簡単なAndroidアプリを作成してみました。ここでは、テキスト認識機能を簡単にアプリに組み込めることを、この検証用アプリの作成過程を通してご紹介します。

検証用アプリ画面

3.1. 前提

Android Studioがインストール済みであること。


3.2. 新規プロジェクトの作成

Android Studioを起動し、New Projectから次の設定で新規プロジェクトを作成します。

Template: Empty Activity

Language: Kotlin

Minimum SDK: API 21 Android 5.0 (Lollipop)

3.3. ライブラリ依存関係を追加

build.gradleを開き、dependencies{} ブロックに以下のML Kitの日本語テキスト認識とカメラ関連のライブラリを追加します。

// ML Kit 日本語テキスト認識
implementation 'com.google.mlkit:text-recognition-japanese:16.0.0-beta3'

// CameraX
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"

また、以降、ビューを操作するコードを簡単に記述できるようにするため、ビューバインディングを有効にしておきます。build.gradleandroid{} ブロック内の末尾に以下のブロックを追加します。

viewBinding {
    enabled = true
}

3.4. カメラ撮影権限が必要であることを宣言

Androidアプリでカメラを利用するためには、Android Manifest内でカメラ撮影の権限を必要とするアプリであることを宣言しておく必要があります。AndroidManifest.xmlを開き、applicationタグの前に以下の2行を追加します。

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

3.5. カメラ撮影画面のレイアウト作成

カメラ撮影画面には、プレビューのためのビューと撮影ボタンを配置します。res/layout/activity_main.xmlを開き、次のように編集します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/captureButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dp"
        android:text="@string/capture"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

撮影ボタンのテキストは、文字列リソースファイルres/layout/strings.xmlで定義します。

<resources>
    <string name="app_name">TextRecognitionTestApp</string>
    <string name="capture">撮影</string>
</resources>

3.6. カメラ撮影画面の処理を実装

続いて、カメラ撮影画面の処理を実装していきます。

実装する処理は以下の通り。

  • アプリ起動と同時にカメラを開始し(撮影権限がない場合はユーザに許可を求める)、プレビューを開始

  • 撮影ボタンが押されたら、カメラ撮影とテキスト認識を実行

MainActivity.ktを開き、次のように編集します。テキスト認識をするオブジェクトのインスタンスは、TextRecognition.getClient(JapaneseTextRecognizerOptions.Builder().build())で作成しています。

package jp.co.ndc_net.app.textrecognitiontestapp

import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.TextRecognizer
import com.google.mlkit.vision.text.japanese.JapaneseTextRecognizerOptions
import jp.co.ndc_net.app.textrecognitiontestapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    private val textRecognizer: TextRecognizer by lazy {
        TextRecognition.getClient(
            JapaneseTextRecognizerOptions.Builder().build())
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(R.layout.activity_main)

        // 画面起動時、ユーザがカメラ撮影許可済みであればカメラ初期化、
        // そうでなければユーザにカメラ撮影の許可を求める
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }

        // 撮影ボタンが押されたら、カメラ撮影してテキスト認識
        viewBinding.captureButton.setOnClickListener { capture() }
    }

    private fun capture() {
        // 撮影してテキスト認識 (後で記載)
    }

    private fun showResultText(resultText: String) {
        // 認識結果を画面表示 (後で記載)
    }

    private fun startCamera() {
        // カメラ初期化、プレビュー開始 (後で記載)
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions,   
            grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                // ユーザがカメラ撮影を許可した場合は、カメラを初期化する
                startCamera()
            } else {
                Toast.makeText(this,
                    "カメラ撮影が許可されなかったため終了します",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    companion object {
        private const val TAG = "TextRecognitionTestApp"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = 
            arrayOf(Manifest.permission.CAMERA)
    }
}

カメラの初期化とプレビューの開始は、カメラ実装サポートライブラリCameraXを利用し、次のように実装します。

private fun startCamera() {
    // カメラ初期化
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        val cameraProvider: ProcessCameraProvider = 
            cameraProviderFuture.get()

        // プレビューのユースケースを作成
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(
                    viewBinding.viewFinder.surfaceProvider)
            }

        // 撮影のユースケースを作成
        imageCapture = ImageCapture.Builder().build()

        // 背面のカメラを選択
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
        try {
            // バインドされているユースケースがあれば、すべて解除する
            cameraProvider.unbindAll()
            // プレビュー、撮影ユースケースをカメラにバインドする
            cameraProvider.bindToLifecycle(
                this, cameraSelector, preview, imageCapture)
        } catch(exc: Exception) {
            Log.e(TAG, "ユースケースのバインディングに失敗", exc)
        }
    }, ContextCompat.getMainExecutor(this))
}

カメラ撮影処理と撮影した写真をテキスト認識する処理は次のとおりです。

private fun capture() {
    // 撮影してテキスト認識 (後で記載)
    imageCapture?.takePicture(ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageCapturedCallback() {
    
        @SuppressLint("UnsafeOptInUsageError")
        override fun onCaptureSuccess(imageProxy: ImageProxy) {
            // 撮影成功時、テキスト認識を実行
            super.onCaptureSuccess(imageProxy)
            imageProxy.image?.let { mediaImage ->
                val image = InputImage.fromMediaImage(
                    mediaImage, imageProxy.imageInfo.rotationDegrees)
                textRecognizer.process(image)
                    .addOnSuccessListener { visionText ->
                        // 認識結果を画面表示
                        showResultText(visionText.text)
                    }
                    .addOnFailureListener { exc ->
                        showResultText("認識に失敗しました: $exc")
                    }
                    .addOnCompleteListener {
                        // 認識が終わったら、画像を解放する
                        imageProxy.close()
                    }
            }
        }
        
        override fun onError(exc: ImageCaptureException) {
            // 撮影失敗時
            Log.e(TAG, exc.message, exc)
            Toast.makeText(this@MainActivity,
                "エラー:" + exc.message, Toast.LENGTH_SHORT).show()
        }
    })
}

テキスト認識は、textRecognizer.process()に撮影した画像を渡すだけです。今回の検証アプリでは、認識したテキスト全体visionText.textを取得して表示するのみですが、認識したテキストの行や単語が画像のどこに位置するのかといった情報を取得することもできます。


3.7. 結果表示画面の作成

次に、認識した結果を表示する画面を作成します。

まずは、Android StudioメニューのFile > New > Activity > Empty Activityを選択し、アクティビティを追加します。Activity NameはResultViewActivityとしました。


画面には、認識したテキストの表示に使うTextViewのみを配置します。res/layout/activity_result_view.xmlは次の通り。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ResultViewActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" />
</androidx.constraintlayout.widget.ConstraintLayout>

続いて、カメラ撮影画面から認識したテキストを受け取って、TextViewに表示するようResultViewActivity.ktを実装します。

package jp.co.ndc_net.app.textrecognitiontestapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.view.MenuItem
import jp.co.ndc_net.app.textrecognitiontestapp.databinding.ActivityResultViewBinding

class ResultViewActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityResultViewBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityResultViewBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // TextViewに認識したテキストを表示
        viewBinding.textView.let {
            it.text = intent.getStringExtra(KEY_RESULT_TEXT)
            it.movementMethod = ScrollingMovementMethod()
        }

        // アクションバーのタイトル設定と戻るボタンの表示
        supportActionBar?.let {
            it.title = "認識結果"
            it.setDisplayShowHomeEnabled(true)
            it.setDisplayHomeAsUpEnabled(true)
        }
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // アクションバーの戻るボタンが押されたらカメラ撮影画面に戻る
        if (item.itemId == android.R.id.home) {
            finish()
        }
        return super.onOptionsItemSelected(item)
    }

    companion object {
        const val KEY_RESULT_TEXT = "RESULT_TEXT"
    }
}

最後に、カメラ撮影画面でテキスト認識をしたときに、結果表示画面に遷移するようにMainActivity.ktshowResultText()を実装します。

private fun showResultText(resultText: String) {
    // 結果表示画面に遷移して認識結果を表示する
    val intent = Intent(applicationContext, ResultViewActivity::class.java)
    intent.putExtra(ResultViewActivity.KEY_RESULT_TEXT, resultText)
    startActivity(intent)
}

3.8. ビルド、実行

あとはAndroid端末をUSBデバッグモードで接続し、Android StudioのメニューからRun > Runを実行するだけです。無事実行できれば、開発した検証アプリが起動し、カメラ撮影許可のダイアログが表示されます。「許可する」を選択して、テキスト認識を試してみましょう。

次の4. テキスト認識の実験では、私がいくつか試してみた結果についてご紹介します。


4. テキスト認識の実験

さて、3. 検証用アプリの作成で作成した検証用アプリでML Kitのオンデバイス テキスト認識を試してみた結果をご紹介します。


4.1. 文字種ごとの認識結果(英字、数字、記号、日本語、半角カナ、合字)

まずは、文字種ごとの認識を確認しました。英字、数字、記号、日本語、半角カナ、合字を含んだ以下のテキスト画像を検証用アプリで認識してみます。


認識結果は次のようになりました。

2022/4/8
#123
test@example.com 
http://www.google.co.jp/ 
apple banana cake
日本語テキスト認識あいうえお
ニホンゴハンカクカナ
7O

英数記号、通常の日本語文字、半角カナは意図したとおり認識されていますが、合字や特殊な記号は上手く認識されませんでした。


4.2. 向きごとの認識結果

次に、撮影の向きによって認識結果が変わるかの確認をしました。試したパターンは、横向き、反対向き、角度をつけて斜め上からの3パターンです。

検証アプリで認識したところ、横向き、反対向きは正面からと変わらない結果が得られたものの、角度をつけて斜め上から撮影した場合は、半角カナや英字を一部誤認識してしまうことがありました。日本語テキストに関しては試した限りでは問題なく認識できましたが、撮影角度は精度に影響しそうです。


4.3. 縦書きテキストの認識結果

縦書きテキストの認識ができるかも確認しておきたいところです。以下の画像を用意しました。


認識結果は次の通りです。


株式会社日本データコントロール
株式会社目本テ,
タコントロ
業書の日本語テキスト能職。

縦書きでもある程度認識できていますが、誤認識が目立ちます。また、二重に認識されてしまっている箇所もあります。縦書きのテキストを認識しようとした場合は、認識した位置などを参照しつつなんらかの事後補正をする必要がありそうです。正式版で改善されているといいですね。


4.3. フォントごとの認識結果

続いてフォントごとの認識結果についても確認してみます。游ゴシック、游明朝、創英角ポップ体で試してみました。

游ゴシック、游明朝は合字を除き意図通りに認識できましたが、ポップ体は誤認識が目立ち、半角カナが認識できませんでした。

ZOZZ/4/8
#123
test@example,com
co,jp/
http://wWWw,google, 
apple banana cake
日本語テキスト認識あいうえお
Ovo

他にもいくつかのフォントで試してみましたが、通常のゴシック体、明朝体は基本的に精度高く認識が可能で、そこから外れるフォントは精度が低くなる傾向があるようでした。


4.4. 手書き文字の認識結果

最後に手書き文字の認識も試してみました。用意した画像は以下の通りです。

認識結果は以下の通りでした。

()
2022/44/823
test@er amp le.c Com
httpl/ wwwgoogle. co.jpl
apple bahhh cake
日た気テキスト認識ないうえむ、

手書き文字の認識は、少なくともベータ版では少し難しい印象です。このあたりも正式版に期待です。


5. まとめ

ML Kitのオンデバイス日本語テキスト認識機能を試してみました。アプリへの組み込みは簡単で、なおかつ、ベータ版ながら用途によっては十分に精度が高く、動作も安定しているように感じました。正式版リリースに期待し、今後のプロダクト開発に利用するツール・ライブラリの一選択肢として憶えておきたいところです。