Android OpenCV实现文字识别

准备工作:

1.下载OpenCV:https://opencv.org/releases/
2.添加tess-two依赖:
建议直接在app的build.gradle下添加tess-two依赖库就可以了:

implementation 'com.rmtheis:tess-two:9.1.0'

最新版本:tess-two
也可以通过以上网址下载自行加入

3.识别库:
用到两个,一个是chi_sim 代表中文,一个是eng代表英文,资源中assets下没有识别库,需要自己添加chi_sim和eng。
识别库下载:识别库
4.图片压缩:

implementation 'top.zibin:Luban:1.1.3'

OpenCV图像处理:

 /**
     * 图像处理
     * 二值化
     * 腐蚀
     */
    public void proSrc2Gray() {
        Mat rgbMat = new Mat();
        Mat grayMat = new Mat();
        Mat binaryMat = new Mat();
        Mat cannyMat = new Mat();

        //获取彩色图像所对应的像素数据
        Utils.bitmapToMat(srcBitmap, rgbMat);
        //图像灰度化,将彩色图像数据转换为灰度图像数据并存储到grayMat中
        Imgproc.cvtColor(rgbMat, grayMat, Imgproc.COLOR_RGB2GRAY);
        //得到边缘图,这里最后两个参数控制着选择边缘的阀值上限和下限
//        Imgproc.Canny(grayMat, cannyMat, 50, 300);
        //二值化 ADAPTIVE_THRESH_MEAN_C    THRESH_BINARY
        Imgproc.threshold(grayMat, binaryMat, Imgproc.THRESH_BINARY_INV, 255, 7);
        //获取自定义核,参数MORPH_RECT表示矩形的卷积核,当然还可以选择椭圆形的、交叉型的
        Mat strElement = Imgproc.getStructuringElement(Imgproc.MORPH_RECT,
                new Size(2, 2));
        //腐蚀
        Imgproc.erode(binaryMat,cannyMat,strElement);
        Imgproc.dilate(binaryMat,cannyMat,strElement);

        //Hough变换倾斜校正
        Imgproc.HoughLinesP(binaryMat,cannyMat,1,3.14/180,1);

        //创建一个图像
        mBitmap = Bitmap.createBitmap(grayMat.cols(), grayMat.rows(),
                Bitmap.Config.RGB_565);
        //将矩阵binaryMat转换为图像

        Utils.matToBitmap(grayMat, mBitmap);
    }

文字识别代码:

/**
     * 识别图像
     * @param activity
     * @param bitmap
     */
    public void recognition(final Activity activity, final Bitmap bitmap) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                /**
                 * 检测sd卡是否存在语言库
                 * 若不存在,从assets获取到本地sd卡
                 */
                if (!checkTrainedDataExists()) {
                    SDUtils.assets2SD(activity.getApplicationContext(), PathUtils.LANGUAGE_PATH, PathUtils.DEFAULT_LANGUAGE_NAME);
                }

                TessBaseAPI tessBaseAPI = new TessBaseAPI();
                tessBaseAPI.setDebug(true);
                tessBaseAPI.init(PathUtils.DATAPATH, PathUtils.DEFAULT_LANGUAGE);
                //识别的图片
                tessBaseAPI.setImage(bitmap);
                //获得识别后的字符串
                String text = "";
                text = "识别结果:" + "\n" + tessBaseAPI.getUTF8Text();
                final String finalText = text;
     
                tessBaseAPI.end();
            }
        }).start();
    }

以上文字识别提取的过程就完成了。
接下来就从手机相册获取照片或者拍照,然后压缩处理,识别文字。

获取图片:

权限:
Android6.0+需要动态申请权限
AndroidManifest中:

	<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 创建/删除文件的权限 -->
    <uses-permission
        android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.CAMERA" />

java代码中:

   /**
     * 申请权限
     */
    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= 23) {
            if ((ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) &&
                    (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)) {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, 1);
            }
        }
    }

由于本人比较懒,所以直接“取消严格模式”来获取图片,为什么要“取消严格模式”我就不多说了,可以自行百度。

/**
     * 取消严格模式 FileProvider
     */
    private void CancelFileProvider(){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
            StrictMode.setVmPolicy( builder.build() );
        }
    }

