7

这是我的情况。它涉及对齐扫描的图像,这将导致不正确的扫描。我必须将扫描的图像与我的 Java 程序对齐。

这些是更多细节:

  • 一张纸上印有一张表格,会被扫描成图像文件。
  • 我会用 Java 打开图片,我会有一个 OVERLAY 的文本框。
  • 文本框应该与扫描的图像正确对齐。
  • 为了正确对齐,我的 Java 程序必须分析扫描图像并检测扫描图像上表格边缘的坐标,从而定位图像和文本框,以便文本框和图像都正确对齐(以防万一扫描不正确)

你看,扫描图像的人可能不一定将图像放置在完全正确的位置,所以我需要我的程序在加载扫描图像时自动对齐它。该程序可在许多此类扫描图像上重复使用,因此我需要该程序以这种方式灵活。

我的问题是以下之一:

  1. 如何使用Java检测表格上边缘的y坐标和表格最左边缘的x坐标。该表格是一张带有许多单元格的常规表格,带有黑色细边框,打印在一张白纸上(水平打印输出)

  2. 如果存在一种更简单的方法来自动对齐扫描图像,以使所有扫描图像的图形表都对齐到相同的 x、y 坐标,那么共享此方法:)。

  3. 如果您不知道上述问题的答案,请告诉我应该从哪里开始。我对图形java编程不太了解,我有大约1个月的时间来完成这个程序。假设我的日程安排很紧,我必须让图形部分对我来说尽可能简单。

干杯,谢谢。

4

4 回答 4

5

尝试从一个简单的场景开始,然后改进方法。

  1. 检测角落。
  2. 找到表格边界中的角。
  3. 使用表格角坐标,计算旋转角度。
  4. 旋转/缩放图像。
  5. 映射表单中每个字段相对于表单原点坐标的位置。
  6. 匹配文本框。

本文末尾提供的程序执行步骤 1 到 3。它是使用Marvin 框架实现的。下图显示了检测到角的输出图像。

在此处输入图像描述

该程序还输出:旋转角度:1.6365770416167182

源代码:

import java.awt.Color;
import java.awt.Point;
import marvin.image.MarvinImage;
import marvin.io.MarvinImageIO;
import marvin.plugin.MarvinImagePlugin;
import marvin.util.MarvinAttributes;
import marvin.util.MarvinPluginLoader;

public class FormCorners {

public FormCorners(){
    // Load plug-in
    MarvinImagePlugin moravec = MarvinPluginLoader.loadImagePlugin("org.marvinproject.image.corner.moravec");
    MarvinAttributes attr = new MarvinAttributes();

    // Load image
    MarvinImage image = MarvinImageIO.loadImage("./res/printedForm.jpg");

    // Process and save output image
    moravec.setAttribute("threshold", 2000);
    moravec.process(image, null, attr);
    Point[] boundaries = boundaries(attr);
    image = showCorners(image, boundaries, 12);
    MarvinImageIO.saveImage(image, "./res/printedForm_output.jpg");

    // Print rotation angle
    double angle =  (Math.atan2((boundaries[1].y*-1)-(boundaries[0].y*-1),boundaries[1].x-boundaries[0].x) * 180 / Math.PI);
    angle =  angle >= 0 ? angle : angle + 360;
    System.out.println("Rotation angle:"+angle);
}

private Point[] boundaries(MarvinAttributes attr){
    Point upLeft = new Point(-1,-1);
    Point upRight = new Point(-1,-1);
    Point bottomLeft = new Point(-1,-1);
    Point bottomRight = new Point(-1,-1);
    double ulDistance=9999,blDistance=9999,urDistance=9999,brDistance=9999;
    double tempDistance=-1;
    int[][] cornernessMap = (int[][]) attr.get("cornernessMap");

    for(int x=0; x<cornernessMap.length; x++){
        for(int y=0; y<cornernessMap[0].length; y++){
            if(cornernessMap[x][y] > 0){
                if((tempDistance = Point.distance(x, y, 0, 0)) < ulDistance){
                    upLeft.x = x; upLeft.y = y;
                    ulDistance = tempDistance;
                } 
                if((tempDistance = Point.distance(x, y, cornernessMap.length, 0)) < urDistance){
                    upRight.x = x; upRight.y = y;
                    urDistance = tempDistance;
                }
                if((tempDistance = Point.distance(x, y, 0, cornernessMap[0].length)) < blDistance){
                    bottomLeft.x = x; bottomLeft.y = y;
                    blDistance = tempDistance;
                }
                if((tempDistance = Point.distance(x, y, cornernessMap.length, cornernessMap[0].length)) < brDistance){
                    bottomRight.x = x; bottomRight.y = y;
                    brDistance = tempDistance;
                }
            }
        }
    }
    return new Point[]{upLeft, upRight, bottomRight, bottomLeft};
}

private MarvinImage showCorners(MarvinImage image, Point[] points, int rectSize){
    MarvinImage ret = image.clone();
    for(Point p:points){
        ret.fillRect(p.x-(rectSize/2), p.y-(rectSize/2), rectSize, rectSize, Color.red);
    }
    return ret;
}

public static void main(String[] args) {
    new FormCorners();
}
}
于 2013-09-29T14:45:20.263 回答
2

