OpenCV3 識別圖中表格-JAVA 實現

關於 JAVA 學習 OpenCV 的內容,函數講解。內容我均整理在 GitHubd的OpenCV3-Study-JAVAhtml

OpenCV 3 識別圖中表格-Java 實現

1. 說明

網上大部分資料,都是針對 C++的,python、java 的例子太少了。因此最近在作這個的時候,把他記錄下來,也能夠幫助一些人少走彎路。java

OpenCV 確實強大,強大到每個方法,都能 google 到一篇專題文章,在寫的過程當中,參考了許多資料,最終完成了實現和註釋。python

可是這僅僅是入門,找到表格後的利用纔是後面的核心。好比:git

  1. 表格的 OCR 識別,識別表頭,內容數據,造成結構化數據。
  2. 圖片按照順序,轉 Word文檔或者保存爲 html,這樣就能夠完成格式的轉化,方便在 web 端查看,用戶下載。
  3. 其餘利用...

本文僅針對效果較好的,無傾斜,背景乾淨的圖片進行識別。複雜的狀況會可能沒法知足,須要進一步處理。僅僅是個入門。github

2. 開發環境

  • macOS Sierra 10.12.4
  • IntelliJ IDEA 2017
  • Junit 4.12
  • JDK 1.8

由於在 mac 下經過 brew 安裝的 opencv ,因此包都是跟當前系統匹配的,安裝目錄也是一致的。web

Windows 下須要根據本身的系統環境,位數,修改代碼的loadLibraries,決定加載的動態庫文件。算法

3. 代碼實現

import org.junit.Test;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;

import java.io.File;
import java.util.*;

/**
 * @Author : alexliu
 * @Description : opencv 測試
 * @Date : Create at 下午3:12 2018/1/26
 */
public class TestOpenCV {

    String test_file_path = System.getProperty("user.dir") + File.separator + "testFiles";

    static {
        //加載動態連接庫時,不使用System.loadLibrary(xxx);。 而是使用 絕對路徑加載:System.load(xxx);

        /*
         * 加載動態庫
         *
         * 第一種方式 --------------System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
         * loadLibrary(Core.NATIVE_LIBRARY_NAME); //使用這種方式加載,須要在 IDE 中配置參數.
         * Eclipse 配置:http://opencv-java-tutorials.readthedocs.io/en/latest/01-installing-opencv-for-java.html#set-up-opencv-for-java-in-eclipse
         * IDEA 配置 :http://opencv-java-tutorials.readthedocs.io/en/latest/01-installing-opencv-for-java.html#set-up-opencv-for-java-in-other-ides-experimental
         *
         * 第二種方式 --------------System.load(path of lib);
         * System.load(your path of lib) ,方式比較靈活,可根據環境的系統,位數,決定加載內容
         */
        loadLibraries();
    }

/**
     * 讀取 table
     */
    @Test
    public void readTable(){

        Mat source_image = Imgcodecs.imread(test_file_path + "/table-3.jpg");
        //灰度處理
        Mat gray_image = new Mat(source_image.height(), source_image.width(), CvType.CV_8UC1);
        Imgproc.cvtColor(source_image,gray_image,Imgproc.COLOR_RGB2GRAY);

        //二值化
        Mat thresh_image = new Mat(source_image.height(), source_image.width(), CvType.CV_8UC1);
        // C 負數,取反色,超過閾值的爲黑色,其餘爲白色
        Imgproc.adaptiveThreshold(gray_image, thresh_image,255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY,7,-2);
        this.saveImage("out-table/1-thresh.png",thresh_image);

        //克隆一個 Mat,用於提取水平線
        Mat horizontal_image = thresh_image.clone();

        //克隆一個 Mat,用於提取垂直線
        Mat vertical_image = thresh_image.clone();

        /*
         * 求水平線
         * 1. 根據頁面的列數(能夠理解爲寬度),將頁面化成若干的掃描區域
         * 2. 根據掃描區域的寬度,建立一根水平線
         * 3. 經過腐蝕、膨脹,將知足條件的區域,用水平線勾畫出來
         *
         * scale 越大,識別的線越多,由於,越大,頁面劃定的區域越小,在腐蝕後,多行文字會造成一個塊,那麼就會有一條線
         * 在識別表格時,咱們能夠理解線是從頁面左邊 到 頁面右邊的,那麼劃定的區域越小,知足的條件越少,線條也更準確
         */
        int scale = 10;
        int horizontalsize = horizontal_image.cols() / scale;
        // 爲了獲取橫向的表格線,設置腐蝕和膨脹的操做區域爲一個比較大的橫向直條
        Mat horizontalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(horizontalsize, 1));
        // 先腐蝕再膨脹 new Point(-1, -1) 以中心原點開始
        // iterations 最後一個參數,迭代次數,越多,線越多。在頁面清晰的狀況下1次便可。
        Imgproc.erode(horizontal_image, horizontal_image, horizontalStructure, new Point(-1, -1),1);
        Imgproc.dilate(horizontal_image, horizontal_image, horizontalStructure, new Point(-1, -1),1);
        this.saveImage("out-table/2-horizontal.png",horizontal_image);

