OpenCV3 识别图中表格

关于 JAVA 学习 OpenCV 的内容,函数讲解。内容我均整理在 GitHubd的OpenCV3-Study-JAVA

OpenCV 3 识别图中表格-Java 实现

1. 说明

网上大部分资料,都是针对 C++的,python、java 的例子太少了。所以最近在做这个的时候,把他记录下来,也可以帮助一些人少走弯路。

OpenCV 确实强大,强大到每一个方法,都能 google 到一篇专题文章,在写的过程中,参考了许多资料,最终完成了实现和注释。

但是这仅仅是入门,找到表格后的利用才是后面的核心。比如:

  1. 表格的 OCR 识别,识别表头,内容数据,形成结构化数据。
  2. 图片按照顺序,转 Word文档或者保存为 html,这样就可以完成格式的转化,方便在 web 端查看,用户下载。
  3. 其他利用...

本文仅针对效果较好的,无倾斜,背景干净的图片进行识别。复杂的情况会可能无法满足,需要进一步处理。仅仅是个入门。

2. 开发环境

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

因为在 mac 下通过 brew 安装的 opencv ,所以包都是跟当前系统匹配的,安装目录也是一致的。

Windows 下需要根据自己的系统环境,位数,修改代码的loadLibraries,决定加载的动态库文件。

3. 代码实现

