【问题标题】:Traverse Pixels in a circle from the center从中心开始以圆形遍历像素
【发布时间】:2015-03-02 11:22:57
【问题描述】:

我需要一个像 Bresenham's circle 算法这样的算法,但需要进行一些修改。 该算法必须访问半径内的所有像素(因此本质上是一个填充)。

  • 算法必须从圆心开始
  • 它必须访问通常会访问的所有点(无漏洞)
  • 它必须准确地访问圆中的每个点一次

我想出的一种技术是首先确定圆内的所有像素坐标,方法是通过圆的矩形并使用 Math.Sqrt 检查它是否在圆内。 然后它会按距离对像素进行排序,然后访问它们中的每一个。

这正是我想要的,除了要快。

所以我的问题是: 有没有一种快速的方法可以做到这一点,而无需获取、排序和访问每个像素?

为了澄清起见,我实际上并不想在图像上绘图,我只想按描述的顺序遍历它们。

【问题讨论】:

  • 它是否必须以螺旋方式访问它们,或者是否每个下一个像素都必须接触前一个像素,或者只要第一个项目位于中心,任何订单都会这样做?
  • 如果您使用平方根,这意味着您已经稍微操纵了通常的圆方程。为什么不使用原始方程,用平方而不是平方根?它们更快。
  • 一旦你这样做了,看看你是否能得到一个表达式来表示 (x, y) 和 (x+1, y) 的值之间的差异。如果这个表达式比从头开始计算 (x+1, y) 处的值更简单,那么你现在有一个更便宜的方法来计算右侧像素的值,因为你已经计算了当前的值像素。
  • 通过“算法必须从圆心开始”你的意思是像素按到中心的距离顺序访问吗?这个要求有多难?如果像素距离相同怎么办?
  • @Vilx & Euphoric:下一个访问点应该始终是与中心距离最小的点。当有多个点共享相同的最小距离时,首先访问哪个点并不重要。螺旋形可以,但不是必需的。顺便说一句,是的,我只使用了正方形,显然不需要 sqrt。

标签: c# .net algorithm geometry


【解决方案1】:

首先,我们可以使用事实,即圆可以分为 8 个八分圆。所以我们只需要填充单个八分圆并使用简单的 +- 坐标变化来获得完整的圆。因此,如果我们尝试只填充一个八分圆,我们只需要担心中心的 2 个方向:左上角和左上角。此外,巧妙地使用优先级队列(.NET 没有它,因此您需要在其他地方找到它)和哈希映射等数据结构可以显着提高性能。

    /// <summary>
    /// Make sure it is structure.
    /// </summary>
    public struct Point
    {
        public int X { get; set; }
        public int Y { get; set; }

        public int DistanceSqrt()
        {
            return X * X + Y * Y;
        }
    }

    /// <summary>
    /// Points ordered by distance from center that are on "border" of the circle.
    /// </summary>
    public static PriorityQueue<Point> _pointsToAdd = new PriorityQueue<Point>();
    /// <summary>
    /// Set of pixels that were already added, so we don't visit single pixel twice. Could be replaced with 2D array of bools.
    /// </summary>
    public static HashSet<Point> _addedPoints = new HashSet<Point>();

    public static List<Point> FillCircle(int radius)
    {
        List<Point> points = new List<Point>();

        _pointsToAdd.Enqueue(new Point { X = 1, Y = 0 }, 1);
        _pointsToAdd.Enqueue(new Point { X = 1, Y = 1 }, 2);
        points.Add(new Point {X = 0, Y = 0});

        while(true)
        {
            var point = _pointsToAdd.Dequeue();
            _addedPoints.Remove(point);

            if (point.X >= radius)
                break;

            points.Add(new Point() { X = -point.X, Y = point.Y });
            points.Add(new Point() { X = point.Y, Y = point.X });
            points.Add(new Point() { X = -point.Y, Y = -point.X });
            points.Add(new Point() { X = point.X, Y = -point.Y });

            // if the pixel is on border of octant, then add it only to even half of octants
            bool isBorder = point.Y == 0 || point.X == point.Y;
            if(!isBorder)
            {
                points.Add(new Point() {X = point.X, Y = point.Y});
                points.Add(new Point() {X = -point.X, Y = -point.Y});
                points.Add(new Point() {X = -point.Y, Y = point.X});
                points.Add(new Point() {X = point.Y, Y = -point.X});
            }

            Point pointToLeft = new Point() {X = point.X + 1, Y = point.Y};
            Point pointToLeftTop = new Point() {X = point.X + 1, Y = point.Y + 1};

            if(_addedPoints.Add(pointToLeft))
            {
                // if it is first time adding this point
                _pointsToAdd.Enqueue(pointToLeft, pointToLeft.DistanceSqrt());
            }

            if(_addedPoints.Add(pointToLeftTop))
            {
                // if it is first time adding this point
                _pointsToAdd.Enqueue(pointToLeftTop, pointToLeftTop.DistanceSqrt());
            }
        }

        return points;
    }

