Yellow Splash

October 23, 2009

Fast, Antialiased Circles and Ellipses from Xiaolin Wu’s concepts

Filed under: Uncategorized — David Moksha @ 10:28 am

If you’ve found your way here, you may be familiar with Xiaolin Wu’s well-known line-drawing algorithm. (If not, a quick visit to wikipedia can straighten that out.)

There’s no shortage of examples and explanations for drawing straight lines using Wu’s algorithm. However, after a pretty exhaustive (or at least exhausting) search around the internet, I was unable to find any satisfactory explanation or examples for drawing circles, ellipses, or other shapes. I did find hokey programs that drew a bunch of wu-lines end-to-end to make a circle. lame.

I ended up looking up Wu’s original article in a library, and so I see it as my duty to share what I learned.

10 randomly-generated ellipses, rendered by the algorithm given below

10 randomly-generated ellipses, rendered by the algorithm given below

Wu dreamt up not just an algorithm, but a principle that can be applied to drawing an anti-aliased line that moves in any varying direction. The crux of the principle is quite simple:

– if the slope is more horizontal then vertical, draw the line in a horizontal fashion, otherwise draw it in a vertical fashion.

What does it mean to draw a line in a horizontal fashion?
1. take an integer x-coordinate
2. calculate y
3. note that y should lie between two vertically adjacent pixels
4. split the full opacity between those two vertically adjacent pixels, proportional to the fractional part of y (and the inverse fraction)
5. take the next integer x-coordinate, and repeat!

for non-straight lines:
6. if the slope has changed from being more-horizontal to more-vertical, we switch to taking integer y-coords, calculating a float-valued x, and sharing the opacity between horizontally adjacent x pixels.

Once you understand the principle, you can apply it to any shape. All you have to do is calculate y from x when horizontal, and x from y when vertical. Thus, the mathematically trickiest part is often found in step 2, calculate y from an integer x (or x from an integer y). Here are a few examples:

Straight lines are very easy: y = mx + b.

Circles have the formula x^2 + y^2 = radius. Solving for y,
y = sqrt (radius^2 - x^2)

Ellipses are described by (x/a)^2 + (y/b)^2 = 1
thus,
y = b * sqrt ( 1 - (x/a)^2 )

2-d cubic splines are generally described by the following:
x(t) = a*t^3 + b*t^2 + c*t + d
y(t) = e*t^3 + f*t^2 + g*t + h

y(x) = damn near impossible

So here’s some pascal code to draw a circle in the true spirit of Wu’s principle:

First, a few types:



type
  pixel_opacity_array = array of byte;  // For use as a 2D array of opacities-- a "bitmap"!

  opacity_bitmap = record
    arr : pixel_opacity_array;
    width, height : longint;
    end;

  plot4_params_type = record
    arr : pixel_opacity_array;
    center_pix, width : longint;
    end;

const
  high_opac = high(byte);

Now for circles:



function wu_circle (const r : single {radius}) : opacity_bitmap;
  var
    xi, yi : longint; // the integer iterators. I could have used 'i' for both x and y but this was less confusing.
    xj, yj : single; // 'single' is a float type. this is the real-value y calculated from the integer x (and vice-versa)
    ffd : longint;  // the forty-five degree coordinate: stop drawing and switch from horizontal to vertical mode.
    rsq : single;  // for convenience: radius squared
    frc : single;  // the fractional part of the calculated real. its inverse will also be used.
    flr : longint;  // xj or yj rounded down (the "floor"). We'll use this twice, so give it a variable.
    p4_params : plot4_params_type;  // parameters to the actual plot function, which plots 4 points at once
  begin
    // initialization section, sets up the resultant plane and the plot4_params_type

    result.arr := nil;
    if r<=0 then exit;
    with result do begin
      width := ceil(r)*2+3;  // we need a little wiggle room in the resultant array, thus the +3
      height := width;
      setlength (arr, width**2);  // I find it easier to use dynamic arrays than 2D static arrays.
      // note, ** is the exponent operator, so width**2 = width*width.
      end;
    p.arr := result.arr;  // dynamic arrays are reference-counted pointers, no copy-on-write.
    p.width := result.width;
    with p do center_pix := (ceil(r)+1)*(width+1);
    // assuming momentarily that r is an integer, let's find center_pix (r,r)
    // first get the row: it is at r*width. Then to get the column, add r.
    // Thus (r,r) is at r*width+r = r*(width+1) -- this holds true for any r
    // r is real, so take ceil(r); also add some wiggle room, so add 1: ceil(r)+1
    // Thus: (ceil(r)+1) * (width+1)

    // Now here's the stuff you've been looking for:

    rsq := r**2;
    ffd := round (r / sqrt(2));  // forty-five-degree coord

    for xi := 0 to ffd do begin
      yj := sqrt (rsq - xi**2);  // the "step 2" formula noted above
      frc := frac(yj);
      flr := floor(yj);
      plot_4_points (xi, flr, 1-frc, p4_params);
      plot_4_points (xi, flr+1, frc, p4_params);
      end;

    for yi := 0 to ffd do begin
      xj := sqrt (rsq-yi**2);
      frc := frac (xj);
      flr := floor(xj);
      plot_4_points (flr, yi, 1-frc, p4_params);
      plot_4_points (flr+1, yi, frc, p4_params);
      end;
  end;

