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標準のファイルピッカーで選び、それを変換するだけのサンプルです。
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