web-dev-qa-db-ja.com

中心の周りの円の点を取得するための効率的なアルゴリズム

問題

特定のポイントの特定の半径の円内にあるすべてのピクセルを取得したい場合、ポイントは整数座標のみを持つことができます。つまり、キャンバス。

Illustration

したがって、(x, y)rを指定して、黄色の領域にあるすべてのポイントを取得します。

アプローチ

私が考えることができる最も効率的な方法は、(x, y)の周りの正方形をループして、各ポイントのユークリッド距離を確認することです。

for (int px = x - r; px <= x + r; px++) {
  for (int py = y - r; py <= y + r; py++) {
    int dx = x - px, dy = y - py;

    if (dx * dx + dy * dy <= r * r) {
      // Point is part of the circle.
    }
  }
}

ただし、これは、このアルゴリズムが円の一部ではない(r * 2)^2 * (4 - pi) / 4ピクセルをチェックすることを意味します。 dx * dx + dy * dy <= r * rはかなり高価に見えますが、ほとんどの場合1 / 4は重複して呼び出されます。

ここで提案 のようなものを統合すると、パフォーマンスが向上する可能性があります。

for (int px = x - r; px <= x + r; px++) {
  for (int py = y - r; py <= y + r; py++) {
    int dx = abs(x - px), dy = abs(y - py);

    if (dx + dy <= r || (!(dx > r || dy > r) && (dx * dx + dy * dy <= r * r))) {
      // Point is part of the circle.
    }
  }
}

ただし、作成者自身が指摘したように、ほとんどのポイントが円の内側になる場合(特にabsのため)、これはpi / 4に含まれているため、速くなることはないでしょう場合。


この質問に関するリソースは見つかりませんでした。私は特にC++での解決策を探しています SQLの場合 ではありません。

では、最初に、円の内側の正方形を計算します。その式は簡単です。

x² + y² = r²    // circle formula
2h² = r²        // all sides of square are of equal length so x == y, lets define h := x
h = r / sqrt(2) // half side length of the inner square

さて、(-h, -h)および(+h, +h)は円の中にあります。これが私の意味するイメージです:

1

残りの青い部分は少しトリッキーですが、あまり複雑でもありません。青い円の一番上から始めます(x = 0, y = -radius)。次に、右に進みます(x++)サークル境界線を離れるまで(x²+y² < r²は保持されなくなりました)。 (0、y)と(x、y)の間はすべて円の中にあります。対称性により、これを8倍に拡張できます。

  • (-x、-y)、(+ x、-y)
  • (-x、+ y)、(+ x、+ y)
  • (-y、-x)、(-y、+ x)
  • (+ y、-x)、(+ y、+ x)

今、私たちは1行下に行きます(y--)と上記の手順を繰り返します(xの最新の値を保持しながら)。円の中心を各ポイントに追加すれば完了です。

これは視覚化です。アップスケーリングのためにいくつかのアーティファクトがあります。赤い点は、各反復でテストしているものを示しています。

1

以下は完全なコードです(opencvを使用してものを描画します)。

#include <opencv2/opencv.hpp>

constexpr double sqrt2 = 1.41421356237309504880168;

int main()
{
    cv::Point center(200, 200);
    constexpr int radius = 180;

    // create test image
    cv::Mat img(400, 400, CV_8UC3);
    cv::circle(img, center, radius, {180, 0, 0}, cv::FILLED);
    cv::imshow("img", img);
    cv::waitKey();

    // calculate inner rectangle
    int halfSideLen = radius / sqrt2;
    cv::Rect innerRect(center.x - halfSideLen, center.y - halfSideLen, halfSideLen * 2, halfSideLen * 2);
    cv::rectangle(img, innerRect, {0, 180, 0}, cv::FILLED);
    cv::imshow("img", img);
    cv::waitKey();

    // probe the rest
    int x = 0;
    for (int y = radius; y >= halfSideLen; y--)
    {
        for (; x * x + y * y < radius * radius; x++)
        {
            // anything between the following points lies within the circle
            // each pair of points represents a line
            // (-x, -y), (+x, -y)
            // (-x, +y), (+x, +y)
            // (-y, -x), (-y, +x)
            // (+y, -x), (+y, +x)

            // center + {(-X..X) x (-Y..Y)} is inside the circle
            cv::line(img, cv::Point(center.x - x, center.y - y), cv::Point(center.x + x, center.y - y), {180, 180, 0});
            cv::line(img, cv::Point(center.x - x, center.y + y), cv::Point(center.x + x, center.y + y), {180, 180, 0});
            cv::line(img, cv::Point(center.x - y, center.y - x), cv::Point(center.x - y, center.y + x), {180, 180, 0});
            cv::line(img, cv::Point(center.x + y, center.y - x), cv::Point(center.x + y, center.y + x), {180, 180, 0});

            cv::imshow("img", img);
            cv::waitKey(20);
        }
    }

    cv::waitKey();
    return 0;
}
2
Timo

