绘制实心圆的快速算法?

时间:2009-07-29 15:42:44

标签: c algorithm graphics geometry

我使用Bresenham's circle algorithm进行快速圆绘制。但是,我也希望(应用户的要求)画一个圆圈。

有这样快速有效的方法吗?与布雷森汉姆相同的东西?

我使用的语言是C.

11 个答案:

答案 0 :(得分:80)

阅读the Wikipedia page on Bresenham's (also 'Midpoint') circle algorithm后,最简单的事情似乎是修改其行为,而不是

setPixel(x0 + x, y0 + y);
setPixel(x0 - x, y0 + y);

和类似的,每次你改为

lineFrom(x0 - x, y0 + y, x0 + x, y0 + y);

也就是说,对于每一对(具有相同y)的点,Bresenham会让你情节,而是连接一条线

答案 1 :(得分:51)

只需使用蛮力。此方法迭代了太多像素,但它只使用整数乘法和加法。你完全避免了Bresenham的复杂性和sqrt的可能瓶颈。

for(int y=-radius; y<=radius; y++)
    for(int x=-radius; x<=radius; x++)
        if(x*x+y*y <= radius*radius)
            setpixel(origin.x+x, origin.y+y);

答案 2 :(得分:21)

这是一个C#粗略指南(不应该很难为C得到正确的想法) - 这是“原始”形式而不使用Bresenham来消除重复的平方根。

Bitmap bmp = new Bitmap(200, 200);

int r = 50; // radius
int ox = 100, oy = 100; // origin

for (int x = -r; x < r ; x++)
{
    int height = (int)Math.Sqrt(r * r - x * x);

    for (int y = -height; y < height; y++)
        bmp.SetPixel(x + ox, y + oy, Color.Red);
}

bmp.Save(@"c:\users\dearwicker\Desktop\circle.bmp");

答案 3 :(得分:10)

您可以使用:

void DrawFilledCircle(int x0, int y0, int radius)
{
    int x = radius;
    int y = 0;
    int xChange = 1 - (radius << 1);
    int yChange = 0;
    int radiusError = 0;

    while (x >= y)
    {
        for (int i = x0 - x; i <= x0 + x; i++)
        {
            SetPixel(i, y0 + y);
            SetPixel(i, y0 - y);
        }
        for (int i = x0 - y; i <= x0 + y; i++)
        {
            SetPixel(i, y0 + x);
            SetPixel(i, y0 - x);
        }

        y++;
        radiusError += yChange;
        yChange += 2;
        if (((radiusError << 1) + xChange) > 0)
        {
            x--;
            radiusError += xChange;
            xChange += 2;
        }
    }
}

答案 4 :(得分:6)

我喜欢palm3D的回答。对于蛮力,这是一个非常快速的解决方案。没有平方根或三角函数来减慢速度。它的一个弱点是嵌套循环。

将此转换为单个循环使此功能几乎快两倍。

int r2 = r * r;
int area = r2 << 2;
int rr = r << 1;

for (int i = 0; i < area; i++)
{
    int tx = (i % rr) - r;
    int ty = (i / rr) - r;

    if (tx * tx + ty * ty <= r2)
        SetPixel(x + tx, y + ty, c);
}

这种单循环解决方案可以与线条绘制解决方案的效率相媲美。

            int r2 = r * r;
            for (int cy = -r; cy <= r; cy++)
            {
                int cx = (int)(Math.Sqrt(r2 - cy * cy) + 0.5);
                int cyy = cy + y;

                lineDDA(x - cx, cyy, x + cx, cyy, c);
            }

答案 5 :(得分:3)

以下是我的表现方式:
我使用两位精度的固定点值(我们必须管理半点和半点的平方值)
正如之前的回答中提到的,我也使用平方值而不是平方根 首先,我在圆圈的1/8部分检测到我的圆圈的边界限制。我正在使用这些点的对称来绘制圆的4个“边界”。然后我在圆圈内画出正方形。

中点圆算法不同,这个算法可以使用均匀直径(也可以使用实数直径,只需稍微改动)。

如果我的解释不清楚,请原谅我,我是法国人;)

void DrawFilledCircle(int circleDiameter, int circlePosX, int circlePosY)
{
    const int FULL = (1 << 2);
    const int HALF = (FULL >> 1);

    int size = (circleDiameter << 2);// fixed point value for size
    int ray = (size >> 1);
    int dY2;
    int ray2 = ray * ray;
    int posmin,posmax;
    int Y,X;
    int x = ((circleDiameter&1)==1) ? ray : ray - HALF;
    int y = HALF;
    circlePosX -= (circleDiameter>>1);
    circlePosY -= (circleDiameter>>1);

    for (;; y+=FULL)
    {
        dY2 = (ray - y) * (ray - y);

        for (;; x-=FULL)
        {
            if (dY2 + (ray - x) * (ray - x) <= ray2) continue;

            if (x < y)
            {
                Y = (y >> 2);
                posmin = Y;
                posmax = circleDiameter - Y;

                // Draw inside square and leave
                while (Y < posmax)
                {
                    for (X = posmin; X < posmax; X++)
                        setPixel(circlePosX+X, circlePosY+Y);
                    Y++;
                }
                // Just for a better understanding, the while loop does the same thing as:
                // DrawSquare(circlePosX+Y, circlePosY+Y, circleDiameter - 2*Y);
                return;
            }

            // Draw the 4 borders
            X = (x >> 2) + 1;
            Y = y >> 2;
            posmax = circleDiameter - X;
            int mirrorY = circleDiameter - Y - 1;

            while (X < posmax)
            {
                setPixel(circlePosX+X, circlePosY+Y);
                setPixel(circlePosX+X, circlePosY+mirrorY);
                setPixel(circlePosX+Y, circlePosY+X);
                setPixel(circlePosX+mirrorY, circlePosY+X);
                X++;
            }
            // Just for a better understanding, the while loop does the same thing as:
            // int lineSize = circleDiameter - X*2;
            // Upper border:
            // DrawHorizontalLine(circlePosX+X, circlePosY+Y, lineSize);
            // Lower border:
            // DrawHorizontalLine(circlePosX+X, circlePosY+mirrorY, lineSize);
            // Left border:
            // DrawVerticalLine(circlePosX+Y, circlePosY+X, lineSize);
            // Right border:
            // DrawVerticalLine(circlePosX+mirrorY, circlePosY+X, lineSize);

            break;
        }
    }
}

