基於Tesseract的OCR識別小程式
一、背景
先說下開發背景,今年有次搬家找房子(2020了應該叫去年了),發現每天都要對著各種租房廣告打很多電話。(當然網上也找了實地也找),每次基本都是對著牆面看電話號碼然後撥打,次數一多就感覺非常麻煩,如果沒看清還容易輸錯一個號碼。
圖片來自於網路
當時就想現在OCR技術那麼流行,為什麼不能做個程式來解決這個問題。因為租房電話有部分還是手寫號碼,所以也要解決手寫識別的問題。同時租房資訊其實也有很多是中介或者其他詐騙類等等。所以有部分並不是我們所需要的,為什麼這塊資訊就不能做個平臺進行共享,類似於手機裡面識別和提交詐騙電話一樣。然後自己也搜尋了下微信小程式裡有沒有類似的小程式,發現基本沒有,有些OCR相關程式但是使用相對繁瑣,並多數需要收費或開通會員等。
所以自己就想著還是自己來開發一個相關的小程式,功能簡單,通過相機拍照獲取電話號碼,進行識別;可直接在裡面呼叫電話進行撥號。同時允許提交該識別後的號碼作為標記,標記成詐騙、中介等。當某個號碼被人標記後,下次任何人再識別該號碼後就能顯示對應的標記資訊。
二、核心問題
本次核心問題可能就是手寫識別了,普通的列印文字識別。已經很常見也很成熟了。我們主要是識別數字,所以只針對數字手寫識別。這些年第三方的例如BAT其實都有類似的服務介面提供,我也研究了下,對接很簡單。識別率還算行,能夠達到70%左右。畢竟手寫每個人千差萬別。但是第三方的終究都是要收費的,當然有一定的免費額度。我開發這個軟體也沒打算收費什麼的,所以第三方的暫且放棄使用了。
MNIST機器學習
現在比較熱門的都是基於機器學習的手寫識別,機器學習框架有很多常用的比如TensorFlow ,在手寫數字識別裡面MNIST 資料集是最常用的,甚至流行到一般機器學習框架都使用MNIST 做入門教程了。關於MNIST我在這裡不做具體介紹,有興趣的可以自行上網瞭解。
然後我就網上找python機器學習手寫數字識別之類的,學習了很久。最後感覺使用MNIST資料集做識別更多還是一種識別演算法的比較。因為MNIST裡本身就包含了樣本和測試資料集。通過機器學習將樣本的進行學習然後生成一個模型資料,然後根據模型資料去讀取樣本資料進行比較以此來查識別成功率。並且網上的案例基本都是類似,即根據資料集來學習和識別正確率。
我還找到了.NET Core使用ML.NET 基於MNIST資料集的手寫數字識別,執行結果如下圖所示。原理和過程和Python處理是一樣的,包括輸出結果。
MNIST資料集是每張均為28*28畫素的黑白圖片,並且是一個字元佔用一張圖片。我們平時OCR識別的時候都是一張圖一起識別所有文字(數字)。所以如果使用MNIST我們必須要對圖片文字(數字)進行精準分割再把影象進行二值化儲存為20*28畫素的。手寫數字很多可能連載在一起,在此基礎上做分割還是有一定難度。
所以這裡暫時放棄使用了,後續抽時間會繼續研究使用MNIST進行識別。
Tesseract
Tesseract 是一個相對於比較有名的開源OCR識別軟體早期由惠普實驗室開發,現在是由Google在開發和維護。支援的平臺有Windows、linux、macos。支援的很多常用語言識別多達幾十種;還可以自己訓練文字型檔,如果使用手寫識別所以需要自己去訓練字型檔進行識別。
具體我就不過多闡述介紹了,感興趣的自行了解。我本次開發就是選用的Tesseract進行識別。
GitHub: https://github.com/tesseract-ocr/tesseract
三、前端開發
本次前端使用的是微信小程式,小程式因為不用安裝並且完全跨平臺。自己不是專業前端開發,所以不過多介紹,只介紹小程式裡如何使用相機拍照並且裁剪指定區域等。
1、拍照
相機拍照介面,因為只識別一個號碼所以拍照區域不需要很大,如果單純的把相機縮小又感覺很醜的樣子。所以有點類似微信掃描那樣的,需要的部分全亮其他部分半透明的效果。
如下圖所示
一開始一直在想小程式的相機是否提供類似的功能,讓我通過設定甚直接達到這樣的效果。但是很可惜沒找到,相機上面加文字圖片都是可以的。其實這個一部分透明一般分半透明就是一個圖片。整個背景是個圖片,這也是自己想好了好久才想到的,然後自己通過PS把一張圖做成透明和半透明的效果。
程式碼如下:
1 <camera device - position="width" flash="off" style="height:{{height}}px;"> 2 <cover-view class='camerabgImage-view'> 3 <cover-image class='bgImage' src='../images/bg2.png'> </cover-image> 4 <cover-view class='cameraTips'>請對準電話號碼掃描</cover-view> 5 <cover-view class="cameraTips2">*支援列印和手寫號碼識別</cover-view> 6 <cover-view class="cameraBgView"> 7 <cover-image class='cancelphoto' src='../images/cancelPhoto2.png' bindtap='cancelPhoto'></cover-image> 8 <cover-view class='cameraButton-view'> 9 <cover-image class='takephoto' src='../images/takephoto.png' bindtap='takePhoto'></cover-image> 10 </cover-view> 11 </cover-view> 12 </cover-view> 13 </camera>
具體詳細程式碼,請看文章結尾github地址。
2、裁剪
拍照介面實現了,下一步就是要實現拍照功能了,拍照程式碼簡單,不做過多闡述。因為我們是要取完全透明的那一塊區域的影象。也就是照片的指定位置,在拍照的api裡面微信沒有提供類似的獲取指定區域影象的功能,所以我們要實現這功能就需要自己針對一個完整的圖片資訊進行裁剪。
如何擷取這部分影象呢?目前我的做法是根據框框的位置去找出在整個影象中的位置即對應的X,Y。因為我們整個框框本來就是一張背景圖的一部分,所以它在程式碼中是沒有一個實際座標位置的。我們需要將裁剪後的影象展示出來,所以在另個頁面還需要一個canvas用來展示裁剪後的影象。
下面程式碼看到了我使用了延遲和出錯重試機制,因為在實際真機測試中偶爾還是會出現canvasToTempFilePath 方法報錯問題。原因就是在呼叫canvasToTempFilePath 前面需要繪製一個矩形框用於展示我們擷取後的圖片。但是canvas.draw()方法是非同步的,這樣就會導致前面還沒繪製完下面canvasToTempFilePath方法報錯。
裁剪主要程式碼如下:
1 canvasToTempFile: function() { 2 var that = this; 3 setTimeout(function() { 4 wx.canvasToTempFilePath({ // 裁剪對引數 5 canvasId: "image-canvas", 6 x: that.data.image_x, // 畫布x軸起點 7 y: that.data.image_y, // 畫布y軸起點 8 width: that.data.width, // 畫布寬度 9 height: that.data.image_height, // 畫布高度 10 destWidth: that.data.width, // 輸出圖片寬度 11 destHeight: that.data.image_height, // 輸出圖片高度 12 canvasId: 'image-canvas', 13 success: function(res) { 14 that.filePath = res.tempFilePath; 15 // 清除畫布上在該矩形區域內的內容。 16 that.canvas.clearRect(0, 0, that.data.width, that.data.height); 17 that.canvas.drawImage(that.filePath, that.data.image_x, that.data.image_y, that.data.width - 20, that.data.image_height); 18 that.canvas.draw(); 19 wx.hideLoading(); 20 // 開始請求識別介面 21 that.startDiscern(res.tempFilePath); 22 // 開始獲取標記型別 23 that.getMarkType(); 24 }, 25 fail: function(e) { 26 // console.log("出錯了:" + e); 27 wx.hideLoading() 28 wx.showToast({ 29 title: '請稍後...', 30 icon: 'loading' 31 }) 32 // 出錯後繼續執行一次。 33 that.canvasToTempFile(); 34 } 35 }); 36 }, 1000); 37 }
具體詳細程式碼,請看文章結尾github地址。
四 、Tesseract 訓練字型檔
首先Tesseract為了提高識別效果,可以允許我們自己訓練自己的字型檔。即當Tesseract識別不正確時我們對它進行人工矯正。或者當他完全無法識別時,我們可以自己去標記要被識別的文字座標和正確的結果值。
使用的工具是jTessBoxEditor,使用之前需要安裝Java。
1、圖片轉換成tif格式
訓練樣板必須為TIFF格式,所以第一步就是需要轉成TIFF格式檔案,同時對於多張圖片是可以合併成一個TIFF檔案格式。這樣就能在一個檔案裡儲存多個圖片。在jTessBoxEditor 裡Tools->Merge即可進行,選擇多張圖片即可。但是儲存為TIFF檔案是格式一定要注意是
[lang].[fontname].exp[num].tif
lang為語言名稱(即自己訓練後的語言名稱),
fontname為字型名稱,
num為序號,可自定義。
2、生成BOX檔案
下一步生成BOX檔案,這一步其實就是使用Tesseract做一個基本的識別,識別後會生成一個box檔案,這個檔案儲存著識別結果和結果對應的座標資訊。
這一步一定要在電腦上安裝tesseract,不然無法執行。
生成BOX檔案命令(注意要在剛生成tif檔案目錄中)
tesseract num.font.exp0.tif num.font.exp0 batch.nochop makebox
3、jTessBoxEditor矯正錯誤並訓練
開啟jTessBoxEditor,選擇Box Editor->Open 開啟剛剛生成的tif檔案所在目錄。該目錄必須包含上一步已經生成的box檔案。選擇開啟的是tif影象檔案,而不是box檔案。
開啟後會看到初步識別的結果,如果不對自行修改矯正。一個是修改座標即藍色框框對應的矩形位置,另個就是修改識別出來的字元。
修改矯正完成後,按Ctrl+S 或者上面的Save按鈕儲存。
4、建立字型檔案
建立一個檔名為font_properties的檔案,放到同一個目錄下,注意沒有副檔名。
內容是:font 0 0 0 0 0
如下圖
其中每個0對應的是各種字型。
分別為:
斜體,黑體,預設字型,襯線字型, 德文黑字型
0代表無1代表有
5、執行批處理
將下面程式碼複製到一個txt檔案中放到相同目錄下,修改副檔名為bat。
程式碼如下:
1 echo Run Tesseract for Training.. 2 tesseract.exe num.font.exp0.tif num.font.exp0 nobatch box.train 3 4 echo Compute the Character Set.. 5 unicharset_extractor.exe num.font.exp0.box 6 mftraining -F font_properties -U unicharset - 7 O num.unicharset num.font.exp0.tr 8 9 10 echo Clustering.. 11 cntraining.exe num.font.exp0.tr 12 13 echo Rename Files.. 14 rename normproto num.normproto 15 rename inttemp num.inttemp 16 rename pffmtable num.pffmtable 17 rename shapetable num.shapetable 18 19 echo Create Tessdata.. 20 combine_tessdata.exe num. 21 22 echo. & pause
最後執行此批處理即可,執行後會生成很多檔案,如下圖,我們只需要拷貝目錄下的num.traineddata檔案到專案的tessdata目錄中。
五、後端開發
後端這塊主要使用.NET Core 寫了一個webapi,小程式將拍照並擷取後的影象轉換成base64格式傳入到後臺,後臺這邊通過呼叫Tesserac進行識別。前面我提到能對識別的號碼進行提交標記和獲取標記等。所以後臺這邊目前使用的是MongoDB進行相關資料的儲存和讀取。
普通介面和MongoDB等操作很簡單,這裡不做介紹。主要說下Tesseract識別相關實現。
安裝Tesseract依賴
Tesseract目前最新版本是4.1.0,
Nuget裡面對應最新版是Genesis.Tesseract4。
所以下載時注意別下載錯了。
Tesseract 提高識別率主要兩個方面,第一個就是訓練更多的相關字型檔,第二個就是影象的處理。影象這一塊原圖重要性最高,在我們這裡就是使用者拍攝圖片,圖片拍攝的清晰可見,文字後面沒有其他干擾等最好。
在影象識別領域,影象處理最常用的就是灰度化、二值化、影象校正等。
灰度化
在RGB模型中,如果R=G=B時,則彩色表示一種灰度顏色,其中R=G=B的值叫灰度值,說通俗點就是把影象處理成黑白影象。
C# 程式碼如下:
1 /// <summary> 2 /// 影象灰度化 3 /// </summary> 4 /// <param name="bmp"></param> 5 /// <returns></returns> 6 public static Bitmap ToGray(Bitmap bmp) 7 { 8 for (int i = 0; i < bmp.Width; i++) 9 { 10 for (int j = 0; j < bmp.Height; j++) 11 { 12 //獲取該點的畫素的RGB的顏色 13 Color color = bmp.GetPixel(i, j); 14 //利用公式計算灰度值 15 int gray = (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11); 16 Color newColor = Color.FromArgb(gray, gray, gray); 17 bmp.SetPixel(i, j, newColor); 18 } 19 } 20 return bmp; 21 }
具體詳細程式碼,請看文章結尾github地址。
二值化
二值化就是將大於某個值的畫素點都修改為255,小於該值的修改為0
即0和1,其實是灰度影象的0~255的簡版,0表示白色,1表示黑色
二值化裡最重要的就是閾值的選取,一般分為固定閾值和自適應閾值。 比較常用的二值化方法則有:雙峰法、P引數法、迭代法和OTSU法等。
c# 程式碼如下:
1 /// <summary> 2 /// 影象二值化(迭代法) 3 /// </summary> 4 /// <param name="bmp"></param> 5 /// <returns></returns> 6 public static Bitmap ToBinaryImage(Bitmap bmp) 7 { 8 int[] histogram = new int[256]; 9 int minGrayValue = 255, maxGrayValue = 0; 10 //求取直方圖 11 for (int i = 0; i < bmp.Width; i++) 12 { 13 for (int j = 0; j < bmp.Height; j++) 14 { 15 Color pixelColor = bmp.GetPixel(i, j); 16 histogram[pixelColor.R]++; 17 if (pixelColor.R > maxGrayValue) maxGrayValue = pixelColor.R; 18 if (pixelColor.R < minGrayValue) minGrayValue = pixelColor.R; 19 } 20 } 21 //迭代計算閥值 22 int threshold = -1; 23 int newThreshold = (minGrayValue + maxGrayValue) / 2; 24 for (int iterationTimes = 0; threshold != newThreshold &&iterationTimes< 100; iterationTime 25 { 26 threshold = newThreshold; 27 int lP1 = 0; 28 int lP2 = 0; 29 int lS1 = 0; 30 int lS2 = 0; 31 //求兩個區域的灰度的平均值 32 for (int i = minGrayValue; i < threshold; i++) 33 { 34 lP1 += histogram[i] * i; 35 lS1 += histogram[i]; 36 } 37 int mean1GrayValue = (lP1 / lS1); 38 for (int i = threshold + 1; i < maxGrayValue; i++) 39 { 40 lP2 += histogram[i] * i; 41 lS2 += histogram[i]; 42 } 43 int mean2GrayValue = (lP2 / lS2); 44 newThreshold = (mean1GrayValue + mean2GrayValue) / 2; 45 } 46 47 //計算二值化 48 for (int i = 0; i < bmp.Width; i++) 49 { 50 for (int j = 0; j < bmp.Height; j++) 51 { 52 Color pixelColor = bmp.GetPixel(i, j); 53 if (pixelColor.R > threshold) bmp.SetPixel(i, j, Color.FromArgb(255, 255, 255)); 54 else bmp.SetPixel(i, j, Color.FromArgb(0, 0, 0)); 55 } 56 } 57 return bmp; 58 }
具體詳細程式碼,請看文章結尾github地址。
下圖是我在這期間為了測試影象處理效果及對比寫的一個工具。
灰度化
二值化
完整影象處理程式碼https://github.com/cfan1236/ImageManipulation
無論灰度和二值化,都是為了強化和突出要被識別的區域,比如文字。弱化背景和其他干擾項,在最後的測試實驗中,發現並不是所有原圖經過一系列的影象處理後都能得到更好的結果,有些圖片不做任何處理反而結果會更好。
所以我在實際處理中,使用三個執行緒同時處理識別3種情況,第一個使用原圖識別、第二個使用灰度化後識別、第三個使用二值化後識別。最後取識別結果最多的一個。
程式碼如下:
1 /// <summary> 2 /// 數字識別 3 /// </summary> 4 /// <param name="base64_image"></param> 5 /// <param name="image_url"></param> 6 /// <returns></returns> 7 public PhoneDiscernResult DiscernNumber(string base64_image, string image_url) 8 { 9 PhoneDiscernResult result = new PhoneDiscernResult(); 10 string imageFile = GetImageFileName(); 11 if (!string.IsNullOrEmpty(image_url)) 12 { 13 Utils.DownLoadWebImage(image_url, imageFile); 14 } 15 else 16 { 17 Utils.SaveBase64Image(base64_image, imageFile); 18 } 19 if (File.Exists(imageFile)) 20 { 21 string[] taskResult = new string[3]; 22 // 三個執行緒同時去處理執行 23 // 每個執行緒處理的圖片都不一樣 取結果最好的一個 24 Task[] tk = new Task[] { 25 Task.Factory.StartNew(()=> 26 { 27 // 原圖識別 28 taskResult[0]=Discern(imageFile); 29 }), 30 Task.Factory.StartNew(()=> 31 { 32 // 灰度處理後識別 33 taskResult[1]=GrayDiscern(imageFile); 34 }), 35 Task.Factory.StartNew(()=> 36 { 37 // 二值化處理後識別 38 taskResult[2]=BinaryzationDiscern(imageFile); 39 }), 40 }; 41 // 超時1分鐘 42 int timeout = (1000 * 60) * 1; 43 Task.WaitAll(tk, timeout); 44 var number_str = taskResult[0]; 45 if (taskResult[1].Length > number_str.Length) 46 { 47 number_str = taskResult[1]; 48 } 49 if (taskResult[2].Length > number_str.Length) 50 { 51 number_str = taskResult[2]; 52 } 53 result.text = number_str; 54 if (number_str.Length == 11) 55 { 56 result.message = "識別成功"; 57 } 58 else 59 { 60 result.message = "當前識別的電話可能有誤,請注意辨別"; 61 } 62 63 } 64 return result; 65 } 66 67 68 /// <summary> 69 /// 直接識別 70 /// </summary> 71 /// <param name="filePath"></param> 72 /// <returns></returns> 73 private string Discern(string imageFile) 74 { 75 string number_str = ""; 76 // 這裡可以選擇不同的語言包 可以是自己訓練的 可以是Tesseract 訓練好的語言包 77 TesseractEngine te_ocr = new TesseractEngine(@"tessdata", "chi_sim", EngineMode.TesseractAndLstm); 78 var img = Pix.LoadFromFile(imageFile); 79 var page = te_ocr.Process(img, PageSegMode.Auto); 80 string text = page.GetText().Trim().Replace("\r", "").Replace("\n", ""); 81 _logger.Info("識別的原始資料:"+text); 82 page.Dispose(); 83 // 只提取數字 84 number_str = System.Text.RegularExpressions.Regex.Replace(text, @"[^0-9]+", ""); 85 _logger.Info("只提取數字結果:" + number_str); 86 return number_str; 87 } 88 89 /// <summary> 90 /// 灰度識別 91 /// </summary> 92 /// <param name="imageFile"></param> 93 /// <returns></returns> 94 private string GrayDiscern(string imageFile) 95 { 96 string number_str = ""; 97 using (Bitmap bmp = new Bitmap(imageFile)) 98 { 99 // 灰度處理 100 var bmps = Utils.ToGray(bmp); 101 var tempFile = GetImageFileName(1); 102 bmps.Save(tempFile); 103 number_str = Discern(tempFile); 104 File.Delete(tempFile); 105 } 106 return number_str; 107 } 108 /// <summary> 109 /// 二值化識別 110 /// </summary> 111 /// <param name="imageFile"></param> 112 /// <returns></returns> 113 private string BinaryzationDiscern(string imageFile) 114 { 115 string number_str = ""; 116 using (Bitmap bmp = new Bitmap(imageFile)) 117 { 118 // 灰度處理 119 var bmps = Utils.ToGray(bmp); 120 // 處理自動校正 121 gmseDeskew sk = new gmseDeskew(bmps); 122 double skewangle = sk.GetSkewAngle(); 123 Bitmap bmpOut = Utils.RotateImage(bmps, -skewangle); 124 var tempFile = GetImageFileName(1); 125 // 將二值化後的影象儲存下 126 Utils.ToBinaryImage(bmpOut).Save(tempFile); 127 number_str = Discern(tempFile); 128 File.Delete(tempFile); 129 } 130 return number_str; 131 }
具體詳細程式碼,請看文章結尾github地址。
六、Linux上執行Tesseract
由於使用的是.NET Core開發的,所以最理想的執行環境自然是Linux了。但是當把專案直接釋出到Linux上,是會報錯的。所以在Linux上還是需要做一些安裝和配置的。
第一步我們需要在Linux上安裝Tesseract
首先Tesseract在github上有Linux平臺的安裝簡介https://github.com/tesseract-ocr/tesseract/wiki
我是Centos安裝命令如下:
yum-config-manager --add-repo https://download.opensuse.org/repositories/home:/Alexander_Pozdnyakov/ScientificLinux_7/
// 更新
yum update
// 安裝tesseract
yum install tesseract
yum install tesseract-langpack-deu
// 如果提示Public key for leptonica-1.76.0-2.2.x86_64.rpm is not installed證明未通過gpg金鑰檢查所以在安裝時加上—nogpgcheck
yum install tesseract-langpack-deu –nogpgcheck
//安裝完成 檢視版本
tesseract --version
這是最新版4.1.0。這個版本最好和自己專案中的版本匹配。
切換到專案釋出的目錄再進入x64 (64位系統選擇此目錄)目錄然後做對映。
對映哪些檔案主要是看我們釋出後X64裡面的dll檔案叫什麼,比如我們發現是libtesseract400.dll 和libtesseract400.dll 。不同版本可能後面的數字不一樣。
然後我們找到剛剛安裝Tesseract的目錄然後搜尋libtesseract和libtesseract開頭的so檔案即可。最後我們會找到libtesseract.so.4.0.1.so、liblept.so.5.0.3.so檔案。然後將這兩個檔案做對映,對映到我們專案目錄中的名稱需要和本身專案中dll檔案一致,只是字尾為so,不再是dll了。
對映命令如下
ln -s /usr/lib64/libtesseract.so.4.0.1 libtesseract400.so
ln -s /usr/lib64/liblept.so.5.0.3 liblept1760.so
注意這些dll或者so後面的版本號即數字不同版本不同時期可能都不一樣,以自己安裝的為準。
對映完畢後檢視專案目錄x64目錄如下
也可以通過ftp來檢視目錄結構。下面箭頭狀的就是對映的,檔案本身不在此目錄。有點類似如桌面快捷方式一樣。
完成以上配置即可在Linux上完成Tesseract識別了。當然如果執行在Windows上,是不要安裝Tesseract即可識別的。
小程式也早已上線,感興趣的可以體驗和提相關建議。名稱叫做:手機號碼識別
小程式二維碼
完整專案程式碼
前端程式碼:
https://github.com/cfan1236/PhoneDiscern_wxapp
後端程式碼
https://github.com/cfan1236/PhoneDiscern
&n