問題はO(n ^ 2)の固定された複雑さを持ちます。ここで、nは円の半径です。正方形または通常の2D形状と同じ複雑さ

対称性を利用しても、複雑さは同じままであっても、円内のピクセル数を減らすことができないという事実を乗り越えることはできません。

したがって、複雑さを無視して最適化を探します。

あなたの質問では、absはピクセル(または4番目のピクセル)あたりが少し高すぎると述べています

1行に1回は1ピクセルに1回よりも優れています

行ごとに1平方根に減らすことができます。円の半径256で128の平方根

void circle(int x, int y, int radius) {
    int y1 = y, y2 = y + 1, r = 0, rSqr = radius * radius;
    while (r < radius) {
        int x1 = x, x2 = x + 1, right = x + sqrt(rSqr - r * r) + 1.5;;
        while (x2 < right) {
            pixel(x1, y1);
            pixel(x2, y1);
            pixel(x1--, y2);
            pixel(x2++, y2);
        }
        y1--;
        y2++;
        r++;
    }
}

それをさらに活用するには、sqrtルート計算用のルックアップテーブルを作成します。

すべて整数

または、平方根をすべての整数演算に置き換える bresenham line のバリエーションを使用できます。しかし、それは混乱であり、デバイスに浮動小数点ユニットがない場合を除いて、何の利点もありません。

void circle(int x, int y, int radius) {
    int l, yy = 0, xx = radius - 1, dx = 1, dy = 1;
    int err = dx - (radius << 1);
    int l2 = x, y0 = y, r2 = x + 1;
    int l1 = x - xx, r1 = r2 + xx;
    int y2 = y0 - xx, y1 = y0 + 1, y3 = y1 + xx;
    while (xx >= yy) {
        l = l1;
        while (l < r1) {
            pixel(l, y1);
            pixel(l++, y0);
        }
        l = l2;
        while (l < r2) {
            pixel(l, y3);
            pixel(l++, y2);
        }
        err += dy;
        dy += 2;
        y0--;
        yy++;
        y1++;
        l2--;
        r2++;
        if (err > 0) {
            dx += 2;
            err += (-radius << 1) + dx;
            xx--;
            r1--
            l1++
            y3--
            y2++
        }
    }
}
2
Blindman67

コード

@ ScottHunter のアイデアに基づいて、次のアルゴリズムを思いつきました。

#include <functional>

// Executes point_callback for every point that is part of the circle
// defined by the center (x, y) and radius r.
void walk_circle(int x, int y, int r,
                 std::function<void(int x, int y)> point_callback) {
  for (int px = x - r; px < x + r; px++)
    point_callback(px, y);
  int mdx = r;
  for (int dy = 1; dy <= r; dy++)
    for (int dx = mdx; dx >= 0; dx--) {
      if (dx * dx + dy * dy > r * r)
        continue;
      for (int px = x - dx; px <= x + dx; px++) {
        point_callback(px, y + dy);
        point_callback(px, y - dy);
      }
      mdx = dx;
      break;
    }
}

アルゴリズムの説明

