版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014665013/article/details/82415427
最近接手一cv项目,挺恶心的,不过也算挺有意思的,cv小白的挣扎之路
网上有很多相关的教程,但是会发现处理效果很多都算是一般般,反正我实验的时候效果都不太好。。。
提取的时候因为甲方的表格各式各样,所以为了提高鲁棒性,只能通过多种方法进行尝试,并确定最终的方案,因此针对普通线条完全的表格,其实代码其中一部分代码就可以实现功能了。
使用前需要安装opencv,我的开发平台是windows的,安装教程详见链接,其他开发平台详见链接
思路整理
整体策略:
有好多种类型的表格,如下图-1,当然第一种表格式最容易提取的,opencv稍做操作可能就能提取成功;但是遇到第二种这种表格就比较麻烦了,因为增加了底色,并且底色还比较深;对于第三种,更是难一些,因为他的表格变的很花哨,没有黑色的边框线条,最之致命的还没有边框线条,所以再识别的时候基于多种表格特征,需要通过多种提取方式,最终在进行筛选和对比抽取结果,从而增强整体提取效果的鲁棒性。
所以基于此,我们的目的是找出表格的轮廓线和中间的所有小的边框,整体抽取策略制定如下:
(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;
}