从中心穿过一个圆圈中的像素

时间:2015-03-02 11:22:57

标签: c# .net algorithm geometry

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

  • 算法必须从圆圈的中心开始
  • 必须访问通常会访问的所有点(无洞)
  • 必须完全访问圈子中的每个点

我提出的一种技术首先通过圆圈的矩形确定圆内的所有像素坐标,并检查Math.Sqrt是否在圆圈内。 然后它将按距离排序像素,然后访问每个像素。

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

所以我的问题是: 有没有快速方法来实现这一点,而无需获取,排序,然后访问每个像素?

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

3 个答案:

答案 0 :(得分:3)

首先,我们可以使用事实,该圈可以分为8个八分圆。所以我们只需要填充单个八分圆并使用简单的+坐标变化来获得完整的圆。因此,如果我们只尝试填充一个八分圆,我们只需要担心中心的两个方向:左上角和左上角。此外,巧妙地使用数据结构,如优先级队列(.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);

答案 1 :(得分:1)

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

    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会占用更多内存...

答案 2 :(得分:0)

您已经提到了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);
    }
}

此代码通过跳过alterante octants上的重合像素来处理不访问两个八分圆的像素。 (编辑:原始代码中仍然存在问题,但现在通过'patch`变量进行修复。)

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

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

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