Java Graphics2D is Drawing One Pixel Off (Rounding error?)

梦想与她 提交于 2019-12-23 02:29:09

问题


I have been trying to make a program in Java using Graphics2D and PaintComponent. However, Java is not rounding some values correctly resulting in a few points being rendered a pixel different from where it supposed to be which gives an unclean image. You can see what I mean in the picture below.

Here is my code. What can I change to fix this? Thank you!

public void paintComponent( Graphics g )
{
  super.paintComponent( g );

  g.setColor(Color.red);

  int orginX = getWidth()/2;
  int orginY = getHeight()/2;

  for(int i=0; i<=360; i+= 10)
  {
      double angle = Math.toRadians(i);
      double centerX = radius * Math.cos(angle) + orginX;
      double centerY = radius * Math.sin(angle) + orginY;
      int[] anglePointsX = {(int) (radius * Math.cos(angle+Math.toRadians(60)) + centerX), (int) (radius * Math.cos(angle-Math.toRadians(60)) + centerX), orginX};
      int[] anglePointsY = {(int) (radius * Math.sin(angle+Math.toRadians(60)) + centerY), (int) (radius * Math.sin(angle-Math.toRadians(60)) + centerY), orginY};
      g.drawPolygon(anglePointsX, anglePointsY, 3);
  }

回答1:


I was going to suggest you could draw two separate lines:

  g.drawPolygon(anglePointsX, anglePointsY, 2); // the outter line.
  g.drawLine(orginX, orginY, anglePointsX[0], anglePointsY[0]); // from origin to one outer point

Almost worked, only one line was not as expected.

So then my next suggestion is again to draw two separate lines, but this time just rotate the line that is drawn from the origin.

super.paintComponent( g );

g.setColor(Color.red);
Graphics2D g2 = (Graphics2D)g.create();

int radius = 100;
int orginX = getWidth()/2;
int orginY = getHeight()/2;
double radians = Math.toRadians(60);

for(int i=0; i < 360; i+= 10)
{
    double angle = Math.toRadians(i);
    double centerX = radius * Math.cos(angle) + orginX;
    double centerY = radius * Math.sin(angle) + orginY;

    int[] anglePointsX = {(int) (radius * Math.cos(angle + radians) + centerX), (int) (radius * Math.cos(angle - radians) + centerX), orginX};
    int[] anglePointsY = {(int) (radius * Math.sin(angle + radians) + centerY), (int) (radius * Math.sin(angle - radians) + centerY), orginY};

    g.drawPolygon(anglePointsX, anglePointsY, 2);
//    g2.drawLine(orginX, orginY, anglePointsX[0], anglePointsY[0]);

    AffineTransform af = new AffineTransform();
    af.translate(orginX, orginY);
    af.rotate( angle );
    g2.setTransform( af );
    g2.drawLine(0, 0, 175, 0);
}

The problem with this approach is that I don't know how to calculate the length of the line from the origin and just hardcoded 175. Maybe your math is better than mine and you know how to calculate the fixed length for the line.

The brute force method to determine the maximum line length can be to create two loops. The first loop paints the outer lines and determines the maximum x value used. The second loop then paints 36 rotated lines using this value as the length:

protected void paintComponent(Graphics g)
{
    super.paintComponent( g );

    g.setColor(Color.red);
    Graphics2D g2 = (Graphics2D)g.create();

    int radius = 100;
    int orginX = getWidth()/2;
    int orginY = getHeight()/2;
    double radians60 = Math.toRadians(60);
    int maxX = 0;

    for(int i=0; i < 360; i+= 10)
    {
        double angle = Math.toRadians(i);
        double centerX = radius * Math.cos(angle) + orginX;
        double centerY = radius * Math.sin(angle) + orginY;

        int[] anglePointsX = {(int) (radius * Math.cos(angle + radians60) + centerX), (int) (radius * Math.cos(angle - radians60) + centerX), orginX};
        int[] anglePointsY = {(int) (radius * Math.sin(angle + radians60) + centerY), (int) (radius * Math.sin(angle - radians60) + centerY), orginY};

        g.drawPolygon(anglePointsX, anglePointsY, 2);

        maxX = Math.max(maxX, anglePointsX[0]);
    }

    for(int i=0; i < 360; i+= 10)
    {
        double angle = Math.toRadians(i);
        AffineTransform af = new AffineTransform();
        af.translate(orginX, orginY);
        af.rotate( angle );
        g2.setTransform( af );
        g2.drawLine(0, 0, maxX - orginX, 0);
    }

    g2.dispose();
}



回答2:


In this case, the reason for the "thick" lines was indeed a rounding error. So what you observed was not really an issue of the drawing, but of the computation. Note that for various reasons (mainly the limited precision of double values in general), you can not assume that all results of the trigonometric functions like cos and sin are what you might expect them to be. For example,

System.out.println(Math.sin(Math.toRadians(180)));

will print 1.2246467991473532E-16, although mathematically, it should be 0.0. Similarly, some of the values that you computed might end up being something like 199.999999999941234234 when the result should be 200.0. Then, when you are casting these values to int, you are truncating the fractional part, and the result will be 199 - causing the odd visual effect that you observed.

In this case, this could, pragmatically, be solved by rounding the results using Math.round, as it is done in the following example in the PixelsOffByOnePanel.

For completeness, the PixelsOffByOnePanelNicer class shows how you can do this with a Path2D and double values.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class PixelsOffByOne
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                JFrame frame = new JFrame("");
                frame.getContentPane().setLayout(new GridLayout(1,2));
                frame.getContentPane().add(new PixelsOffByOnePanel());
                frame.getContentPane().add(new PixelsOffByOnePanelNicer());
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}

