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


    前言

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

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

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

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


     
    图片文字识别1

     
    图片文字识别2

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

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

    请求模块定义

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

     
    OCR Android SDK

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

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

      根据百度OCR官方给出的接口
     
    api请求说明.png

      我们定义出如下的接口方法。
     /**
         * 通过图片URL的形式,获取图片内的文字信息
         * @param accessToken 通过API Key和Secret Key获取的access_token
         * @param url 图片的url
         * @return observable对象用于rxjava,从RecognitionResultBean中可以获得图片文字识别的信息
         */
        

      在第一个方法中,我们需要传入两个参数,一个是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显示识别结果。

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


     
    公交卡识别.jpg

      嗯,不错。

    最后

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



    作者:reggie1996
    链接:https://www.jianshu.com/p/0ed2c5656035
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    一个简单XQuery查询的例子
    《Microsoft Sql server 2008 Internals》读书笔记第七章Special Storage(1)
    《Microsoft Sql server 2008 Internals》读书笔记第八章The Query Optimizer(4)
    《Microsoft Sql server 2008 Internal》读书笔记第七章Special Storage(4)
    SQL Server中SMO备份数据库进度条不显示?
    《Microsoft Sql server 2008 Internal》读书笔记第七章Special Storage(5)
    《Microsoft Sql server 2008 Internal》读书笔记第七章Special Storage(3)
    《Microsoft Sql server 2008 Internal》读书笔记第八章The Query Optimizer(2)
    省市三级联动的DropDownList+Ajax的三种框架(aspnet/Jquery/ExtJs)示例
    FireFox意外崩溃时的手工恢复命令
  • 原文地址:https://www.cnblogs.com/Alex80/p/13127293.html
Copyright © 2020-2023  润新知