快速入門開發實現訂單類圖片識別結果抽象解析

宜信技術學院發表於2019-11-12

一、背景

面對訂單資料紙質檔案或圖片,僅靠人眼識別的話效率很低,需引入機器學習來識別和解析圖片以提高效率。當前市面上已有收費的圖片識別服務,包括阿里、百度等,識別效果較好,但針對訂單類圖片,不僅要關注圖片上的文字,還要關注文字所在的行列,來分出每條資料和資料詳細欄位。

本文主要介紹一種針對訂單類圖片識別結果進行行列解析的抽象流程和方案,幫助提高開發效率。

注:本文只提供思路,不提供原始碼。另外,本文不介紹人工智慧圖片識別,感興趣的同學可以上網查詢相關資料。

二、解析流程

對於影象處理,opencv算是比較優秀的工具,因此將其選做本文影象處理首選軟體。

  • 為了使圖片識別率更高,需要先做圖片矯正,這裡採用較為簡單的霍夫變換加去噪聲點演算法矯正圖片。
  • 圖片矯正後,呼叫圖片識別服務獲取結果,一般結果格式包括響應碼、錯誤描述、文字塊列表(文字和四點座標)等。
  • 然後使用抽象的俄羅斯方塊法根據識別結果獲取行列資訊。
  • 最後根據行列資訊組裝每一行資料並顯示。

三、細節處理

3.1 opencv安裝概要

opencv安裝,本文只做簡單提示,不展開介紹,以後有時間單獨發文。

1)windows

  • 下載編譯好的包,https://opencv.org/releases/
  • 解壓縮到自定義資料夾。

2)linux

  • 推薦使用ubuntu,並且最好是全新的系統,因為opencv會依賴很多包,對版本要求也高,解決衝突會很麻煩。
  • 下載原始碼
  • 安裝依賴包
  • 編譯安裝

我們使用java呼叫opencv,這裡需要安裝獲取到開發包,windows為opencv_javaxxx.dll,linux為libopencv_javaxxx.so,程式初始化時需要載入到jvm。詳細程式碼如下:

