Calculate pi by counting pixels

0

To celebrate Pi Day (3/14) this year, I thought it would be fun to calculate pi using a nonstandard method. For this method, I used DOS graphics mode to draw a circle, then counted the pixels to measure the circle. This is a very simple way to measure pi, but it was a fun exercise and I wanted to share it.

DOS graphics

For this exercise, I wrote my sample programs in FreeDOS using the Open Watcom compiler. I used the standard 640×480 graphics mode, which you may recognize as the classic “VGA” graphics mode from the IBM PC era.

Programming graphics in DOS is fairly simple. With DOS graphics, you don’t have to set up windowing or make a specialized call to a game library just to draw to the screen. Simply use the _setvideomode function to set up the display with a specific video mode, and then you’re ready to start drawing to the screen.

Open Watcom provides several other functions in graph.h to support graphics elements. For example, the _setcolor function sets the color to draw in, using the iRGB color space: that’s intensity, Red, Green, and Blue. The value 0001 (1) is low-intensity blue, 0010 (2) is low-intensity green, and 0111 (7) is low-intensity white.

To draw a circle on the screen, use the _ellipse function, with an option to either fill in the ellipse (_GFILLINTERIOR) or draw just an outline (`_GBORDER). Graphics uses x,y screen coordinates, so define the ellipse as the upper-left and bottom-right “corners.” To draw a circle, draw an ellipse with the same x and y dimensions.

Counting pixels

A core part of measuring a circle is counting pixels. For that, Open Watcom provides a _getpixel function to query the color at a specific x,y coordinate. This makes it easy to count pixels to measure a circle; draw the circle in white on a black background and _getpixel will return either 7 (white) or 0 (black).

I wrote this function (count.c) to count the pixels in an area from x0,y0 to x1,y1:

#include <graph.h>

unsigned long count_pixels(short x0, short y0, short x1, short y1)
{
    unsigned long count = 0;
    short x, y;

    for (x = x0; x <= x1; x++) {
        for (y = y0; y <= y1; y++) {
            /* count pixels .. change colors as we go, so we can see
               the counting process */

            if (_getpixel(x, y)) {
                _setcolor(14);         /* br yellow */
                count++;
            }
            else {
                _setcolor(1);          /* blue */
            }

            _setpixel(x, y);
        }
    }

    return count;
}

The function loops through every x,y coordinate in the specified area, and examines each pixel. If the pixel has any color, the function increments a counter and turns that pixel bright yellow. If the pixel was originally black, the function simply turns the x,y coordinate into a blue pixel. This allows me to watch the function as it counts pixels.

Measure the circumference

My first attempt drew the outline of a circle to the screen, and counted the pixels for the circumference. This is a very naive way to estimate the circumference using pixels, and gives a completely wrong result.

But let’s see it anyway! The formula for the circumference C of a circle is 2 π r, where r is the radius. The diameter d is 2r, so really the circumference is C = π d. With a little algebra, we can calculate π as C/d.

The program defines a few values for the mode and diameter; this allowed me to update the program more easily if I wanted to instead use a higher graphics mode like 1024×768. After that, the program uses _setvideomode to put the display into graphics mode at 640×480 resolution, then uses _ellipse to draw a circle from 0,0 to 479,479, then counts the pixels with my other function.

#include <stdio.h>
#include <graph.h>

extern unsigned long count_pixels(short x0, short y0, short x1, short y1);

#define MODE _VRES16COLOR
#define DIAM 480

int main()
{
    unsigned long circ;

    if (_setvideomode(MODE) == 0) {    /* 640x480 */
        puts("cannot set video mode");
        return 1;
    }

    /* calculate pi by circumference (will get the wrong value) */
    /* C = 2 pi r .. or C = pi d ... or pi = C / d */

    _setcolor(7);                      /* white */
    _ellipse(_GBORDER, 0, 0, DIAM - 1, DIAM - 1);

    circ = count_pixels(0, 0, DIAM - 1, DIAM - 1);

    /* done */

    _setvideomode(_DEFAULTMODE);

    printf("pi = C / d = %f\n", ((float) circ) / ((float) DIAM));

    return 0;
}

Save this file as circ.c and combine it with the count.c function to create a DOS program:

> wcl circ.c count.c

Run the circ.exe program to calculate pi from the circumference of a circle:

Counting the pixels in the circumference

Unfortunately, this calculates pi at 2.825. And because of the naive method to count pixels to “measure” the circumference, this is about as good as it gets. The reason is because pixels are square and estimating the circumference of a circle using square pixels will always be off.

I’ll save you the math, but look at the simple case: if you tried to measure the circumference of a circle of diameter d by drawing a square around it, you’d have a square with sides of length d, and a perimeter that’s four times that, or 4d.

Measuring a circle with a square

Calculating “pi” with this circumference is C/d, or 4.

You can break down the “square” into a series of straight-line “steps,” shown above. But this doesn’t really improve the measurement, because the perimeter is the sum of each of the “lines” in the “steps.” Counting with pixels is just a simplification of that.

Measure the area

A better way to calculate pi by counting pixels is to fill the circle, and count the pixels to estimate the area. The formula for the area A of a circle of radius r is π r². Doing a little algebra reveals the calculation for π as A/r².

The program to calculate pi from the area is a minor update to the previous program. Instead of using _GBORDER to draw an outline, use _GFILLINTERIOR to fill the area. Counting pixels gives an estimate of the area. The final calculation divides the area by the square of the radius:

#include <stdio.h>
#include <graph.h>

#define MODE _VRES16COLOR
#define DIAM 480

extern unsigned long count_pixels(short x0, short y0, short x1, short y1);

int main()
{
    unsigned long area;
    float r = DIAM / 2.0;

    if (_setvideomode(MODE) == 0) {    /* 640x480 */
        puts("cannot set video mode");
        return 1;
    }

    /* calculate pi by area (not perfect, but closer) */
    /* A = pi r^2 .. or pi = A / r^2 */

    _setcolor(7);                      /* white */
    _ellipse(_GFILLINTERIOR, 0, 0, DIAM - 1, DIAM - 1);

    area = count_pixels(0, 0, DIAM - 1, DIAM - 1);

    /* done */

    _setvideomode(_DEFAULTMODE);

    printf("pi = A / r^2 = %f\n", ((float) area) / r / r);

    return 0;
}

Save this file as area.c and combine it with the count.c function to create a new DOS program:

> wcl area.c count.c
Counting the pixels in the area

Since we’re using pixels to “measure” the area of the circle, the final calculation of pi will be off, but not by much: 3.1444.

Leave a Reply