边缘检测通常是通过增强相邻像素之间的对比度来完成的,这样您就可以获得一条易于检测的线,适合进一步处理。

为此,“内核”根据像素的初始值和该像素的相邻像素的值来转换像素。一个好的边缘检测内核将增强相邻像素之间的差异,并降低具有相似邻居的像素的强度。

我将从查看 Sobel 运算符开始。这可能不会返回对您立即有用的结果;但是,与您在对该领域知之甚少的情况下解决问题相比,它会让您更接近。

在你有一些清晰干净的边缘之后,你可以使用更大的内核来检测两条线似乎发生 90% 弯曲的点,这可能会为你提供外部矩形的像素坐标,这可能足以满足你的目的。

使用这些外部坐标,将新像素与旋转并移动到“匹配”的旧像素之间的平均值进行堆肥仍然需要一些数学运算。结果(特别是如果您不了解抗锯齿数学)可能非常糟糕,给图像添加了模糊。

锐化滤镜可能是一种解决方案,但它们也有自己的问题,主要是它们通过增加颗粒感来使图片更清晰。太多了,很明显原始图像不是高质量的扫描。

于 2013-09-17T22:52:52.987 回答
1

我研究了这些库,但最后我发现编写自己的边缘检测方法更方便。

下面的类将检测包含此类边缘的扫描纸张的黑色/灰色边缘,并将返回纸张边缘的 x 和 y 坐标,从最右端开始 (reverse = true) 或从下端 (reverse = true) 或从上边缘 (reverse = false) 或从左边缘 (reverse = false)。此外...该程序将沿垂直边缘 (rangex) 测量以像素为单位的范围,以及以像素为单位测量的水平范围 (rangey)。范围确定收到的点中的异常值。

该程序使用指定的数组进行 4 次垂直切割和 4 次水平切割。它检索暗点的值。它使用范围来消除异常值。有时,纸上的一个小点可能会导致异常点。范围越小,异常值越少。但是,有时边缘会略微倾斜,因此您不希望将范围设置得太小。

玩得开心。它非常适合我。

import java.awt.image.BufferedImage;
import java.awt.Color;
import java.util.ArrayList;
import java.lang.Math;
import java.awt.Point;
public class EdgeDetection {

    public App ap;
        public int[] horizontalCuts = {120, 220, 320, 420};
        public int[] verticalCuts = {300, 350, 375, 400};



    public void printEdgesTest(BufferedImage image, boolean reversex, boolean reversey, int rangex, int rangey){
        int[] mx = horizontalCuts;
        int[] my = verticalCuts;

            //you are getting edge points here
            //the "true" parameter indicates that it performs a cut starting at 0. (left edge)
        int[] xEdges = getEdges(image, mx, reversex, true);
        int edgex = getEdge(xEdges, rangex);
        for(int x = 0; x < xEdges.length; x++){
            System.out.println("EDGE = " + xEdges[x]);
        }
        System.out.println("THE EDGE = " + edgex);
            //the "false" parameter indicates you are doing your cut starting at the end (image.getHeight)
            //and ending at 0
            //if the parameter was true, it would mean it would start the cuts at y = 0
        int[] yEdges = getEdges(image, my, reversey, false);
        int edgey = getEdge(yEdges, rangey);
        for(int y = 0; y < yEdges.length; y++){
            System.out.println("EDGE = " + yEdges[y]);
        }
        System.out.println("THE EDGE = " + edgey);
    }

    //This function takes an array of coordinates...detects outliers, 
    //and computes the average of non-outlier points.