このアルゴリズムは、minute回のチェックを実行します。具体的には、円の一部である最初の点に到達するまで、各行のみをチェックします。さらに、次の行で以前に識別されたポイントの左側にあるポイントをスキップします。さらに、対称性を使用することにより、行の半分のみ(n/2 + 1/2 0から開始するため、チェックされます。

Visualization

これは私が作成したアルゴリズムを視覚化したものです。赤いアウトラインは、以前にチェックされた正方形を示し、黒いピクセルは実際の円を示します(中央の赤いピクセルが中心です)。アルゴリズムはポイント(青でマーク)をチェックし、有効なポイント(緑でマーク)をループします。
ご覧のとおり、最後の青いピクセルの数はほんの数分です。つまり、ループの一部であり、円の一部ではない点がいくつかあります。さらに、毎回最初の緑のピクセルだけがチェックを必要とし、他のピクセルはループされるだけなので、それらがすぐに表示される理由に注意してください。

ノート

もちろん、軸は簡単に逆にすることができます。

これは、対称性をさらに活用することで最適化できます。つまり、行は列と同じになります(すべての行を通過することは、すべての列を通過することと同じで、左から右、上から下、逆も同様です)万力ヴェラ)そして、中心から行の4分の1だけ下がれば、どの点が円の一部になるかを正確に決定するのに十分でしょう。ただし、これによってもたらされる小さなパフォーマンスバンプは、追加のコードに値するものではないと思います。
誰かがそれをコード化したい場合は、この回答の編集を提案します。

コメント付きのコード

#include <functional>

// Executes point_callback for every point that is part of the circle
// defined by the center (x, y) and radius r.
void walk_circle(int x, int y, int r,
                 std::function<void(int x, int y)> point_callback) {
  // Walk through the whole center line as it will always be completely
  // part of the circle.
  for (int px = x - r; px < x + r; px++)
    point_callback(px, y);
  // Define a maximum delta x that shrinks whith every row as the arc
  // is closing.
  int mdx = r;
  // Start directly below the center row to make use of symmetry.
  for (int dy = 1; dy <= r; dy++)
    for (int dx = mdx; dx >= 0; dx--) {
      // Check if the point is part of the circle using Euclidean distance.
      if (dx * dx + dy * dy > r * r)
        continue;

      // If a point in a row left to the center is part of the circle,
      // all points to the right of it until the center are going to be
      // part of the circle as well.
      // Then, we can use horizontal symmetry to move the same distance
      // to the right from the center.
      for (int px = x - dx; px <= x + dx; px++) {
        // Use y - dy and y + dy thanks to vertical symmetry
        point_callback(px, y + dy);
        point_callback(px, y - dy);
      }

      // The next row will never have a point in the circle further left.
      mdx = dx;
      break;
    }
}
for (line = 1; line <= r; line++) {
   dx = (int) sqrt(r * r - line * line);
   for (ix = 1; ix <= dx; ix++) {
       putpixel(x - ix, y + line)
       putpixel(x + ix, y + line)
       putpixel(x - ix, y - line)
       putpixel(x + ix, y - line)
   } 
}

軸でのピクセルの繰り返し生成を回避するには、ループを1から開始し、中心線(ix == 0またはline == 0)を別のループで描画することをお勧めします。

円周上の点を生成するための純粋な整数ブレゼンハムアルゴリズムもあることに注意してください。

2
MBo

円の中に収まる正方形を描くことができ、点が当てはまるかどうかを見つけるのは非常に簡単です。

これは、すべての(4 * r ^ 2)ポイントを検索する代わりに、O(1)時間でほとんどのポイント(2 * r ^ 2)を解決します。

編集:残りのポイントについては、他のすべてのピクセルをループする必要はありません。次の正方形の4辺(北、東、南、西)の寸法[(2r/sqrt(2))、r-(r/sqrt(2))]のサイズの4つの長方形をループする必要があります。内部。つまり、角の四角を探す必要がないということです。完全に対称であるため、入力ポイントの絶対値を取得し、そのポイントが座標平面の正の側の半正方形の内側にあるかどうかを検索できます。つまり、4ではなく1回だけループします。

int square_range = r/sqrt(2);
int abs_x = abs(x);
int abs_y = abs(y);

if(abs_x < square_range && abs_y < square_range){
    //point is in
}
else if(abs_x < r && abs_y < r){  // if it falls in the outer square
    // this is the only loop that has to be done
    if(abs_x < abs_y){
        int temp = abs_y;
        abs_y = abs_x;
        abs_x = temp;
    }
    for(int x = r/sqrt(2) ; x < r ; x++){
        for(int y = 0 ; y < r/sqrt(2) ; y++){
             if(x*x + y*y < r*r){
                 //point is in
             }
         }    
    }    
}        

コードの全体的な複雑さはO((r-r/sqrt(2))*(r/sqrt(2)))です。これは、内側の正方形と円の外側の境界線の間にある単一の長方形(8方向対称)の半分に対してのみループします。

1
heapoverflow