Android 图片文字识别DEMO(基于百度OCR)

前言

  OCR 是 Optical Character Recognition 的缩写,翻译为光学字符识别,指的是针对印刷体字符,采用光学的方式将纸质文档中的文字转换成为黑白点阵的图像文件,通过识别软件将图像中的文字转换成文本格式,供文字处理软件进一步编辑加工的技术(好吧,这是我查来的)。简单的来说,OCR技术就是可以把图片上的文字识别出来,并以文本格式的形式提取出来。

  这个技术的应用方面很广泛,比如说把纸质书籍的内容转化为电子书,之前都需要人手打,但是现在只要扫描一下,将扫描出来的图片通过OCR技术转化成文本格式,效率和成本不知提升了多少倍。

  有人可能会想,这个技术听起来好高端,不懂计算机图形学,模式识别,机器学习巴拉巴拉东西的人,是不是无法接触到这样的技术,实现这样的功能,(好像是的,emmm...),不过,虽然咱们自己实现不了,但是有人把轮子造好了呀,我们只要使用人家造好的轮子,就能实现图片文字识别的功能。

  先来看一下我实现的效果吧,随手拿了办公桌上有字的东西(拿了枸杞茶和菊花茶的包装...),用自己写的demo拍了照识别了一下


 
Android 图片文字识别DEMO(基于百度OCR)
图片文字识别1

 
Android 图片文字识别DEMO(基于百度OCR)
图片文字识别2

  界面下方显示的图片是手机拍的照片,界面上方显示的是从照片中识别出来的文字信息。可以看出识别的正确率还是挺高的。

  我这边用的轮子是 百度文字识别 。下面我将介绍一下我是如何实现上述的文字识别功能。

请求模块定义

  百度其实有提供图片识别Android的SDK,就像其他的SDK一样,只要导入一系列包之后就可以调用识别。寻求快速开发的小伙伴可以了解一下,我看了一下文档,实现还是十分容易的。

 
Android 图片文字识别DEMO(基于百度OCR)
OCR Android SDK

  但是,我在demo中使用的并非是SDK,而是使用另外一种方法——以网络api的方式来进行识别。涉及到的技术有 retrofit+rxjava 进行网络请求(在之前的一篇博客中有介绍如何使用 retrofit+rxjava ,贴一下链接),Android应用动态权限的申请,FileProvider,图片的base64转码,以及热门的MVP框架。  先看一下项目结构
 
Android 图片文字识别DEMO(基于百度OCR)
项目结构.png

  module目录下存放的是MVP架构的三个模块,bean目录下存放的是网络请求返回的数据类型,apiservice中存放的是retrofit有关网络请求的接口。

  根据百度OCR官方给出的接口
 
Android 图片文字识别DEMO(基于百度OCR)
api请求说明.png

  我们定义出如下的接口方法。
/**
     * 通过图片URL的形式,获取图片内的文字信息
     * @param accessToken 通过API Key和Secret Key获取的access_token
     * @param url 图片的url
     * @return observable对象用于rxjava,从RecognitionResultBean中可以获得图片文字识别的信息
     */
    @POST("rest/2.0/ocr/v1/general_basic")
    @FormUrlEncoded
    Observable<RecognitionResultBean> getRecognitionResultByUrl(@Field("access_token") String accessToken, @Field("url") String url);

    /**
     * 通过图片,获取图片内的文字信息
     * @param accessToken 通过API Key和Secret Key获取的access_token
     * @param image 图像数据base64编码后进行urlencode后的String
     * @return  observable对象用于rxjava,从RecognitionResultBean中可以获得图片文字识别的信息
     */
    @POST("rest/2.0/ocr/v1/general_basic")
    @FormUrlEncoded
    Observable<RecognitionResultBean> getRecognitionResultByImage(@Field("access_token") String accessToken, @Field("image") String image);

  在第一个方法中,我们需要传入两个参数,一个是access_token,需申请百度文字识别的开发者资格,得到API key和Secret Key后获取,还有一个参数是图片的网络地址url,可以直接通过这个url直接访问到图片。

  第二个方法中,第一个参数也是同上的access_token,第二个参数则是String类型,这个参数是在本地将图片base64转码之后生成。

  因为我们要实现的功能是用手机拍照,然后将照片信息传递给服务器,因此我们之后调用的是第二个方法(第一个方法只是我按照api说明随手写了一下),这些参数我们以POST的形式发送,按照百度OCRapi 的要求,需要加上@FormUrlEncode注释,我们使用@Field的方式将参数加入请求体。可以看到Observable中的是RecognitionResultBean类型,我们可以从里面拿到服务器返回的文字识别信息。

  定义好这两个方法之后,我们便可以构造retrofit对象进行调用

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://aip.baidubce.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();

        baiduOCRService = retrofit.create(BaiduOCRService.class);

  我们来看一下 rxjava+retrofit 在接口方法中的具体实现

