在今年的圆周率日,我想编写一个程序,通过在 FreeDOS 图形模式下绘制一个圆,然后计数像素来估算圆周长。我天真地认为这会给我一个圆周率的近似值。我没指望得到 3.14,但我想这个值会有点接近 3.0。
我错了。通过计数绘制圆所需的像素来估算圆的周长会得到错误的结果。无论我尝试什么分辨率,最终的圆周率计算(周长除以直径)总是大约 2.8。
你不能通过计数像素来计算圆周率
我用 OpenWatcom C 编写了一个 FreeDOS 程序,该程序在屏幕上绘制一个圆,然后计数构成该圆的像素。我在 FreeDOS 中编写它,因为 DOS 程序可以通过使用 OpenWatcom `_setvideomode` 函数轻松进入图形模式。`_VRES16COLOR` 视频模式将显示器置于 640×480 分辨率,16 色,这是一种常见的“经典 VGA”屏幕分辨率。在标准的 16 色 DOS 调色板中,颜色 0 是黑色,颜色 1 是蓝色,颜色 7 是低强度白色,颜色 15 是高强度白色。
在图形模式下,您可以使用 `_ellipse` 函数在屏幕上绘制一个椭圆,从左上角的某个起始 x,y 坐标到右下角的最终 x,y 坐标。如果高度和宽度相同,则椭圆是一个圆。请注意,在图形模式下,x 和 y 从零开始计数,因此左上角始终为 0,0。

(Jim Hall,CC BY-SA 4.0)
您可以使用 `_getpixel` 函数获取屏幕上指定 x,y 坐标处像素的颜色。为了显示我的程序的进度,我还使用了 `_setpixel` 函数在屏幕上的任何 x,y 处绘制单个像素。当程序找到定义圆的像素时,我将该像素更改为亮白色。对于其他像素,我将颜色设置为蓝色。

(Jim Hall,CC BY-SA 4.0)
使用这些图形函数,您可以编写一个程序,该程序在屏幕上绘制一个圆,然后迭代圆的所有 x,y 坐标以计数像素。对于任何颜色为 7(圆的颜色)的像素,将像素计数加一。最后,您可以使用像素总数作为圆周长的估计值
#include <stdio.h>
#include <graph.h>
int
main()
{
unsigned long count;
int x, y;
/* draw a circle */
_setvideomode(_VRES16COLOR); /* 640x480 */
_setcolor(7); /* white */
_ellipse(_GBORDER, 0, 0, 479, 479);
/* count pixels */
count = 0;
for (x = 0; x <= 479; x++) {
for (y = 0; y <= 479; y++) {
if (_getpixel(x, y) == 7) {
count++;
/* highlight the pixel */
_setcolor(15); /* br white */
_setpixel(x, y);
}
else {
/* highlight the pixel */
_setcolor(1); /* blue */
_setpixel(x, y);
}
}
}
/* done */
_setvideomode(_DEFAULTMODE);
printf("pixel count (circumference?) = %lu\n", count);
puts("diameter = 480");
printf("pi = c/d = %f\n", (double) count / 480.0);
return 0;
}
但是,通过计数像素来确定周长会低估圆的实际周长。因为圆周率是圆的周长与其直径的比率,所以我的圆周率计算值明显低于 3.14。我尝试了几种视频分辨率,但最终结果始终约为 2.8。
pixel count (circumference?) = 1356
diameter = 480
pi = c/d = 2.825000
您需要测量像素之间的距离才能获得圆周率
通过计数像素来估算周长的问题在于,像素只是圆形绘图的样本。像素是网格中的离散点,而圆是连续的图形。为了提供更好的周长估计值,您必须测量像素之间的距离,并将该总测量值用于周长。
要更新程序,您必须编写一个函数来计算任意两个像素之间的距离:x0,y0 和 x,y。您不需要一堆花哨的数学或算法,只需要知道 OpenWatcom `_ellipse` 函数仅以您为圆设置的颜色绘制实心像素。该函数不尝试通过以某种中间颜色绘制附近的像素来提供抗锯齿功能。这使您可以简化数学运算。在圆中,像素始终彼此直接相邻:垂直、水平或对角线。
对于垂直或水平相邻的像素,像素“距离”很简单。距离为 1。
对于对角线相邻的像素,您可以使用勾股定理 a²+b²=c² 来计算两个对角线像素之间的距离,即 2 的平方根,或约 1.414。
double
pixel_dist(int x0, int y0, int x, int y)
{
if (((x - x0) == 0) && ((y0 - y) == 1)) {
return 1.0;
}
if (((y0 - y) == 0) && ((x - x0) == 1)) {
return 1.0;
}
/* if ( ((y0-y)==1) && ((x-x0)==1) ) { */
return 1.414;
/* } */
}
我将最后一个“if”语句包装在注释中,以便您了解该条件应该表示什么。
为了测量周长,我们不需要检查整个圆。我们可以通过仅处理左上象限来节省一些时间和精力。这也使我们能够知道圆中第一个像素的起始坐标;我们将跳过 0,239 处的第一个像素,而是将其假定为我们在测量四分之一周长时的第一个 x0,y0 坐标。

(Jim Hall,CC BY-SA 4.0)
最终程序类似于我们的“计数像素”程序,但改为测量圆的左上象限中像素之间的微小距离。您可能会注意到,程序向下计数 y 坐标,从 238 到 0。这符合四分之一圆中已知的起始 x0,y0 坐标为 0,239 的假设。在该假设下,程序只需要评估 0 到 238 之间的 y 坐标。为了估算圆的总周长,将四分之一测量值乘以 4。
#include <stdio.h>
#include <graph.h>
double
pixel_dist(int x0, int y0, int x, int y)
{
...
}
int
main()
{
double circum;
int x, y;
int x0, y0;
/* draw a circle */
_setvideomode(_VRES16COLOR); /* 640x480 */
_setcolor(7); /* white */
_ellipse(_GBORDER, 0, 0, 479, 479);
/* calculate circumference, use upper left quadrant only */
circum = 0.0;
x0 = 0;
y0 = 479 / 2;
for (x = 0; x <= 479 / 2; x++) {
for (y = (479 / 2) - 1; y >= 0; y--) {
if (_getpixel(x, y) == 7) {
circum += pixel_dist(x0, y0, x, y);
x0 = x;
y0 = y;
/* highlight the pixel */
_setcolor(15); /* br white */
_setpixel(x, y);
}
else {
/* highlight the pixel */
_setcolor(1); /* blue */
_setpixel(x, y);
}
}
}
circum *= 4.0;
/* done */
_setvideomode(_DEFAULTMODE);
printf("circumference = %f\n", circum);
puts("diameter = 480");
printf("pi = c/d = %f\n", circum / 480.0);
return 0;
}
这提供了更好的周长估计值。它仍然有点偏差,因为使用像素测量圆仍然是一个相当粗略的近似值,但最终的圆周率计算值更接近预期的 3.14。
circumference = 1583.840000
diameter = 480
pi = c/d = 3.299667
评论已关闭。