class PixelsOffByOnePanel extends JPanel
{
    int radius = 100; 

    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(400,400);
    }

    @Override
    public void paintComponent( Graphics g )
    {
        super.paintComponent( g );

        g.setColor(Color.RED);

        int orginX = getWidth()/2;
        int orginY = getHeight()/2;

        for(int i=0; i<=360; i+=10)
        {
            double angle = Math.toRadians(i);
            double centerX = radius * Math.cos(angle) + orginX;
            double centerY = radius * Math.sin(angle) + orginY;

            int x0 = (int) Math.round(radius * Math.cos(angle+Math.toRadians(60)) + centerX);
            int x1 = (int) Math.round(radius * Math.cos(angle-Math.toRadians(60)) + centerX);
            int[] anglePointsX = {x0, x1, orginX};

            int y0 = (int) Math.round(radius * Math.sin(angle+Math.toRadians(60)) + centerY);
            int y1 = (int) Math.round(radius * Math.sin(angle-Math.toRadians(60)) + centerY);
            int[] anglePointsY = {y0, y1, orginY};

            g.drawPolygon(anglePointsX, anglePointsY, 3);

        }    
    }
}

class PixelsOffByOnePanelNicer extends JPanel
{
    int radius = 100; 

    @Override
    public Dimension getPreferredSize()
    {
        return new Dimension(400,400);
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.setColor(Color.RED);

        int originX = getWidth()/2;
        int originY = getHeight()/2;

        double d = Math.toRadians(60);

        Path2D path = new Path2D.Double();
        for(int i=0; i<=360; i+=10)
        {
            double angle = Math.toRadians(i);
            double centerX = radius * Math.cos(angle) + originX;
            double centerY = radius * Math.sin(angle) + originY;

            double x0 = (int) Math.round(radius * Math.cos(angle+d) + centerX);
            double x1 = (int) Math.round(radius * Math.cos(angle-d) + centerX);
            double x2 = originX;

            double y0 = (int) Math.round(radius * Math.sin(angle+d) + centerY);
            double y1 = (int) Math.round(radius * Math.sin(angle-d) + centerY);
            double y2 = originY;

            path.moveTo(x0, y0);
            path.lineTo(x1, y1);
            path.lineTo(x2, y2);
            path.closePath();
        }    
        g.draw(path);
    }
}

A side note: Usually, the drawing may look even nicer when you enable anti-aliasing, by calling

g.setRenderingHint(
    RenderingHints.KEY_ANTIALIASING, 
    RenderingHints.VALUE_ANTIALIAS_ON);

before starting to draw. But as you are drawing several lines multiple times, the result may not look as pleasing as it could if you dedicatedly only drew each line only once.




回答3:


Try using i < 360 instead i<=360, as you've already drawen a line at this angle (0 == 360)

Try using a Path2D or GeneralPath rather then drawPolygon as this will give you float and double precision

Have a look at Drawing Arbitrary Shapes for more details

Updated...

So I changed i <= 360 to i < 360 and didn't see any immediate changes, however, then applied some rendering hints and it seemed to improve the problem.

I started by reducing the number of elements to 6 (this is where I started seeing an issue) with

int dif = (int) (360 / 6d);

for (int i = 0; i < 360; i += dif) {

Then I applied some rendering hints...

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2d = (Graphics2D) g.create();

    g2d.setColor(Color.red);
    g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
    g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

    int orginX = getWidth() / 2;
    int orginY = getHeight() / 2;

    int dif = (int) (360 / 6d);

    for (int i = 0; i < 360; i += dif) {
        double angle = Math.toRadians(i);
        double centerX = radius * Math.cos(angle) + orginX;
        double centerY = radius * Math.sin(angle) + orginY;
        int[] anglePointsX = {(int) (radius * Math.cos(angle + Math.toRadians(60)) + centerX), (int) (radius * Math.cos(angle - Math.toRadians(60)) + centerX), orginX};
        int[] anglePointsY = {(int) (radius * Math.sin(angle + Math.toRadians(60)) + centerY), (int) (radius * Math.sin(angle - Math.toRadians(60)) + centerY), orginY};
        g2d.drawPolygon(anglePointsX, anglePointsY, 3);
    }
    g2d.dispose();
}

Before and after comparison...

I then changed the drawing from using drawPolygon to use the Shape API, in particular a Path2D

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2d = (Graphics2D) g.create();

    g2d.setColor(Color.red);
    g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
    g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

    int orginX = getWidth() / 2;
    int orginY = getHeight() / 2;

    int dif = (int) (360 / 6d);
    for (int i = 0; i < 360; i += dif) {
        double angle = Math.toRadians(i);
        double centerX = radius * Math.cos(angle) + orginX;
        double centerY = radius * Math.sin(angle) + orginY;

        Path2D path = new Path2D.Double();
        path.moveTo(radius * Math.cos(angle + Math.toRadians(60)) + centerX, radius * Math.sin(angle + Math.toRadians(60)) + centerY);
        path.lineTo((radius * Math.cos(angle - Math.toRadians(60)) + centerX), (radius * Math.sin(angle - Math.toRadians(60)) + centerY));
        path.lineTo(orginX, orginY);

        g2d.draw(path);
    }
    g2d.dispose();
}

With and without rendering hints

As you can, even without rendering hints, the issue is gone




回答4:


Two different problems, perhaps.

Problem 1, aliasing. You need to turn it on, using something like the following:

https://docs.oracle.com/javase/tutorial/2d/advanced/quality.html

As for the lines that appear to be thicker than they should, you are drawing them twice, which makes them appear thicker. So the best solution is to avoid drawing them twice. Try to refactor your code to draw lines instead of polygons so each line is drawn only once.



来源:https://stackoverflow.com/questions/32532038/java-graphics2d-is-drawing-one-pixel-off-rounding-error

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!