我需要像Bresenham的圆算法这样的算法,但需要进行一些修改。 该算法必须访问半径中的所有像素(因此基本上是填充)。
我提出的一种技术首先通过圆圈的矩形确定圆内的所有像素坐标,并检查Math.Sqrt是否在圆圈内。 然后它将按距离排序像素,然后访问每个像素。
这正是我想要的,除了快速。
所以我的问题是: 有没有快速方法来实现这一点,而无需获取,排序,然后访问每个像素?
为了澄清我实际上并不想画图像,我只想按照描述的顺序遍历它们。
答案 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
函数也有点复杂。在该函数中绘制的像素从不超过两个,通常只有一个。
如果搜索顺序的粗糙度困扰您,您可以在程序开头运行一次更精确的算法,在此计算所有圆的行程顺序,直到合理的最大半径。然后,您可以保留该数据并使用它来迭代半径较小的所有圆。