Android OCR文字識別 實時掃描手機號(極速掃描單行文本方案)

身份證識別: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

字庫訓練 詳情參考http://www.javashuo.com/article/p-vqltyuob-hq.html


第二個: 就是在把圖片交給tess-two解析以前,先進行簡單的內容過濾,如上面所說的,即使是我把一張圖片的解析速度壓縮到了300-500ms,依然存在一個問題,那就是識別頻率,要作連續掃描,相機確定是一直開着的,那一秒鐘幾十幀的圖片,你該解析哪一張呢? 
每一張都解析的話,對性能是很大的消耗,也要考慮一些用低端機的用戶,並且每次解析的時間不等,識別結果也很混亂,那就只有每次取一幀解析,拿到解析結果後,再去解析下一幀

那麼問題又來了:相機一秒幾十幀,一打開相機,第一幀就開始解析了,這樣下一次開始解析就在300-500ms以後了,若是用戶在對準手機號的前一刻,正好開始了一幀畫面的解析,那等到開始解析手機號,至少也在幾百毫秒之後了,加上手機號自己的解析時間,從對準到拿到結果,隨隨便便就超過了1秒,加上每次識別速度不定,可能特殊狀況耗時更久,這樣必然會感到很明顯的延遲,那該怎麼處理呢? 
解決辦法就是: 
在圖片交給tess-two以前,先進行圖片二級裁切,第一次裁切就是利用界面的掃描框,拿到須要掃描的區域,而後進行內容過濾,把明顯不可能包含手機號的圖像直接忽略,不進行解析,這個過程須要遍歷圖片的像素,用jni處理時間不超過10ms,即使是用java處理,也只有10-50ms,只要能忽略大部分的無用的圖像,那就解決了這個延遲的問題,而且在過濾的同時,若是被判斷爲有用圖片,那就能同時拿到須要解析的文字塊,而後進行第二次裁切,拿到更小的圖片,進一步提高解析速度

至於過濾的方式,我寫了針對手機號的過濾,在文章最下面的單行文本優化方案部分,有類似需求的能夠看看,而後針對本身的需求,來寫過濾算法


至於最後掃描的內容的提取,能夠用正則公式來篩選關鍵信息如:手機號、網址、郵箱、身份證、銀行卡號 等

Demo截圖

圖一

這裏寫圖片描述

圖二

這裏寫圖片描述

圖三

這裏寫圖片描述

水印清除

圖四

這裏寫圖片描述

圖五

這裏寫圖片描述


圖一:是掃描線沒有對準手機號碼,未捕捉到手機號的狀態,這種狀態下,每一幀都會在10-30ms以內被肯定掃描線沒有對準一個手機號而被過濾掉,不交給tess-two解析,直接放棄這一幀數據

圖二:是掃描線對準了手機號,通過過濾算法後,捕捉到一個包含11位字符的蚊子塊,基本確認存在手機號

圖三:是 圖二 狀態下的識別結果

圖四:是被水印干擾的手機號所獲得的二值化圖片

圖五:是清除水印後取到的手機號區域(只適用於圖五這種文字底部的干擾)

tess-two基本使用

這裏是基本用法,我最先寫的,效率不高但代碼易讀,是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!");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

