Android OCR (Optical Character Recognition)

Introduction

Android ocr 開発」なんかで検索すると、古い記事が多く、C++コンパイルするとか出てきました。
今は gradle の dependencies に1行書くだけで使えるようです。
大元は、Tesseract OCR というApache License, Version 2.0なライブラリのようです。
それのAndroid fork tess-twoを使います。
参考HPにはクラウドで変換とかもありますが、今回はスタンドアロンです。
画像をAndroid標準のファイルピッカーで選び、それを変換するだけのサンプルです。

YouTubeyoutu.be

Development environment

Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_76-release-b03 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o

Overview

dependencies でtess-twoを使えるようにすることと、学習データを使えるようにすること。
OCR処理のメインは

TessBaseAPI baseApi = new TessBaseAPI();
baseApi.init(DATA_PATH, LANG);
baseApi.setImage(bitmap);
String recognizedText = baseApi.getUTF8Text();
baseApi.end();

だけです。
それ以外の部分は、Activity classはファイルピッカーの動作、OCR classは学習データの用意の部分が大半です。

How to

app/build.gradle の dependencies に以下を追記
バージョンは ここ 参照

dependencies {
	compile 'com.rmtheis:tess-two:7.0.0'
}

app/src/ に assetsディレクトリを作り、tesseract-ocr/tessdataから必要な学習データをDLし、入れます。
以下のコードでは eng.traineddata(約30MB)を入れてます。
tessdata-master.zipは1GB以上ありました。(2017/7/15)

Activity class

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class MainActivity extends Activity implements View.OnClickListener {

	private static final int REQUEST_CODE_PICK_CONTENT = 0;

	private OCR _ocr;

	@Override
	protected void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		findViewById(R.id.button).setOnClickListener(this);

		_ocr = new OCR(getApplicationContext());
	}
	
	@Override
	public void onClick(View v){
		if(v == findViewById(R.id.button)){
			Intent intent;
			if(Build.VERSION.SDK_INT < 19){
				intent = new Intent(Intent.ACTION_PICK);
				intent.setAction(Intent.ACTION_GET_CONTENT);
			}else{
				intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
				intent.addCategory(Intent.CATEGORY_OPENABLE);
			}
			intent.setType("image/*");
			startActivityForResult(intent, REQUEST_CODE_PICK_CONTENT);
		}
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data){
		super.onActivityResult(requestCode, resultCode, data);

		if(requestCode == REQUEST_CODE_PICK_CONTENT){
			String ocrString;
			if(resultCode == RESULT_OK && data != null){
				Bitmap bitmap = null;
				if(Build.VERSION.SDK_INT < 19){
					try{
						InputStream in = getContentResolver().openInputStream(data.getData());
						bitmap = BitmapFactory.decodeStream(in);
						try{
							if(in != null){ in.close(); }
						}catch(IOException e){
							e.printStackTrace();
						}
					}catch(FileNotFoundException e){
						e.printStackTrace();
					}
				}else{
					Uri uri = data.getData();
					try{
						ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
						if(parcelFileDescriptor != null){
							FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
							bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
							parcelFileDescriptor.close();
						}

					}catch(IOException e){
						e.printStackTrace();
					}
				}
				if(bitmap != null){
					ImageView imageView = (ImageView)findViewById(R.id.image);
					imageView.setScaleType(ImageView.ScaleType.FIT_CENTER);
					imageView.setImageBitmap(bitmap);
					ocrString = _ocr.getString(getApplicationContext(), bitmap);
				}else{
					ocrString = "bitmap is null";
				}
			}else{
				ocrString = "something wrong?";
			}
			((TextView)findViewById(R.id.OCRString)).setText(ocrString);
		}
	}

}

OCR class

import android.content.Context;
import android.graphics.Bitmap;

