13

I have written an implementation of Bresenham's circle drawing algorithm. This algorithms takes advantage of the highly symmetrical properties of a circle (it only computes points from the 1st octant and draws the other points by taking advantage of symmetry). Therefore I was expecting it to be very fast. The Graphics programming black book, chapter #35 was titled "Bresenham is fast, and fast is good", and though it was about the line drawing algorithm, I could reasonably expect the circle drawing algorithm to also be fast (since the principle is the same).

Here is my java, swing implementation

public static void drawBresenhamsCircle(int r, double width, double height, Graphics g) {
    int x,y,d;
    y = r;
    x = 0;

    drawPoint(x, y, width, height,g);
    d = (3-2*(int)r);
    while (x <= y) {
        if (d <= 0) {
            d = d + (4*x + 6);
        } else {
            d = d + 4*(x-y) + 10;
            y--;
        }
        x++;

        drawPoint(x, y, width, height,g);

        drawPoint(-x, y, width, height,g);
        drawPoint(x, -y, width, height,g);

        drawPoint(-x, -y, width, height,g);
        drawPoint(y, x, width, height,g);
        drawPoint(-y, x, width, height,g);
        drawPoint(y, -x, width, height,g);

        drawPoint(-y, -x, width, height,g);
    }   
}

This method uses the following drawPointmethod:

public static void drawPoint(double x, double y,double width,double height, Graphics g) {
    double nativeX = getNativeX(x, width);
    double nativeY = getNativeY(y, height);
    g.fillRect((int)nativeX, (int)nativeY, 1, 1);
}

The two methods getNativeX and getNativeY are used to switch coordinates from originating in the upper left corner of the screen to a system that has it origin in the center of the panel with a more classic axis orientation.

public static double getNativeX(double newX, double width) {
    return newX + (width/2);
}

public static double getNativeY(double newY, double height) {
    return (height/2) - newY;
}

I have also created an implementation of a circle drawing algorithm based on trigonometrical formulaes (x=R*Math.cos(angle)and y= R*Math.sin(angle)) and a third implementation using a call to the standard drawArc method (available on the Graphics object). These additional implementations are for the sole purpose of comparing Bresenham's algorithm to them.

I then created methods to draw a bunch of circles in order to be able to get good measures of the spent time. Here is the method I use to draw a bunch of circles using Bresenham's algorithm

public static void drawABunchOfBresenhamsCircles(int numOfCircles, double width, double height, Graphics g) {
    double r = 5;
    double step = (300.0-5.0)/numOfCircles;

    for (int i = 1; i <= numOfCircles; i++) {
        drawBresenhamsCircle((int)r, width, height, g);
        r += step;
    }
}

Finally I override the paint method of the JPanel I am using, to draw the bunch of circles and to measure the time it took each type to draw. Here is the paint method:

public void paint(Graphics g) {
    Graphics2D g2D = (Graphics2D)g;

    g2D.setColor(Color.RED);

    long trigoStartTime = System.currentTimeMillis();
    drawABunchOfTrigonometricalCircles(1000, this.getWidth(), this.getHeight(), g);
    long trigoEndTime = System.currentTimeMillis();
    long trigoDelta = trigoEndTime - trigoStartTime;

    g2D.setColor(Color.BLUE);

    long bresenHamsStartTime = System.currentTimeMillis();
    drawABunchOfBresenhamsCircles(1000, this.getWidth(), this.getHeight(), g);
    long bresenHamsEndTime = System.currentTimeMillis();
    long bresenDelta = bresenHamsEndTime - bresenHamsStartTime;

    g2D.setColor(Color.GREEN);

    long standardStarTime = System.currentTimeMillis();
    drawABunchOfStandardCircles(1000, this.getWidth(), this.getHeight(),g);
    long standardEndTime = System.currentTimeMillis();
    long standardDelta = standardEndTime - standardStarTime;

    System.out.println("Trigo : " + trigoDelta  + " milliseconds");
    System.out.println("Bresenham :" + bresenDelta +  " milliseconds");
    System.out.println("Standard :" + standardDelta +  " milliseconds");
}

Here is the kind of rendering it would generate (drawing 1000 circles of each type)

Bresenham and other methods

Unfortunately my Bresenham's implementation is very slow. I took many comparatives measures, and the Bresenham's implementation is not only slower than the Graphics.drawArcbut also slower than the trigonometrical approach. Take a look at the following measures for a various number of circles drawn.

What part of my implementation is more time-consuming? Is there any workaround I could use to improve it? Thanks for helping.

Comparing Bresenham to other implementations

[EDITION]: as requested by @higuaro, here is my trigonometrical algorithm for drawing a circle

public static void drawTrigonometricalCircle (double r, double width, double height, Graphics g) {

    double x0 = 0;
    double y0 = 0;
    boolean isStart = true;

    for (double angle = 0; angle <= 2*Math.PI; angle = angle + Math.PI/36) {

        double x = r * Math.cos(angle);
        double y = r * Math.sin(angle);

        drawPoint((double)x, y, width, height, g);

        if (!isStart) {
            drawLine(x0,  y0, x, y, width, height, g);
        }

        isStart = false;

        x0 = x;
        y0 = y;
    }
}