从相册获取图片:
打开相册:

/**
     * 打开相册
     * @param activity
     */
    public void openAlbum(Activity activity) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        activity.startActivityForResult(intent, MainActivity.PICK_PHOTO);
    }

拿到照片裁剪:

/**
     * 从相册获取照片返回,裁剪
     * @param activity
     */
    public void PickPhotoResult(Activity activity){
        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(MainActivity.imageUri, "image/*");
        //intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.putExtra("crop", true);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, MainActivity.imageUri);
        activity.startActivityForResult(intent, MainActivity.CROP_PHOTO); // 启动裁剪程序
    }

裁剪:

/**
     * 裁剪照片
     * @param activity
     */
    public void CropPhotoResult(Activity activity){
        try {
            OpenCVUtils.getInstance().srcBitmap = BitmapFactory.decodeStream(activity.getContentResolver().
                    openInputStream(MainActivity.imageUri));

            OpenCVUtils.getInstance().proSrc2Gray();
            activity.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
                    Uri.fromFile(ImageUtils.getInstance().createImageFile())));

            if (ImageUtils.getInstance().mBitmap != null) {
                ImageUtils.getInstance().showPicFileByLuban(activity);
                MainActivity.imgView.setImageBitmap(ImageUtils.getInstance().mBitmap); // 将裁剪后的照片显示出来
                MainActivity.imgView.setVisibility(View.VISIBLE);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

执行startActivityForResult后的回调函数

 @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            if (requestCode == PICK_PHOTO) {
                //将content类型的Uri转化为文件类型的Uri
                imageUri = ImageUtils.getInstance().convertUri(this,data.getData());
                CameraUtils.getInstance().PickPhotoResult(this);
            } else if (requestCode == TAKE_PHOTO) {
                CameraUtils.getInstance().TakePhotoResult(this);
            } else if (requestCode == CROP_PHOTO) {
                CameraUtils.getInstance().CropPhotoResult(this);
            }
        }
    }

图像裁剪后,会得到一个灰度化的图像,如果直接识别拍照的图片耗费时间很长,所以我在这对裁剪后的图片进行了压缩处理。

