私は Bresenhamのサークルアルゴリズム を使用して高速なサークル描画を行っています。ただし、(ユーザーの要求に応じて)塗りつぶされた円を描きたいです。
これを行うための高速で効率的な方法はありますか?ブレゼンハムの同じ線に沿って何か?
私が使用している言語はCです。
ブレゼンハム(「ミッドポイント」)サークルアルゴリズムのWikipediaページ を読んだ後、最も簡単なことはそのアクションを変更することであるように見えます。
setPixel(x0 + x, y0 + y);
setPixel(x0 - x, y0 + y);
似たようなことをするたびに
lineFrom(x0 - x, y0 + y, x0 + x, y0 + y);
つまり、Bresenhamがあなたに持ってもらうplotと同じポイント(同じy
)で、代わりにラインで接続します。
総当たり攻撃を使用します。このメソッドは、少数のピクセルを繰り返し処理しますが、整数の乗算と加算のみを使用します。 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);
これが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");
これを使用できます:
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;
}
}
}
Palm3Dの答えが好きです。総当たりであるため、これは驚くほど高速なソリューションです。速度を落とす平方根や三角関数はありません。その弱点の1つは、ネストされたループです。
これを単一のループに変換すると、この関数の速度がほぼ2倍になります。
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);
}
ここに私がそれをやっている方法があります:
2ビット精度の固定小数点値を使用しています(ハーフポイントとハーフポイントの平方値を管理する必要があります)
前の回答で言及したように、平方根の代わりに平方値も使用しています。
最初に、円の1/8の部分で円の境界の境界を検出しています。これらの点の対称を使用して、円の4つの「境界線」を描きます。次に、円の中に正方形を描きます。
midpoint circle algorithとは異なり、この直径は偶数の直径で動作します(実数の直径でも、わずかな変化があります)。
私の説明が明確でない場合はご容赦ください、私はフランス語です;)
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²)の違いに応じて、一種のアンチエイリアスを作成することも可能です。
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座標よりも境界ボックスの左側に近いためです。注意すべきことは、円も垂直に対称であるため、見つけた各線は自由に2本目の線を描画し、円の上半分で線を見つけるたびに、半径の下半分で線を取得することです。 -y y座標。したがって、線が見つかった場合、2本を描画でき、y値の上半分のみを反復処理する必要があります。
最後に注意することは、円の中心にあるy値から始めてyの上部に向かって移動する場合、次の各行の最小x値は中心のx座標により近くなければならないということです。最後の行よりも円。これは、円が上に行くほど中心のx値に近づく曲線になっているためです。 これがどのように当てはまるかを視覚的に示しています。
要約すれば:
また、複数回計算する代わりに、(radius * radius)
の値と(y * y)
を保存することもできます。
高速なアルゴリズムが必要な場合は、N辺を持つポリゴンを描画することを検討してください。Nが高いほど、円はより正確になります。
ポイントのリストを生成し、レンダリングにポリゴン描画機能を使用します。