Android 字符识别
Android 光学字符识别。
引言
在本文中,我将展示一个 OCR Android 演示应用程序,该应用程序可以从位图源中识别单词。
有一个支持 Android 的开源 OCR 库:Tesseract。
除了访问相机、处理位图、制作相机焦点框视图、内部存储访问等之外,此演示项目还包含其他部分。
背景
OCR 可用于许多用途:从图像中读取文本、扫描特定服务的数字或代码...
内容
-
准备 Tesseract
-
将 tess-two 添加到 Android Studio 项目
-
Tesseract 库的使用
-
Android 实现
Using the Code
演示项目是在 Windows PC 上开发的,使用 Android Studio IDE。
-
准备 Tesseract
- 从 github 安装 tesseract 源代码
- 将内容解压到 tesseract 文件夹中
- 需要 Android 2.2 或更高版本
- 下载 v3.02 训练数据文件,用于一种语言(例如 英语数据)。
- 在移动端,数据文件必须解压到名为
tessdata
的子目录中。
要将 tesseract 导入到您的 Android 项目中,您必须先构建它
- 您必须安装 Android NDK,如果您没有安装,请从 此处 安装。
- 安装 Android NDK 后,您必须将其安装目录添加到 Path 下的环境变量中
- 转到 控制面板\系统和安全\系统 - 高级系统设置 - 环境变量:
- 将 Android 目录添加到路径后,我们可以在 cmd.exe 中使用 ndk 命令
- 现在使用 cmd 窗口构建 tesseract ocr 库,(此过程可能需要一些时间 ~30 分钟)
- 转到 tess-two 文件夹并打开 cmd 窗口,(按 Shift + 右键)
- 使用以下命令构建项目
-
ndk-build android update project --path C:\...\tess-two ant release
- 转到 tess-two 文件夹并打开 cmd 窗口,(按 Shift + 右键)
-
将 Tess-Two 添加到 Android Studio 项目
在我们构建了 tess-two 库项目后,我们必须将其导入到 Android Studio 中的 Android 应用程序项目中。
- 在您的 Android Studio 项目树中,添加一个新的目录 "libraries",然后添加一个子目录命名为 "tess-two"。
- 在 Windows 资源管理器中,将 tess-two 构建项目的内容移动到 Android Studio 的 libraries tess-two 目录中。
-
您必须在 libraries\tess-two 文件夹中添加一个 build.gradle [新文件]
- 确保应用程序项目中的所有 build.gradle 文件都具有相同的 targetSdk 版本
- 确保 tess-two 库具有 build.gradle 文件
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.14.0' } } apply plugin: 'com.android.library' android { compileSdkVersion 21 buildToolsVersion "21.0.2" defaultConfig { minSdkVersion 15 targetSdkVersion 21 } sourceSets.main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] resources.srcDirs = ['src'] res.srcDirs = ['res'] jniLibs.srcDirs = ['libs'] } }
- 接下来,将 tess-two 库添加到 main settings.gradle 文件中
include ':app', ':tess-two' include ':libraries:tess-two' project(':tess-two').projectDir = new File('libraries/tess-two')
- 接下来,将 tess-two 添加为模块依赖项到 app 模块的项目结构中(ctrl+alt+shift+s)
-
现在可以在我们的 Android 项目中使用 tesseract 库了
public String detectText(Bitmap bitmap) {
TessDataManager.initTessTrainedData(context);
TessBaseAPI tessBaseAPI = new TessBaseAPI();
String path = "/mnt/sdcard/packagename/tessdata/eng.traineddata";
tessBaseAPI.setDebug(true);
tessBaseAPI.init(path, "eng"); //Init the Tess with the trained data file, with english language
//For example if we want to only detect numbers
tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_WHITELIST, "1234567890");
tessBaseAPI.setVariable(TessBaseAPI.VAR_CHAR_BLACKLIST, "!@#$%^&*()_+=-qwertyuiop[]}{POIU" +
"YTREWQasdASDfghFGHjklJKLl;L:'\"\\|~`xcvXCVbnmBNM,./<>?");
tessBaseAPI.setImage(bitmap);
String text = tessBaseAPI.getUTF8Text();
Log.d(TAG, "Got data: " + result);
tessBaseAPI.end();
return text;
}
-
Android 端
仍然需要从相机拍摄照片,或从文件中加载照片。
我们将制作一个 CameraEngine 类,该类加载相机硬件,并在 SurfaceView 上显示实时流。
在 CameraUtils 中
//Check if the device has a camera
public static boolean deviceHasCamera(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
}
//Get available camera
public static Camera getCamera() {
try {
return Camera.open();
} catch (Exception e) {
Log.e(TAG, "Cannot getCamera()");
return null;
}
}
在 CameraEngine 中
public class CameraEngine {
static final String TAG = "DBG_" + CameraUtils.class.getName();
boolean on;
Camera camera;
SurfaceHolder surfaceHolder;
Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
}
};
public boolean isOn() {
return on;
}
private CameraEngine(SurfaceHolder surfaceHolder){
this.surfaceHolder = surfaceHolder;
}
static public CameraEngine New(SurfaceHolder surfaceHolder){
Log.d(TAG, "Creating camera engine");
return new CameraEngine(surfaceHolder);
}
public void requestFocus() {
if (camera == null)
return;
if (isOn()) {
camera.autoFocus(autoFocusCallback);
}
}
public void start() {
Log.d(TAG, "Entered CameraEngine - start()");
this.camera = CameraUtils.getCamera();
if (this.camera == null)
return;
Log.d(TAG, "Got camera hardware");
try {
this.camera.setPreviewDisplay(this.surfaceHolder);
this.camera.setDisplayOrientation(90);//Portrait Camera
this.camera.startPreview();
on = true;
Log.d(TAG, "CameraEngine preview started");
} catch (IOException e) {
Log.e(TAG, "Error in setPreviewDisplay");
}
}
public void stop(){
if(camera != null){
//this.autoFocusEngine.stop();
camera.release();
camera = null;
}
on = false;
Log.d(TAG, "CameraEngine Stopped");
}
public void takeShot(Camera.ShutterCallback shutterCallback,
Camera.PictureCallback rawPictureCallback,
Camera.PictureCallback jpegPictureCallback ){
if(isOn()){
camera.takePicture(shutterCallback, rawPictureCallback, jpegPictureCallback);
}
}
}
现在在 MainActivity 中,我们将需要
- 在 SurfaceView 上显示相机预览 [On Resume]
- 停止相机预览并释放相机资源,以供其他应用程序使用。[On Pause]
- 添加两个按钮:一个用于拍照(中间),另一个用于对焦(右侧)。
- 添加一个自定义 FocusBoxView 以裁剪相机预览区域,从中提取文本。
布局 xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/camera_frame"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<engenoid.tessocrdtest.Core.ExtraViews.FocusBoxView
android:id="@+id/focus_box"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
<Button
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/shutter_button"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="50dp"
android:background="@drawable/shutter_layout" />
<Button
style="?android:attr/buttonStyleSmall"
android:layout_width="75dp"
android:layout_height="75dp"
android:id="@+id/focus_button"
android:layout_gravity="end|bottom"
android:layout_marginRight="50dp"
android:layout_marginEnd="50dp"
android:layout_marginBottom="65dp"
android:background="@drawable/focus_layout" />
</FrameLayout>
对于 FocusBoxView, 创建一个扩展 View 的类,我们需要一个 Rect,它将代表焦点框,并在事件发生时更改其尺寸,之后当调用 onDraw 时,它将绘制焦点框矩形(设计、框架、边框和角)来显示裁剪后的照片。
public class FocusBoxView extends View {
private static final int MIN_FOCUS_BOX_WIDTH = 50;
private static final int MIN_FOCUS_BOX_HEIGHT = 20;
private final Paint paint;
private final int maskColor;
private final int frameColor;
private final int cornerColor;
public FocusBoxView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Resources resources = getResources();
maskColor = resources.getColor(R.color.focus_box_mask);
frameColor = resources.getColor(R.color.focus_box_frame);
cornerColor = resources.getColor(R.color.focus_box_corner);
this.setOnTouchListener(getTouchListener());
}
private Rect box;
private static Point ScrRes;
private Rect getBoxRect() {
if (box == null) {
//FocusBoxUtils class contains some helper methods
ScrRes = FocusBoxUtils.getScreenResolution(getContext());
int width = ScrRes.x * 6 / 7;
int height = ScrRes.y / 9;
width = width == 0
? MIN_FOCUS_BOX_WIDTH
: width < MIN_FOCUS_BOX_WIDTH ? MIN_FOCUS_BOX_WIDTH : width;
height = height == 0
? MIN_FOCUS_BOX_HEIGHT
: height < MIN_FOCUS_BOX_HEIGHT ? MIN_FOCUS_BOX_HEIGHT : height;
int left = (ScrRes.x - width) / 2;
int top = (ScrRes.y - height) / 2;
box = new Rect(left, top, left + width, top + height);
}
return box;
}
public Rect getBox() {
return box;
}
private void updateBoxRect(int dW, int dH) {
...
.... UPDATE THE FOCUS BOX DIMENSIONS
...
}
private OnTouchListener touchListener;
private OnTouchListener getTouchListener() {
if (touchListener == null)
touchListener = new OnTouchListener() {
int lastX = -1;
int lastY = -1;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = -1;
lastY = -1;
return true;
case MotionEvent.ACTION_MOVE:
int currentX = (int) event.getX();
int currentY = (int) event.getY();
try {
...
... updateBoxRect(dx, dy);
...
}
} catch (NullPointerException e) {
}
return true;
case MotionEvent.ACTION_UP:
lastX = -1;
lastY = -1;
return true;
}
return false;
}
};
return touchListener;
}
@Override
public void onDraw(Canvas canvas) {
Rect frame = getBoxRect();
int width = canvas.getWidth();
int height = canvas.getHeight();
...
.... DRAW FOCUS BOX
...
paint.setColor(cornerColor);
canvas.drawCircle(frame.left - 32, frame.top - 32, 32, paint);
canvas.drawCircle(frame.right + 32, frame.top - 32, 32, paint);
canvas.drawCircle(frame.left - 32, frame.bottom + 32, 32, paint);
canvas.drawCircle(frame.right + 32, frame.bottom + 32, 32, paint);
...
...
}
}
请注意,您必须在 AndroidManifest.xml 中添加使用相机的权限和其他使用的功能
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
android:name="android.hardware.camera.flash"
android:required="false" />
<uses-feature android:name="android.hardware.camera" />
现在让我们返回到 MainActivity,当点击焦点按钮时,我们将从相机请求焦点,
当单击相机按钮时,相机将拍照,并回调 onPictureTaken(byte[] data, Camera camera) 在这里,我们将解码字节数组到位图并调整大小,在 Tools.getFocusedBitmap(this, camera, data, focusBox.getBox()) 中执行图像裁剪, 并调用 Async 类 TessAsyncEngine 下的 TesseractBaseApi 来提取并显示一个对话框,该对话框包含文本并显示裁剪后的照片。
对于您的自定义用法,您将根据您的需要更改或更新您的代码。
public class MainActivity extends Activity implements SurfaceHolder.Callback, View.OnClickListener,
Camera.PictureCallback, Camera.ShutterCallback {
static final String TAG = "DBG_" + MainActivity.class.getName();
Button shutterButton;
Button focusButton;
FocusBoxView focusBox;
SurfaceView cameraFrame;
CameraEngine cameraEngine;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "Surface Created - starting camera");
if (cameraEngine != null && !cameraEngine.isOn()) {
cameraEngine.start();
}
if (cameraEngine != null && cameraEngine.isOn()) {
Log.d(TAG, "Camera engine already on");
return;
}
cameraEngine = CameraEngine.New(holder);
cameraEngine.start();
Log.d(TAG, "Camera engine started");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
@Override
protected void onResume() {
super.onResume();
cameraFrame = (SurfaceView) findViewById(R.id.camera_frame);
shutterButton = (Button) findViewById(R.id.shutter_button);
focusBox = (FocusBoxView) findViewById(R.id.focus_box);
focusButton = (Button) findViewById(R.id.focus_button);
shutterButton.setOnClickListener(this);
focusButton.setOnClickListener(this);
SurfaceHolder surfaceHolder = cameraFrame.getHolder();
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
cameraFrame.setOnClickListener(this);
}
@Override
protected void onPause() {
super.onPause();
if (cameraEngine != null && cameraEngine.isOn()) {
cameraEngine.stop();
}
SurfaceHolder surfaceHolder = cameraFrame.getHolder();
surfaceHolder.removeCallback(this);
}
@Override
public void onClick(View v) {
if(v == shutterButton){
if(cameraEngine != null && cameraEngine.isOn()){
cameraEngine.takeShot(this, this, this);
}
}
if(v == focusButton){
if(cameraEngine!=null && cameraEngine.isOn()){
cameraEngine.requestFocus();
}
}
}
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Log.d(TAG, "Picture taken");
if (data == null) {
Log.d(TAG, "Got null data");
return;
}
Bitmap bmp = Tools.getFocusedBitmap(this, camera, data, focusBox.getBox());
Log.d(TAG, "Got bitmap");
new TessAsyncEngine().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, this, bmp);
}
@Override
public void onShutter() {
}
}
在 Imaging.Tools 类中进行位图裁剪
public static Bitmap getFocusedBitmap(Context context, Camera camera, byte[] data, Rect box){
Point CamRes = FocusBoxUtils.getCameraResolution(context, camera);
Point ScrRes = FocusBoxUtils.getScreenResolution(context);
int SW = ScrRes.x; //SCREEN WIDTH - HEIGHT
int SH = ScrRes.y;
int RW = box.width(); // FOCUS BOX RECT WIDTH - HEIGHT - TOP - LEFT
int RH = box.height();
int RL = box.left;
int RT = box.top;
float RSW = (float) (RW * Math.pow(SW, -1)); //DIMENSION RATIO OF FOCUSBOX OVER SCREEN
float RSH = (float) (RH * Math.pow(SH, -1));
float RSL = (float) (RL * Math.pow(SW, -1));
float RST = (float) (RT * Math.pow(SH, -1));
float k = 0.5f;
int CW = CamRes.x;
int CH = CamRes.y;
int X = (int) (k * CW); //SCALED BITMAP FROM CAMERA
int Y = (int) (k * CH);
//SCALING WITH SONY TOOLS
// http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/
Bitmap unscaledBitmap = Tools.decodeByteArray(data, X, Y, Tools.ScalingLogic.CROP);
Bitmap bmp = Tools.createScaledBitmap(unscaledBitmap, X, Y, Tools.ScalingLogic.CROP);
unscaledBitmap.recycle();
if (CW > CH)
bmp = Tools.rotateBitmap(bmp, 90);
int BW = bmp.getWidth(); //NEW FULL CAPTURED BITMAP DIMENSIONS
int BH = bmp.getHeight();
int RBL = (int) (RSL * BW); // NEW CROPPED BITMAP IN THE FOCUS BOX
int RBT = (int) (RST * BH);
int RBW = (int) (RSW * BW);
int RBH = (int) (RSH * BH);
Bitmap res = Bitmap.createBitmap(bmp, RBL, RBT, RBW, RBH);
bmp.recycle();
return res;
}
最后,这是一张结果照片

关注点
如果您有兴趣使用 OCR 引擎,我希望这篇简单的文章对您有所帮助。谢谢。