【问题标题】:Rounding Inaccuracies When Combining Areas in Java?在 Java 中组合区域时舍入不准确?
【发布时间】:2012-03-20 13:47:46
【问题描述】:

我正在使用 Java 中的 Areas

我的测试程序随机绘制三个三角形并将它们组合成一个或多个多边形。将Areas.add() 编在一起后,我使用PathIterator 跟踪边缘。

然而,有时Area 对象不会按应有的方式组合...正如您在我发布的最后一张图片中看到的那样,会绘制额外的边缘。

认为问题是由 Java 的 Area 类中的舍入误差引起的(当我调试测试程序时,Area 在使用 PathIterator 之前显示了差距),但是我不认为 Java 提供了任何其他组合形状的方法。

有什么解决办法吗?

示例代码和图片:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.util.ArrayList;
import java.util.Random;

import javax.swing.JFrame;

public class AreaTest extends JFrame{
    private static final long serialVersionUID = -2221432546854106311L;


    Area area = new Area();
    ArrayList<Line2D.Double> areaSegments = new ArrayList<Line2D.Double>();

    AreaTest() {
        Path2D.Double triangle = new Path2D.Double();
        Random random = new Random();

        // Draw three random triangles
        for (int i = 0; i < 3; i++) {
            triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.closePath();
            area.add(new Area(triangle));
        }       

        // Note: we're storing double[] and not Point2D.Double
        ArrayList<double[]> areaPoints = new ArrayList<double[]>();
        double[] coords = new double[6];

        for (PathIterator pi = area.getPathIterator(null); !pi.isDone(); pi.next()) {

            // Because the Area is composed of straight lines
            int type = pi.currentSegment(coords);
            // We record a double array of {segment type, x coord, y coord}
            double[] pathIteratorCoords = {type, coords[0], coords[1]};
            areaPoints.add(pathIteratorCoords);
        }

        double[] start = new double[3]; // To record where each polygon starts
        for (int i = 0; i < areaPoints.size(); i++) {
            // If we're not on the last point, return a line from this point to the next
            double[] currentElement = areaPoints.get(i);

            // We need a default value in case we've reached the end of the ArrayList
            double[] nextElement = {-1, -1, -1};
            if (i < areaPoints.size() - 1) {
                nextElement = areaPoints.get(i + 1);
            }

            // Make the lines
            if (currentElement[0] == PathIterator.SEG_MOVETO) {
                start = currentElement; // Record where the polygon started to close it later
            } 

            if (nextElement[0] == PathIterator.SEG_LINETO) {
                areaSegments.add(
                        new Line2D.Double(
                            currentElement[1], currentElement[2],
                            nextElement[1], nextElement[2]
                        )
                    );
            } else if (nextElement[0] == PathIterator.SEG_CLOSE) {
                areaSegments.add(
                        new Line2D.Double(
                            currentElement[1], currentElement[2],
                            start[1], start[2]
                        )
                    );
            }
        }

        setSize(new Dimension(500, 500));
        setLocationRelativeTo(null); // To center the JFrame on screen
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setResizable(false);
        setVisible(true);
    }

    public void paint(Graphics g) {
        // Fill the area
        Graphics2D g2d = (Graphics2D) g;
        g.setColor(Color.lightGray);
        g2d.fill(area);

        // Draw the border line by line
        g.setColor(Color.black);
        for (Line2D.Double line : areaSegments) {
            g2d.draw(line);
        }
    }

    public static void main(String[] args) {
        new AreaTest();
    }
}

成功案例:

一个失败的案例:

【问题讨论】:

    标签: java geometry java-2d area rounding-error


    【解决方案1】:

    这里:

        for (int i = 0; i < 3; i++) {
            triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.closePath();
            area.add(new Area(triangle));
        }       
    

    您实际上是在添加 第一个循环中的 1 个三角形 第二个循环中有 2 个三角形 第三个循环中的3个三角形

    这就是您的不准确之处。试试这个,看看你的问题是否仍然存在。

        for (int i = 0; i < 3; i++) {
            triangle.moveTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.lineTo(random.nextInt(400) + 50, random.nextInt(400) + 50);
            triangle.closePath();
            area.add(new Area(triangle));
            triangle.reset();
        }    
    

    注意每次循环后路径重置。

    编辑:解释更多错误来自这里的三个路径,您尝试结合。这使得可能出现错误的位置一目了然。

    【讨论】:

    • 在我的实际实现中,我重置了Path。我想我忘了把它添加到演示中......但这不是答案。
    • @Peter:那么你能提供一个实际的例子,它每次都会产生失败,而不仅仅是随机的,包括你正在使用的实际来源?
    • @Peter:我的意思是我尝试了数百个随机生成的组合,在添加reset 后没有一个失败。因此,如果您可以为随机生成器或实际坐标找出种子,那就太好了。否则很麻烦。
    • 有趣。我做了一些测试,使用.reset() 方法确实看起来更可靠。但是,在我的实现中(代理相互避开速度障碍),即使重置方法一直存在,我也会偶尔看到错误。也许错误来自不同的来源。我会再做一些测试。
    • 我认为@stryba 已经回答了最初的问题。如果您有其他问题,也许在一个新问题上会更好?当一个问题从“修复 X”变成“当我引起你的注意,请修复我的代码中的所有错误”时,这有点烦人/令人困惑/不公平......
    【解决方案2】:

    我已经重构了您的示例以简化测试,并添加了两个答案的功能。恢复 triangle.reset() 似乎消除了我的工件。此外,

    • event dispatch thread 上构建 GUI。

    • 对于渲染,扩展 JComponent,例如JPanel,并覆盖paintComponent()

    • 缺少具有首选大小的子组件,覆盖getPreferredSize()

    • 使用RenderingHints

    SSCCE:

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.event.ActionEvent;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.Area;
    import java.awt.geom.Line2D;
    import java.awt.geom.Path2D;
    import java.awt.geom.PathIterator;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import javax.swing.AbstractAction;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JSpinner;
    import javax.swing.SpinnerNumberModel;
    import javax.swing.event.ChangeEvent;
    import javax.swing.event.ChangeListener;
    
    /** @see http://stackoverflow.com/q/9526835/230513 */
    public class AreaTest extends JPanel {
    
        private static final int SIZE = 500;
        private static final int INSET = SIZE / 10;
        private static final int BOUND = SIZE - 2 * INSET;
        private static final int N = 5;
        private static final AffineTransform I = new AffineTransform();
        private static final double FLATNESS = 1;
        private static final Random random = new Random();
        private Area area = new Area();
        private List<Line2D.Double> areaSegments = new ArrayList<Line2D.Double>();
        private int count = N;
    
        AreaTest() {
            setLayout(new BorderLayout());
            create();
            add(new JPanel() {
    
                @Override
                public void paintComponent(Graphics g) {
                    Graphics2D g2d = (Graphics2D) g;
                    g2d.setRenderingHint(
                        RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
                    g.setColor(Color.lightGray);
                    g2d.fill(area);
                    g.setColor(Color.black);
                    for (Line2D.Double line : areaSegments) {
                        g2d.draw(line);
                    }
                }
    
                @Override
                public Dimension getPreferredSize() {
                    return new Dimension(SIZE, SIZE);
                }
            });
    
            JPanel control = new JPanel();
            control.add(new JButton(new AbstractAction("Update") {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    create();
                    repaint();
                }
            }));
            JSpinner countSpinner = new JSpinner();
            countSpinner.setModel(new SpinnerNumberModel(N, 3, 42, 1));
            countSpinner.addChangeListener(new ChangeListener() {
                @Override
                public void stateChanged(ChangeEvent e) {
                    JSpinner s = (JSpinner) e.getSource();
                    count = ((Integer) s.getValue()).intValue();
                }
            });
            control.add(countSpinner);
            add(control, BorderLayout.SOUTH);
        }
    
        private int randomPoint() {
            return random.nextInt(BOUND) + INSET;
        }
    
        private void create() {
            area.reset();
            areaSegments.clear();
            Path2D.Double triangle = new Path2D.Double();
    
            // Draw three random triangles
            for (int i = 0; i < count; i++) {
                triangle.moveTo(randomPoint(), randomPoint());
                triangle.lineTo(randomPoint(), randomPoint());
                triangle.lineTo(randomPoint(), randomPoint());
                triangle.closePath();
                area.add(new Area(triangle));
                triangle.reset();
            }
    
            // Note: we're storing double[] and not Point2D.Double
            List<double[]> areaPoints = new ArrayList<double[]>();
            double[] coords = new double[6];
    
            for (PathIterator pi = area.getPathIterator(I, FLATNESS);
                !pi.isDone(); pi.next()) {
    
                // Because the Area is composed of straight lines
                int type = pi.currentSegment(coords);
                // We record a double array of {segment type, x coord, y coord}
                double[] pathIteratorCoords = {type, coords[0], coords[1]};
                areaPoints.add(pathIteratorCoords);
            }
    
            // To record where each polygon starts
            double[] start = new double[3];
            for (int i = 0; i < areaPoints.size(); i++) {
                // If we're not on the last point, return a line from this point to the next
                double[] currentElement = areaPoints.get(i);
    
                // We need a default value in case we've reached the end of the List
                double[] nextElement = {-1, -1, -1};
                if (i < areaPoints.size() - 1) {
                    nextElement = areaPoints.get(i + 1);
                }
    
                // Make the lines
                if (currentElement[0] == PathIterator.SEG_MOVETO) {
                    // Record where the polygon started to close it later
                    start = currentElement;
                }
    
                if (nextElement[0] == PathIterator.SEG_LINETO) {
                    areaSegments.add(
                        new Line2D.Double(
                        currentElement[1], currentElement[2],
                        nextElement[1], nextElement[2]));
                } else if (nextElement[0] == PathIterator.SEG_CLOSE) {
                    areaSegments.add(
                        new Line2D.Double(
                        currentElement[1], currentElement[2],
                        start[1], start[2]));
                }
            }
        }
    
        public static void main(String[] args) {
            EventQueue.invokeLater(new Runnable() {
    
                @Override
                public void run() {
                    JFrame f = new JFrame();
                    f.add(new AreaTest());
                    f.pack();
                    f.setLocationRelativeTo(null);
                    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    f.setResizable(false);
                    f.setVisible(true);
                }
            });
        }
    }
    

    【讨论】:

    • 我通过覆盖的JPanel 在我的应用程序中使用主动渲染。在演示中,我想保持简单。此外,问题是计算问题,而不是渲染问题......我使用边缘从PathIterator 获取后进行进一步计算。
    【解决方案3】:

    我玩弄了这个,并找到了一种摆脱这些的 hacky 方法。我不能 100% 确定这是否适用于所有情况,但它可能会。

    读完Area.transform的JavaDoc提到

    使用指定的转换此区域的几何图形 仿射变换。 几何体在原地变换,这 永久更改此对象定义的封闭区域。

    我有一种预感,并增加了通过按住一个键来旋转区域的可能性。随着区域的旋转,“向内”的边缘开始慢慢消失,直到只剩下轮廓。我怀疑“向内”边缘实际上是两个非常接近的边缘(因此它们看起来像单个边缘),并且旋转区域会导致非常小的舍入误差,因此旋转类型将它们“融化”在一起。

    然后我添加了一个代码,在按键上以非常小的步长将区域旋转一整圈,看起来工件消失了:

    左边的图像是由 10 个不同的随机三角形构建的区域(我增加了三角形的数量以更频繁地获得“失败”区域),右边的图像是相同的区域,经过 360 度旋转度以非常小的增量(10000 步)。

    这是用于以小步长旋转区域的代码(对于大多数情况,小于 10000 步可能就可以正常工作):

            final int STEPS = 10000; //Number of steps in a full 360 degree rotation
            double theta = (2*Math.PI) / STEPS; //Single step "size" in radians
    
            Rectangle bounds = area.getBounds();    //Getting the bounds to find the center of the Area
            AffineTransform trans = AffineTransform.getRotateInstance(theta, bounds.getCenterX(), bounds.getCenterY()); //Transformation matrix for theta radians around the center
    
            //Rotate a full 360 degrees in small steps
            for(int i = 0; i < STEPS; i++)
            {
                area.transform(trans);
            }
    

    正如我之前所说,我不确定这是否适用于所有情况,并且所需的步骤数量可能会更小或更大,具体取决于场景。 YMMV。

    【讨论】:

    • 如果这是问题所在,那么更简单的解决方法是永远不要生成两条完全重叠的行。您可以通过将 random.nextInt(400) 更改为 400*random.nextDouble() 来做到这一点 - 对于 0-400 区间的读取次数比读取次数多得多,因此完全匹配的机会可以忽略不计。
    • 尝试我的建议并不能解决问题,顺便说一句。
    • 这是一个聪明的解决方案,但它在计算上是否可行?我的程序会做很多这样的计算。
    • @Peter:不是每个区域旋转 10000 步(在我自己的计算机上每个区域需要几秒钟),尽管我没有测试完全额外边缘的最小步数消失,我只是从我的帽子中拉出 10000 步。如果实际所需的步数要少得多,则可能可行,具体取决于您的速度要求。
    • @Peter:我测试了更多,似乎有时多余的边缘会消失,低至 3 步,有时需要几百步。你可能需要一些逻辑来检查额外的边缘是否消失了,如果没有,做更多的旋转(可能有更多的步骤),所以步骤量会自行调整。我想知道是否可以通过检查轮廓中的任何点是否在形成它的任何三角形内来检测“失败”区域..?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-13
    • 2023-03-27
    • 1970-01-01
    相关资源
    最近更新 更多