/**
     * 压缩图片
     * @param activity
     */
    public void showPicFileByLuban(final Activity activity) {
        try {
            Luban.with(activity)
                    .load(new File(createImageFile().getPath()))
                    .setCompressListener(new OnCompressListener() {
                        @Override
                        public void onStart() {
                            // TODO 压缩开始前调用,可以在方法内启动 loading UI
                        }

                        @Override
                        public void onSuccess(File file) {
                            // TODO 压缩成功后调用,返回压缩后的图片文件
                            mBitmap = BitmapFactory.decodeFile(file.getPath());
                            Toast.makeText(activity,
                                    file.length() / 1024 + "K",Toast.LENGTH_LONG).show();
                        }

                        @Override
                        public void onError(Throwable e) {
                            // TODO 当压缩过去出现问题时调用
                        }
                    }).launch();//启动压缩
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

保存图片:

/**
     * 将content类型的Uri转化为文件类型的Uri
     * @param activity
     * @param uri
     * @return
     */
    public Uri convertUri(Activity activity,Uri uri){
        InputStream is;
        try {
            //Uri ----> InputStream
            is = activity.getContentResolver().openInputStream(uri);
            //InputStream ----> Bitmap
            Bitmap bm = BitmapFactory.decodeStream(is);
            //关闭流
            is.close();
            return ImageUtils.getInstance().saveBitmap(bm);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将Bitmap写入SD卡中的一个文件中
     * 并返回写入文件的Uri
     * @param bm
     * @return
     */
    public Uri saveBitmap(Bitmap bm) {
        //新建文件夹用于存放裁剪后的图片
        File appDir = new File(PathUtils.DATAPATH + "/DCIM/Camera/");
        if (!appDir.exists()) {
            appDir.mkdirs();
        }

        try {
            File file = createImageFile();
            //打开文件输出流
            FileOutputStream fos = new FileOutputStream(file);
            //将bitmap压缩后写入输出流(参数依次为图片格式、图片质量和输出流)
            bm.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            //刷新输出流
            fos.flush();
            //关闭输出流
            fos.close();
            //返回File类型的Uri
            return Uri.fromFile(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

调用相机拍照:

/**
     * 启动相机
     * @param activity
     */
    public void openCamera(Activity activity) {
        try {
            MainActivity.imageUri = Uri.fromFile(ImageUtils.getInstance().createImageFile());
        } catch (IOException e) {
            e.printStackTrace();
        }
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        //传递你要保存的图片的路径
        intent.putExtra(MediaStore.EXTRA_OUTPUT, MainActivity.imageUri);
        activity.startActivityForResult(intent, MainActivity.TAKE_PHOTO);
    }

裁剪的方法和从相册获取图片的一样!!

工具类:
将assets中的识别库复制到SD卡中

public class SDUtils {
    /**
     * 将assets中的识别库复制到SD卡中
     *
     * @param path 要存放在SD卡中的 完整的文件名。这里是"/storage/emulated/0/tessdata/chi_sim.traineddata"
     * @param name assets中的文件名 这里是 "chi_sim.traineddata"
     */
    public static void assets2SD(Context context, String path, String name) {
        //如果存在就删掉
        File f = new File(path);
        if (f.exists()) {
            f.delete();
        }
        if (!f.exists()) {
            File p = new File(f.getParent());//返回此抽象路径名父目录的路径名字符串
            if (!p.exists()) {
                p.mkdirs();//建立多级文件夹
            }
            try {
                f.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        InputStream is = null;
        OutputStream os = null;
        try {
            //打开assets文件获得一个InputStream字节输入流
            is = context.getAssets().open(name);
            File file = new File(path);
            // 创建一个向指定 File 对象表示的文件中写入数据的文件输出流
            os = new FileOutputStream(file);
            byte[] bytes = new byte[4096];
            int len = 0;
            //从输入流中读取一定数量的字节,并将其存储在缓冲区数组bytes中
            //如果因为流位于文件末尾而没有可用的字节,则返回值-1
            while ((len = is.read(bytes)) != -1) {
                //将指定byte数组中从偏移量off开始的len个字节写入此缓冲的输出流
                os.write(bytes, 0, len);
            }
            //java在使用流时,都会有一个缓冲区,按一种它认为比较高效的方法来发数据:把要发的数据先放到缓冲区,
            //缓冲区放满以后再一次性发过去,而不是分开一次一次地发
            //flush()强制将缓冲区中的数据发送出去,不必等到缓冲区满
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                //关闭输入流和输出流
                if (is != null)
                    is.close();
                if (os != null)
                    os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

保存的路径:

public class PathUtils {

    //TessBaseAPI初始化用到的第一个参数,是个目录
    public static final String DATAPATH = Environment.getExternalStorageDirectory()
            .getAbsolutePath() + File.separator;
    //在DATAPATH中新建这个目录,TessBaseAPI初始化要求必须有这个目录
    public static final String tessdata = DATAPATH + File.separator + "tessdata";
    //TessBaseAPI初始化测第二个参数,就是识别库的名字不要后缀名。
    public static String DEFAULT_LANGUAGE = "chi_sim";
    //assets中的文件名
    public static String DEFAULT_LANGUAGE_NAME = DEFAULT_LANGUAGE + ".traineddata";
    //保存到SD卡中的完整文件名
    public static String LANGUAGE_PATH = tessdata + File.separator + DEFAULT_LANGUAGE_NAME;

}

最后,识别按钮操作:

			case R.id.btn_rec:
                if (imgView.getVisibility() != View.VISIBLE) {
                    Toast.makeText(getApplicationContext(), "请先拍照或者选一张图片", Toast.LENGTH_SHORT).show();
                    return;
                } else {
                    resultTv.setText("");
                    txtFinal.setText("");
                    try {
                        ImageUtils.getInstance().mBitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    OpenCVUtils.getInstance().recognition(this,ImageUtils.getInstance().mBitmap);
                }
                break;

完整demo下载
参考:https://blog.csdn.net/qq_35820350/article/details/78802276

感谢博主的分享!!

  • 2
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值