而後就是使用了,這裏個人字體庫文件都放在 「根目錄/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(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

好了,識別工具寫好了,接下要作的就是,打開相機、獲取預覽圖、裁切出須要的區域,而後交給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() { if (isPreviewOn && mCamera != null) { isPreviewOn = false; mCamera.setPreviewCallback(null); mCamera.stopPreview(); mCamera.release(); mCamera = null; } } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276

佈局文件:

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal"> <!--相機預覽窗口,上面設置的預覽大小是1080x1920,爲保證比例,記得Activity的style不要加ActionBar,保證全屏顯示--> <test.com.ocrtest.CameraView android:id="@+id/main_camera" android:layout_width="match_parent" android:layout_height="match_parent" /> <!--掃描框,和上面裁切規則同樣,寬度爲屏幕的一半,高度對應上面的x50(1080P分辨率下爲168px)--> <TextView android:layout_width="@dimen/x160" android:layout_height="@dimen/x50" android:layout_centerInParent="true" android:background="@drawable/fillet_gray_border_btn" /> <!--顯示被裁切出的圖片,須要setTag到CameraView中,詳見上面CameraView代碼--> <ImageView android:id="@+id/main_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" /> </RelativeLayout> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

更新

對於文字識別這塊,我以後還嘗試了幾種方案,這裏列舉一下

一、tess-two

適用場景:小區域連續掃描解析 (好比識別手機號、單詞 等)

優勢:免費開源、本地解析、英文數字識別率可觀

缺點:識別速度慢、須要作大量優化(下面我會貼出我針對本身的項目作出的一些優化,避免解析大部分無心義的畫面,二值化提升識別率等)


二、各個平臺的OCR API,好比百度、騰訊、合合信息 等

適用場景:識別頻率不高、須要識別大圖(好比拍一張照,點確認,拿到結果,就OK了 像身份證 銀行卡識別)

優勢:識別率高

缺點: 收費(費用不高)、解析速度太依賴網絡質量、無本地解析SDK,須要上傳圖片而後獲取解析結果,由於不能每一幀都上傳解析,因此不能用做連續掃描 
我以前嘗試過百度ocr,方案是給用戶一個按鈕,用戶點擊以後,取相機最近的一幀照片上傳給百度,而後跳過其餘幀,等待用戶下一次點擊解析按鈕。經過壓縮裁切圖片,我已經把圖片壓縮到10+kb、在網速良好的狀況下,解析速度能達到0.5秒,但若是網速很差,體驗急劇降低


三、有一些平臺提供本地Ocr解析的SDK(好比合合信息)

適用場景:大部分需求都能實現

優勢: 解析速度快、識別率高

缺點: 費用奇高 -_-(企業合做級別的費用)


對於tess-two的進一步優化(這裏針對個人需求,只識別單行手機號):

文章開頭說過了,提升效率最重要的就是訓練出爲本身需求量身定作的字庫,我須要識別的面單上的手機號,所有是黑體的數字,那我就針對「黑體 數字」來訓練個人字庫,我訓練出來的字庫大小100+KB,識別優化後的手機號圖片,只要300-500ms,再過濾掉大部分無心義圖像,就能夠實現連續掃描,而官方的包識別至少1.5-3秒,若是再沒法過濾無心義圖像,那識別一個手機號10秒鐘能搞定你就謝天謝地了

訓練方法在文章開頭有連接,至於訓練用的模板圖片,文章最下面的優化代碼中,把最終取到的圖像保存下來去訓練就行了

對於把圖片交給tess-two以前的優化 
主要包括:減少圖片的尺寸大小、二值化圖片使文字黑白分明、判斷圖片內容是否無心義

一、裁切圖片

根據上面文章的代碼,是先把一幀的數據轉爲圖片,而後旋轉90°,而後根據掃描框在界面上的位置,裁切出須要的區域,以下

ByteArrayOutputStream stream = new ByteArrayOutputStream(); //將幀數據轉爲圖片 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));
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

那麼問題就在裁切和旋轉的時候了,假如相機一幀的像素是1920*1080,而我須要的只是掃描框內的一點內容,把一整張圖片都提取出來,加上旋轉這張大圖片,而後再裁切,無疑浪費了不少時間

解決辦法:

直接計算逆向90°狀況下的提取區域 
上邊裁切範圍是:屏幕正中間、寬度爲屏幕的一半、高度爲R.dimen.x50 的一個矩形 
那麼矩形的位置就取圖片正中間 
left=bmp.getWidth() / 4 
top=bmp.getHeight()/ 2 - R.dimen.x25 
width=bmp.getWidth() / 2 
height= R.dimen.x50

若是逆向90°,長寬倒轉,矩形的位置就變成 
left=bmp.getWidth()/ 2 - R.dimen.x25 (原來的top) 
top=bmp.getHeight()/4 (原來的right) 
width=R.dimen.x50 (原來的height) 
height= bmp.getHeight()/2 (原來的width)

ByteArrayOutputStream stream = new ByteArrayOutputStream(); 
image.compressToJpeg(new Rect(left , top, left+width, top+height), 80, 
stream); 
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); 
這樣直接提取須要的區域,就節省了整張圖片旋轉和第二次裁切的時間

二、旋轉、二值化 圖片,過濾無用內容

接下來的旋轉和二值化,是純像素算法,若是能放在jni中實現更好,通過我測試效率會快好幾倍(Java大概10-50ms,Jni基本在10ms如下,雖然幾十毫秒的時間差,跟tess-two的解析時間比,效果不明顯),這裏仍是用Java來表現邏輯

上面已經直接提取出了須要解析的矩形區域,接下來只須要旋轉一張像素小了不少倍的圖片 仍是上面文章中的方法
rotateToDegrees(bmp, 90)

