前言
OCR 是 Optical Character Recognition 的缩写,翻译为光学字符识别,指的是针对印刷体字符,采用光学的方式将纸质文档中的文字转换成为黑白点阵的图像文件,通过识别软件将图像中的文字转换成文本格式,供文字处理软件进一步编辑加工的技术(好吧,这是我查来的)。简单的来说,OCR技术就是可以把图片上的文字识别出来,并以文本格式的形式提取出来。
这个技术的应用方面很广泛,比如说把纸质书籍的内容转化为电子书,之前都需要人手打,但是现在只要扫描一下,将扫描出来的图片通过OCR技术转化成文本格式,效率和成本不知提升了多少倍。
有人可能会想,这个技术听起来好高端,不懂计算机图形学,模式识别,机器学习巴拉巴拉东西的人,是不是无法接触到这样的技术,实现这样的功能,(好像是的,emmm...),不过,虽然咱们自己实现不了,但是有人把轮子造好了呀,我们只要使用人家造好的轮子,就能实现图片文字识别的功能。
先来看一下我实现的效果吧,随手拿了办公桌上有字的东西(拿了枸杞茶和菊花茶的包装...),用自己写的demo拍了照识别了一下
界面下方显示的图片是手机拍的照片,界面上方显示的是从照片中识别出来的文字信息。可以看出识别的正确率还是挺高的。
我这边用的轮子是 百度文字识别 。下面我将介绍一下我是如何实现上述的文字识别功能。
请求模块定义
百度其实有提供图片识别Android的SDK,就像其他的SDK一样,只要导入一系列包之后就可以调用识别。寻求快速开发的小伙伴可以了解一下,我看了一下文档,实现还是十分容易的。
但是,我在demo中使用的并非是SDK,而是使用另外一种方法——以网络api的方式来进行识别。涉及到的技术有 retrofit+rxjava 进行网络请求(在之前的一篇博客中有介绍如何使用 retrofit+rxjava ,贴一下链接),Android应用动态权限的申请,FileProvider,图片的base64转码,以及热门的MVP框架。 先看一下项目结构
module目录下存放的是MVP架构的三个模块,bean目录下存放的是网络请求返回的数据类型,apiservice中存放的是retrofit有关网络请求的接口。
我们定义出如下的接口方法。
/**
* 通过图片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显示识别结果。
至此,我们就完成了从拍照到拿到识别结果的全部功能。再来识别一下公交卡
嗯,不错。
最后
这个demo实现了单纯的图片文字识别,就是把图片上的字读取了出来,在此之上我们可以进一步做很多有意思的事,比如用正则表达式把字符串里的一些字提取出来进行操作,像食品的营养成分表啊,发票单子啊,都可以拿来识别,将里面的有用信息提取出来,进行分析处理操作,将功能进一步扩展。
当然,在实现这些功能之后,我们最后还是得感谢为我们提供轮子的大佬,哈哈。
贴上本项目的github地址 reggie1996/CharacterRecognition