opencv之表格提取

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014665013/article/details/82415427

最近接手一cv项目,挺恶心的,不过也算挺有意思的,cv小白的挣扎之路
网上有很多相关的教程,但是会发现处理效果很多都算是一般般,反正我实验的时候效果都不太好。。。

提取的时候因为甲方的表格各式各样,所以为了提高鲁棒性,只能通过多种方法进行尝试,并确定最终的方案,因此针对普通线条完全的表格,其实代码其中一部分代码就可以实现功能了。


使用前需要安装opencv,我的开发平台是windows的,安装教程详见链接,其他开发平台详见链接

思路整理

整体策略:
有好多种类型的表格,如下图-1,当然第一种表格式最容易提取的,opencv稍做操作可能就能提取成功;但是遇到第二种这种表格就比较麻烦了,因为增加了底色,并且底色还比较深;对于第三种,更是难一些,因为他的表格变的很花哨,没有黑色的边框线条,最之致命的还没有边框线条,所以再识别的时候基于多种表格特征,需要通过多种提取方式,最终在进行筛选和对比抽取结果,从而增强整体提取效果的鲁棒性。

这里写图片描述

图-1

所以基于此,我们的目的是找出表格的轮廓线和中间的所有小的边框,整体抽取策略制定如下:
(1)通过常规方式进行处理:

  • 将常规的图片转化为灰度图像
  • 对灰度图片进行腐蚀,增强线条的强度,以及图片表格存在的质量问题导致线条有几个像素不连续的问题
  • 设置阈值,过滤图片底色等问题
  • 使用高斯模糊
  • 封装二值化
  • 定义横线提取规模,提取横线,对横线再次腐蚀和膨胀
  • 定义竖线提取规模,提取竖线,对竖线再次腐蚀和膨胀
  • 将横线和竖线叠加(为了便于观察,鼠标提取出表格内定点的坐标值),通过横竖线重新构成表格
  • 找出图像中的轮廓线(为了便于观察,画出轮廓线)
  • 对提取到的轮廓做多边形近似提取,这里做的是矩形提取
  • 对于提取出的矩形,找出表格的边框线坐标和内部小矩形的坐标
  • 计算并返回包围轮廓点集的最小矩形

(2)针对有底色的并且无边框黑色线条的表格,首先进行图像反转,然后采用上面方式进行提取。
(3)对上面两种方式提取的结果,最终采用提取cells(小矩形)数目多的方式作为最终提取效果。
完整代码详见git地址
整体逻辑入口:(对应步骤三)

public RectData getTableBoundingRectangles(Mat inImage) throws IOException {    
        //tmpfun(inImage);
        //inImage = Imgcodecs.imread("F:\\java_program_workspace\\pdf_github\\resources\\0.jpg",CvType.CV_8UC1);
        //getRectFormImageMat2();  //method2  此方法准确率较低,但是可以作为尝试
        RectData outInvert = disposeInvertTableImage(inImage); 
        RectData out = disposeTableImage(inImage);
        RectData finalRect;
        if(settings.hasDebugImages()) {
            System.out.println("原图像识别结果:\n\tboundary size:"+out.boundary.size()+" cells size:"+out.cells.size()
            +"\n图像反转识别结果:\n\tboundary size:"+outInvert.boundary.size()+" cells size:"+outInvert.cells.size());
        }
        if(out.cells.size()>outInvert.cells.size()) {
            finalRect = out;
        }else {
            finalRect = outInvert;
        }
        return finalRect;

    }

对应步骤1:

public RectData disposeTableImage(Mat inImage) {
        settings.setDebugFilePrefix("origiImage_");
        return getRectFormImageMat(inImage,inImage,settings.getBitThreshold());
    }

对于无线条表格,先反转在采取步骤一方式:

public RectData disposeInvertTableImage(Mat image) {
        settings.setDebugFilePrefix("inverImage_");
        Mat inImage = image.clone();
        Imgcodecs.imwrite(buildDebugFilename("original_grayscaled"), inImage);
         int num_rows = inImage.rows();
         int num_cols = inImage.cols();
         int [] dic = new int [1000];
         for (int i=0;i<1000;i++)
           dic[i] = 0;
         System.out.println("(总)num_rows:"+num_rows+"___num_col:"+num_cols);
         for (int row = 0; row < num_rows; row++) {
             for (int col= 0; col < num_cols; col++) {
                double revPoint = 255.0-inImage.get(row, col)[0];
                inImage.put(row, col, revPoint);
             } 
         }  
        Imgcodecs.imwrite(buildDebugFilename("original_reverse"), inImage);

        return getRectFormImageMat(inImage,image, settings.getBitInvertThreshold());
    }