我会把扩展的完整列表留给你。还要确保八分圆的边界不会导致点数加倍。

好的,我无法处理它,我自己做了。另外,为了确保它具有您想要的属性,我做了简单的测试:

        var points = FillCircle(50);

        bool hasDuplicates = points.Count != points.Distinct().Count();
        bool isInOrder = points.Zip(points.Skip(1), (p1, p2) => p1.DistanceSqrt() <= p2.DistanceSqrt()).All(x => x);

【讨论】:

  • 您确定在同一个八分圆内所有等距的像素都将彼此相邻吗?我认为不是……
  • @Vilx- 什么? “所有等距像素”是什么意思?
  • 确认,抱歉。好的,这是一张图片。 i62.tinypic.com/6dxopf.png 我为 10 像素半径内的所有像素计算了 x*x+y*y。 80 到 90 之间的值已被着色为洋红色。请注意,即使在同一个八分圆内,“85”的三个值也彼此严重分开。
  • @Vilx- 是的,但这不是算法的要求。唯一的要求是点必须按距中心的距离排序。没有什么说他们需要彼此相邻。
  • 否,但如果不是,您的算法将失败。您只考虑相邻的像素。
【解决方案2】:

我找到了满足我的性能需求的解决方案。 很简单,就是一个偏移数组。

    static Point[] circleOffsets;
    static int[] radiusToMaxIndex;

    static void InitCircle(int radius)
    {
        List<Point> results = new List<Point>((radius * 2) * (radius * 2));

        for (int y = -radius; y <= radius; y++)
            for (int x = -radius; x <= radius; x++)
                results.Add(new Point(x, y));

        circleOffsets = results.OrderBy(p =>
        {
            int dx = p.X;
            int dy = p.Y;
            return dx * dx + dy * dy;
        })
        .TakeWhile(p =>
        {
            int dx = p.X;
            int dy = p.Y;
            var r = dx * dx + dy * dy;
            return r < radius * radius;
        })
        .ToArray();

        radiusToMaxIndex = new int[radius];
        for (int r = 0; r < radius; r++)
            radiusToMaxIndex[r] = FindLastIndexWithinDistance(circleOffsets, r);
    }

    static int FindLastIndexWithinDistance(Point[] offsets, int maxR)
    {
        int lastIndex = 0;

        for (int i = 0; i < offsets.Length; i++)
        {
            var p = offsets[i];
            int dx = p.X;
            int dy = p.Y;
            int r = dx * dx + dy * dy;

            if (r > maxR * maxR)
            {
                return lastIndex + 1;
            }
            lastIndex = i;
        }

        return 0;
    }

使用此代码,您只需从 radiusToMaxIndex 获取停止位置的索引,然后遍历 circleOffsets 并访问这些像素。 这样会消耗大量内存,但您始终可以将偏移量的数据类型从 Point 更改为以 Bytes 作为成员的自定义数据类型。

这个解决方案非常快,足以满足我的需求。它显然有使用一些内存的缺点,但老实说,实例化 System.Windows.Form 占用的内存比这更多......