1import org.junit.Test; 2import org.opencv.core.*; 3import org.opencv.imgcodecs.Imgcodecs; 4import org.opencv.imgproc.Imgproc; 5import org.opencv.objdetect.CascadeClassifier; 6 7import java.io.File; 8import java.util.*; 9 10/** 11 * @Author : alexliu 12 * @Description : opencv 测试 13 * @Date : Create at 下午3:12 2018/1/26 14 */ 15public class TestOpenCV { 16 17 String test_file_path = System.getProperty("user.dir") + File.separator + "testFiles"; 18 19 static { 20 //加载动态链接库时,不使用System.loadLibrary(xxx);。 而是使用 绝对路径加载:System.load(xxx); 21 22 /* 23 * 加载动态库 24 * 25 * 第一种方式 --------------System.loadLibrary(Core.NATIVE_LIBRARY_NAME); 26 * loadLibrary(Core.NATIVE_LIBRARY_NAME); //使用这种方式加载,需要在 IDE 中配置参数. 27 * Eclipse 配置:http://opencv-java-tutorials.readthedocs.io/en/latest/01-installing-opencv-for-java.html#set-up-opencv-for-java-in-eclipse 28 * IDEA 配置 :http://opencv-java-tutorials.readthedocs.io/en/latest/01-installing-opencv-for-java.html#set-up-opencv-for-java-in-other-ides-experimental 29 * 30 * 第二种方式 --------------System.load(path of lib); 31 * System.load(your path of lib) ,方式比较灵活,可根据环境的系统,位数,决定加载内容 32 */ 33 loadLibraries(); 34 } 35 36/** 37 * 读取 table 38 */ 39 @Test 40 public void readTable(){ 41 42 Mat source_image = Imgcodecs.imread(test_file_path + "/table-3.jpg"); 43 //灰度处理 44 Mat gray_image = new Mat(source_image.height(), source_image.width(), CvType.CV_8UC1); 45 Imgproc.cvtColor(source_image,gray_image,Imgproc.COLOR_RGB2GRAY); 46 47 //二值化 48 Mat thresh_image = new Mat(source_image.height(), source_image.width(), CvType.CV_8UC1); 49 // C 负数,取反色,超过阈值的为黑色,其他为白色 50 Imgproc.adaptiveThreshold(gray_image, thresh_image,255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY,7,-2); 51 this.saveImage("out-table/1-thresh.png",thresh_image); 52 53 //克隆一个 Mat,用于提取水平线 54 Mat horizontal_image = thresh_image.clone(); 55 56 //克隆一个 Mat,用于提取垂直线 57 Mat vertical_image = thresh_image.clone(); 58 59 /* 60 * 求水平线 61 * 1. 根据页面的列数(可以理解为宽度),将页面化成若干的扫描区域 62 * 2. 根据扫描区域的宽度,创建一根水平线 63 * 3. 通过腐蚀、膨胀,将满足条件的区域,用水平线勾画出来 64 * 65 * scale 越大,识别的线越多,因为,越大,页面划定的区域越小,在腐蚀后,多行文字会形成一个块,那么就会有一条线 66 * 在识别表格时,我们可以理解线是从页面左边 到 页面右边的,那么划定的区域越小,满足的条件越少,线条也更准确 67 */ 68 int scale = 10; 69 int horizontalsize = horizontal_image.cols() / scale; 70 // 为了获取横向的表格线,设置腐蚀和膨胀的操作区域为一个比较大的横向直条 71 Mat horizontalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(horizontalsize, 1)); 72 // 先腐蚀再膨胀 new Point(-1, -1) 以中心原点开始 73 // iterations 最后一个参数,迭代次数,越多,线越多。在页面清晰的情况下1次即可。 74 Imgproc.erode(horizontal_image, horizontal_image, horizontalStructure, new Point(-1, -1),1); 75 Imgproc.dilate(horizontal_image, horizontal_image, horizontalStructure, new Point(-1, -1),1); 76 this.saveImage("out-table/2-horizontal.png",horizontal_image); 77 78 // 求垂直线 79 scale = 30; 80 int verticalsize = vertical_image.rows() / scale; 81 Mat verticalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(1, verticalsize)); 82 Imgproc.erode(vertical_image, vertical_image, verticalStructure, new Point(-1, -1),1); 83 Imgproc.dilate(vertical_image, vertical_image, verticalStructure, new Point(-1, -1),1); 84 this.saveImage("out-table/3-vertical.png",vertical_image); 85 86 /* 87 * 合并线条 88 * 将垂直线,水平线合并为一张图 89 */ 90 Mat mask_image = new Mat(); 91 Core.add(horizontal_image,vertical_image,mask_image); 92 this.saveImage("out-table/4-mask.png",mask_image); 93 94 /* 95 * 通过 bitwise_and 定位横线、垂直线交汇的点 96 */ 97 Mat points_image = new Mat(); 98 Core.bitwise_and(horizontal_image, vertical_image, points_image); 99 this.saveImage("out-table/5-points.png",points_image); 100 101 /* 102 * 通过 findContours 找轮廓 103 * 104 * 第一个参数,是输入图像,图像的格式是8位单通道的图像,并且被解析为二值图像(即图中的所有非零像素之间都是相等的)。 105 * 第二个参数,是一个 MatOfPoint 数组,在多数实际的操作中即是STL vectors的STL vector,这里将使用找到的轮廓的列表进行填充(即,这将是一个contours的vector,其中contours[i]表示一个特定的轮廓,这样,contours[i][j]将表示contour[i]的一个特定的端点)。 106 * 第三个参数,hierarchy,这个参数可以指定,也可以不指定。如果指定的话,输出hierarchy,将会描述输出轮廓树的结构信息。0号元素表示下一个轮廓(同一层级);1号元素表示前一个轮廓(同一层级);2号元素表示第一个子轮廓(下一层级);3号元素表示父轮廓(上一层级) 107 * 第四个参数,轮廓的模式,将会告诉OpenCV你想用何种方式来对轮廓进行提取,有四个可选的值: 108 * CV_RETR_EXTERNAL (0):表示只提取最外面的轮廓; 109 * CV_RETR_LIST (1):表示提取所有轮廓并将其放入列表; 110 * CV_RETR_CCOMP (2):表示提取所有轮廓并将组织成一个两层结构,其中顶层轮廓是外部轮廓,第二层轮廓是“洞”的轮廓; 111 * CV_RETR_TREE (3):表示提取所有轮廓并组织成轮廓嵌套的完整层级结构。 112 * 第五个参数,见识方法,即轮廓如何呈现的方法,有三种可选的方法: 113 * CV_CHAIN_APPROX_NONE (1):将轮廓中的所有点的编码转换成点; 114 * CV_CHAIN_APPROX_SIMPLE (2):压缩水平、垂直和对角直线段,仅保留它们的端点; 115 * CV_CHAIN_APPROX_TC89_L1 (3)or CV_CHAIN_APPROX_TC89_KCOS(4):应用Teh-Chin链近似算法中的一种风格 116 * 第六个参数,偏移,可选,如果是定,那么返回的轮廓中的所有点均作指定量的偏移 117 */ 118 List<MatOfPoint> contours = new ArrayList<MatOfPoint>(); 119 Mat hierarchy = new Mat(); 120 Imgproc.findContours(mask_image,contours,hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE,new Point(0,0)); 121 122 123 List<MatOfPoint> contours_poly = contours; 124 Rect[] boundRect = new Rect[contours.size()]; 125 126 LinkedList<Mat> tables = new LinkedList<Mat>(); 127 128 //循环所有找到的轮廓-点 129 for(int i=0 ; i< contours.size(); i++){ 130 131 MatOfPoint point = contours.get(i); 132 MatOfPoint contours_poly_point = contours_poly.get(i); 133 134 /* 135 * 获取区域的面积 136 * 第一个参数,InputArray contour:输入的点,一般是图像的轮廓点 137 * 第二个参数,bool oriented = false:表示某一个方向上轮廓的的面积值,顺时针或者逆时针,一般选择默认false 138 */ 139 double area = Imgproc.contourArea(contours.get(i)); 140 //如果小于某个值就忽略,代表是杂线不是表格 141 if(area < 100){ 142 continue; 143 } 144 145 /* 146 * approxPolyDP 函数用来逼近区域成为一个形状,true值表示产生的区域为闭合区域。比如一个带点幅度的曲线,变成折线 147 * 148 * MatOfPoint2f curve:像素点的数组数据。 149 * MatOfPoint2f approxCurve:输出像素点转换后数组数据。 150 * double epsilon:判断点到相对应的line segment 的距离的阈值。(距离大于此阈值则舍弃,小于此阈值则保留,epsilon越小,折线的形状越“接近”曲线。) 151 * bool closed:曲线是否闭合的标志位。 152 */ 153 Imgproc.approxPolyDP(new MatOfPoint2f(point.toArray()),new MatOfPoint2f(contours_poly_point.toArray()),3,true); 154 155 //为将这片区域转化为矩形,此矩形包含输入的形状 156 boundRect[i] = Imgproc.boundingRect(contours_poly.get(i)); 157 158 // 找到交汇处的的表区域对象 159 Mat table_image = points_image.submat(boundRect[i]); 160 161 List<MatOfPoint> table_contours = new ArrayList<MatOfPoint>(); 162 Mat joint_mat = new Mat(); 163 Imgproc.findContours(table_image, table_contours,joint_mat, Imgproc.RETR_CCOMP, Imgproc.CHAIN_APPROX_SIMPLE); 164 //从表格的特性看,如果这片区域的点数小于4,那就代表没有一个完整的表格,忽略掉 165 if (table_contours.size() < 4) 166 continue; 167 168 //保存图片 169 tables.addFirst(source_image.submat(boundRect[i]).clone()); 170 171 //将矩形画在原图上 172 Imgproc.rectangle(source_image, boundRect[i].tl(), boundRect[i].br(), new Scalar(0, 255, 0), 1, 8, 0); 173 174 } 175 176 for(int i=0; i< tables.size(); i++ ){ 177 178 //拿到表格后,可以对表格再次处理,比如 OCR 识别等 179 this.saveImage("out-table/6-table-"+(i+1)+".png",tables.get(i)); 180 } 181 182 this.saveImage("out-table/7-source.png",source_image); 183 184 } 185 186 private void saveImage(String path,Mat image){ 187 188 String outPath = this.test_file_path + File.separator + path; 189 190 File file = new File(outPath); 191 //目录是否存在 192 this.dirIsExist(file.getParent()); 193 194 Imgcodecs.imwrite(outPath,image); 195 196 } 197 198 private void dirIsExist(String dirPath){ 199 File dir = new File(dirPath); 200 if(!dir.exists()){ 201 dir.mkdirs(); 202 } 203 } 204 205 /** 206 * 加载动态库 207 */ 208 private static void loadLibraries() { 209 210 try { 211 String osName = System.getProperty("os.name"); 212 String opencvpath = System.getProperty("user.dir"); 213 214 //windows 215 if(osName.startsWith("Windows")) { 216 int bitness = Integer.parseInt(System.getProperty("sun.arch.data.model")); 217 //32位系统 218 if(bitness == 32) { 219 opencvpath=opencvpath+"\\opencv\\x86\\Your path to .dll"; 220 } 221 //64位系统 222 else if (bitness == 64) { 223 opencvpath=opencvpath+"\\opencv\\x64\\Your path to .dll"; 224 } else { 225 opencvpath=opencvpath+"\\opencv\\x86\\Your path to .dll"; 226 } 227 } 228 // mac os 229 else if(osName.equals("Mac OS X")){ 230 opencvpath = "/usr/local/Cellar/opencv/3.4.0_1/share/OpenCV/java/libopencv_java340.dylib"; 231 } 232 System.out.println(opencvpath); 233 System.load(opencvpath); 234 } catch (Exception e) { 235 throw new RuntimeException("Failed to load opencv native library", e); 236 } 237 } 238

4. 实现效果

输入图片说明

5. 参考资料

OpenCV处理拍照表格 此文是一个专题,有多篇

OpenCV-检测并提取表格

广告栏: 欢迎关注我的 个人博客

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )