【问题标题】:fast algorithm for drawing filled circles?绘制实心圆的快速算法?
【发布时间】:2009-07-29 15:42:44
【问题描述】:

我正在使用Bresenham's circle algorithm 快速绘制圆。但是,我也想(应用户的要求)画一个实心圆圈。

有没有一种快速有效的方法来做到这一点?与 Bresenham 类似的东西?

我使用的语言是 C。

【问题讨论】:

    标签: c algorithm graphics geometry


    【解决方案1】:

    读过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);
    

    也就是说,对于 Bresenham 的每一对点(具有相同的 y绘图,你改为用一条线连接。。 p>

    【讨论】:

    • 真的有用吗?我试过了..它并没有完全填满这个圈子。我错过了什么吗?无论如何,下面有一些正确的答案。
    • @AJed 要知道你是否遗漏了什么,我们需要查看你的代码,a new question of your own
    • 这会不会导致第 2/3 和第 6/7 象限的扫描线重复?在这些 x 每一步都会发生变化,并且决策变量控制 y。
    • @AakashM 好吧,看来我误读了答案,这似乎是一种扫描线算法。我不记得确切,但我认为已经将对称点连接了 180 度以使线条和这不起作用,因为它们中有很多重叠。进行一些编辑以回答,以便我可以通过downvote删除。
    • 我正在尝试为边框添加一种颜色,并为填充另一种颜色。我完全按照你所说的,在我用算法的基本命令构建边界之后。问题是在圆圈的上部和底部,边框被填充颜色覆盖,知道为什么吗?
    【解决方案2】:

    只需使用蛮力。这个方法迭代了太多的像素,但它只使用整数乘法和加法。您完全避免了 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);
    

    【讨论】:

    • 我喜欢这个答案,因为它以非常直接的方式解决了问题。唯一的问题是它生成的圆与中点圆算法不同。这些圆圈比中点的等效圆圈“更薄”,这似乎是更正确的形状。有什么办法解决吗?
    • 这听起来很可怕,但我发现在大多数情况下,如果我稍微修改检查,我可以从这个算法中得到完全相同的圆作为中点。那些时候它并不完全匹配,它非常接近。对校验的修改为:x * x + y * y
    • 我和 Dwight 有同样的问题,但为了避免浮动转换,只需用这个替换 if 语句:if(x*x+y*y &lt; radius*radius + radius) 也只是为了得到圆圈(环),你可以这样做if(x*x+y*y &gt; radius*radius - radius &amp;&amp; x*x+y*y &lt; radius*radius + radius)
    • @Marcin 为了避免浮点转换,您添加了一个全新的分支和 5 个新的算术运算,这应该更快?
    • @Slight 没有额外的操作,将&lt;= 替换为&lt;,将radius * 0.8f 替换为+ radius 另一个if 语句不相关(当您只需要一个大纲时)。跨度>
    【解决方案3】:

    这是一个 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");
    

    【讨论】:

    • 或者在y上循环并画水平线。偶尔选择其中一个是有原因的,但在大多数情况下,这并不重要。无论哪种方式,您都可以使用相同的 Bresenham 逻辑来快速找到端点。
    • 否,但您可以使用 Bresenham 来避免这种情况。基本思想是在圆所覆盖的每个 x 坐标处的上下点之间“连接点”。
    • 配置文件看看哪个是最好的。如果有任何区别,水平方向应该会更好。它摆脱了大步的乘法,可能会导致更少的错误。
    • “所有这些 Math.Sqrts 都不会特别快” - 反汇编显示 C#/JIT 组合在我的 64 位机器上发出内联 SQRTSD 指令,所以难怪它运行得非常快。我无法衡量 Math.Sqrt 和简单加法之间的差异。所以我觉得你的评论很可能是基于pre-FP指令集的猜测!
    • 这个长期存在的问题 - 当基本面发生变化时,如何处理经过精心调整的有根据的猜测引擎?
    【解决方案4】:

    你可以用这个:

    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;
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      我喜欢 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);
                  }
      

      【讨论】:

      • 我有点惊讶您的解决方案比 palm3d 快。你量过吗?你有数字吗?
      • 此算法绘制的像素比原始算法少。对于 10 的半径,它减少了 41 个循环。在我的笔记本电脑上,对这个函数和原始函数运行 100,000,000 次测试,我们得到:ORIGINAL: 43nsTHIS:42ns 运行良好..运行不佳:ORIGINAL:87nsTHIS: 54ns.. 但这些都是纳秒级,所以就效率而言,几乎没有区别。测试于:Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz
      • 嗯,我对应该更快的地方感到很困惑。我目前正在尝试quick-bench.com/VtgMOwU8IQoa7biFNHFZ2ws5hlk 中的所有变体,但速度要慢得多。不过,这并没有让我感到惊讶,因为划分通常非常缓慢。
      【解决方案6】:

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

      http://quick-bench.com/mwTOodNOI81k1ddaTCGH_Cmn_Ag

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

      明显的赢家是 Daniel Earwicker 的方法 (DrawCircleBruteforcePrecalc),它预先计算 Y 值以避免不必要的半径检查。有点令人惊讶的是,它否定了 sqrt 调用引起的额外计算。

      一些 cmets 建议使用单循环的 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;
      }
      

      【讨论】:

        【解决方案7】:

        我发现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 值的上半部分。

        最后要注意的是,如果你从位于圆心的y值开始,然后向顶部移动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/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² and r²) 之间的差异来制作一种抗锯齿

          【讨论】:

            【解决方案9】:

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

            【讨论】:

            • 这可能需要某种硬件加速才能最快
            【解决方案10】:

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

            【讨论】:

            • 如果他已经为开放版本实现了 Bresenham,那么他在较低层工作,然后使用 API...用于学习目的或实施 API。
            【解决方案11】:

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

            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);
            
                  }
            
               }
            
            }
            
            

            【讨论】:

              【解决方案12】:

              以下两种方法通过一次绘制圆的多个部分来避免重复的平方根计算,因此应该相当快:

              void circleFill(const size_t centerX, const size_t centerY, const size_t radius, color fill) {
                  if (centerX < radius || centerY < radius || centerX + radius > width || centerY + radius > height) 
                      return;
              
                  const size_t signedRadius = radius * radius;
                  for (size_t y = 0; y < radius; y++) {
                      const size_t up = (centerY - y) * width;
                      const size_t down = (centerY + y) * width;
                      const size_t halfWidth = roundf(sqrtf(signedRadius - y * y));
                      for (size_t x = 0; x < halfWidth; x++) {
                          const size_t left = centerX - x;
                          const size_t right = centerX + x;
                          pixels[left + up] = fill;
                          pixels[right + up] = fill;
                          pixels[left + down] = fill;
                          pixels[right + down] = fill;
                      }
                  }
              }
              
              
              void circleContour(const size_t centerX, const size_t centerY, const size_t radius, color stroke) {
                  if (centerX < radius || centerY < radius || centerX + radius > width || centerY + radius > height) 
                      return;
                  
                  const size_t signedRadius = radius * radius;
                  const size_t maxSlopePoint = ceilf(radius * 0.707106781f); //ceilf(radius * cosf(TWO_PI/8));
              
                  for (size_t i = 0; i < maxSlopePoint; i++) {
                      const size_t depth = roundf(sqrtf(signedRadius - i * i));
              
                      size_t left = centerX - depth;
                      size_t right = centerX + depth;
                      size_t up = (centerY - i) * width;
                      size_t down = (centerY + i) * width;
              
                      pixels[left + up] = stroke;
                      pixels[right + up] = stroke;
                      pixels[left + down] = stroke;
                      pixels[right + down] = stroke;
              
                      left = centerX - i;
                      right = centerX + i;
                      up = (centerY - depth) * width;
                      down = (centerY + depth) * width;
              
                      pixels[left + up] = stroke;
                      pixels[right + up] = stroke;
                      pixels[left + down] = stroke;
                      pixels[right + down] = stroke;
                  }
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-10-26
                • 2019-01-21
                • 1970-01-01
                • 2015-12-03
                • 1970-01-01
                • 2021-04-19
                相关资源
                最近更新 更多