And the method used to draw a bunch of trigonometrical circles

public static void drawABunchOfTrigonometricalCircles(int numOfCircles, double width, double height, Graphics g) {

    double r = 5;
    double step = (300.0-5.0)/numOfCircles;

    for (int i = 1; i <= numOfCircles; i++) {
        drawTrigonometricalCircle(r, width, height, g);
        r += step;
    }
}
TT.
  • 15,774
  • 6
  • 47
  • 88
alainlompo
  • 4,414
  • 4
  • 32
  • 41
  • 1
    This is a great question if I've ever seen one. – Niklas B. Apr 05 '15 at 23:40
  • 1
    Are you also using `fillRect` to draw a point in your trigonometric solution? My first thought is that that may not be the best way to draw a point, if you're thinking about speed. You could also save a bit of time by eliminating `getNativeX` and `getNativeY`; you'd have to keep 8 values in your `drawBressenhamsCircle` method that you'd increment or decrement at the same time you increment or decrement `x` and `y`. However, this would make things ugly, and I doubt it would save all that much time. – ajb Apr 06 '15 at 00:37
  • 2
    As suggested [here](http://stackoverflow.com/q/29440313/230513), `drawArc()` leverages the host platform's implementation; [profile](http://stackoverflow.com/q/2064427/230513) to compare the other two. – trashgod Apr 06 '15 at 02:44
  • 2
    Native is going to be faster for a lot of reasons (p.e., direct access to frame buffer for a interruption-less pixel painting), so it should be out of the comparison. It would be really nice if you could post your trig circle drawing routine to see why is running faster than your Bressenham – higuaro Apr 06 '15 at 08:09
  • 2
    just a silly thing but your `width` and `height` variables are `double` not `integer`, also the pixel drawing uses `double` so the conversions between float and integer is my bet the bottleneck.... (but I am no JAVA coder so may be double means something different then I am used to) – Spektre Apr 06 '15 at 08:17
  • 1
    so the bresenham implementation is providing integer x,y float width,height and I bet the trig implementation provides all in float so it has 2 less conversions per point ... but inside the point drawing you have 4 conversions ... also the native functions use double instead of integer for comparison which is considerably slower ... – Spektre Apr 06 '15 at 08:23
  • @ajb thanks. That's a good point worth trying. If It speeds it up by a few milliseconds, then that's worth considering – alainlompo Apr 06 '15 at 08:50
  • @trashgod, do you mean that I can't beat swing's drawArc's implementation? That's fair for now, I have a smaller ambition :). But I would like to beat my own trigonometrical implementation at least... – alainlompo Apr 06 '15 at 08:59
  • @higuaro, OK, I will edit my question to add the trigo implementation – alainlompo Apr 06 '15 at 09:25
  • @Spektre, yes that's a good remark, thanks. Let me try to reduce the values to integers instead of double and I will see how better it turns out to be. – alainlompo Apr 06 '15 at 09:55
  • `What part of my implementation is more time consumming?` -- you can use a Java profiler to find out. – Victor Sorokin Apr 06 '15 at 16:39
  • @alainlompo another thing your trig function is not drawing pixel by pixel but in 5 degree steps / lines instead so it has much less iterations then Bresenham approach the ratio is increasing with radius ... you should compute angle step to cover little bit less then single pixel and use pixel drawing instead lines to make comparison ... or use formula `(x-x0)^2+(y-y0)^2=r*r` loop one variable (x) and compute the other (y) ... also can mirror all 8 sections like in Bresenham ... – Spektre Apr 07 '15 at 06:27
  • @Spektre, that's excellent. Thanks. So I am not comparing them on an equal basis. It is most likely that drawing an arc of 5 degree will draw more than 8 pixels / iteration (especially when r gets bigger). I will see how to make this even – alainlompo Apr 07 '15 at 08:54

3 Answers3

5

Your Bresenham method isn't slow per se, it's just comparatively slow.

Swing's drawArc() implementation is machine-dependent, using native code. You'll never beat it using Java, so don't bother trying. (I'm actually surprised the Java Bresenham method is as fast as it is compared to drawArc(), a testament to the quality of the virtual machine executing the Java bytecode.)

Your trigonometric method, however, is unnecessarily fast, because you're not comparing it to Bresenham on an equal basis.

The trig method has a set angular resolution of PI/36 (~4.7 degrees), as in this operation at the end of the for statement:

angle = angle + Math.PI/36  

Meanwhile, your Bresenham method is radius-dependent, computing a value at each pixel change. As each octant produces sqrt(2) points, multiplying that by 8 and dividing by 2*Pi will give you the equivalent angular resolution. So to be on equal footing with the Bresenham method, your trig method should therefore have:

resolution = 4 * r * Math.sqrt(2) / Math.PI;