import com.googlecode.tesseract.android.TessBaseAPI;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class OCR {

	private static final String LANG = "eng";
	private static final String TESS_DATA_DIR = "tessdata" + File.separator;
	private static final String TESS_TRAINED_DATA = LANG + ".traineddata";

	public OCR(Context context){
		checkTrainedData(context);
	}

	private void checkTrainedData(Context context) {
		String dataPath = context.getFilesDir() + File.separator + TESS_DATA_DIR;
		File dir = new File(dataPath);
		if (!dir.exists() && dir.mkdirs()){
			copyFiles(context);
		}
		if(dir.exists()) {
			String dataFilePath = dataPath + TESS_TRAINED_DATA;
			File datafile = new File(dataFilePath);
			if (!datafile.exists()) {
				copyFiles(context);
			}
		}
	}

	private void copyFiles(Context context) {
		try {
			String filePath = context.getFilesDir() + File.separator + TESS_DATA_DIR + TESS_TRAINED_DATA;

			InputStream inputStream = context.getAssets().open(TESS_DATA_DIR + TESS_TRAINED_DATA);
			OutputStream outStream = new FileOutputStream(filePath);

			byte[] buffer = new byte[1024];
			int read;
			while ((read = inputStream.read(buffer)) != -1) {
				outStream.write(buffer, 0, read);
			}
			outStream.flush();
			outStream.close();
			inputStream.close();

			File file = new File(filePath);
			if (!file.exists()) {
				throw new FileNotFoundException();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public String getString(Context context, Bitmap bitmap){
		final String DATA_PATH = context.getFilesDir().toString();

		TessBaseAPI baseApi = new TessBaseAPI();
		baseApi.init(DATA_PATH, LANG);
		baseApi.setImage(bitmap);
		String recognizedText = baseApi.getUTF8Text();
		baseApi.end();

		return recognizedText;
	}

}

activity_main

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:id="@+id/activity_main"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	android:paddingBottom="@dimen/activity_vertical_margin"
	android:paddingLeft="@dimen/activity_horizontal_margin"
	android:paddingRight="@dimen/activity_horizontal_margin"
	android:paddingTop="@dimen/activity_vertical_margin"
	android:orientation="vertical"
	tools:context=".MainActivity">

	<ImageView
		android:id="@+id/image"
		android:layout_width="match_parent"
		android:layout_height="0dp"
		android:layout_weight="1"/>

	<Button
		android:id="@+id/button"
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:text="Choose pic and do ocr"/>

	<TextView
		android:id="@+id/OCRString"
		android:layout_width="match_parent"
		android:layout_height="0dp"
		android:layout_weight="1"
		android:text="ocr string here"/>

</LinearLayout>

Appendix

動画でもそうですが、読み取りは完ぺきとは言えません。
精度を上げるには traineddata をいじるようです。
参考
tesstrain.sh で Tesseract-OCR の言語データをカスタマイズするqiita.com

Memo

学習データは直接assetsかres/rawから直接読み取ろうとしましたが、TessBaseAPI.initが以下のようになっていて、ローカルのデータエリアにコピーしないと読めないようです。
assets はInputStream、AssetFileDescriptor、XmlResourceParser経由でしか読めない。
res/rawは配下にディレクトリを作れない。

    public boolean init(String datapath, String language, int ocrEngineMode) {
        if (datapath == null)
            throw new IllegalArgumentException("Data path must not be null!");
        if (!datapath.endsWith(File.separator))
            datapath += File.separator;

        File datapathFile = new File(datapath);
        if (!datapathFile.exists())
            throw new IllegalArgumentException("Data path does not exist!");

        File tessdata = new File(datapath + "tessdata");
        if (!tessdata.exists() || !tessdata.isDirectory())
            throw new IllegalArgumentException("Data path must contain subfolder tessdata!");

openFileDescriptorにあるリテラル"r"が気になったけど、

getContentResolver().openFileDescriptor(uri, "r")

ContentProvider developer.android.com
どうやらこれでよさそう。

参考HP

ほぼこれだけで十分。
Simple OCR Android App Using Tesseract Tutorialhttp://imperialsoup.com/2016/04/29/simple-ocr-android-app-using-tesseract-tutorial/imperialsoup.com
rmtheis/tess-twogithub.com
tesseract-ocr/tessdatagithub.com
Android and OCR

Androidのファイルピッカーに関して

ギャラリーから画像を取得する編集するseesaawiki.jp
[Android] ギャラリーの画像を取得するakira-watson.com