65.9K
CodeProject 正在变化。 阅读更多。
Home

Android 字符识别

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (38投票s)

2014年11月10日

CPOL

4分钟阅读

viewsIcon

347294

downloadIcon

29724

Android 光学字符识别。

引言

在本文中,我将展示一个 OCR Android 演示应用程序,该应用程序可以从位图源中识别单词。

有一个支持 Android 的开源 OCR 库:Tesseract

除了访问相机、处理位图、制作相机焦点框视图、内部存储访问等之外,此演示项目还包含其他部分。

背景

OCR 可用于许多用途:从图像中读取文本、扫描特定服务的数字或代码...

内容

  1. 准备 Tesseract

  2. 将 tess-two 添加到 Android Studio 项目

  3. Tesseract 库的使用

  4. Android 实现

Using the Code

演示项目是在 Windows PC 上开发的,使用 Android Studio IDE。

  1. 准备 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
  1. 将 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)

  1. 现在可以在我们的 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;
    }
  1. 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 引擎,我希望这篇简单的文章对您有所帮助。谢谢。

© . All rights reserved.