前言
??OCR 是 Optical Character Recognition 的縮寫(xiě),翻譯為光學(xué)字符識(shí)別,指的是針對(duì)印刷體字符,采用光學(xué)的方式將紙質(zhì)文檔中的文字轉(zhuǎn)換成為黑白點(diǎn)陣的圖像文件,通過(guò)識(shí)別軟件將圖像中的文字轉(zhuǎn)換成文本格式,供文字處理軟件進(jìn)一步編輯加工的技術(shù)(好吧体啰,這是我查來(lái)的)。簡(jiǎn)單的來(lái)說(shuō)嗽仪,OCR技術(shù)就是可以把圖片上的文字識(shí)別出來(lái)荒勇,并以文本格式的形式提取出來(lái)。
??這個(gè)技術(shù)的應(yīng)用方面很廣泛闻坚,比如說(shuō)把紙質(zhì)書(shū)籍的內(nèi)容轉(zhuǎn)化為電子書(shū)沽翔,之前都需要人手打,但是現(xiàn)在只要掃描一下窿凤,將掃描出來(lái)的圖片通過(guò)OCR技術(shù)轉(zhuǎn)化成文本格式仅偎,效率和成本不知提升了多少倍。
??有人可能會(huì)想雳殊,這個(gè)技術(shù)聽(tīng)起來(lái)好高端橘沥,不懂計(jì)算機(jī)圖形學(xué),模式識(shí)別夯秃,機(jī)器學(xué)習(xí)巴拉巴拉東西的人座咆,是不是無(wú)法接觸到這樣的技術(shù),實(shí)現(xiàn)這樣的功能寝并,(好像是的箫措,emmm...)腹备,不過(guò)衬潦,雖然咱們自己實(shí)現(xiàn)不了,但是有人把輪子造好了呀植酥,我們只要使用人家造好的輪子镀岛,就能實(shí)現(xiàn)圖片文字識(shí)別的功能。
??先來(lái)看一下我實(shí)現(xiàn)的效果吧友驮,隨手拿了辦公桌上有字的東西(拿了枸杞茶和菊花茶的包裝...)漂羊,用自己寫(xiě)的demo拍了照識(shí)別了一下
??界面下方顯示的圖片是手機(jī)拍的照片,界面上方顯示的是從照片中識(shí)別出來(lái)的文字信息卸留∽咴剑可以看出識(shí)別的正確率還是挺高的。
??我這邊用的輪子是 百度文字識(shí)別 耻瑟。下面我將介紹一下我是如何實(shí)現(xiàn)上述的文字識(shí)別功能旨指。
請(qǐng)求模塊定義
??百度其實(shí)有提供圖片識(shí)別Android的SDK赏酥,就像其他的SDK一樣,只要導(dǎo)入一系列包之后就可以調(diào)用識(shí)別谆构。尋求快速開(kāi)發(fā)的小伙伴可以了解一下裸扶,我看了一下文檔,實(shí)現(xiàn)還是十分容易的搬素。
??但是呵晨,我在demo中使用的并非是SDK,而是使用另外一種方法——以網(wǎng)絡(luò)api的方式來(lái)進(jìn)行識(shí)別熬尺。涉及到的技術(shù)有 retrofit+rxjava 進(jìn)行網(wǎng)絡(luò)請(qǐng)求(在之前的一篇博客中有介紹如何使用 retrofit+rxjava 摸屠,貼一下鏈接),Android應(yīng)用動(dòng)態(tài)權(quán)限的申請(qǐng)猪杭,F(xiàn)ileProvider餐塘,圖片的base64轉(zhuǎn)碼,以及熱門(mén)的MVP框架皂吮。 ??先看一下項(xiàng)目結(jié)構(gòu)
??module目錄下存放的是MVP架構(gòu)的三個(gè)模塊戒傻,bean目錄下存放的是網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù)類(lèi)型,apiservice中存放的是retrofit有關(guān)網(wǎng)絡(luò)請(qǐng)求的接口蜂筹。
??我們定義出如下的接口方法需纳。
/**
* 通過(guò)圖片URL的形式,獲取圖片內(nèi)的文字信息
* @param accessToken 通過(guò)API Key和Secret Key獲取的access_token
* @param url 圖片的url
* @return observable對(duì)象用于rxjava,從RecognitionResultBean中可以獲得圖片文字識(shí)別的信息
*/
@POST("rest/2.0/ocr/v1/general_basic")
@FormUrlEncoded
Observable<RecognitionResultBean> getRecognitionResultByUrl(@Field("access_token") String accessToken, @Field("url") String url);
/**
* 通過(guò)圖片艺挪,獲取圖片內(nèi)的文字信息
* @param accessToken 通過(guò)API Key和Secret Key獲取的access_token
* @param image 圖像數(shù)據(jù)base64編碼后進(jìn)行urlencode后的String
* @return observable對(duì)象用于rxjava,從RecognitionResultBean中可以獲得圖片文字識(shí)別的信息
*/
@POST("rest/2.0/ocr/v1/general_basic")
@FormUrlEncoded
Observable<RecognitionResultBean> getRecognitionResultByImage(@Field("access_token") String accessToken, @Field("image") String image);
??在第一個(gè)方法中不翩,我們需要傳入兩個(gè)參數(shù),一個(gè)是access_token麻裳,需申請(qǐng)百度文字識(shí)別的開(kāi)發(fā)者資格口蝠,得到API key和Secret Key后獲取,還有一個(gè)參數(shù)是圖片的網(wǎng)絡(luò)地址url津坑,可以直接通過(guò)這個(gè)url直接訪(fǎng)問(wèn)到圖片妙蔗。
??第二個(gè)方法中,第一個(gè)參數(shù)也是同上的access_token疆瑰,第二個(gè)參數(shù)則是String類(lèi)型眉反,這個(gè)參數(shù)是在本地將圖片base64轉(zhuǎn)碼之后生成。
??因?yàn)槲覀円獙?shí)現(xiàn)的功能是用手機(jī)拍照穆役,然后將照片信息傳遞給服務(wù)器寸五,因此我們之后調(diào)用的是第二個(gè)方法(第一個(gè)方法只是我按照api說(shuō)明隨手寫(xiě)了一下),這些參數(shù)我們以POST的形式發(fā)送耿币,按照百度OCRapi 的要求梳杏,需要加上@FormUrlEncode注釋?zhuān)覀兪褂聾Field的方式將參數(shù)加入請(qǐng)求體。可以看到Observable中的是RecognitionResultBean類(lèi)型十性,我們可以從里面拿到服務(wù)器返回的文字識(shí)別信息叭莫。
??定義好這兩個(gè)方法之后,我們便可以構(gòu)造retrofit對(duì)象進(jìn)行調(diào)用
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://aip.baidubce.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
baiduOCRService = retrofit.create(BaiduOCRService.class);
??我們來(lái)看一下 rxjava+retrofit 在接口方法中的具體實(shí)現(xiàn)
@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() {
}
});
}
??可以看到傳入了一個(gè)Bitmap類(lèi)型的圖片參數(shù)烁试,這個(gè)參數(shù)經(jīng)過(guò) String encodeResult = bitmapToString(bitmap); 方法轉(zhuǎn)成了String類(lèi)型雇初。是因?yàn)榻涌谝蟮膮?shù)數(shù)據(jù)為String類(lèi)型,所以我們對(duì)圖片進(jìn)行了base64轉(zhuǎn)碼减响,具體的轉(zhuǎn)碼方法如下:
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);
}
??調(diào)用此方法靖诗,便可以把圖片類(lèi)型轉(zhuǎn)化成字符串類(lèi)型,之后的操作便是對(duì)網(wǎng)路接口調(diào)用之后的回調(diào)方法進(jìn)行定義支示,我們?cè)谡{(diào)用成功后的onNext操作中刊橘,拿到了RecognitionResultBean類(lèi)型參數(shù),這個(gè)參數(shù)里含有圖片所包含文字的信息颂鸿,我們將所有的文字一一取出促绵,用StringBuilder連接成一個(gè)字符串,返回給View層嘴纺,調(diào)用View層的updateUI進(jìn)行UI界面的更新败晴,對(duì)于這個(gè)字符串我們?cè)谥筮€可以進(jìn)行進(jìn)一步的分析操作。
??以上栽渴,百度OCR接口請(qǐng)求模塊定義部分便已完成尖坤,接下來(lái),我們要做的就是調(diào)用系統(tǒng)的相機(jī)功能闲擦,拍照得到照片慢味,將照片傳遞給我們上面定義的請(qǐng)求接口,進(jìn)行文字識(shí)別墅冷。
相機(jī)功能調(diào)用
??首先纯路,由于要對(duì)相機(jī)功能進(jìn)行調(diào)用,我們需要在A(yíng)ndroidManifest清單文件中寫(xiě)明我們需要用到的權(quán)限
<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"/>
??分別是網(wǎng)絡(luò)請(qǐng)求權(quán)限寞忿,數(shù)據(jù)的讀存取權(quán)限驰唬,以及相機(jī)權(quán)限。在A(yíng)ndroid 6.0 之前應(yīng)用的權(quán)限在安裝時(shí)全部授予罐脊,也就是說(shuō)只要在A(yíng)ndroidManifest中申請(qǐng)過(guò)的權(quán)限定嗓,都會(huì)給予蜕琴。而在 Android 6.0 或更高版本之后萍桌,對(duì)權(quán)限的管理作出了改變,對(duì)某些涉及到用戶(hù)隱私的權(quán)限可在運(yùn)行時(shí)根據(jù)用戶(hù)的需要?jiǎng)討B(tài)授予凌简,也就是說(shuō)上炎,在A(yíng)ndroidManifest中申請(qǐng)的權(quán)限,在用戶(hù)使用的過(guò)程中還得詢(xún)問(wèn)用戶(hù)是否給予,用戶(hù)給予權(quán)限了藕施,應(yīng)用才能進(jìn)行相關(guān)的權(quán)限操作寇损。因此我們需在代碼中增加動(dòng)態(tài)權(quán)限申請(qǐng)的模塊(對(duì)用戶(hù)安全性友好了,但是對(duì)開(kāi)發(fā)者增加了不友好度....)裳食,以下是動(dòng)態(tài)權(quán)限申請(qǐng)部分的代碼:
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;
}
}
??代碼的主要邏輯是矛市,在程序運(yùn)行的時(shí)候,檢查是否有相應(yīng)的權(quán)限诲祸,如果有權(quán)限浊吏,則可以進(jìn)行相關(guān)操作,如果沒(méi)有權(quán)限救氯,就調(diào)用申請(qǐng)權(quán)限的方法找田。在完成這部分代碼的編寫(xiě)之后還需重寫(xiě)onRequestPermissionsResult方法,對(duì)申請(qǐng)權(quán)限的結(jié)果進(jìn)行反應(yīng)着憨。
??接下來(lái)是調(diào)用相機(jī)功能的代碼
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);
}
??需要提到的是墩衙,在A(yíng)ndroid 7.0之后,如果你使用Intent攜帶這樣的上面的imageUri去打開(kāi)相機(jī)拍照甲抖,會(huì)拋出FileUriExposedException異常漆改。這時(shí)候就需要用到google官方的解決方案——FileProvider。使用的方法可以參照 Android 7.0適配-應(yīng)用之間共享文件
??調(diào)用相機(jī)之后我們需要重寫(xiě)onActivityResult方法准谚,對(duì)返回拍照結(jié)果進(jìn)行處理籽懦,
@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);
}
}
}
??如果拍照成功,我們就把照片作為參數(shù)傳遞給之前定義好的接口方法氛魁,調(diào)用進(jìn)行圖片文字識(shí)別暮顺。可以看到我還把照片放入imageview中方便與識(shí)別結(jié)果進(jìn)行對(duì)比秀存。等服務(wù)器成功返回識(shí)別結(jié)構(gòu)之后捶码,就會(huì)調(diào)用VIew層的updateUI,更新textview顯示識(shí)別結(jié)果或链。
??至此惫恼,我們就完成了從拍照到拿到識(shí)別結(jié)果的全部功能。再來(lái)識(shí)別一下公交卡
??嗯澳盐,不錯(cuò)祈纯。
最后
??這個(gè)demo實(shí)現(xiàn)了單純的圖片文字識(shí)別,就是把圖片上的字讀取了出來(lái)叼耙,在此之上我們可以進(jìn)一步做很多有意思的事腕窥,比如用正則表達(dá)式把字符串里的一些字提取出來(lái)進(jìn)行操作,像食品的營(yíng)養(yǎng)成分表啊筛婉,發(fā)票單子啊簇爆,都可以拿來(lái)識(shí)別,將里面的有用信息提取出來(lái),進(jìn)行分析處理操作入蛆,將功能進(jìn)一步擴(kuò)展响蓉。
??當(dāng)然,在實(shí)現(xiàn)這些功能之后哨毁,我們最后還是得感謝為我們提供輪子的大佬枫甲,哈哈。
??貼上本項(xiàng)目的github地址 reggie1996/CharacterRecognition