旋轉以後,就是一張方向正確的識別區域了,如今須要作的就是二值化,將圖片變爲黑白兩色,提升識別率(由於要遍歷全部的像素,爲了節省時間,在二值化的同時,同步進行無用內容過濾

無用內容過濾: 
如文章開頭介紹,在相機打開以後,每一秒都有幾十幀數據,何時解析呢?這裏我作出了一些過濾

(下面的過濾算法,只適用於和個人需求相似的場景(掃描手機號、單行文本))

怎麼過濾呢?先來想一想場景,什麼樣的圖片能夠認爲圖中可能有手機號呢?

第一:手機號完整的在矩形區域內,不會有超出矩形區域的部分,也就是說手機號部分不會有貼邊的像素

第二:若是要掃描手機號,確定會將手機號至少填充掃描框的50%高度(這個比例本身掌握,看你的掃描距離,我後來減到了10%,捕捉手機號依然很準確) 
有了這兩個條件,就有了判斷標準,圖片中必須有 上下左右沒有貼邊,且高度大於50%的有色區域,才能初步判斷圖中可能存在手機號碼

而後我就實現方式,個人思路是: 
這裏實現一個單行文字捕捉,首先準備 left、top、right、bottom 四個變量,就是最終須要的單行文字區域

一、先黑白化圖片,這個過程須要遍歷像素,在遍歷期間,同時來作過濾,這裏遍歷是一行一行的,因此在第一次遍歷中,能判斷文字行數:好比在遍歷某一行的像素時,只要發現一個黑色像素,說明這一行不是空行,那就記錄一下這裏已經有文字佔了一行像素,下一行若是仍是找到黑色像素,那就把當前記錄的文字加一行像素高度,直到某一行所有是白色像素,說明這一行文字結束了,下面再有黑色像素就算是第二行文字了

二、若是第一行像素就發現了黑色像素點,說明這行文字是貼着文字上邊緣的,八成是隻露出了一半的文字,確定不是解析對象,那就不用記錄他,直到遇到一行全是白色像素,表示這行貼邊的文字結束了,接下來的文字就要開始記錄了(沒錯,若是有一條豎着的黑線,從上貫穿到下,那這個圖片確定被認爲全是貼邊文字,直接過濾掉,個人識別環境不會有這個狀況,因此沒有作更細緻的過濾,須要判斷這種狀況的,本身寫算法 -_-)

三、每一行文字記錄結束都跟上一行文字比較,選高度更高的一行文字留下,其餘的跳過(前面說了這裏是單行識別,只選沒有貼邊的文字最高的一行),等遍歷結束,最高的一行的top 和 bottom留下,就獲得的解析對象的上下邊緣

四、須要留意的是,上一個過濾貼邊文字的條件,只過濾了超出上邊緣的文字,那超出下邊緣的文字呢?很簡單,每行文字記錄完成後纔會和上一行比較,就是說每次遇到一整行白色像素的空白行時,纔會更新top和bottom,若是最後一行貼邊了那就不會再遇到空白行,自動就放棄了

下面在代碼中解釋細節

/** * 轉爲二值圖像 並判斷圖像中是否可能有手機號 * * @param bmp 原圖bitmap * @param tmp 二值化閾值 超出閾值的像素置爲白色,不然爲黑色 * @return */ public Bitmap convertToBMW(final Bitmap bmp, int tmp) { int width = bmp.getWidth(); // 獲取位圖的寬 int height = bmp.getHeight(); // 獲取位圖的高 int[] pixels = new int[width * height]; // 經過位圖的大小建立像素點數組 bmp.getPixels(pixels, 0, width, 0, 0, width, height);//獲得圖片的全部像素 int lineHeight = 0;//當前記錄的一行文字已經累計的高度,每次遇到一行有黑色像素點時 +1 //目標行,每遇到一個黑色像素,就會+1,本行就不會在記錄lineHeight,下一行在遇到黑色像素,就繼續+1,保證每行lineHeight最多 +1 一次 int row = 0; //當前記錄的一行文字是否超出邊緣(若是第一行就發現黑色像素,就爲true了,直到遇到空白行,還原false) boolean isOutOfRect= false; //最終捕捉到的單行文字在圖片中的矩形區域 int left = 0; int top = 0; int right = 0; int bottom = 0; int alpha = 0xFF << 24; for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { int grey = pixels[width * i + j]; // 分離三原色 alpha = ((grey & 0xFF000000) >> 24); int red = ((grey & 0x00FF0000) >> 16); int green = ((grey & 0x0000FF00) >> 8); int blue = (grey & 0x000000FF); if (red > tmp) { red = 255; } else { red = 0; } if (blue > tmp) { blue = 255; } else { blue = 0; } if (green > tmp) { green = 255; } else { green = 0; } pixels[width * i + j] = alpha << 24 | red << 16 | green << 8 | blue; //這裏是二值化化的判斷,if裏是白色,else裏是黑色 if (pixels[width * i + j] == -1 || (i == height - 1 && j == width - 1)) { //將當前像素賦值爲白色 pixels[width * i + j] = -1; /** lineHeight>0 : 若是當前記錄行的文字高度大於0 row == i : 當前是否是目標行,每行第一次發現黑色像素就會+1,因此只有當前行還沒出現黑色像素時,纔會 == i j == width - 1 : 當前像素是否是本行的最後一個像素點 綜上所述,這裏的判斷條件爲 : 已經捕捉到一行文字,並且這一行已經結束了還沒發現黑色像素,這行文字該結束了 */ if (lineHeight > 0 && row == i && j == width - 1) { //這行文字是否是超出邊緣的文字,若是是,直接跳過,開始記錄下一行 if (!isOutOfBorder) { //跟上一行的文字高度比較,記錄下高度更高的一行文字的top 和 bottom int h = bottom - top; if (lineHeight > h) { //這裏我把top 和 bottom 都加了1/4的行高,爲了有一點留白,其實加不加無所謂 top = i - lineHeight - (lineHeight / 4); bottom = i - 1 + (lineHeight / 4); } } //這行文字既然已經結束了,下一行文字確定不是超出邊緣的了 isOutOfRect= false; //上一行文字已經處理完成,行高歸0,開始記錄下一行 lineHeight = 0; } } else { //這裏是黑色像素,將當前像素點賦值爲黑色 pixels[width * i + j] = -16777216; //若是當前行 = 目標行(遇到這行第一個黑色像素就會+1,到下一行纔會相等) if (i >= row) { //若是當前的黑色像素 位於第一行像素 或 最後一行像素,那就是超出邊緣的文字 if (i == 0 || i == height - 1) isOutOfRect= true; //行高+1 lineHeight++; //目標行轉移到下一行 row = i + 1; } } } } /** 若是經過第一次過濾後,沒有找到一行有意義的文字,或者找到了,文字高度佔比還不到解析圖片的20%, 那這張圖片八成是無心義的圖片,不用解析,直接下一幀(當你對着牆或者什麼無聊的東西掃描的時候, 這裏就會直接結束,不會浪費時間去作文字識別) */ if (bottom - top < height * 0.2f) { isScanning = false; return null; } /** 到這裏,上面的篩選已經經過了,咱們已經定位到了一行目標文字的 top 和 bottom 接下來就要定位left 和 right 了 仍是須要遍歷一次,不過只須要 top-bottom 正中間的一行像素,思路同上,經過文字間距 來將這一行文字分紅橫向的幾個文字塊,至於區分條件,就看文字間的間隔,超過正常寬度就 算是一個文字塊的結束,至於正常的文字間隔就要按需求而定了,好比這裏掃描手機號,手機 號是11位的,那兩個數字之間的距離說破天也不會超過圖片寬度的 1/11,那我就定爲1/11 那問題又來了,若是恰好手機號在這塊圖像右邊的上半部分,下半部分是在手機號左邊的無用文字, 只是由於高度重疊,上面取行高時被當成了一行,那這裏只取top-bottom正中間的一條像素, 遍歷到手機號所在的右邊一半時,不是隻能找到空白像素? 這就沒辦法了,只取一條像素行,一是爲了減小耗時,二是讓個人腦細胞少死一點,你要掃描手機號, 還非要把手機號完美躲開正中間,那我就無論了..... */ //文字間隔,每次遇到白色像素點+1,每次遇到黑色像素點歸0,當space > 寬度的1/11時,就算超過正常文字間距了 int space = 0; //當前文字塊寬度,每當遇黑色像素點時,更新寬度,space 超過寬度的1/11時,歸0,文字塊結束 int textWidth = 0; //當前文字開始X座標,文字塊寬度 = 結束點 - startX int startX = 0; //遍歷top-bottom 正中間一行像素 for (int j = 0; j < width; j++) { //若是是白色像素 if (pixels[width * (top + (bottom - top) / 2) + j] == -1) { /** 若是已經捕捉到了文字塊,並且space > width / 11 或者已經遍歷結束了, 那這個文字塊的寬度就取到了 */ if (textWidth > 0 && (space > width / 11 || j == width - 1)) { //同高度同樣,比較上一個文字塊的寬度,留下最大的一個 top 和 bottom if (textWidth > right - left) { //這裏取left 和 right 同樣加了一個space/2的留白 left = j - space - textWidth - (space / 2); right = j - 1; } //既然當前文字塊已經結束,就把參數重置,繼續捕捉下一個文字塊 space = 0; startX = 0; } space++; } else { //這裏是黑色像素 //記錄文字塊的開始X座標 if (startX == 0) startX = j; //文字塊當前寬度 textWidth = j - startX; //文字間隔歸0 space = 0; } } //若是最終捕捉到的文字塊,寬度還不到圖片寬度的30%,一樣跳過,八成不是手機號,就不要浪費時間識別了 if (right - left < width * 0.3f) { isScanning = false; return null; } /** 到這裏 已經捕捉到了一個極可能是手機號碼的文字塊,區域就是 left、top、right、bottom 把這個區域的像素,取出來放到一個新的像素數組 */ int targetWidth = right - left; int targetHeight = bottom - top; int[] targetPixels = new int[targetWidth * targetHeight]; int index = 0; for (int i = top; i < bottom; i++) { for (int j = left; j < right; j++) { if (index < targetPixels.length) targetPixels[index] = pixels[width * i + j]; index++; } } //銷燬以前的圖片 bmp.recycle(); // 新建圖片 final Bitmap newBmp = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); //把捕捉到的圖塊,寫進新的bitmap中 newBmp.setPixels(targetPixels, 0, targetWidth, 0, 0, targetWidth, targetHeight); //將裁切的圖片顯示出來(測試用,須要爲CameraView setTag(ImageView)) //主線程 { // @Override // public void run() { // ImageView imageView = (ImageView) getTag(); // imageView.setVisibility(View.VISIBLE); // imageView.setImageBitmap(newBmp); // } //}; //這裏能夠把這塊圖像保存到本地,能夠作個按鈕,點擊時把saveBmp=true,就能夠採集一張,採集幾張以後,拿去作tesseract 訓練,訓練出適合本身需求的字庫,纔是提升效率的關鍵 // if (saveBmp) { // saveBmp = false; // ImageUtils.saveBitmap(scanBmp, System.currentTimeMillis() + ".jpg"); // } //返回須要交給tess-two識別的內容 return newBmp; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210

更新

圖1:捕捉到有 11 位字符的文字塊,取到文字塊的精準位置,交給tess-two解析

這裏寫圖片描述

圖2:捕捉到有 12 位字符的文字塊,不符合手機號碼特徵,則不進行位置獲取和內容識別,直接跳過

這裏寫圖片描述

以前的算法還有一些缺陷,會有少數不符合手機號特徵的文字塊也被捕捉到了,我又換了一種算法,能夠捕捉到文字塊的精準位置,和包含多少個字符(字符數量不符合特徵就也能夠過濾掉,如上圖2),且有必定的抗干擾能力(效果通常,主要解決我遇到的水印問題) 
這裏先封裝一個工具類,直接調用catchPhoneRect(bitmp,imageView)方法,便可獲取一個只包含手機號的精準bitmap,若是返回null,表示沒有發現符合手機號特徵的文字塊(這裏捕獲時,是先取圖片中間一行的像素來初步判斷手機號位置,因此UI上須要一條中間線類輔助掃描,如上圖1) 
我遇到的水印問題:有些面單上的手機號,會被一種免單編號的水印遮住底邊,手機號仍是能看清楚,可是少數數字的底部被水印連在了一塊兒,致使tesseract 沒法識別 
這裏解決辦法就是:經過遞歸算法,獲取每個字符的精準位置,在獲取位置的過程當中,若是發現寬度或高度延伸到了不合理的範圍,即視爲被水印干擾的字符,先跳過這個字符,繼續捕捉下一個,直到捕捉到一個沒有發現干擾的字符,就能夠肯定這個文字塊中每一個字符的正確寬高,這時從頭再遍歷一次,根據正確的寬高範圍來清除水印部分像素

public class TesseractUtil { private static TesseractUtil mTesseractUtil = null; private float proportion = 0.5f; private TesseractUtil() { } public static TesseractUtil getInstance() { if (mTesseractUtil == null) synchronized (TesseractUtil.class) { if (mTesseractUtil == null) mTesseractUtil = new TesseractUtil(); } return mTesseractUtil; } /** * 調整閾值 * * @param pro 調整比例 */ public int adjustThresh(float pro) { this.proportion += pro; if (proportion > 1f) proportion = 1f; if (proportion < 0) proportion = 0; return (int) (proportion * 100); } /** * 識別數字 * * @param bmp 須要識別的圖片 * @param callBack 結果回調 */ public void scanNumber(final Bitmap bmp, final SimpleCallBack callBack) { if (checkTesseract()) { TessBaseAPI baseApi = new TessBaseAPI(); //初始化OCR的字體數據(Constants.BASE_PATH爲字庫所在路徑,Constants.NUMBER_LANGUAGE爲字庫文件名(不加後綴)) if (baseApi.init(Constants.BASE_PATH, Constants.NUMBER_LANGUAGE)) { //設置識別模式(單行識別) baseApi.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_LINE); //設置要識別的圖片 baseApi.setImage(bmp); baseApi.setVariable(TessBaseAPI.VAR_SAVE_BLOB_CHOICES, TessBaseAPI.VAR_TRUE); //開始識別 String result = baseApi.getUTF8Text(); baseApi.clear(); baseApi.end(); bmp.recycle(); callBack.response(result); } } } private void showImage(final Bitmap bmp, final ImageView imageView) { //顯示當前圖片處理進度(測試用) MainThread.getInstance(). execute(new Runnable() { @Override public void run() { imageView.setVisibility(View.VISIBLE); imageView.setImageBitmap(bmp); } }); } //白色色值 private final int PX_WHITE = -1; //黑色色值 private final int PX_BLACK = -16777216; //佔位色值(這個算法加入了排除干擾的模式,若是在捕捉一個文字的位置時,發現文字的寬度或者高度超出了正常高度,則頗有可能這裏被水印之類的干擾了,那就把超出正常的範圍像素色值變成-2,顏色和白色很接近,會被看成背景色,至關於清除了干擾,不直接變成-1是爲了在其餘數字被誤判爲干擾水印時,能夠還原) private final int PX_UNKNOW = -2; /** * 轉爲二值圖像 並判斷圖像中是否可能有手機號 * * @param bmp 原圖bitmap * @param imageView 顯示當前圖片處理進度,測試用 * @return */ public Bitmap catchPhoneRect(final Bitmap bmp, ImageView imageView) { int width = bmp.getWidth(); // 獲取位圖的寬 int height = bmp.getHeight(); // 獲取位圖的高 int[] pixels = new int[width * height]; // 經過位圖的大小建立像素點數組 bmp.getPixels(pixels, 0, width, 0, 0, width, height); int left = width; int top = height; int right = 0; int bottom = 0; //計算閾值 measureThresh(pixels, width, height); /** * 二值化 * */ binarization(pixels, width, height); int space = 0; int textWidth = 0; int startX = 0; int centerY = height / 2 - 1; int textLength = 0; int textStartX = 0; /** * 遍歷中間一行像素,粗略捕捉手機號 * 在掃描框中定義了一條中心線,若是每次掃描使用中心線來對準手機號,那麼捕捉手機號的速度和準確度都有了很大的提升 * 實現邏輯:先對從幀數據中裁切好的圖片進行二值化,而後取最中間一行的像素遍歷,初步判斷是否可能含有手機號 * 即遍歷這一行時,每次遇到一段連續黑色像素,就記錄一次textLength++(沒一段黑色像素表明一個筆畫的寬度),手機號都是11位數字 * 由此能夠得知,合理的筆畫範圍是 最少:11111111111 ,攔腰遍歷,會獲得11個筆畫寬度,textLength=11 * 最多:00000000000 ,攔腰遍歷,會獲得22個筆畫寬度,textLength=22 * 也就是說,最中間一行遍歷完成若是 textLength>11 && textLength<22 表示有必定可能有手機號存在(同時獲得文字塊的left、right),不然必定不存在,直接跳過,解析下一幀 * */ for (int j = 0; j < width; j++) { if (pixels[width * centerY + j] == PX_WHITE) { //白色像素,若是發現了連續黑色像素,到這裏出現第一個白色像素,那麼一個筆畫寬度就確認了,textLength++ if (space == 1) textLength++; if (textWidth > 0 && startX > 0 && startX < height - 1 && (space > width / 10 || j == width - 1)) { //若是捕捉到的合理的比劃截面,就更新left、right ,若是出現了多個合理的文字塊,就取寬度最大的 if (textLength > 10 && textLength < 22) if (textWidth > right - left) { left = j - space - textWidth - (space / 2); if (left < 0) left = 0; right = j - 1 - (space / 2); if (right > width) right = width - 1; textStartX = startX; } textLength = 0; space = 0; startX = 0; } space++; } else { //一段連續黑色像素的開始座標 if (startX == 0) startX = j; //文字塊的寬度 textWidth = j - startX; space = 0; } } //若是寬度佔比太小,直接跳過 if (right - left < width * 0.3f) { if (imageView != null) { bmp.setPixels(pixels, 0, width, 0, 0, width, height); //將裁切的圖片顯示出來 showImage(bmp, imageView); } else bmp.recycle(); return null; } /** *粗略計算文字高度 *這裏先粗略取一塊高度,肯定包含文字,如今已經得知了文字塊寬度,那麼合理的字符寬度就是 (right - left) / 11,數字一般高度更大,這裏就算寬度的1.5倍,而後爲了確保包含文字,在中間線的上下各加一個文字高度 *接下來就要捕捉文字塊的具體信息了,包括精準的寬度、高度 以及 字符數量 */ top = (int) (centerY - (right - left) / 11 * 1.5); bottom = (int) (centerY + (right - left) / 11 * 1.5); if (top < 0) top = 0; if (bottom > height) bottom = height - 1; /** * 判斷區域中有幾個字符 * */ //已經使用過的像素標記 int[] usedPixels = new int[width * height]; int[] textRect = new int[]{right, bottom, 0, 0}; //當前捕捉文字的rect int[] charRect = new int[]{textStartX, centerY, 0, centerY}; //在文字塊中捕捉到的字符個數 int charCount = 0; //是否發現干擾 boolean hasStain = false; startX = left; int charMaxWidth = (right - left) / 11; int charMaxHeight = (int) ((right - left) / 11 * 1.5); int charWidth = 0;//捕獲到一個完整字符後獲得標準的字符寬度 boolean isInterfereClearing = false; //循環獲取每個字符的寬高位置 while (true) { //當前字符的寬高是否正常 boolean isNormal = false; //是否已經清除干擾,若是在捕捉字符寬高的過程當中,發現有水印干擾,會調用clearInterfere()方法,將超出寬高的像素部分置爲-2,而後繼續捕捉下一個字符 if (!isInterfereClearing){ //若是被水印干擾,在進行遞歸算法的過程當中,高度或寬度會超出正常範圍,這裏會返回false,若是是沒有水印的乾淨文字塊,不會觸發else(方法實如今下面) isNormal = catchCharRect(pixels, usedPixels, charRect, width, height, charMaxWidth, charMaxHeight, charRect[0], charRect[1]); }else isNormal = clearInterfere(pixels, usedPixels, charRect, width, height, charWidth, charWidth, charRect[0], charRect[1]); //記錄已經捕捉的字符數量 charCount++; if (!isNormal) { //若是第一個字符發現干擾,這裏記錄有干擾,而後捕捉下一個字符,若是仍是有干擾,繼續捕捉下一個,直到找到一個正常的字符,那就能夠獲得一個字符的精準寬度,和高度,而後根據這個正確的寬高,從頭再遍歷一次,這個就能夠把超出這個寬高的像素置爲-2 hasStain = true; if (charWidth != 0) { usedPixels = new int[width * height]; charRect = new int[]{textStartX, centerY, 0, centerY}; charCount = 0; isInterfereClearing = true; } } else { //到這裏就表示:發現了干擾,並且已經捕捉到一個正確的字符寬高,能夠開始清除水印了 if (hasStain && !isInterfereClearing) { //把位置還原,從新開始遍歷,此次一邊獲取寬高,一邊清除水印 usedPixels = new int[width * height]; charWidth = charRect[3] - charRect[1]; charRect = new int[]{textStartX, centerY, 0, centerY}; charCount = 0; isInterfereClearing = true; continue; } else { if (charWidth == 0) { charWidth = charRect[3] - charRect[1]; } //若是沒有發現干擾,直接更新文字塊的精準位置 if (textRect[0] > charRect[0]) textRect[0] = charRect[0]; if (textRect[1] > charRect[1]) textRect[1] = charRect[1]; if (textRect[2] < charRect[2]) textRect[2] = charRect[2]; if (textRect[3] < charRect[3]) textRect[3] = charRect[3]; } } //是否找到下一個字符 boolean isFoundChar = false; if (!hasStain || isInterfereClearing) { //獲取下一個字符的rect開始點(若是上一個字符的寬高正常這裏就能夠直接找到下一個字符的起始座標) for (int x = charRect[2] + 1; x <= right; x++) if (pixels[width * centerY + x] != PX_WHITE) { isFoundChar = true; charRect[0] = x; charRect[1] = centerY; charRect[2] = 0; charRect[3] = 0; break; } } else { //若是發現干擾,那麼可能尚未獲得合理的寬高,仍是用開頭獲取筆畫的方式,尋找下一個字符開始捕獲的座標, for (int x = left; x <= right; x++) if (pixels[width * centerY + x] != PX_WHITE && pixels[width * centerY + x - 1] == PX_WHITE) { if (x <= startX) continue; startX = x; isFoundChar = true; charRect[0] = x; charRect[1] = centerY; charRect[2] = x; charRect[3] = centerY; break; } } if (!isFoundChar) { break; } } //獲得文字塊的精準位置 left = textRect[0]; top = textRect[1]; right = textRect[2]; bottom = textRect[3]; //若是高度合理,且捕捉到的字符數量爲11個,那麼基本能夠肯定,這是一個11位的文字塊,不然跳過,解析下一幀 if (bottom - top > (right - left) / 5 || bottom - top == 0 || charCount != 11) { if (imageView != null) { bmp.setPixels(pixels, 0, width, 0, 0, width, height); //將裁切的圖片顯示出來 showImage(bmp, imageView); } else bmp.recycle(); return null; } /** * 將最終捕捉到的手機號區域像素提取到新的數組 * */ int targetWidth = right - left; int targetHeight = bottom - top; int[] targetPixels = new int[targetWidth * targetHeight]; int index = 0; for (int i = top; i < bottom; i++) { for (int j = left; j < right; j++) { if (index < targetPixels.length) { if (pixels[width * i + j] == PX_WHITE) targetPixels[index] = PX_WHITE; else targetPixels[index] = PX_BLACK; } index++; } } bmp.recycle(); // 新建圖片 final Bitmap newBmp = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); newBmp.setPixels(targetPixels, 0, targetWidth, 0, 0, targetWidth, targetHeight); //將裁切的圖片顯示出來 if (imageView != null ) showImage(newBmp, imageView); return newBmp; } private final int MOVE_LEFT = 0; private final int MOVE_TOP = 1; private final int MOVE_RIGHT = 2; private final int MOVE_BOTTOM = 3; /** * 捕捉字符 * 這裏用遞歸的算法,從字符的第一個黑色像素,開始,分別進行上下左右的捕捉,若是相鄰的像素,也是黑色,就能夠擴大這個字符的定位,以此類推,最後獲得的就是字符的準確寬高 * 這裏之因此沒有用遞歸,而是用循環,是由於遞歸嵌套的層級太多,會致使 棧溢出 */ private boolean catchCharRect(int[] pixels, int[] used, int[] charRect, int width, int height, int maxWidth, int maxHeight, int x, int y) { int nowX = x; int nowY = y; //記錄動做() Stack<Integer> stepStack = new Stack<>(); while (true) { if (used[width * nowY + nowX] == 0) { used[width * nowY + nowX] = -1; if (charRect[0] > nowX) charRect[0] = nowX; if (charRect[1] > nowY) charRect[1] = nowY; if (charRect[2] < nowX) charRect[2] = nowX; if (charRect[3] < nowY) charRect[3] = nowY; if (charRect[2] - charRect[0] > maxWidth) { return false; } if (charRect[3] - charRect[1] > maxHeight) { return false; } if (nowX == 0 || nowX >= width - 1 || nowY == 0 || nowY >= height - 1) { return false; } } //當前像素的左邊是否還有黑色像素點 int leftX = nowX - 1; if (leftX >= 0 && pixels[width * nowY + leftX] != PX_WHITE && used[width * nowY + leftX] == 0) { nowX = leftX; stepStack.push(MOVE_LEFT); continue; } //當前像素的上邊是否還有黑色像素點 int topY = nowY - 1; if (topY >= 0 && pixels[width * topY + nowX] != PX_WHITE && used[width * topY + nowX] == 0) { nowY = topY; stepStack.push(MOVE_TOP); continue; } //當前像素的右邊是否還有黑色像素點 int rightX = nowX + 1; if (rightX < width && pixels[width * nowY + rightX] != PX_WHITE && used[width * nowY + rightX] == 0) { nowX = rightX; stepStack.push(MOVE_RIGHT); continue; } //當前像素的下邊是否還有黑色像素點 int bottomY = nowY + 1; if (bottomY < height && pixels[width * bottomY + nowX] != PX_WHITE && used[width * bottomY + nowX] == 0) { nowY = bottomY; stepStack.push(MOVE_BOTTOM); continue; } //用循環模擬遞歸,當一個像素的周圍,沒有發現未記錄的黑色像素,就能夠退回上一步,最終效果就和遞歸同樣了,並且不會引發棧溢出 if (stepStack.size() > 0) { int step = stepStack.pop(); switch (step) { case MOVE_LEFT: nowX++; break; case MOVE_RIGHT: nowX--; break; case MOVE_TOP: nowY++; break; case MOVE_BOTTOM: nowY--; break; } } else { break; } } if (charRect[2] - charRect[0] == 0 || charRect[3] - charRect[1] == 0) { return false; } return true; } /** * 清除干擾 * 和catchCharRect()方法原理同樣,可是多了一個邏輯,即超出正常範圍的黑色像素,會被看成干擾,置爲-2,這一步會致使有些被幹擾連在一塊兒的多個字符都被清空,因此在捕捉其餘字符時,當發現沒有超出範圍,又被置爲-2的像素,就還原爲黑色,這樣最終就能實現大部分的水印被清除(只針對我遇到的文字底部的水印) */ private final int WAIT_HANDLE = 0;//待處理像素 private final int HANDLED = -1;//已處理像素 private final int HANDLING = -2;//處理過但未處理完成的像素 /** * 清除干擾 */ private boolean clearInterfere(int[] pixels, int[] used, int[] charRect, int width, int height, int maxWidth, int maxHeight, int x, int y) { int nowX = x; int nowY = y; //記錄動做 Stack<Integer> stepStack = new Stack<>(); boolean needReset = true; while (true) { if (used[width * nowY + nowX] == WAIT_HANDLE) { used[width * nowY + nowX] = HANDLED; if (charRect[2] - charRect[0] <= maxWidth && charRect[3] - charRect[1] <= maxHeight) { if (charRect[0] > nowX) charRect[0] = nowX; if (charRect[1] > nowY) charRect[1] = nowY; if (charRect[2] < nowX) charRect[2] = nowX; if (charRect[3] < nowY) charRect[3] = nowY; } else { if (needReset) needReset = false; used[width * nowY + nowX] = HANDLING; pixels[width * nowY + nowX] = PX_UNKNOW; } } else if (pixels[width * nowY + nowX] == PX_UNKNOW) { if (charRect[2] - charRect[0] <= maxWidth && charRect[3] - charRect[1] <= maxHeight) { pixels[width * nowY + nowX] = PX_BLACK; if (charRect[0] > nowX) charRect[0] = nowX; if (charRect[1] > nowY) charRect[1] = nowY; if (charRect[2] < nowX) charRect[2] = nowX; if (charRect[3] < nowY) charRect[3] = nowY; used[width * nowY + nowX] = HANDLED; } else { if (needReset) needReset = false; } } //當前像素的左邊是否還有黑色像素點 int leftX = nowX - 1; int leftIndex = width * nowY + leftX; if (leftX >= 0 && pixels[leftIndex] != PX_WHITE && (used[leftIndex] == WAIT_HANDLE || (needReset && used[leftIndex] == HANDLING))) { nowX = leftX; stepStack.push(MOVE_LEFT); continue; } //當前像素的上邊是否還有黑色像素點 int topY = nowY - 1; int topIndex = width * topY + nowX; if (topY >= 0 && pixels[topIndex] != PX_WHITE && (used[topIndex] == WAIT_HANDLE || (needReset && used[topIndex] == HANDLING))) { nowY = topY; stepStack.push(MOVE_TOP); continue; } //當前像素的右邊是否還有黑色像素點 int rightX = nowX + 1; int rightIndex = width * nowY + rightX; if (rightX < width && pixels[rightIndex] != PX_WHITE && (used[rightIndex] == WAIT_HANDLE || (needReset && used[rightIndex] == HANDLING))) { nowX = rightX; stepStack.push(MOVE_RIGHT); continue; } //當前像素的下邊是否還有黑色像素點 int bottomY = nowY + 1; int bottomIndex = width * bottomY + nowX; if (bottomY < height && pixels[bottomIndex] != PX_WHITE && (used[bottomIndex] == WAIT_HANDLE || (needReset && used[bottomIndex] == HANDLING))) { nowY = bottomY; stepStack.push(MOVE_BOTTOM); continue; } if (stepStack.size() > 0) { int step = stepStack.pop(); switch (step) { case MOVE_LEFT: nowX++; break; case MOVE_RIGHT: nowX--; break; case MOVE_TOP: nowY++; break; case MOVE_BOTTOM: nowY--; break; } } else { break; } } return true; } private int redThresh = 130; private int blueThresh = 130; private int greenThresh = 130; /** * 計算掃描線所在像素行的平均閾值 * 我找了一些自動計算閾值的算法,都不太好用,這裏就直接採集了中間一行像素的平均值 */ private void measureThresh(int[] pixels, int width, int height) { int centerY = height / 2; int redSum = 0; int blueSum = 0; int greenSum = 0; for (int j = 0; j < width; j++) { int gray = pixels[width * centerY + j]; redSum += ((gray & 0x00FF0000) >> 16); blueSum += ((gray & 0x0000FF00) >> 8); greenSum += (gray & 0x000000FF); } redThresh = (int) (redSum / width * 1.5f * proportion); blueThresh = (int) (blueSum / width * 1.5f * proportion); greenThresh = (int) (greenSum / width * 1.5f * proportion); } /** * 二值化 */ private void binarization(int[] pixels, int width, int height) { for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { int gray = pixels[width * i + j]; pixels[width * i + j] = getColor(gray); if (pixels[width * i + j] != PX_WHITE) pixels[width * i + j] = PX_BLACK; } } } /** * 獲取顏色 */ private int getColor(int gray) { int alpha = 0xFF << 24; // 分離三原色 alpha = ((gray & 0xFF000000) >> 24); int red = ((gray & 0x00FF0000) >> 16); int green = ((gray & 0x0000FF00) >> 8); int blue = (gray & 0x000000FF); if (red > redThresh) { red = 255; } else { red = 0; } if (blue > blueThresh) { blue = 255; } else { blue = 0; } if (green > greenThresh) { green = 255; } else { green = 0; } return alpha << 24 | red << 16 | green << 8 | blue; } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491
  • 492
  • 493
  • 494
  • 495
  • 496
  • 497
  • 498
  • 499
  • 500
  • 501
  • 502
  • 503
  • 504
  • 505
  • 506
  • 507
  • 508
  • 509
  • 510
  • 511
  • 512
  • 513
  • 514
  • 515
  • 516
  • 517
  • 518
  • 519
  • 520
  • 521
  • 522
  • 523
  • 524
  • 525
  • 526
  • 527
  • 528
  • 529
  • 530
  • 531
  • 532
  • 533
  • 534
  • 535
  • 536
  • 537
  • 538
  • 539
  • 540
  • 541
  • 542
  • 543
  • 544
  • 545
  • 546
  • 547
  • 548
  • 549
  • 550
  • 551
  • 552
  • 553
  • 554
  • 555
  • 556
  • 557
  • 558
  • 559
  • 560
  • 561
  • 562
  • 563
  • 564
  • 565
  • 566
  • 567
  • 568
  • 569
  • 570
  • 571
  • 572
  • 573
  • 574
  • 575
  • 576
  • 577
  • 578
  • 579
  • 580
  • 581
  • 582
  • 583
  • 584
  • 585
  • 586
  • 587
  • 588
  • 589
  • 590
  • 591
  • 592
  • 593
  • 594
  • 595
  • 596
  • 597
  • 598
  • 599
  • 600
  • 601
  • 602
  • 603
  • 604
  • 605
  • 606
  • 607
  • 608
  • 609
  • 610
  • 611
  • 612
  • 613
  • 614
  • 615
  • 616
  • 617
  • 618
  • 619
  • 620
  • 621
  • 622
  • 623
  • 624
  • 625
  • 626
  • 627
  • 628
  • 629
  • 630
  • 631
  • 632
  • 633
  • 634
  • 635
  • 636
  • 637
  • 638
  • 639
  • 640
  • 641
 
 
相關文章
相關標籤/搜索