从上图可以看出我们找到了两个大四边形(如果看不清的话可以放大观看)。对比原图可以发现,外围的四边形是我们想要的发票边缘,而内部的四边形则是发票内的表格边框。因此我们要找到最大的正方形来当作发票边缘。实现方式很简单,找到最大的width和height就行。
// 找到最大的正方形轮廓 private static int findLargestSquare(List<MatOfPoint> squares) { if (squares.size() == 0) return -1; int max_width = 0; int max_height = 0; int max_square_idx = 0; int currentIndex = 0; for (MatOfPoint square : squares) { Rect rectangle = Imgproc.boundingRect(square); if (rectangle.width >= max_width && rectangle.height >= max_height) { max_width = rectangle.width; max_height = rectangle.height; max_square_idx = currentIndex; } currentIndex++; } return max_square_idx; } // 找出外接矩形最大的四边形 int index = findLargestSquare(squares); MatOfPoint largest_square = squares.get(index); if (largest_square.rows() == 0 || largest_square.cols() == 0) return result; 6、重新执行步骤3,提升精度接下来,对于该四边形,重新进行凸包与多边形拟合,用来提升精度。
// 找到这个最大的四边形对应的凸边框,再次进行多边形拟合,此次精度较高,拟合的结果可能是大于4条边的多边形 MatOfPoint contourHull = hulls.get(index); MatOfPoint2f tmp = new MatOfPoint2f(); contourHull.convertTo(tmp, CvType.CV_32F); Imgproc.approxPolyDP(tmp, approx, 3, true); List<Point> newPointList = new ArrayList<>(); double maxL = Imgproc.arcLength(approx, true) * 0.02; 7、找到长方形四条边,即为纸张的外围四边形之后的步骤就很简单了,首先排除多边形中距离非常近的点,然后找到距离大于某个阈值的四个点,便为长方形的四个顶点。最后连接四个顶点,提取四边形边框的步骤就完成了。
// 点到点的距离 private static double getSpacePointToPoint(Point p1, Point p2) { double a = p1.x - p2.x; double b = p1.y - p2.y; return Math.sqrt(a * a + b * b); } // 两直线的交点 private static Point computeIntersect(double[] a, double[] b) { if (a.length != 4 || b.length != 4) throw new ClassFormatError(); double x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3], x3 = b[0], y3 = b[1], x4 = b[2], y4 = b[3]; double d = ((x1 - x2) * (y3 - y4)) - ((y1 - y2) * (x3 - x4)); if (d != 0) { Point pt = new Point(); pt.x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d; pt.y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d; return pt; } else return new Point(-1, -1); } // 找到高精度拟合时得到的顶点中 距离小于低精度拟合得到的四个顶点maxL的顶点,排除部分顶点的干扰 for (Point p : approx.toArray()) { if (!(getSpacePointToPoint(p, largest_square.toList().get(0)) > maxL && getSpacePointToPoint(p, largest_square.toList().get(1)) > maxL && getSpacePointToPoint(p, largest_square.toList().get(2)) > maxL && getSpacePointToPoint(p, largest_square.toList().get(3)) > maxL)) { newPointList.add(p); } } // 找到剩余顶点连线中,边长大于 2 * maxL的四条边作为四边形物体的四条边 List<double[]> lines = new ArrayList<>(); for (int i = 0; i < newPointList.size(); i++) { Point p1 = newPointList.get(i); Point p2 = newPointList.get((i+1) % newPointList.size()); if (getSpacePointToPoint(p1, p2) > 2 * maxL) { lines.add(new double[]{p1.x, p1.y, p2.x, p2.y}); } } // 计算出这四条边中 相邻两条边的交点,即物体的四个顶点 List<Point> corners = new ArrayList<>(); for (int i = 0; i < lines.size(); i++) { Point corner = computeIntersect(lines.get(i),lines.get((i+1) % lines.size())); corners.add(corner); } 8、透视变换,提取四边形