Here’s the plot_4_points function if you’re interested:



procedure plot_4_points (const x,y : longint; const f : single;
    var p4_params : plot4_params_type;
    const take_max : boolean = false);
  var
    opac : byte;
    yw : longint;
  procedure plot (pt : longint);
    begin
      pt := pt + p.center_pix;
      if not take_max
        then p.arr[pt] := opac
        else p.arr[pt] := max (p.arr[pt], opac);  // don't overwrite a touched pixel with a less intense value
    end;
  begin
    opac := round (high_opac*f);  // high_opac is probably 255, that is, high(byte)
    if opac=0 then exit;
    yw := y*p.width;  // I could call plot (x,y) but then I'd be calculating y*width over and over
    if (x>0) and (y>0) then begin
      plot ( x+yw);
      plot ( x-yw);
      plot (-x+yw);
      plot (-x-yw);
      end
    else if x=0 then begin
      plot ( x+yw);
      plot ( x-yw);
      end
    else if y=0 then begin
      plot ( x+yw);
      plot (-x+yw);
      end;
  end;

And finally, the ellipse routine:



function wu_ellipse (const ell_width, ell_height : single) : opacity_bitmap;
  var
    xi, yi : longint;
    xj, yj : single;
    a, b : single;  // the defining characteristics of an ellipse, width/2 and height/2
    asq, bsq : single;  // a squared and b squared
    ffd : longint;  // forty-five-degree coord
    frc : single;  // frac, as with the circle
    flr : longint;  // floor, as with the circle
    p4_params : plot4_params_type;
  begin
    // initialization stuff...

    result.arr := nil;
    if (ell_width<=0) or (ell_height<=0) then exit;
    if ell_width=ell_height then begin
      result := wucircle_to_gamma_plane(ell_width/2);
      exit;
      end;
    a := ell_width/2;
    asq := a**2;
    b := ell_height/2;
    bsq := b**2;
    with result do begin
      width := ceil(ell_width)+3;
      height := ceil(ell_height)+3;
      setlength (arr, width*height);
      p4_params.arr := arr;
      p4_params.width := width;
      p4_params.center_pix := (ceil(a)+1) + (ceil(b)+1)*width;
      end;

    // let's do this thing!

    ffd := round (asq/sqrt(bsq+asq));
    for xi := 0 to ffd do begin
      yj := b*sqrt(1-xi**2/asq);
      frc := frac(yj);
      flr := floor(yj);
      plot_4_points (xi, flr, 1-frc, p4_params);
      plot_4_points (xi, flr+1, frc, p4_params);
      end;

    ffd := round (bsq/sqrt(bsq+asq));
    for yi := 0 to ffd do begin
      xj := a*sqrt(1-yi**2/bsq);
      frc := frac (xj);
      flr := floor(xj);
      plot_4_points (flr, yi, 1-frc, p4_params);
      plot_4_points (flr+1, yi, frc, p4_params);
      end;

  end;

