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 引擎,我希望这篇简单的文章对您有所帮助。谢谢。