@Override
    public void getRecognitionResultByImage(Bitmap bitmap) {

        String encodeResult = bitmapToString(bitmap);

        baiduOCRService.getRecognitionResultByImage(ACCESS_TOKEN,encodeResult)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<RecognitionResultBean>() {
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    @Override
                    public void onNext(RecognitionResultBean recognitionResultBean) {
                        Log.e("onnext",recognitionResultBean.toString());
                        StringBuilder s = new StringBuilder();
                        List<RecognitionResultBean.WordsResultBean> wordsResult = recognitionResultBean.getWords_result();
                        for (RecognitionResultBean.WordsResultBean words:wordsResult) {
                            s.append(words.getWords());
                        }

                        mView.updateUI(s.toString());

                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("onerror",e.toString());
                    }

                    @Override
                    public void onComplete() {

                    }
                });

    }

  可以看到传入了一个Bitmap类型的图片参数,这个参数经过 String encodeResult = bitmapToString(bitmap); 方法转成了String类型。是因为接口要求的参数数据为String类型,所以我们对图片进行了base64转码,具体的转码方法如下:

private String bitmapToString(Bitmap bitmap){
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        byte[] bytes = baos.toByteArray();
        return Base64.encodeToString(bytes, Base64.DEFAULT);
    }

  调用此方法,便可以把图片类型转化成字符串类型,之后的操作便是对网路接口调用之后的回调方法进行定义,我们在调用成功后的onNext操作中,拿到了RecognitionResultBean类型参数,这个参数里含有图片所包含文字的信息,我们将所有的文字一一取出,用StringBuilder连接成一个字符串,返回给View层,调用View层的updateUI进行UI界面的更新,对于这个字符串我们在之后还可以进行进一步的分析操作。

  以上,百度OCR接口请求模块定义部分便已完成,接下来,我们要做的就是调用系统的相机功能,拍照得到照片,将照片传递给我们上面定义的请求接口,进行文字识别。

相机功能调用

  首先,由于要对相机功能进行调用,我们需要在AndroidManifest清单文件中写明我们需要用到的权限

<uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.CAMERA"/>

  分别是网络请求权限,数据的读存取权限,以及相机权限。在Android 6.0 之前应用的权限在安装时全部授予,也就是说只要在AndroidManifest中申请过的权限,都会给予。而在 Android 6.0 或更高版本之后,对权限的管理作出了改变,对某些涉及到用户隐私的权限可在运行时根据用户的需要动态授予,也就是说,在AndroidManifest中申请的权限,在用户使用的过程中还得询问用户是否给予,用户给予权限了,应用才能进行相关的权限操作。因此我们需在代码中增加动态权限申请的模块(对用户安全性友好了,但是对开发者增加了不友好度....),以下是动态权限申请部分的代码:

private boolean hasPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                || 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.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA}, PERMISSIONS_REQUEST_CODE);
            return false;
        }else {
            return true;
        }
    }

  代码的主要逻辑是,在程序运行的时候,检查是否有相应的权限,如果有权限,则可以进行相关操作,如果没有权限,就调用申请权限的方法。在完成这部分代码的编写之后还需重写onRequestPermissionsResult方法,对申请权限的结果进行反应。

  接下来是调用相机功能的代码

private void takePhoto(){

        if (!hasPermission()) {
            return;
        }

        Intent intent = new Intent();
        intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
        String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/img";
        if (new File(path).exists()) {
            try {
                new File(path).createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        String filename = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        mTmpFile = new File(path, filename + ".jpg");
        mTmpFile.getParentFile().mkdirs();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String authority = getPackageName() + ".provider";
            imageUri = FileProvider.getUriForFile(this, authority, mTmpFile);
        } else {
            imageUri = Uri.fromFile(mTmpFile);
        }
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);

        startActivityForResult(intent, CAMERA_REQUEST_CODE);

    }

  需要提到的是,在Android 7.0之后,如果你使用Intent携带这样的上面的imageUri去打开相机拍照,会抛出FileUriExposedException异常。这时候就需要用到google官方的解决方案——FileProvider。使用的方法可以参照 Android 7.0适配-应用之间共享文件

  调用相机之后我们需要重写onActivityResult方法,对返回拍照结果进行处理,

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == CAMERA_REQUEST_CODE) {
            if (requestCode == RESULT_OK){
                Bitmap photo = BitmapFactory.decodeFile(mTmpFile.getAbsolutePath());
                mPresenter.getRecognitionResultByImage(photo);
                imageView.setImageBitmap(photo);
            }
        }
    }

  如果拍照成功,我们就把照片作为参数传递给之前定义好的接口方法,调用进行图片文字识别。可以看到我还把照片放入imageview中方便与识别结果进行对比。等服务器成功返回识别结构之后,就会调用VIew层的updateUI,更新textview显示识别结果。

  至此,我们就完成了从拍照到拿到识别结果的全部功能。再来识别一下公交卡


 
Android 图片文字识别DEMO(基于百度OCR)
公交卡识别.jpg

  嗯,不错。

最后

  这个demo实现了单纯的图片文字识别,就是把图片上的字读取了出来,在此之上我们可以进一步做很多有意思的事,比如用正则表达式把字符串里的一些字提取出来进行操作,像食品的营养成分表啊,发票单子啊,都可以拿来识别,将里面的有用信息提取出来,进行分析处理操作,将功能进一步扩展。
  当然,在实现这些功能之后,我们最后还是得感谢为我们提供轮子的大佬,哈哈。
  贴上本项目的github地址 reggie1996/CharacterRecognition



作者:reggie1996
链接:https://www.jianshu.com/p/0ed2c5656035
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