There are a number of things you may notice about these:

  • The circle and ellipse algorithms really are very similar!
  • Although the shape can have a real-valued width/height, its location must be integer. That makes it possible to plot 4 points at a time. (Actually, the circle could have plotted 8 points at a time, but then the algorithm is a little trickier to write, and more different from the ellipse one)
  • (At some point I’ll implement these for real-valued locations)
  • I ignored the boundary conditions! What happens at 45 degrees? Well, you can see from my screenshot that it’s just not that bad. Making the boundaries look nice can be the biggest challenge involved in using Wu’s principles, though, in some cases.
  • I have not actually compiled and run the code in this particular form. Sloppy, I know. I copied and pasted it from my working project, and tweaked some things around to make it easier for y’all to read and use. Bug reports and suggestions welcome; I’ll be happy to link alternate implementations if you’ve got one.
  • Thickening the line is an interesting project that will make the boundaries really ugly!

A word on thickening:
I’ve not finished my own implementation yet, but the idea is as follows. The diagram may be helpful…
– the red line indicates the thickness, which is shown perpendicular to the tangent of the line being drawn.
– thus the red line should also be approximately perpendicular to the tangents of the outer and inner boundaries
– the cyan line is the vertical strip that needs to be plotted (while moving in the more-horizontal fashion)
– the angle between them is shown in yellow

on the trigenometry of thickening a line or curve

on the trigenometry of thickening a line or curve

Do a little trig, and you’ve got the length of the blue line (i.e. thickness / cosine (slope)).
I take that value and divide it by two, then alternately add and subtract that amount from the real calculated point. Then the very top and very bottom points get antialiased in Wu’s fashion.

When the line gets thick, then suddenly the boundary conditions become quite imporant! That right angle shown in green will correspond to a bite taken out of the thickened circle, where we switched from drawing horizontal lines to drawing vertical lines.

The best way I’ve thought of dealing with this is to draw lines in both directions near the boundary.

5 Comments »

  1. There is a simple way to make line a little thiker.
    Just replace
    plot_4_points (xi, flr, 1-frc, p4_params);
    plot_4_points (xi, flr+1, frc, p4_params);
    with
    plot_4_points (xi, flr-1, 1-frc, p4_params);
    plot_4_points (xi, flr, 1, p4_params);
    plot_4_points (xi, flr+1, frc, p4_params);
    I also have not compiled code in this particular form. But it works in my c++ project. Boundary conditions are ugly, I’m now triing to improve it.

    Comment by Simon — February 20, 2012 @ 6:56 am

  2. You could add a hint on how to set the x / y position of the center of the circle.

    Comment by padmalcom — November 4, 2013 @ 8:43 am

  3. Thanks for your comments, guys!

    Simon: The devil’s in the details, and I’m not a fan of those ugly boundary cases, which is why I have omitted any code for making thick ellipses. I discovered AGG (Anti-Grain Geometry) and AggPas some time ago, and have started using those libraries for all my rendering. AggPas is not quite as fast as this, but it is fast enough for my projects. More importantly, it has all sorts of capabilities, including thick lines and fills and a zillion other things I never would have imagined.

    padmalcom: I put an explanation below the circle’s center_pix assignment to clear that up. Or did you mean you want to position the circle in a different place on the canvas? In that case, an assignment of the center_pix which uses the canvas width, the x/y position, and the radius, should do the trick. It also will be necessary to do clipping (make sure you don’t try to plot points that are outside the canvas).

    Comment by David Laurence Emerson — November 4, 2013 @ 6:05 pm

  4. Note 1: I suggest that line thickness should be taken into account for thin lines as well. I mean, for the same interval in X axis, when the slope increases, the line covers more pixels. According to Pythagorean theorem, diagonal (45°) line should be widened 1.4142 times vertically, compared to horizontal line, or have 1.4142 times more weight, when you draw them iterated over a certain X interval. Look at the yellow circle in the first picture, tilt your head by 45° and notice how thin are diagonal segments and how thick are horizontal and vertical ones.
    Note 2: For good visual result, sRGB gamma correction should be applied after all drawing tasks, to avoid inappropriate darkening.

    Comment by Hs — January 1, 2021 @ 10:08 am

  5. […] In case anyone is interested why I need this: I'm trying to see if I can speed up Xiaolin Wu's anti-aliased circle drawing functions by calculating the needed variables incrementally […]

    Pingback by Calculate fractional part of square root without taking square root – Math Solution — March 15, 2022 @ 1:00 am


RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.