java openvc java opencv 提取图片中的表格_新手上路

原图

java openvc java opencv 提取图片中的表格_新手上路_02

处理完成后


最近公司在合并藏文双层pdf的时候遇到图片中表格影响合并效果,于是用opencv进行了处理,在网上看了很多案例,但是用java写的比较少,所以分享一下;

第一次写,直接上干货:

/**
     * 删除table
     * @author mc
     * @time 2019-9-26
     */
    public void delTable(String url,String path){
        Mat srcImage = Imgcodecs.imread(url);
        ImgUtil.save(path + "/source.png",srcImage);
        // 图片二值化
        Mat thresh_image = ImgUtil.ImgBinarization(srcImage);
        ImgUtil.save(path + "/binarization.png",thresh_image);
        
        //获取图片上的竖线 1
        Mat vertical_line = ImgUtil.getLines(thresh_image,Math.PI/1,1);
        ImgUtil.save(path + "/vertical_line.png",vertical_line);
        
        //获取图片上的横线 2
        Mat horizontal_line = ImgUtil.getLines(thresh_image,Math.PI/180,2);
        ImgUtil.save(path + "/horizontal_line.png",horizontal_line);
        
        // 将横线和竖线合并为一张图片 
        Mat mask_image = new Mat();
        Core.add(horizontal_line,vertical_line,mask_image);
        ImgUtil.save(path + "/mask_image.png",mask_image);
        /*
         * 通过 bitwise_and 定位横线、垂直线交汇的点
         */
        Mat points_image = new Mat();
        Core.bitwise_and(horizontal_line, vertical_line, points_image);
        ImgUtil.save(path + "/points_image.png",points_image);
        
        /**
         * 处理图片中的表格
         */
        ImgUtil.editTable(srcImage,mask_image,points_image,path);
        
        //将处理后的图像保存为文件,可做调试使用
        ImgUtil.save(path + "/result_image.png",srcImage);
        
    }

处理图片类

package com.ltkj.test;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

public class ImgUtil {
    /**
     * 根据角度分辨率获取直线
     * 并将图片二值化返回
     * @author mc
     * @time 2019-9-26
     */
    public static Mat getLines(Mat thresh_image,double theta,int i){
        Mat lines = new Mat();
        // 检测直线:图片,线段,角度分辨率,半径分辨率,判断直线点数的阈值,最小长度,最大间隙
        //去除table
//        Imgproc.HoughLinesP(thresh_image, lines, 1, theta, 200, 100, 1);
        //去除矩形图片
        Imgproc.HoughLinesP(thresh_image, lines, 1, theta, 200, 100, 1);
        Mat allLines = thresh_image.clone();
        
        Point p = new Point(0,0);
        // 设置宽和高
        Size s = new Size(allLines.width(),allLines.height());
        Rect boundRect = new Rect(p,s);
        //将白色矩形覆盖至表格,实现删除图像中表格内容
        Imgproc.rectangle(allLines, boundRect.tl(), boundRect.br(), new Scalar(0, 0, 0), -1, 4, 0);
        for (int x = 0; x < lines.rows(); x++){
            double[] vec = lines.get(x, 0);
            double x1 = vec[0], y1 = vec[1], x2 = vec[2], y2 = vec[3];
//            System.out.println(String.format("y1:%s----y2:%s", new Object[]{y1,y2}));
            // i:1 为竖线 2为横线
            if(i==1){
                //将竖线拉长两百
                y1 = y1 + (thresh_image.height()*(0.002));
                y2 = y2 - (thresh_image.height()*(0.002));
                Point start = new Point(x1, y1);
                Point end = new Point(x2, y2);
                // 图片,开始坐标,结束坐标,颜色,线条宽度,直线类型,比例
                Imgproc.line(allLines, start, end, new Scalar(255, 255, 255, 255), 5, Imgproc.LINE_4, 0);
            }
            if(i==2 && y1==y2){
                //将横线拉长两百
                x1 = x1 - (thresh_image.width()*(0.02));
                x2 = x2 + (thresh_image.width()*(0.02));
                Point start = new Point(x1, y1);
                Point end = new Point(x2, y2);
                // 图片,开始坐标,结束坐标,颜色,线条宽度,直线类型,比例
                Imgproc.line(allLines, start, end, new Scalar(255, 255, 255, 255), 5, Imgproc.LINE_4, 0);
            }
        }
        return allLines;
    }
    
    /**
     * 根据图片进行二值化返回
     * @author mc
     * @time 2019-9-26
     */
    public static Mat ImgBinarization(Mat srcImage){
        
        Mat dstImage = srcImage.clone();
        Imgproc.Canny(srcImage, dstImage, 400, 500, 5, false);
        //灰度处理
        Mat gray_image = new Mat(srcImage.height(), srcImage.width(), CvType.CV_8UC1);
        Imgproc.cvtColor(srcImage,gray_image,Imgproc.COLOR_RGB2GRAY);

        //二值化
        Mat thresh_image = new Mat(srcImage.height(), srcImage.width(), CvType.CV_8UC1);
        // C 负数,取反色,超过阈值的为黑色,其他为白色
        Imgproc.adaptiveThreshold(gray_image, thresh_image,255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY,7,-2);
        return thresh_image;
    }
    /**
     * 将图片中的表格处理掉
     * @author mc
     * @time 2019-9-26
     * srcImage 原图
     * mask_image 横线竖线合并图
     * points_image 焦点图
     * path 保存文件夹名
     */
    public static void editTable(Mat srcImage, Mat mask_image, Mat points_image,String path){
         /*
         * 通过 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>();

        int tableNums = 0;    //图像中的表格个数
        //循环所有找到的轮廓-点
        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(srcImage.submat(boundRect[i]).clone());

            //将矩形画在原图上
//            Imgproc.rectangle(srcImage, boundRect[i].tl(), boundRect[i].br(), new Scalar(255, 0, 25), 1, 8, 0);
            
            //将白色矩形覆盖至表格,实现删除图像中表格内容
            Imgproc.rectangle(srcImage, boundRect[i].tl(), boundRect[i].br(), new Scalar(255, 255, 255), -1, 4, 0);
            tableNums++;
        }
//        for(int i=0;i<boundRect.length;i++){
//            System.out.println(boundRect[i]);
//        }
        //所有的表格区域图像
        for(int i=0; i< tables.size(); i++ ){

            //拿到表格后,可以对表格再次处理,比如 OCR 识别等
            save(path + "/table-"+(i+1)+".png",tables.get(i));
        }
        System.out.println("表格数量:"+tableNums);
    }
    
    /**
     * 保存图片
     * @author mc
     * @time 2019-9-25
     */
    public static void save(String name,Mat mat){
        String outPath = "C:/Users/Administrator/Desktop/opencv/" + name;
        File file = new File(outPath);
        //目录是否存在
        dirIsExist(file.getParent());
        Imgcodecs.imwrite(outPath,mat);
    }
    public static void dirIsExist(String dirPath){
        File dir = new File(dirPath);
        if(!dir.exists()){
            dir.mkdirs();
        }
    }
}