void DrawSquare(int x, int y, int size)
{
    for( int i=0 ; i<size ; i++ )
        DrawHorizontalLine(x, y+i, size);
}

void DrawHorizontalLine(int x, int y, int width)
{
    for(int i=0 ; i<width ; i++ )
        SetPixel(x+i, y);
}

void DrawVerticalLine(int x, int y, int height)
{
    for(int i=0 ; i<height ; i++ )
        SetPixel(x, y+i);
}

要使用非整数直径,可以提高定点精度或使用双精度值。 根据dY2 +(ray-x)*(ray-x)和ray2(dx²+dy²和r²)之间的差异,甚至可以制作一种抗锯齿效果

答案 6 :(得分:2)

如果你想要一个快速算法,考虑绘制一个N边的多边形,N越高,圆就越精确。

答案 7 :(得分:1)

我发现

palm3D的蛮力算法是一个很好的起点。该方法使用相同的前提,但是它包含了两种跳过大多数像素检查的方法。

首先,这是代码:

int largestX = circle.radius;
for (int y = 0; y <= radius; ++y) {
    for (int x = largestX; x >= 0; --x) {
        if ((x * x) + (y * y) <= (circle.radius * circle.radius)) {
            drawLine(circle.center.x - x, circle.center.x + x, circle.center.y + y);
            drawLine(circle.center.x - x, circle.center.x + x, circle.center.y - y);
            largestX = x;
            break; // go to next y coordinate
        }
    }
}

接下来,是解释。

首先要注意的是,如果找到给定水平线的圆内的最小x坐标,则立即知道最大x坐标。 这是由于圆的对称性。如果最小x坐标比圆的边界框的左边靠前10个像素,那么最大x比圆的边界框的右边靠后10个像素。

从高x值迭代到低x值的原因是,可以通过较少的迭代找到最小的x值。这是因为对于大多数线,由于最小的x值比圆的中心x坐标更靠近边界框的左侧,如this image所示,因为圆是向外弯曲的 接下来要注意的是,由于圆也是垂直对称的,因此您找到的每条线都会给您免费绘制第二条线,每次在圆的上半部找到一条线时,您在下半部都会得到一条半径-y坐标。因此,找到任何一条线时,都可以绘制两条线,并且只需要迭代y值的上半部分。

最后要注意的是,如果您从圆心的ay值开始,然后向y的顶部移动,则下一行的最小x值必须更靠近中心圆的x坐标比最后一行高。这也是由于当您沿着圆圈向上移动时,圆圈朝着中心x值弯曲得更近。 Here is a visual on how that is the case.

总结:

  1. 如果找到一条线的最小x坐标,则可以免费获得最大x坐标。
  2. 您发现在圆的上半部画的每条线都免费为您在圆的下半部画一条线。
  3. 从中心y坐标到顶部迭代时,每条线的每个最小x坐标必须比以前的x坐标更靠近圆心。

您也可以存储(radius * radius)(y * y)的值,而不用计算它们  多次。

答案 8 :(得分:1)

好主意! 由于我从事的项目需要绘制成千上万个圆圈,因此我在这里评估了所有建议(并通过预先计算半径的平方来改进了一些建议):

http://quick-bench.com/mwTOodNOI81k1ddaTCGH_Cmn_Ag

enter image description here

Rev变体仅交换了x和y,因为沿y轴的连续访问使用我的网格/画布结构的工作方式更快。<​​/ p>

显而易见的赢家是Daniel Earwicker的方法(DrawCircleBruteforcePrecalc),该方法会预先计算Y值以避免不必要的半径检查。令人惊讶的是,它抵消了由sqrt调用引起的额外计算。

一些评论表明,与单个循环配合使用的kmillen变体(DrawCircleSingleLoop)应该非常快,但在这里是最慢的。我认为这是由于所有分歧。但是也许我已将其错误地修改为该代码中的全局变量。如果有人看看会很好。

编辑:自从大学时代以来第一次以某种汇编代码进行查找之后,我设法发现该圆的原点的最后增加是罪魁祸首。 对这些进行预先计算,根据基准,我将最快的方法提高了3.7-3.9倍! http://quick-bench.com/7ZYitwJIUgF_OkDUgnyMJY4lGlA  很棒。

这是我的代码:

for (int x = -radius; x < radius ; x++)
{
    int hh = (int)std::sqrt(radius_sqr - x * x);
    int rx = center_x + x;
    int ph = center_y + hh;

    for (int y = center_y-hh; y < ph; y++)
        canvas[rx][y] = 1;
}

答案 9 :(得分:0)

我只会生成一个点列表,然后使用多边形绘制函数进行渲染。

答案 10 :(得分:0)

这可能不是您正在寻找的算法,也不是性能最高的算法,
但我总是这样做:

void fillCircle(int x, int y, int radius){

   // fill a circle
   for(int rad = radius; rad >= 0; rad--){

      // stroke a circle
      for(double i = 0; i <= PI * 2; i+=0.01){

         int pX = x + rad * cos(i);
         int pY = y + rad * sin(i);

         drawPoint(pX, pY);

      }

   }

}