somewhere outside the loop, and increment your for by it as in:

angle += resolution

Since we will now be back to pixel-level resolutions, you can actually improve the trig method and cut out the subsequent drawline call and assignments to x0 and y0, eliminate unnecessarily casts, and furthermore reduce calls to Math. Here's the new method in its entirety:

public static void drawTrigonometricalCircle (double r, double width, double height, 
    Graphics g) {

    double localPi = Math.PI;
    double resolution = 4 * r * Math.sqrt(2) / Math.PI;

    for (double angle = 0; angle <= localPi; angle += resolution) {
        double x = r * Math.cos(angle);
        double y = r * Math.sin(angle);
        drawPoint(x, y, width, height, g);
    }

}

The trig method will now be executing more often by several orders of magnitude depending on the size of r.

I'd be interested to see your results.

1201ProgramAlarm
  • 32,384
  • 7
  • 42
  • 56
Erich
  • 2,408
  • 18
  • 40
4

Your problem lies in that Bresenham's algorithm does a variable number of iterations depending on the size of the circle whereas your trigonometric approach always does a fixed number of iterations.

This also means that Bresenham's algorithm will always produce a smooth looking circle whereas your trigonometric approach will produce worse looking circles as the radius increases.

To make it more even, change the trigonometric approach to produce approximately as many points as the Bresenham implementation and you'll see just how much faster it is.

I wrote some code to benchmark this and also print the number of points produced and here are the initial results:

Trigonometric: 181 ms, 73 points average
Bresenham: 120 ms, 867.568 points average

After modifying your trigonometric class to do more iterations for smoother circles:

    int totalPoints = (int)Math.ceil(0.7 * r * 8);
    double delta = 2 * Math.PI / totalPoints;
    for (double angle = 0; angle <= 2*Math.PI; angle = angle + delta) {

These are the results:

Trigonometric: 2006 ms, 854.933 points average
Bresenham: 120 ms, 867.568 points average

Raniz
  • 10,882
  • 1
  • 32
  • 64
1

I lately wrote a bresenham circle drawing implemenation myself for a sprite rasterizer and tried to optimize it a bit. I'm not sure if it will be faster or slower than what you did but i think it should have a pretty decent execution time.

Also unfortunately it is written in C++. If i have time tomorrow i might edit my answer with a ported Java version and an example picture for the result but for now you'd have to do it yourself if you want (or someone else who would want to take his time and edit it.)

Bascically, what it does is use the bresenham algorithm to aquire the positions for the outer edges of the circle, then perform the algorithm for 1/8th of the circle and mirror that for the the remaining 7 parts by drawing straight lines from the center to the outer edge.

Color is just an rgba value

Color* createCircleColorArray(const int radius, const Color& color, int& width, int& height) {
  // Draw circle with custom bresenham variation
  int decision = 3 - (2 * radius);
  int center_x = radius;
  int center_y = radius;
  Color* data;

  // Circle is center point plus radius in each direction high/wide
  width = height = 2 * radius + 1;
  data = new Color[width * height];

  // Initialize data array for transparency
  std::fill(data, data + width * height, Color(0.0f, 0.0f, 0.0f, 0.0f));

  // Lambda function just to draw vertical/horizontal straight lines
  auto drawLine = [&data, width, height, color] (int x1, int y1, int x2, int y2) {
    // Vertical
    if (x1 == x2) {
      if (y2 < y1) {
        std::swap(y1, y2);
      }

      for (int x = x1, y = y1; y <= y2; y++) {
        data[(y * width) + x] = color;
      }
    }

    // Horizontal
    if (y1 == y2) {
      if (x2 < x1) {
        std::swap(x1, x2);
      }

      for (int x = x1, y = y1; x <= x2; x++) {
        data[(y * width) + x] = color;
      }
    }
  };

  // Lambda function to draw actual circle split into 8 parts
  auto drawBresenham = [color, drawLine] (int center_x, int center_y, int x, int y) {
    drawLine(center_x + x, center_y + x, center_x + x, center_y + y);
    drawLine(center_x - x, center_y + x, center_x - x, center_y + y);
    drawLine(center_x + x, center_y - x, center_x + x, center_y - y);
    drawLine(center_x - x, center_y - x, center_x - x, center_y - y);
    drawLine(center_x + x, center_y + x, center_x + y, center_y + x);
    drawLine(center_x - x, center_y + x, center_x - y, center_y + x);
    drawLine(center_x + x, center_y - x, center_x + y, center_y - x);
    drawLine(center_x - x, center_y - x, center_x - y, center_y - x);
  };

  for (int x = 0, y = radius; y >= x; x++) {
    drawBresenham(center_x, center_y, x, y);

    if (decision > 0) {
      y--;
      decision += 4 * (x - y) + 10;
    }
    else {
      decision += 4 * x + 6;
    }
  }

  return data;
}

//Edit
Oh wow, I just realized how old this question is.

DJSchaffner
  • 562
  • 7
  • 22