    public int getEdge(int[] edges, int range){
        ArrayList<Integer> result = new ArrayList<Integer>();
        boolean[] passes = new boolean[edges.length];
        int[][] differences = new int[edges.length][edges.length-1];
        //THIS CODE SEGMENT SAVES THE DIFFERENCES BETWEEN THE POINTS INTO AN ARRAY
        for(int n = 0; n<edges.length; n++){
            for(int m = 0; m<edges.length; m++){
                if(m < n){
                    differences[n][m] = edges[n] - edges[m];
                }else if(m > n){
                    differences[n][m-1] = edges[n] - edges[m];
                }
            }
        }
         //This array determines which points are outliers or nots (fall within range of other points)
        for(int n = 0; n<edges.length; n++){
            passes[n] = false;
            for(int m = 0; m<edges.length-1; m++){
                if(Math.abs(differences[n][m]) < range){
                    passes[n] = true;
                    System.out.println("EDGECHECK = TRUE" + n);
                    break;
                }
            }
        }
         //Create a new array only using valid points
        for(int i = 0; i<edges.length; i++){
            if(passes[i]){
                result.add(edges[i]);
            }
        }

        //Calculate the rounded mean... This will be the x/y coordinate of the edge
        //Whether they are x or y values depends on the "reverse" variable used to calculate the edges array
        int divisor = result.size();
        int addend = 0;
        double mean = 0;
        for(Integer i : result){
            addend += i;
        }
        mean = (double)addend/(double)divisor;

        //returns the mean of the valid points: this is the x or y coordinate of your calculated edge.
        if(mean - (int)mean >= .5){
            System.out.println("MEAN " + mean);
            return (int)mean+1;
        }else{
            System.out.println("MEAN " + mean);
            return (int)mean;
        }       
    }


     //this function computes "dark" points, which include light gray, to detect edges.
     //reverse - when true, starts counting from x = 0 or y = 0, and ends at image.getWidth or image.getHeight()
     //verticalEdge - determines whether you want to detect a vertical edge, or a horizontal edge
     //arr[] - determines the coordinates of the vertical or horizontal cuts you will do
     //set the arr[] array according to the graphical layout of your scanned image
     //image - this is the image you want to detect black/white edges of
    public int[] getEdges(BufferedImage image, int[] arr, boolean reverse, boolean verticalEdge){
        int red = 255;
        int green = 255;
        int blue = 255;
        int[] result = new int[arr.length];
        for(int n = 0; n<arr.length; n++){
            for(int m = reverse ? (verticalEdge ? image.getWidth():image.getHeight())-1:0; reverse ? m>=0:m<(verticalEdge ? image.getWidth():image.getHeight());){
                Color c = new Color(image.getRGB(verticalEdge ? m:arr[n], verticalEdge ? arr[n]:m));
                red = c.getRed();
                green = c.getGreen();
                blue = c.getBlue();
                        //determine if the point is considered "dark" or not.
                        //modify the range if you want to only include really dark spots.
                        //occasionally, though, the edge might be blurred out, and light gray helps
                if(red<239 && green<239 && blue<239){
                    result[n] = m;
                    break;
                }
                        //count forwards or backwards depending on reverse variable
                if(reverse){
                    m--;
                }else{
                    m++;
                }
            }
        }
    return result;
    }

}
于 2013-11-16T02:10:47.720 回答
1

我过去做过的一个类似的问题基本上是弄清楚表格的方向,重新对齐它,重新缩放它,然后我就准备好了。您可以使用霍夫变换来检测图像的角度偏移(即:旋转了多少),但您仍然需要检测表单的边界。它还必须适应纸张本身的边界。

这对我来说是一个幸运的突破,因为它基本上在一个黑色的大边框中间显示了一个黑白图像。

  1. 应用积极的 5x5 中值滤波器来消除一些噪声。
  2. 从灰度转换为黑白(将强度值从 [0,255] 重新调整为 [0,1])。
  3. 计算主成分分析(即:根据计算的特征值计算图像的协方差矩阵的特征向量)(http://en.wikipedia.org/wiki/Principal_component_analysis#Derivation_of_PCA_using_the_covariance_method) 4)这为您提供了一个基向量。您只需使用它将图像重新定向到标准基矩阵(即:[1,0],[0,1])。

您的图像现在对齐得很漂亮。我这样做是为了使整个人脑的 MRI 扫描方向正常化。

您还知道实际图像周围有一个巨大的黑色边框。您只需从图像的顶部和底部以及两侧删除行,直到它们全部消失。到目前为止,您可以暂时将 7x7 中值或众数过滤器应用于图像的副本。它有助于从指纹、污垢等中排除最终图像中剩余的过多边框。

于 2013-09-17T23:01:10.997 回答