【讨论】:

    【解决方案3】:

    您已经提到了 Bresenhams 的圆算法。这是一个很好的起点:您可以从中心像素开始,然后画出越来越大的 Bresenham 圆。

    问题在于 Bresenham 圆算法会在一种莫尔效应中错过对角线附近的像素。在另一个问题中,我有adopted the Bresenham algorithm for drawing between an inner and outer circle。以该算法为基础,循环画圆的策略奏效了。

    由于 Bresenham 算法只能将像素放置在离散的整数坐标上,因此访问像素的顺序不会严格按照距离递增的顺序。但距离始终在您正在绘制的当前圆的一个像素内。

    下面是一个实现。那是在 C 中,但它只使用标量,因此适应 C# 应该不难。 setPixel 是您在迭代时对每个像素所做的操作。

    void xLinePos(int x1, int x2, int y)
    {
        x1++;
        while (x1 <= x2) setPixel(x1++, y);
    }
    
    void yLinePos(int x, int y1, int y2)
    {
        y1++;
        while (y1 <= y2) setPixel(x, y1++);
    }
    
    void xLineNeg(int x1, int x2, int y)
    {
        x1--;
        while (x1 >= x2) setPixel(x1--, y);
    }
    
    void yLineNeg(int x, int y1, int y2)
    {
        y1--;
        while (y1 >= y2) setPixel(x, y1--);
    }
    
    void circle2(int xc, int yc, int inner, int outer)
    {
        int xo = outer;
        int xi = inner;
        int y = 0;
        int erro = 1 - xo;
        int erri = 1 - xi;
    
        int patch = 0;
    
        while (xo >= y) {         
            if (xi < y) {
                xi = y;
                patch = 1;
            }
    
            xLinePos(xc + xi, xc + xo, yc + y);
            yLineNeg(xc + y,  yc - xi, yc - xo);
            xLineNeg(xc - xi, xc - xo, yc - y);
            yLinePos(xc - y,  yc + xi, yc + xo);
    
            if (y) {
                yLinePos(xc + y,  yc + xi, yc + xo);
                xLinePos(xc + xi, xc + xo, yc - y);
                yLineNeg(xc - y,  yc - xi, yc - xo);
                xLineNeg(xc - xi, xc - xo, yc + y);
            }
    
            y++;
    
            if (erro < 0) {
                erro += 2 * y + 1;
            } else {
                xo--;
                erro += 2 * (y - xo + 1);
            }
    
            if (y > inner) {
                xi = y;
            } else {
                if (erri < 0) {
                    erri += 2 * y + 1;
                } else {
                    xi--;
                    erri += 2 * (y - xi + 1);
                }
            }
        }
    
        if (patch) {
            y--;
            setPixel(xc + y, yc + y);
            setPixel(xc + y, yc - y);
            setPixel(xc - y, yc - y);
            setPixel(xc - y, yc + y);
        }
    }
    
    /*
     *      Scan pixels in circle in order of increasing distance
     *      from centre
     */
    void scan(int xc, int yc, int r)
    {
        int i;
    
        setPixel(xc, yc);
        for (i = 0; i < r; i++) {
            circle2(xc, yc, i, i + 1);
        }
    }
    

    此代码通过跳过交替八分圆上的重合像素来处理不访问两个八分圆中的像素。 (编辑:原始代码中仍然存在错误,但现在已通过“补丁”变量修复。)

    还有改进的空间:内圈基本上就是上一次迭代的外圈,所以计算两次也没有意义;您可以保留前一个圆的外部点的数组。

    xLinePos 函数也有点太复杂了。在该函数中绘制的像素永远不会超过两个,通常只有一个。

    如果搜索顺序的粗糙度困扰您,您可以在程序开始时运行一次更精确的算法,计算所有圆的遍历顺序,直到合理的最大半径。然后,您可以保留该数据并将其用于迭代所有半径较小的圆。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-04-10
      • 1970-01-01
      • 1970-01-01
      • 2011-09-19
      • 1970-01-01
      • 2013-10-10
      • 1970-01-01
      相关资源
      最近更新 更多