快速入門開發(fā)實(shí)現(xiàn)訂單類圖片識(shí)別結(jié)果抽象解析
一、背景
面對(duì)訂單數(shù)據(jù)紙質(zhì)文件或圖片,僅靠人眼識(shí)別的話效率低,需引入機(jī)器學(xué)習(xí)來識(shí)別和解析圖片以提高效率。當(dāng)前市面已有收費(fèi)的圖片識(shí)別服務(wù),包括阿里、百度等,識(shí)別效果較好,但針對(duì)訂單類圖片,不僅需要關(guān)注圖片上的文字,還需要關(guān)注文字所在的行列,來分出每條數(shù)據(jù)和數(shù)據(jù)詳細(xì)字段。
本篇內(nèi)容主要介紹一種針對(duì)識(shí)別完結(jié)果進(jìn)行行列解析的抽象流程和方案,來提高開發(fā)效率。
本文只提供思路,不提供源碼。另外,本文不介紹人工智能圖片識(shí)別,有興趣的同學(xué)請(qǐng)關(guān)注大神們的文章,這里推薦“宜信技術(shù)學(xué)院官網(wǎng)文章AI板塊”。
二、解析流程
對(duì)于圖像處理,opencv算是比較優(yōu)秀的,所以選做本文圖像處理首選軟件。為了使圖片識(shí)別率更高,需要先做圖片矯正,這里采用較為簡單的霍夫變換加去噪聲點(diǎn)算法矯正圖片。圖片矯正后,調(diào)用圖片識(shí)別服務(wù)獲取結(jié)果,一般結(jié)果格式包括響應(yīng)碼、錯(cuò)誤描述、文字塊列表(文字和四點(diǎn)坐標(biāo))等。根據(jù)識(shí)別結(jié)果使用抽象的俄羅斯方塊法來識(shí)別結(jié)果獲取行列信息。最后根據(jù)行列信息組裝每一行數(shù)據(jù)并顯示。
三、處理細(xì)節(jié)
3.1 opencv安裝概要
opencv安裝簡單提示,這里不細(xì)說,以后有時(shí)間單獨(dú)發(fā)文。
1)windows
- 下載編譯好的包:https://opencv.org/releases/
- 解壓縮到自定義文件夾。
2)linux
- 推薦使用ubuntu,并且最好是全新的系統(tǒng),因?yàn)閛pencv會(huì)依賴很多包,對(duì)版本要求也高,解決沖突會(huì)很麻煩。
- 下載源碼
- 安裝依賴包
- 編譯安裝
我們使用java調(diào)用opencv,這里需要安裝獲取到開發(fā)包,windows為opencv_javaxxx.dll,linux為libopencv_javaxxx.so,程序初始化時(shí)需要加載到j(luò)vm。詳細(xì)代碼如下:
- System.load(PropertieUtil.getPropertie("這里是dll或so的完整路徑");
3.2 圖片矯正
3.2.1 矯正探索
圖片矯正探索之路較為艱辛,起初我們想了一個(gè)比較簡單的方案:先調(diào)用圖片識(shí)別服務(wù),獲取到結(jié)果,再根據(jù)每一個(gè)字塊的四角坐標(biāo)判斷出每個(gè)字塊的傾斜角,然后再根據(jù)去噪算法算出平均的傾斜角。理論上這個(gè)方案是可行的,但實(shí)踐證明我們是錯(cuò)的,因?yàn)閳D片識(shí)別服務(wù)返回的坐標(biāo)圖片不準(zhǔn)確,多數(shù)的圖片算出的結(jié)果都是錯(cuò)誤的。
經(jīng)查發(fā)現(xiàn)霍夫變換有可能解決這個(gè)問題,于是開始嘗試學(xué)習(xí)霍夫變換和去燥算法,最終發(fā)現(xiàn)可行,并抽象出公共方法,僅需簡單配置一些參數(shù)就能完成矯正。
圖片矯正分為兩步:
第一步:正反矯正,判斷圖片傾斜角度是90°、180°、270°、0°,這個(gè)通過數(shù)學(xué)方法是無法判斷的,需要引用機(jī)器學(xué)習(xí)。
第二步:角度微調(diào),一般為確定圖片是正的,且傾斜角度在+-30°左右。
需要注意:上面說的辦法不可能通過一套參數(shù)來對(duì)所有圖片進(jìn)行微調(diào),但是線上數(shù)據(jù)證明,針對(duì)一類圖片,一套參數(shù)基本能讓大多數(shù)圖片都矯正正確。
3.2.2 霍夫變換概要
霍夫變換是數(shù)學(xué)界經(jīng)典空間變換算法,用于檢測直線,通過大量檢測到的直線的斜率就能計(jì)算出圖片傾斜角度。先進(jìn)行二值化和邊緣檢測,再進(jìn)行霍夫變換效果更佳。詳細(xì)算法內(nèi)容請(qǐng)自行搜索,本文不細(xì)聊。
3.2.3 去噪聲點(diǎn)算法
基本公式:
上限=均值+n*標(biāo)準(zhǔn)差
下限=均值-n*標(biāo)準(zhǔn)差
其中n取值一般為1-4,數(shù)值越大表示篩選率越高。最后再將符合的數(shù)據(jù)求均值。
核心代碼如下:
- /**
- * 利用標(biāo)準(zhǔn)差篩選
- * @param values
- * @return
- */
- private static double[] calcBestCornList(double[] values) {
- // 計(jì)算標(biāo)準(zhǔn)差
- StandardDeviation variance = new StandardDeviation();
- double evaluate = variance.evaluate(values);
- Mean mean = new Mean();
- double meanValue = mean.evaluate(values);
- double biggerValue = meanValue + CHOOSE_POWER * evaluate;
- double smallerValue = meanValue - CHOOSE_POWER * evaluate;
- List<Double> selected = Lists.newArrayList();
- for (double value : values) {
- if (value >= smallerValue && value <= biggerValue) {
- selected.add(value);
- }
- }
- double[] selectedValue = new double[selected.size()];
- for (int i = 0; i < selected.size(); i++) {
- selectedValue[i] = selected.get(i);
- }
- logger.info("占比:{}%,篩選后角度數(shù)組:{}", (selectedValue.length / (float)values.length) * 100F, selected);
- return selectedValue;
- }
3.2.4 霍夫變化抽象封裝
基本流程:
1、定義相關(guān)參數(shù)。
2、讀取圖片。
3、灰度二值化處理。
4、使用opencv畫出輪廓。
5、根據(jù)參數(shù)要求多次畫霍夫變換線,直到線數(shù)量滿足參數(shù)為止。
6、遍歷畫出的線,分出橫線和豎線,根據(jù)配置計(jì)算出每條線角度。
7、使用去噪聲算法(需要根據(jù)非0數(shù)自動(dòng)重復(fù)計(jì)算)算出平均傾斜角度。
8、使用opencv旋轉(zhuǎn)圖片。
核心代碼如下:
- /**
- * 矯正圖片,通過霍夫變換矯正
- * @param oldImg 原始圖片
- * @param rotateParam 旋轉(zhuǎn)參數(shù)
- * @return
- */
- public static String rotateHoughLines(File oldFile, String oldImg, RotateParam rotateParam, String cid, String bankCode) throws Exception {
- Mat src= Imgcodecs.imread(oldFile.getAbsolutePath());
- //讀取圖像到矩陣中
- if(src.empty()){
- throw new Exception("no file " + oldFile.getAbsolutePath());
- }
- // 用于計(jì)算的圖片矩陣
- Mat mathImg = src.clone();
- // 灰度化
- Imgproc.cvtColor(src, mathImg, Imgproc.COLOR_BGR2GRAY);
- logger.info("二值化完成");
- // 獲取輪廓
- Imgproc.Canny(src, mathImg, rotateParam.getCvtThreshould1(), rotateParam.getCvtThreshould2());
- logger.info("輪廓完成");
- // 霍夫變換獲取角度,詳細(xì)代碼略
- double corn = houghLines(mathImg, rotateParam, cid);
- logger.info("霍夫變換完成,角度:{}", corn);
- if(corn == 0) {
- return oldImg;
- }
- return rotateOpenv(oldFile, corn, cid, bankCode);
- }
3.3 常用圖片識(shí)別方案
阿里、百度都有提供圖片識(shí)別服務(wù),另外如果有實(shí)力也可以自己實(shí)現(xiàn),當(dāng)然不建議自研,因?yàn)闃颖拘枨罅烤薮螅瑫r(shí)間成本過高。
3.4 識(shí)別結(jié)果解析
3.4.1 探索之路
本章節(jié)為本文重點(diǎn)內(nèi)容,因?yàn)榍懊嫠岬降亩际禽^為基礎(chǔ)的服務(wù)和算法,大量開發(fā)內(nèi)容都在本章。前期要開發(fā)的訂單圖片類型巨量(大于100種),每一類圖片區(qū)別很大,我們有幾個(gè)人分類型開發(fā),但是每個(gè)人所用的方法都不同,且張三開發(fā)出來的李四看不懂,不過畢竟面對(duì)的是圖片,比較抽象,是可以理解的。
開發(fā)一段時(shí)間后,我們發(fā)現(xiàn)了問題。每種類型最快也要一周才能開發(fā)完成,而且解析成功率極低。開發(fā)出一套抽象的方法來把行列數(shù)據(jù)提取出來迫在眉睫。
通過調(diào)研發(fā)現(xiàn)大家常用兩種方法來提取行列數(shù)據(jù),分別為坐標(biāo)法和標(biāo)題法,但是這兩種方法解析率都不高。經(jīng)過幾周思考,終于想出了一套較好的方法,命名為俄羅斯方塊法,解決了問題。
3.4.2 俄羅斯方塊法
思路概要:
1. 拿到識(shí)別結(jié)果數(shù)據(jù)。
2. 先把所有數(shù)據(jù)的y坐標(biāo)進(jìn)行排序。
3. 遍歷排序結(jié)果,先把第一條放入第一列結(jié)果集中。
4. 從第二條開始和第一列結(jié)果集對(duì)比。
5. 對(duì)比方法:
--如果在第一列結(jié)果集其中一條數(shù)據(jù)的右側(cè),則認(rèn)為是新列。
--如果在y軸方法和第一列結(jié)果其中某些數(shù)據(jù)重疊了,則認(rèn)為是新列。
6. 如果以上兩條都不是,則認(rèn)為本條數(shù)據(jù)還在當(dāng)前列中,放入第一列結(jié)果集。
7. 以此類推,繼續(xù)對(duì)比,直到對(duì)比到最后一列最后一條數(shù)據(jù)。
8. 按照上面方法,反過來,以x軸為標(biāo)準(zhǔn),能夠得到行結(jié)果集。
思路圖如下:
概要代碼如下:
- // 按照最左上角的x坐標(biāo)排序
- OcrWordInfo[] sortL = NoTableParseResult.ParseUtil.bubbleSortX(ocrResponse.getPrism_wordsInfo(), false);
- NoTableParseResult ntpr = new NoTableParseResult(param);
- ntpr.setHeight(converImg.height());
- ntpr.setWight(converImg.width());
- for (int i = 0; i < sortL.length; i++) {
- // 當(dāng)前要比較的數(shù)據(jù)
- OcrWordInfo ocrWordInfo = sortL[i];
- // 處理當(dāng)前列數(shù)據(jù)
- ntpr.getUtil().testCurColData(ocrWordInfo);
- }
- // 處理最后一列
- ntpr.lastCol();
- /**
- * 判斷是否為下一列,并處理
- * @param ocrWordInfo
- * @return
- */
- public void testCurColData(OcrWordInfo ocrWordInfo) {
- // 遍歷當(dāng)前列已存在的所有數(shù)據(jù)
- int size = this.test.getCol().size();
- if(size == 0) {
- this.test.addCol(ocrWordInfo);
- return;
- }
- for (int i = 0; i < size; i++) {
- OcrWordInfo temp = this.test.getCol().get(i);
- // 最右邊的數(shù)據(jù)
- int x1 = temp.getPos().get(1).getX();
- int x2 = temp.getPos().get(2).getX();
- // 當(dāng)前數(shù)據(jù)最左邊
- int xx0 = ocrWordInfo.getPos().get(0).getX();
- int xx3 = ocrWordInfo.getPos().get(3).getX();
- int threholdx = this.test.param == null ? 0 : this.test.param.getCoverColXThrehold();
- if(xx0 >= (x1 - threholdx) && xx0 >= (x2 - threholdx) && xx3 >= (x1 - threholdx) && xx3 >= (x2 - threholdx)) {
- // 當(dāng)前數(shù)據(jù)在右邊,說明換列了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- this.test.colAdd();
- this.test.addCol(ocrWordInfo);
- return;
- } else {
- // 判斷是否覆蓋坐標(biāo)
- int y0 = temp.getPos().get(0).getY();
- int y3 = temp.getPos().get(3).getY();
- int yy0 = ocrWordInfo.getPos().get(0).getY();
- int yy3 = ocrWordInfo.getPos().get(3).getY();
- int threhold = (int)Math.round((y3 - y0) * (this.test.param == null ? 0.25 : this.test.param.getCoverThrehold()));
- if(!(yy3 <= (y0 + threhold) || yy0 >= (y3 - threhold))) {
- // 當(dāng)前列表數(shù)據(jù)重疊,說明換列了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- this.test.colAdd();
- this.test.addCol(ocrWordInfo);
- return;
- }
- }
- }
- // 執(zhí)行到這說明沒覆蓋
- this.test.addCol(ocrWordInfo);
- }
3.4.3 解析行數(shù)據(jù)技巧
技巧總結(jié):
1、俄羅斯方塊法提供去除干擾項(xiàng)的參數(shù),可以根據(jù)圖片特點(diǎn),去除上下左右干擾數(shù)據(jù)來減少串行列現(xiàn)象。
2、解析數(shù)據(jù)大致有兩種方法:
- 根據(jù)標(biāo)題列號(hào)來判斷數(shù)據(jù),這種方法不通用,簡單、規(guī)范的圖片識(shí)別率高,但是無法適配亂的圖。
- 把每一行數(shù)據(jù)以間隔符號(hào)分割拼到一起,使用正則表達(dá)式來‘扣’數(shù)據(jù),因?yàn)橐话阃愋陀唵螆D片,關(guān)鍵字段的位置是有特點(diǎn)的,例如金額格式、借貸方向、日期等,這種方法通用,但識(shí)別率不高。
*具體使用哪種方法,還需要根據(jù)圖片特點(diǎn)進(jìn)行取舍。
3、 俄羅斯方塊法提供一些微調(diào)參數(shù),用于適配一些特殊場景,例如換行列閥值之類的。
4、中間需要保存一些過程圖片,例如矯正過程的若干張圖、俄羅斯方塊法識(shí)別結(jié)果的連線圖等,畢竟這種項(xiàng)目,查問題時(shí)靠日志是沒用的,還得靠這些中間圖才能更快查到問題。
四、總結(jié)
本文提到的方案不能完全解決所有訂單類圖片解析問題,但可以做到新手快速入門快速開發(fā),如果您有更好思路歡迎交流。
【本文是51CTO專欄機(jī)構(gòu)宜信技術(shù)學(xué)院的原創(chuàng)文章,微信公眾號(hào)“宜信技術(shù)學(xué)院( id: CE_TECH)”】