OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

实习的公司有对增值税发票进行OCR识别的需求。OCR部分实现起来不难(有现成的SDK可以调用),但是实际情况中,用户提供的照片中的发票往往会有一些偏斜,而公司提供的OCR SDK并不能检测偏斜的字符,因此需要先进行图像预处理,摆正发票(效果类似于Office Lens)。要实现的效果如下图:

OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

算法的具体步骤如下:

转灰度,降噪

边缘检测

轮廓提取

寻找凸包,拟合多边形

找到最大的正方形

重新执行步骤3,提升精度

找到长方形四条边,即为纸张的外围四边形

透视变换,提取四边形

纸张四边形检测与提取的教程网上比较少,而且也不够详细,这是我写这篇博文的动力。接下来我会一步步详细分析这个算法:

1、转灰度,降噪

第一步就是对图像进行预处理。为了应用Canny算法要先将图片转为灰度图。由于要进行边缘检测所以肯定要预先降噪,降噪算法方面尝试了Gaussian滤波与MeanShift滤波。MeanShift滤波的效果比Gaussian滤波要好,可以把桌面的纹理,发票内的字符等冗余信息都涂抹掉,但是由于MeanShift聚类效率实在是低,因此还是采用了Gaussian滤波。

// MeanShift滤波,降噪(速度太慢!) //Imgproc.pyrMeanShiftFiltering(img, img, 30, 10); // 彩色转灰度 Imgproc.cvtColor(img, img, Imgproc.COLOR_BGR2GRAY); // 高斯滤波,降噪 Imgproc.GaussianBlur(img, img, new Size(3,3), 2, 2);

OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

2、边缘检测

接下来进行边缘检测。这是整个算法非常关键的一步,阈值选的好不好直接关系到后续的轮廓线是否正确,以及能否检测出四边形。
采用Canny算法检测边缘,Canny算法的原理这里不再赘述,网上有很多优质的资源可以帮助你理解这个伟大的边缘检测算法。阈值选取方面,要尽量选取低阈值!!!因为如果阈值选取太高,会导致发票的外围四边形未闭合,导致无法正确寻找轮廓线。低阈值虽然会产生很多噪点,但是由于后续还要进行轮廓线检测和多边形拟合,所以噪点会在后续步骤被忽略。
Canny算法过后,要再执行一次膨胀操作,确保发票边缘已经连接。

// Canny边缘检测 Imgproc.Canny(img, img, 20, 60, 3, false); // 膨胀,连接边缘 Imgproc.dilate(img, img, new Mat(), new Point(-1,-1), 3, 1, new Scalar(1));

OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

3、轮廓提取

对边缘检测的结果图再进行轮廓提取,使用的是OpenCV内置的findContours函数,该函数的原理详见OpenCV Reference Manual。实际应用中采用了RETR_EXTERNAL参数,只提取外部的轮廓。

List<MatOfPoint> contours = new ArrayList<>(); Mat hierarchy = new Mat(); Imgproc.findContours(img, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

4、寻找凸包,拟合多边形

检测出的轮廓看起来依旧很乱,该怎么办呢?首先对于每个轮廓,求出它的凸包,并使用多边形拟合凸包边框。接下来筛选出面积大于某个阈值的,而且四个角都约等于九十度的凸四边形。找出的凸四边形就是候选的外围四边形。
这段代码中会有很多类型转换。OpenCV Java中有MatOfInt,MatOfPoint,MatOfPoint2f等等许多类型,Imgproc中函数的参数类型也五花八门,因此调用函数的时候要格外注意。
之后的代码中,调用的自己实现的函数都会贴在代码的最上方,拷贝代码的时候要注意不要拷错了哦。

// 根据三个点计算中间那个点的夹角 pt1 pt0 pt2 private static double getAngle(Point pt1, Point pt2, Point pt0) { double dx1 = pt1.x - pt0.x; double dy1 = pt1.y - pt0.y; double dx2 = pt2.x - pt0.x; double dy2 = pt2.y - pt0.y; return (dx1*dx2 + dy1*dy2)/Math.sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10); } // 找出轮廓对应凸包的四边形拟合 List<MatOfPoint> squares = new ArrayList<>(); List<MatOfPoint> hulls = new ArrayList<>(); MatOfInt hull = new MatOfInt(); MatOfPoint2f approx = new MatOfPoint2f(); approx.convertTo(approx, CvType.CV_32F); for (MatOfPoint contour: contours) { // 边框的凸包 Imgproc.convexHull(contour, hull); // 用凸包计算出新的轮廓点 Point[] contourPoints = contour.toArray(); int[] indices = hull.toArray(); List<Point> newPoints = new ArrayList<>(); for (int index : indices) { newPoints.add(contourPoints[index]); } MatOfPoint2f contourHull = new MatOfPoint2f(); contourHull.fromList(newPoints); // 多边形拟合凸包边框(此时的拟合的精度较低) Imgproc.approxPolyDP(contourHull, approx, Imgproc.arcLength(contourHull, true)*0.02, true); // 筛选出面积大于某一阈值的,且四边形的各个角度都接近直角的凸四边形 MatOfPoint approxf1 = new MatOfPoint(); approx.convertTo(approxf1, CvType.CV_32S); if (approx.rows() == 4 && Math.abs(Imgproc.contourArea(approx)) > 40000 && Imgproc.isContourConvex(approxf1)) { double maxCosine = 0; for (int j = 2; j < 5; j++) { double cosine = Math.abs(getAngle(approxf1.toArray()[j%4], approxf1.toArray()[j-2], approxf1.toArray()[j-1])); maxCosine = Math.max(maxCosine, cosine); } // 角度大概72度 if (maxCosine < 0.3) { MatOfPoint tmp = new MatOfPoint(); contourHull.convertTo(tmp, CvType.CV_32S); squares.add(approxf1); hulls.add(tmp); } } }

OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正

5、找到最大的正方形

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwfxgg.html