        // 求垂直線
        scale = 30;
        int verticalsize = vertical_image.rows() / scale;
        Mat verticalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(1, verticalsize));
        Imgproc.erode(vertical_image, vertical_image, verticalStructure, new Point(-1, -1),1);
        Imgproc.dilate(vertical_image, vertical_image, verticalStructure, new Point(-1, -1),1);
        this.saveImage("out-table/3-vertical.png",vertical_image);

        /*
         * 合併線條
         * 將垂直線,水平線合併爲一張圖
         */
        Mat mask_image = new Mat();
        Core.add(horizontal_image,vertical_image,mask_image);
        this.saveImage("out-table/4-mask.png",mask_image);

        /*
         * 經過 bitwise_and 定位橫線、垂直線交匯的點
         */
        Mat points_image = new Mat();
        Core.bitwise_and(horizontal_image, vertical_image, points_image);
        this.saveImage("out-table/5-points.png",points_image);

        /*
         * 經過 findContours 找輪廓
         *
         * 第一個參數,是輸入圖像,圖像的格式是8位單通道的圖像,而且被解析爲二值圖像(即圖中的全部非零像素之間都是相等的)。
         * 第二個參數,是一個 MatOfPoint 數組,在多數實際的操做中便是STL vectors的STL vector,這裏將使用找到的輪廓的列表進行填充(即,這將是一個contours的vector,其中contours[i]表示一個特定的輪廓,這樣,contours[i][j]將表示contour[i]的一個特定的端點)。
         * 第三個參數,hierarchy,這個參數能夠指定,也能夠不指定。若是指定的話,輸出hierarchy,將會描述輸出輪廓樹的結構信息。0號元素表示下一個輪廓(同一層級);1號元素表示前一個輪廓(同一層級);2號元素表示第一個子輪廓(下一層級);3號元素表示父輪廓(上一層級)
         * 第四個參數,輪廓的模式,將會告訴OpenCV你想用何種方式來對輪廓進行提取,有四個可選的值:
         *      CV_RETR_EXTERNAL (0):表示只提取最外面的輪廓;
         *      CV_RETR_LIST (1):表示提取全部輪廓並將其放入列表;
         *      CV_RETR_CCOMP (2):表示提取全部輪廓並將組織成一個兩層結構,其中頂層輪廓是外部輪廓,第二層輪廓是「洞」的輪廓;
         *      CV_RETR_TREE (3):表示提取全部輪廓並組織成輪廓嵌套的完整層級結構。
         * 第五個參數,見識方法,即輪廓如何呈現的方法,有三種可選的方法:
         *      CV_CHAIN_APPROX_NONE (1):將輪廓中的全部點的編碼轉換成點;
         *      CV_CHAIN_APPROX_SIMPLE (2):壓縮水平、垂直和對角直線段,僅保留它們的端點;
         *      CV_CHAIN_APPROX_TC89_L1  (3)or CV_CHAIN_APPROX_TC89_KCOS(4):應用Teh-Chin鏈近似算法中的一種風格
         * 第六個參數,偏移,可選,若是是定,那麼返回的輪廓中的全部點均做指定量的偏移
         */
        List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
        Mat hierarchy = new Mat();
        Imgproc.findContours(mask_image,contours,hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE,new Point(0,0));


        List<MatOfPoint> contours_poly = contours;
        Rect[] boundRect = new Rect[contours.size()];

        LinkedList<Mat> tables = new LinkedList<Mat>();

        //循環全部找到的輪廓-點
        for(int i=0 ; i< contours.size(); i++){

            MatOfPoint point = contours.get(i);
            MatOfPoint contours_poly_point = contours_poly.get(i);

            /*
             * 獲取區域的面積
             * 第一個參數,InputArray contour:輸入的點,通常是圖像的輪廓點
             * 第二個參數,bool oriented = false:表示某一個方向上輪廓的的面積值,順時針或者逆時針,通常選擇默認false
             */
            double area = Imgproc.contourArea(contours.get(i));
            //若是小於某個值就忽略,表明是雜線不是表格
            if(area < 100){
                continue;
            }

            /*
             * approxPolyDP 函數用來逼近區域成爲一個形狀,true值表示產生的區域爲閉合區域。好比一個帶點幅度的曲線,變成折線
             *
             * MatOfPoint2f curve:像素點的數組數據。
             * MatOfPoint2f approxCurve:輸出像素點轉換後數組數據。
             * double epsilon:判斷點到相對應的line segment 的距離的閾值。(距離大於此閾值則捨棄,小於此閾值則保留,epsilon越小,折線的形狀越「接近」曲線。)
             * bool closed:曲線是否閉合的標誌位。
             */
            Imgproc.approxPolyDP(new MatOfPoint2f(point.toArray()),new MatOfPoint2f(contours_poly_point.toArray()),3,true);

            //爲將這片區域轉化爲矩形,此矩形包含輸入的形狀
            boundRect[i] = Imgproc.boundingRect(contours_poly.get(i));

            // 找到交匯處的的表區域對象
            Mat table_image = points_image.submat(boundRect[i]);

            List<MatOfPoint> table_contours = new ArrayList<MatOfPoint>();
            Mat joint_mat = new Mat();
            Imgproc.findContours(table_image, table_contours,joint_mat, Imgproc.RETR_CCOMP, Imgproc.CHAIN_APPROX_SIMPLE);
            //從表格的特性看,若是這片區域的點數小於4,那就表明沒有一個完整的表格,忽略掉
            if (table_contours.size() < 4)
                continue;

            //保存圖片
            tables.addFirst(source_image.submat(boundRect[i]).clone());

            //將矩形畫在原圖上
            Imgproc.rectangle(source_image, boundRect[i].tl(), boundRect[i].br(), new Scalar(0, 255, 0), 1, 8, 0);

        }

        for(int i=0; i< tables.size(); i++ ){

            //拿到表格後,能夠對錶格再次處理,好比 OCR 識別等
            this.saveImage("out-table/6-table-"+(i+1)+".png",tables.get(i));
        }

        this.saveImage("out-table/7-source.png",source_image);

    }

    private void saveImage(String path,Mat image){

        String outPath = this.test_file_path + File.separator + path;

        File file = new File(outPath);
        //目錄是否存在
        this.dirIsExist(file.getParent());

        Imgcodecs.imwrite(outPath,image);

    }

    private void dirIsExist(String dirPath){
        File dir = new File(dirPath);
        if(!dir.exists()){
            dir.mkdirs();
        }
    }

    /**
     * 加載動態庫
     */
    private static void loadLibraries() {

        try {
            String osName = System.getProperty("os.name");
            String opencvpath = System.getProperty("user.dir");

            //windows
            if(osName.startsWith("Windows")) {
                int bitness = Integer.parseInt(System.getProperty("sun.arch.data.model"));
                //32位系統
                if(bitness == 32) {
                    opencvpath=opencvpath+"\\opencv\\x86\\Your path to .dll";
                }
                //64位系統
                else if (bitness == 64) {
                    opencvpath=opencvpath+"\\opencv\\x64\\Your path to .dll";
                } else {
                    opencvpath=opencvpath+"\\opencv\\x86\\Your path to .dll";
                }
            }
            // mac os
            else if(osName.equals("Mac OS X")){
                opencvpath = "/usr/local/Cellar/opencv/3.4.0_1/share/OpenCV/java/libopencv_java340.dylib";
            }
            System.out.println(opencvpath);
            System.load(opencvpath);
        } catch (Exception e) {
            throw new RuntimeException("Failed to load opencv native library", e);
        }
    }

4. 實現效果

輸入圖片說明

5. 參考資料

OpenCV處理拍照表格 此文是一個專題,有多篇windows

OpenCV-檢測並提取表格數組

廣告欄: 歡迎關注個人 我的博客app

相關文章
相關標籤/搜索