身份證識別:https://github.com/wenchaosong/OCR_identifyhtml
遇到一個需求,要用手機掃描紙質面單,獲取面單上的手機號,最後決定用tesseract這個開源OCR庫,移植到Android平臺是tess-twojava
Android平臺tess-two地址:https://github.com/tesseract-ocrandroid
本文Demo地址:http://blog.csdn.net/mr_sk/article/details/79077271git
評論裏有人想要我訓練的數字字庫,這裏貼出來(只訓練了 黑體、微軟雅黑、宋體 0-9的數字,其餘字體識別率會下降)
數字字庫地址:http://download.csdn.net/download/mr_sk/10186145 (如今上傳資源好像不能免費下載了,至少要收兩個積分….)github
這篇博客主要是記錄個人思路,大可能是散亂的筆記,因此你們遇到報錯什麼的不要急,看看Log總能找到問題,接下來我也準備寫一個library,直接封裝好 手機號掃描、身份證掃描、郵箱掃描等,寫好後我會更新算法
Tesseract雖然是個很強大的庫,但直接使用的話,並不適用於連續識別的需求,由於tess-two對解析圖像的清晰度和文字規範度有很高的要求,用相機隨便獲取的一張預覽圖掃出來錯誤率很是高(若是用電腦截圖文字區域,識別很高),手寫的就更不用說了,幾乎全是亂碼,並且識別速度很慢,一張200*300的圖片都要好幾秒數組
因此在沒有優化的狀況下,直接用tess-two 來做文字識別,只能是拍一張照,而後等待識別結果,好比識別文章、掃描身份證等,若是像個人需求,須要識別面單上的手機號,可能一分鐘須要掃描幾十個手機號,那就必需要達到毫秒級的解析速度,直接使用常規的方法確定是不行的,那怎麼辦呢?markdown
tess-two的識別算法固然是沒辦法處理了,那就得從其餘方面去想辦法網絡
第一個:是在字庫方面,官方的一個英文字庫 30M,可是你面臨的需求須要這麼重量級的字庫嗎?好比我掃描手機號的功能,面單上都是黑體字,手機號只有純數字, 就這麼點識別範圍去檢索一個30M的字庫,顯然多了不少無用功
解決辦法就是:
訓練本身的字庫,若是你須要毫秒級的掃描速度,那你的需求涉及的掃描內容 範圍必定很小(前面說過,若是你要作文章識別之類的,那就用官方字庫,拍一張照片,等幾秒鐘,徹底是能夠接受的),這樣就能夠根據需求範圍內 常見的 」字體「 和 」字符「來訓練專門的字庫,這樣你就能使用一個輕量級的定製字庫,極大的減小了解析時間,好比我手機號的數字子庫,只有100KB,識別我處理後的圖片,從官方字庫的1.5-3秒,減小到了300-500msapp
第二個: 就是在把圖片交給tess-two解析以前,先進行簡單的內容過濾,如上面所說的,即使是我把一張圖片的解析速度壓縮到了300-500ms,依然存在一個問題,那就是識別頻率,要作連續掃描,相機確定是一直開着的,那一秒鐘幾十幀的圖片,你該解析哪一張呢?
每一張都解析的話,對性能是很大的消耗,也要考慮一些用低端機的用戶,並且每次解析的時間不等,識別結果也很混亂,那就只有每次取一幀解析,拿到解析結果後,再去解析下一幀那麼問題又來了:相機一秒幾十幀,一打開相機,第一幀就開始解析了,這樣下一次開始解析就在300-500ms以後了,若是用戶在對準手機號的前一刻,正好開始了一幀畫面的解析,那等到開始解析手機號,至少也在幾百毫秒之後了,加上手機號自己的解析時間,從對準到拿到結果,隨隨便便就超過了1秒,加上每次識別速度不定,可能特殊狀況耗時更久,這樣必然會感到很明顯的延遲,那該怎麼處理呢?
解決辦法就是:
在圖片交給tess-two以前,先進行圖片二級裁切,第一次裁切就是利用界面的掃描框,拿到須要掃描的區域,而後進行內容過濾,把明顯不可能包含手機號的圖像直接忽略,不進行解析,這個過程須要遍歷圖片的像素,用jni處理時間不超過10ms,即使是用java處理,也只有10-50ms,只要能忽略大部分的無用的圖像,那就解決了這個延遲的問題,而且在過濾的同時,若是被判斷爲有用圖片,那就能同時拿到須要解析的文字塊,而後進行第二次裁切,拿到更小的圖片,進一步提高解析速度至於過濾的方式,我寫了針對手機號的過濾,在文章最下面的單行文本優化方案部分,有類似需求的能夠看看,而後針對本身的需求,來寫過濾算法
至於最後掃描的內容的提取,能夠用正則公式來篩選關鍵信息如:手機號、網址、郵箱、身份證、銀行卡號 等
圖一
圖二
圖三
圖四
圖五
圖一:是掃描線沒有對準手機號碼,未捕捉到手機號的狀態,這種狀態下,每一幀都會在10-30ms以內被肯定掃描線沒有對準一個手機號而被過濾掉,不交給tess-two解析,直接放棄這一幀數據
圖二:是掃描線對準了手機號,通過過濾算法後,捕捉到一個包含11位字符的蚊子塊,基本確認存在手機號
圖三:是 圖二 狀態下的識別結果
圖四:是被水印干擾的手機號所獲得的二值化圖片
圖五:是清除水印後取到的手機號區域(只適用於圖五這種文字底部的干擾)
這裏是基本用法,我最先寫的,效率不高但代碼易讀,是tess-two的使用方法,識別仍是有明顯延遲,優化方案我放在了文章後面的優化部分,Demo也更新了最新的優化方案,若是對這方面比較熟練,能夠從後面開始看,這裏由簡入繁
集成很簡單,build.gradle中加入:
compile ‘com.rmtheis:tess-two:6.0.0’
//後面我已經換到8.0.0,上傳的demo是在6.0.0下運行的
compile ‘com.rmtheis:tess-two:8.0.0’
編譯一下,框架的集成就ok了,不過tess-two的文字庫是須要另外下載的,咱們通常只須要中文和英文兩種就能夠了,特殊需求能夠本身訓練
字體庫下載地址:https://github.com/tesseract-ocr/tessdata
英文:eng.traineddata
簡體中文:chi_sim.traineddata
將這兩個字體庫文件,放到sd卡,路徑必須爲 **/tessdata/
路徑爲何必定要爲**/tessdata/呢?在TessBaseApi類的初始化方法中會檢查你的文字庫目錄,代碼以下
/** * datapath是你傳入的文字庫路徑,能夠看到這裏在傳入的datapath後加了一個"tessdata"目錄 * 而後驗證了這個目錄是否存在,若是不在,就會報錯"數據目錄必須包含tessdata目錄" */ File tessdata = new File(datapath + "tessdata"); //tessdata是否存在且是個目錄 if (!tessdata.exists() || !tessdata.isDirectory()) throw new IllegalArgumentException("Data path must contain subfolder tessdata!");
而後就是使用了,這裏個人字體庫文件都放在 「根目錄/Download/tessdata「中
解析圖片代碼以下:
public class OcrUtil { //字體庫路徑,此路徑下必須包含tessdata文件夾,但不用把tessdata寫上 static final String TESSBASE_PATH = Environment.getExternalStorageDirectory() + File.separator + "Download" + File.separator; //英文 static final String ENGLISH_LANGUAGE = "eng"; //簡體中文 static final String CHINESE_LANGUAGE = "chi_sim"; /** * 識別英文 * * @param bmp 須要識別的圖片 * @param callBack 結果回調(攜帶一個String 參數便可) */ public static void ScanEnglish(final Bitmap bmp, final MyCallBack callBack) { new Thread(new Runnable() { @Override public void run() { TessBaseAPI baseApi = new TessBaseAPI(); //初始化OCR的字體數據,TESSBASE_PATH爲路徑,ENGLISH_LANGUAGE指明要用的字體庫(不用加後綴) if (baseApi.init(TESSBASE_PATH, ENGLISH_LANGUAGE)) { //設置識別模式 baseApi.setPageSegMode(TessBaseAPI.PageSegMode.PSM_AUTO); //設置要識別的圖片 baseApi.setImage(bmp); //開始識別 String result = baseApi.getUTF8Text(); baseApi.clear(); baseApi.end(); callBack.response(result); } } }).start(); } }
好了,識別工具寫好了,接下要作的就是,打開相機、獲取預覽圖、裁切出須要的區域,而後交給tess-two識別,這裏我直接吧SurfaceView封裝了一下,自動打開相機開始預覽,下面是掃描手機號的代碼:
public class CameraView extends SurfaceView implements SurfaceHolder.Callback, Camera.PreviewCallback { private final String TAG = "CameraView"; private SurfaceHolder mHolder; private Camera mCamera; private boolean isPreviewOn; //默認預覽尺寸 private int imageWidth = 1920; private int imageHeight = 1080; //幀率 private int frameRate = 30; public CameraView(Context context) { super(context); init(); } public CameraView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mHolder = getHolder(); //設置SurfaceView 的SurfaceHolder的回調函數 mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } @Override public void surfaceCreated(SurfaceHolder holder) { //Surface建立時開啓Camera openCamera(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //設置Camera基本參數 if (mCamera != null) initCameraParams(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { try { release(); } catch (Exception e) { } } private boolean isScanning = false; /** * Camera幀數據回調用 */ @Override public void onPreviewFrame(byte[] data, Camera camera) { //識別中不處理其餘幀數據 if (!isScanning) { isScanning = true; new Thread(new Runnable() { @Override public void run() { try { //獲取Camera預覽尺寸 Camera.Size size = camera.getParameters().getPreviewSize(); //將幀數據轉爲bitmap YuvImage image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null); if (image != null) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); //將幀數據轉爲圖片(new Rect()是定義一個矩形提取區域,我這裏是提取了整張圖片,而後旋轉90度後再才裁切出須要的區域,效率會較慢,實際使用的時候,照片默認橫向的,能夠直接計算逆向90°時,left、top的值,而後直接提取須要區域,提出來以後再壓縮、旋轉 速度會快一些) image.compressToJpeg(new Rect(0, 0, size.width, size.height), 80, stream); Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); //這裏返回的照片默認橫向的,先將圖片旋轉90度 bmp = rotateToDegrees(bmp, 90); //而後裁切出須要的區域,具體區域要和UI佈局中配合,這裏取圖片正中間,寬度取圖片的一半,高度這裏用的適配數據,能夠自定義 bmp = bitmapCrop(bmp, bmp.getWidth() / 4, bmp.getHeight() / 2 - (int) getResources().getDimension(R.dimen.x25), bmp.getWidth() / 2, (int) getResources().getDimension(R.dimen.x50)); if (bmp == null) return; //將裁切的圖片顯示出來(測試用,須要爲CameraView setTag(ImageView)) ImageView imageView = (ImageView) getTag(); imageView.setImageBitmap(bmp); stream.close(); //開始識別 OcrUtil.ScanEnglish(bmp, new MyCallBack() { @Override public void response(String result) { //這是區域內掃除的全部內容 Log.d("scantest", "掃描結果: " + result); //檢索結果中是否包含手機號 Log.d("scantest", "手機號碼: " + getTelnum(result)); isScanning = false; } }); } } catch (Exception ex) { isScanning = false; } }).start(); } } /** * 獲取字符串中的手機號 */ public String getTelnum(String sParam) { if (sParam.length() <= 0) return ""; Pattern pattern = Pattern.compile("(1|861)(3|5|8)\\d{9}$*"); Matcher matcher = pattern.matcher(sParam); StringBuffer bf = new StringBuffer(); while (matcher.find()) { bf.append(matcher.group()).append(","); } int len = bf.length(); if (len > 0) { bf.deleteCharAt(len - 1); } return bf.toString(); } /** * Bitmap裁剪 * * @param bitmap 原圖 * @param width 寬 * @param height 高 */ public static Bitmap bitmapCrop(Bitmap bitmap, int left, int top, int width, int height) { if (null == bitmap || width <= 0 || height < 0) { return null; } int widthOrg = bitmap.getWidth(); int heightOrg = bitmap.getHeight(); if (widthOrg >= width && heightOrg >= height) { try { bitmap = Bitmap.createBitmap(bitmap, left, top, width, height); } catch (Exception e) { return null; } } return bitmap; } /** * 圖片旋轉 * * @param tmpBitmap * @param degrees * @return */ public static Bitmap rotateToDegrees(Bitmap tmpBitmap, float degrees) { Matrix matrix = new Matrix(); matrix.reset(); matrix.setRotate(degrees); return Bitmap.createBitmap(tmpBitmap, 0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight(), matrix, true); } /** * 攝像頭配置 */ public void initCameraParams() { stopPreview(); //獲取camera參數 Camera.Parameters camParams = mCamera.getParameters(); List<Camera.Size> sizes = camParams.getSupportedPreviewSizes(); //肯定前面定義的預覽寬高是camera支持的,不支持取就更大的 for (int i = 0; i < sizes.size(); i++) { if ((sizes.get(i).width >= imageWidth && sizes.get(i).height >= imageHeight) || i == sizes.size() - 1) { imageWidth = sizes.get(i).width; imageHeight = sizes.get(i).height; // break; } } //設置最終肯定的預覽大小 camParams.setPreviewSize(imageWidth, imageHeight); //設置幀率 camParams.setPreviewFrameRate(frameRate); //啓用參數 mCamera.setParameters(camParams); mCamera.setDisplayOrientation(90); //開始預覽 startPreview(); } /** * 開始預覽 */ public void startPreview() { try { mCamera.setPreviewCallback(this); mCamera.setPreviewDisplay(mHolder);//set the surface to be used for live preview mCamera.startPreview(); mCamera.autoFocus(autoFocusCB); } catch (IOException e) { mCamera.release(); mCamera = null; } } /** * 中止預覽 */ public void stopPreview() { if (mCamera != null) { mCamera.setPreviewCallback(null); mCamera.stopPreview(); } } /** * 打開指定攝像頭 */ public void openCamera() { Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); cameraId++) { Camera.getCameraInfo(cameraId, cameraInfo); if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { try { mCamera = Camera.open(cameraId); } catch (Exception e) { if (mCamera != null) { mCamera.release(); mCamera = null; } } break; } } } /** * 攝像頭自動聚焦 */ Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() { public void onAutoFocus(boolean success, Camera camera) { postDelayed(doAutoFocus, 1000); } }; private Runnable doAutoFocus = new Runnable() { public void run() { if (mCamera != null) { try { mCamera.autoFocus(autoFocusCB); } catch (Exception e) { } } } }; /** * 釋放 */ public void release() {