System.load(PropertieUtil.getPropertie("這裡是dll或so的完整路徑");

3.2 圖片矯正

3.2.1 矯正探索

圖片矯正探索之路較為艱辛,起初我們想了一個比較簡單的方案:

  • 先呼叫圖片識別服務,獲取到結果。
  • 然後根據每一個字塊的四角座標判斷出每個字塊的傾斜角。
  • 再根據去燥演算法算出平均的傾斜角。

理論上這個方案是可行的,但實踐證明我們錯了,因為圖片識別服務返回的座標圖片不準確,多數圖片算出的結果都是錯誤的。

經查發現霍夫變換有可能解決這個問題,於是開始嘗試學習霍夫變換和去燥演算法,最終發現可行,並抽象出公共方法,僅需簡單配置一些引數就能完成矯正。

圖片矯正分為兩步:

  • 第一步:正反矯正,判斷圖片傾斜角度是90°、180°、270°、0°,這個通過數學方法是無法判斷的,需要引用機器學習。
  • 第二步:角度微調,一般為確定圖片是正的,且傾斜角度在+-30°左右。

需要注意的是,上面說的辦法不可能通過一套引數來對所有圖片進行微調,但線上資料證明,針對一類圖片,一套引數基本能讓大多數圖片都矯正正確。

3.2.2 霍夫變換概要

霍夫變換是數學界經典空間變換演算法,用於檢測直線,通過大量檢測到的直線的斜率就能計算出圖片傾斜角度。先進行二值化和邊緣檢測再進行霍夫變換效果更佳,詳細演算法內容請自行搜尋,本文不展開。

3.2.3 去噪聲點演算法

基本公式:

上限=均值+n*標準差

下限=均值-n*標準差

其中n取值一般為1-4,數值越大表示篩選率越高。

最後再將符合的資料求均值。

核心程式碼如下:

/**
     * 利用標準差篩選
     * @param values
     * @return
     */
    private static double[] calcBestCornList(double[] values) {
        // 計算標準差
        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("佔比:{}%,篩選後角度陣列:{}", (selectedValue.length / (float)values.length) * 100F, selected);
        return selectedValue;
    }

3.2.4 霍夫變化抽象封裝

基本流程:

  • 定義相關引數
  • 讀取圖片
  • 灰度二值化處理
  • 使用opencv畫出輪廓
  • 根據引數要求多次畫霍夫變換線,直到線數量滿足引數為止
  • 遍歷畫出的線,分出橫線和豎線,根據配置計算出每條線的角度
  • 使用去噪聲演算法(需要根據非0數自動重複計算)算出平均傾斜角度
  • 使用opencv旋轉圖片

核心程式碼如下:

/**
     * 矯正圖片,通過霍夫變換矯正
     * @param oldImg 原始圖片
     * @param rotateParam 旋轉引數
     * @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());
        }
        // 用於計算的圖片矩陣
        Mat mathImg = src.clone();
        // 灰度化
        Imgproc.cvtColor(src, mathImg, Imgproc.COLOR_BGR2GRAY);
        logger.info("二值化完成");
        // 獲取輪廓
        Imgproc.Canny(src, mathImg, rotateParam.getCvtThreshould1(), rotateParam.getCvtThreshould2());
        logger.info("輪廓完成");
        // 霍夫變換獲取角度,詳細程式碼略
        double corn = houghLines(mathImg, rotateParam, cid);
        logger.info("霍夫變換完成,角度:{}", corn);
        if(corn == 0) {
            return oldImg;
        }
        return rotateOpenv(oldFile, corn, cid, bankCode);
    }

3.3 常用圖片識別方案

阿里、百度都有提供圖片識別服務,如果有實力也可以自己實現,不過不建議自研,因為樣本需求量巨大,時間成本過高。

3.4 識別結果解析

3.4.1 探索之路

本章節為本文重點內容,因為前文所提到的都是較為基礎的服務和演算法,大量開發內容都在本章。前期要開發的訂單圖片型別巨量(大於100種),每一類圖片區別很大,我們有幾個人分型別開發,但每個人所用的方法都不同,且張三開發出來的李四看不懂,不過畢竟面對的是圖片,比較抽象,這是可以理解的。

開發一段時間後我們發現了問題:每種型別最快也要一週才能開發完成,而且解析成功率極低。開發出一套抽象的方法來把行列資料提取出來迫在眉睫。

通過調研發現,大家常用兩種方法來提取行列資料,分別為座標法和標題法,但這兩種方法解析率都不高。經過幾周思考,終於想出了一套較好的方法,命名為俄羅斯方塊法,最終解決了問題。

3.4.2 俄羅斯方塊法

思路概要:

  • 拿到識別結果資料。
  • 先把所有資料的y座標進行排序。
  • 遍歷排序結果,先把第一條放入第一列結果集中。
  • 從第二條開始和第一列結果集對比。
  • 對比方法:如果在第一列結果集其中一條資料的右側,則認為是新列;如果在y軸方法和第一列結果集中某些資料重疊了,則認為是新列。
  • 如果以上兩條都不是,則認為本條資料還在當前列中,放入第一列結果集。
  • 以此類推,繼續對比,直到對比到最後一列最後一條資料。
  • 按照上述方法,反過來,以x軸為標準,能夠得到行結果集。

思路圖如下:

概要程式碼如下:

// 按照最左上角的x座標排序
        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++) {
            // 當前要比較的資料
            OcrWordInfo ocrWordInfo = sortL[i];
            // 處理當前列資料
            ntpr.getUtil().testCurColData(ocrWordInfo);
        }
        // 處理最後一列
        ntpr.lastCol();
        /**
         * 判斷是否為下一列,並處理
         * @param ocrWordInfo
         * @return
         */
        public void testCurColData(OcrWordInfo ocrWordInfo) {
            // 遍歷當前列已存在的所有資料
            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);
                // 最右邊的資料
                int x1 = temp.getPos().get(1).getX();
                int x2 = temp.getPos().get(2).getX();
                // 當前資料最左邊
                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)) {
                    // 當前資料在右邊,說明換列了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                    this.test.colAdd();
                    this.test.addCol(ocrWordInfo);
                    return;
                } else {
                    // 判斷是否覆蓋座標
                    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))) {
                        // 當前列表資料重疊,說明換列了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                        this.test.colAdd();
                        this.test.addCol(ocrWordInfo);
                        return;
                    }
                }
            }
            // 執行到這說明沒覆蓋
            this.test.addCol(ocrWordInfo);
        }

3.4.3 解析行資料技巧

技巧總結:

1)俄羅斯方塊法提供去除干擾項的引數,可以根據圖片特點去除上下左右干擾資料來減少序列列現象。

2)解析資料大致有兩種方法

  • 根據標題列號來判斷資料,這種方法不通用,簡單、規範的圖片識別率高,但無法適配亂的圖。
  • 把每一行資料以間隔符號分割拼到一起,使用正規表示式來‘扣’資料。因為一般同型別訂單圖片,關鍵欄位的位置是有特點的,例如金額格式、借貸方向、日期等,這種方法通用,但識別率不高。

具體使用哪種方法,還需要根據圖片特點進行取捨。

3)俄羅斯方塊法提供一些微調引數,用於適配一些特殊場景,例如換行列閥值之類的。

4)中間需要儲存一些過程圖片,例如矯正過程的若干張圖、俄羅斯方塊法識別結果的連線圖等。畢竟這種專案在查問題時靠日誌是沒用的,還得靠這些中間圖才能更快查到問題。

四、總結

本文提到的方案不能完全解決所有訂單類圖片解析問題,可以做到新手快速入門快速開發,如果您有更好思路歡迎交流。

作者:劉鵬飛

來源:宜信技術學院


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2663645/,如需轉載,請註明出處,否則將追究法律責任。

相關文章