核心算法,主要算法流程:

/*
     * 
     * 
     * 提取线条核心算法 
     *
     * @param inImage Input image
     * @return Mat 包含提取出的所有矩形和边框矩形
     * 
     * 
     */
    public RectData getRectFormImageMat(Mat inImage,Mat debugImage,double threshold) {

        Mat image = inImage.clone();


        Mat lines = new Mat();
        List<Rect> out = new ArrayList<>();

        Mat canny=new Mat(),gray=new Mat(),sobel=new Mat(), edge,erod=new Mat(), blur=new Mat();

        double src_height=inImage.cols(), src_width=inImage.rows();

        //若非灰度图片,先转为灰度
        if (inImage.channels() != 1) {
            cvtColor(inImage, gray, COLOR_BGR2GRAY);
        }else {
            gray = inImage.clone();
        }
        if (settings.hasDebugImages()) {
          Imgcodecs.imwrite(buildDebugFilename("original_grayscaled"), gray);
        }


        //腐蚀(黑色区域变大)
        int erodeSize = (int) (src_height / 200);
        if (erodeSize % 2 == 0)
            erodeSize++;
        Mat element = getStructuringElement(MORPH_RECT,new Size(3, 3));
        erode(gray, erod, element);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("erod"), erod);
        }




        Mat bit = binaryInvertedThreshold(erod ,threshold);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("invert_bit"),bit);
        }

        //高斯模糊化
        int blurSize = (int) (src_height / 200);
        if (blurSize % 2 == 0)
            blurSize++;
        GaussianBlur(erod, blur,new Size(blurSize, blurSize), 0, 0);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("blur"), blur);
        }
        //封装二值化
        Mat thresh = inImage.clone();
        adaptiveThreshold(bit, thresh, 255, 0, THRESH_BINARY, 15, -2);
        //Canny(blur, canny, low, high);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("thresh"), thresh);
        }
        /*
        这部分的思想是将线条从横纵的方向处理后抽取出来,再进行交叉,矩形的点,进而找到矩形区域的过程
        */
        // Create the images that will use to extract the horizonta and vertical lines
        Mat horizontal = thresh.clone();
        Mat vertical = thresh.clone();

        int scale = 20; // play with this variable in order to increase/decrease the amount of lines to be detected
        // Specify size on horizontal axis
        int horizontalsize = horizontal.cols() / scale;
        // Create structure element for extracting horizontal lines through morphology operations
        Mat horizontalStructure = getStructuringElement(MORPH_RECT, new Size(horizontalsize, 1));
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("horizontalStructure"), horizontalStructure);
        }
        // Apply morphology operations
        erode(horizontal, horizontal, horizontalStructure);
        dilate(horizontal, horizontal, horizontalStructure);
        // dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1)); // expand horizontal lines
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("horizontal"), horizontal);
        }

        // Specify size on vertical axis
        int scaleVer = 50;
        int verticalsize = vertical.rows() / scaleVer;
        // Create structure element for extracting vertical lines through morphology operations
        Mat verticalStructure = getStructuringElement(MORPH_RECT, new Size(1, verticalsize));
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("verticalStructure"), verticalStructure);
        }
        // Apply morphology operations
        erode(vertical, vertical, verticalStructure);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("vertical_erode"), vertical);
        }
        dilate(vertical, vertical, verticalStructure);
        //      dilate(vertical, vertical, verticalStructure, Point(-1, -1)); // expand vertical lines
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("vertical"), vertical);
        }

        Mat mask = new Mat();
        Core.add(horizontal, vertical, mask);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("mask"), mask);
        }


        // find the joints between the lines of the tables, we will use this information in order to descriminate tables from pictures (tables will contain more than 4 joints while a picture only 4 (i.e. at the corners))
        Mat joints = new Mat();
        bitwise_and(horizontal, vertical, joints);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("joints"), joints);
        }

        List<MatOfPoint> contours = new ArrayList<>();

        //int  [] modes =  new int[] {RETR_TREE,RETR_EXTERNAL};//这两个参数分别用于提取cells和 boundary(边框坐标)
        findContours(mask, contours, new Mat(),RETR_TREE , CHAIN_APPROX_SIMPLE);

        // draw contour
        Mat contourMask = bit.clone();
        drawContours(contourMask, contours, -1, new Scalar(255, 255, 255), Core.FILLED);
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("contour_mask"), contourMask);
        }


        List<MatOfPoint2f> contours_poly = new ArrayList<MatOfPoint2f>(contours.size());
        List<Rect> boundRect = new ArrayList<>();
        List<Mat> rois;

        if (settings.hasDebugImages()) {
            System.out.println("线条数目为:"+contours.size());
        }
        Mat outImage = inImage.clone();
        int count = 0;
        for (int i = 0; i < contours.size(); i++)
        {
            // find the area of each contour
            double area = contourArea(contours.get(i));

            // filter individual lines of blobs that might exist and they do not represent a table
            if (area < 50) // value is randomly chosen, you will need to find that by yourself with trial and error procedure
                continue;
            MatOfPoint2f contour2f = new MatOfPoint2f(contours.get(i).toArray());
            MatOfPoint2f tmPoint2f  = new MatOfPoint2f();



            Imgproc.approxPolyDP(contour2f, tmPoint2f, 3, true);

            if (settings.hasDebugImages()) {
                System.out.println("tmPoint2f.length:"+tmPoint2f.toArray().length);
            }
            MatOfPoint points = new MatOfPoint(tmPoint2f.toArray());
            Rect rect = Imgproc.boundingRect(points);
            if (rect.width<100 || rect.height<20) {
                continue;
            }
            count+=1;
            if (settings.hasDebugImages()) {
                System.out.println(count+":[("+rect.x+","+rect.y+"),("+(rect.x+rect.width)+","+(rect.y+rect.height)+")"+"] === rectangle information: [center:("+(rect.x*2+rect.width)/2+","+(rect.y*2+rect.height)/2+")   w:"+rect.width+" h:"+rect.height+"]\n");
            }
            boundRect.add(rect);

            rectangle(outImage, rect.tl(), rect.br(),new  Scalar(0, 255, 0), 10);

        }
        if (settings.hasDebugImages()) {
            Imgcodecs.imwrite(buildDebugFilename("tmp_result"), outImage);
        }
        Collections.reverse(boundRect);
        List<Rect> cellRectList = new ArrayList<>();
        List<Rect> boundaryRectList = new ArrayList<>();
        int countCell = 0 ,countBoundary = 0;
        for (int i=0;i<boundRect.size();i++) {
            Rect rect = boundRect.get(i);
            boolean cellMark = true;
            for (int j=0;j<boundRect.size();j++) {
                if (j==i) {
                    continue;
                }
                Rect tmpRect = boundRect.get(j);
                int centerX = tmpRect.x;
                int centerY = tmpRect.y;
                if (rect.x<=centerX && rect.y<=centerY && rect.x+rect.width>=centerX && rect.y+rect.height>=centerY) {
                    cellMark = false;
                    break;
                }

            }
            Mat tmpout = debugImage.clone(); 
            rectangle(tmpout, rect.tl(), rect.br(),new  Scalar(0, 255, 0), 10);
            if(cellMark) {
                countCell+=1;
                cellRectList.add(rect);
                if (settings.hasDebugImages()) {
                    Imgcodecs.imwrite(buildDebugFilename(String.format("cellbox_%03d", countCell)), tmpout);
                }
            }else {
                countBoundary +=1;
                boundaryRectList.add(rect);
                if (settings.hasDebugImages()) {
                    Imgcodecs.imwrite(buildDebugFilename(String.format("boundaryBox_%03d", countBoundary)), tmpout);
                }
            }
        }
        if(settings.hasDebugImages()) {
            System.out.println("原矩阵数目"+boundRect.size()+"\n过滤后cell数目为:"+cellRectList.size()+"\n过滤后边框数目为:"+boundaryRectList.size());
        }
        RectData rectData = new RectData();
        rectData.boundary= boundaryRectList;
        rectData.cells = cellRectList;

        return rectData;
    }

参考文献

猜你喜欢

转载自blog.csdn.net/u014665013/article/details/82415427
今日推荐