Efficient Algorithm to obtain Points in a Circle around a Center
Alright, first of all we calculate the inner square of the circle. The formula for it is straight forward:
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
Now, every point between (-h, -h)
and (+h, +h)
lies within the circle. Here is an image of what I mean:
The remaining blue part is a bit tricky, but not too complicated either. We start at the very top of the blue circle (x = 0, y = -radius)
. Next, we walk right (x++
) until we leave the circle perimiter (until x²+y² < r²
doesn't hold anymore). Everything between (0, y) and (x, y) is within the circle. Because of symmetry we can extend this 8 fold by
- (-x, -y), (+x, -y)
- (-x, +y), (+x, +y)
- (-y, -x), (-y, +x)
- (+y, -x), (+y, +x)
now we go down 1 line (y--
) and repeat the steps above (while keeping the most recent value of x
). Add the center of the circle to each of the points and you're done.
Here is a visualization. There are some artifacts because of the upscaling. The red dot shows what we're testing at each iteration:
Here is the full code (using opencv to draw the stuff):
#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;
}
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)
}
}
To avoid repeated generation of pixels at axes, it is worth to start loops from 1 and draw central lines (ix==0 or line==0) in separate loop.
Note that there is also pure integer Bresenham algorithm to generate circumference points.
Alright here are the benchmarks I promised.
Setup
I used google benchmark and the task was to insert all points within the perimiter of the circle into a std::vector<point>
. I benchmark for a set of radii and a constant center:
radii = {10, 20, 50, 100, 200, 500, 1000}
center = {100, 500}
- language: C++17
- compiler: msvc 19.24.28316 x64
- platform: windows 10
- optimization: O2 (full optimization)
- threading: single threaded execution
The results of each algorithm is tested for correctness (compared against the output of OPs algorithm).
So far the following algorithms are benchmarked:
- OP's algorithm
enclosing_square
. - My algorithm
containing_square
. - creativecreatorormaybenot's algorithm
edge_walking
. - Mandy007's algorithm
binary_search
.
Results
Run on (12 X 3400 MHz CPU s)
CPU Caches:
L1 Data 32K (x6)
L1 Instruction 32K (x6)
L2 Unified 262K (x6)
L3 Unified 15728K (x1)
-----------------------------------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------------------------------
binary_search/10/manual_time 804 ns 3692 ns 888722
binary_search/20/manual_time 2794 ns 16665 ns 229705
binary_search/50/manual_time 16562 ns 105676 ns 42583
binary_search/100/manual_time 66130 ns 478029 ns 10525
binary_search/200/manual_time 389964 ns 2261971 ns 1796
binary_search/500/manual_time 2286526 ns 15573432 ns 303
binary_search/1000/manual_time 9141874 ns 68384740 ns 77
edge_walking/10/manual_time 703 ns 5492 ns 998536
edge_walking/20/manual_time 2571 ns 49807 ns 263515
edge_walking/50/manual_time 15533 ns 408855 ns 45019
edge_walking/100/manual_time 64500 ns 1794889 ns 10899
edge_walking/200/manual_time 389960 ns 7970151 ns 1784
edge_walking/500/manual_time 2286964 ns 55194805 ns 308
edge_walking/1000/manual_time 9009054 ns 234575321 ns 78
containing_square/10/manual_time 629 ns 4942 ns 1109820
containing_square/20/manual_time 2485 ns 40827 ns 282058
containing_square/50/manual_time 15089 ns 361010 ns 46311
containing_square/100/manual_time 62825 ns 1565343 ns 10990
containing_square/200/manual_time 381614 ns 6788676 ns 1839
containing_square/500/manual_time 2276318 ns 45973558 ns 312
containing_square/1000/manual_time 8886649 ns 196004747 ns 79
enclosing_square/10/manual_time 1056 ns 4045 ns 660499
enclosing_square/20/manual_time 3389 ns 17307 ns 206739
enclosing_square/50/manual_time 18861 ns 106184 ns 37082
enclosing_square/100/manual_time 76254 ns 483317 ns 9246
enclosing_square/200/manual_time 421856 ns 2295571 ns 1654
enclosing_square/500/manual_time 2474404 ns 15625000 ns 284
enclosing_square/1000/manual_time 9728718 ns 68576389 ns 72
Code
The complete test code is below, you can copy & paste it and test it yourself. fill_circle.cpp
contains the implementation of the different algorithms.
main.cpp
#include <string>
#include <unordered_map>
#include <chrono>
#include <benchmark/benchmark.h>
#include "fill_circle.hpp"
using namespace std::string_literals;
std::unordered_map<const char*, circle_fill_func> bench_tests =
{
{"enclosing_square", enclosing_square},
{"containing_square", containing_square},
{"edge_walking", edge_walking},
{"binary_search", binary_search},
};
std::vector<int> bench_radii = {10, 20, 50, 100, 200, 500, 1000};
void postprocess(std::vector<point>& points)
{
std::sort(points.begin(), points.end());
//points.erase(std::unique(points.begin(), points.end()), points.end());
}
std::vector<point> prepare(int radius)
{
std::vector<point> vec;
vec.reserve(10ull * radius * radius);
return vec;
}
void bm_run(benchmark::State& state, circle_fill_func target, int radius)
{
using namespace std::chrono;
constexpr point center = {100, 500};
auto expected_points = prepare(radius);
enclosing_square(center, radius, expected_points);
postprocess(expected_points);
for (auto _ : state)
{
auto points = prepare(radius);
auto start = high_resolution_clock::now();
target(center, radius, points);
auto stop = high_resolution_clock::now();
postprocess(points);
if (expected_points != points)
{
auto text = "Computation result incorrect. Expected size: " + std::to_string(expected_points.size()) + ". Actual size: " + std::to_string(points.size()) + ".";
state.SkipWithError(text.c_str());
break;
}
state.SetIterationTime(duration<double>(stop - start).count());
}
}
int main(int argc, char** argv)
{
for (auto [name, target] : bench_tests)
for (int radius : bench_radii)
benchmark::RegisterBenchmark(name, bm_run, target, radius)->Arg(radius)->UseManualTime();
benchmark::Initialize(&argc, argv);
if (benchmark::ReportUnrecognizedArguments(argc, argv))
return 1;
benchmark::RunSpecifiedBenchmarks();
}
fill_circle.hpp
#pragma once
#include <vector>
struct point
{
int x = 0;
int y = 0;
};
constexpr bool operator<(point const& lhs, point const& rhs) noexcept
{
return lhs.x != rhs.x
? lhs.x < rhs.x
: lhs.y < rhs.y;
}
constexpr bool operator==(point const& lhs, point const& rhs) noexcept
{
return lhs.x == rhs.x && lhs.y == rhs.y;
}
using circle_fill_func = void(*)(point const& center, int radius, std::vector<point>& points);
void enclosing_square(point const& center, int radius, std::vector<point>& points);
void containing_square(point const& center, int radius, std::vector<point>& points);
void edge_walking(point const& center, int radius, std::vector<point>& points);
void binary_search(point const& center, int radius, std::vector<point>& points);
fill_circle.cpp
#include "fill_circle.hpp"
constexpr double sqrt2 = 1.41421356237309504880168;
constexpr double pi = 3.141592653589793238462643;
void enclosing_square(point const& center, int radius, std::vector<point>& points)
{
int sqr_rad = radius * radius;
for (int px = center.x - radius; px <= center.x + radius; px++)
{
for (int py = center.y - radius; py <= center.y + radius; py++)
{
int dx = center.x - px, dy = center.y - py;
if (dx * dx + dy * dy <= sqr_rad)
points.push_back({px, py});
}
}
}
void containing_square(point const& center, int radius, std::vector<point>& points)
{
int sqr_rad = radius * radius;
int half_side_len = radius / sqrt2;
int sq_x_end = center.x + half_side_len;
int sq_y_end = center.y + half_side_len;
// handle inner square
for (int x = center.x - half_side_len; x <= sq_x_end; x++)
for (int y = center.y - half_side_len; y <= sq_y_end; y++)
points.push_back({x, y});
// probe the rest
int x = 0;
for (int y = radius; y > half_side_len; y--)
{
int x_line1 = center.x - y;
int x_line2 = center.x + y;
int y_line1 = center.y - y;
int y_line2 = center.y + y;
while (x * x + y * y <= sqr_rad)
x++;
for (int i = 1 - x; i < x; i++)
{
points.push_back({x_line1, center.y + i});
points.push_back({x_line2, center.y + i});
points.push_back({center.x + i, y_line1});
points.push_back({center.x + i, y_line2});
}
}
}
void edge_walking(point const& center, int radius, std::vector<point>& points)
{
int sqr_rad = radius * radius;
int mdx = radius;
for (int dy = 0; dy <= radius; dy++)
{
for (int dx = mdx; dx >= 0; dx--)
{
if (dx * dx + dy * dy > sqr_rad)
continue;
for (int px = center.x - dx; px <= center.x + dx; px++)
{
for (int py = center.y - dy; py <= center.y + dy; py += 2 * dy)
{
points.push_back({px, py});
if (dy == 0)
break;
}
}
mdx = dx;
break;
}
}
}
void binary_search(point const& center, int radius, std::vector<point>& points)
{
constexpr auto search = []( const int &radius, const int &squad_radius, int dx, const int &y)
{
int l = y, r = y + radius, distance;
while (l < r)
{
int m = l + (r - l) / 2;
distance = dx * dx + (y - m) * (y - m);
if (distance > squad_radius)
r = m - 1;
else if (distance < squad_radius)
l = m + 1;
else
r = m;
}
if (dx * dx + (y - l) * (y - l) > squad_radius)
--l;
return l;
};
int squad_radius = radius * radius;
for (int px = center.x - radius; px <= center.x + radius; ++px)
{
int upper_limit = search(radius, squad_radius, px - center.x, center.y);
for (int py = 2*center.y - upper_limit; py <= upper_limit; ++py)
{
points.push_back({px, py});
}
}
}
This is an optimization that reduce 1/4 the dimension of search:
for (int px = x; px <= x + r; ++px) {
bool find = false;
int dx = x - px, dy;
for (int py = y; !find && py <= y + r; ++py) {
dy = y - py;
if (dx * dx + dy * dy <= r * r)) {
/* (px, py), (px, y+y-py+r), (x+x-px+r, py)
& (x+x-px+r, y+y-py+r) are part of the circle.*/
}else{
find = true; //Avoid increasing on the axis y
}
}
}
or better, improving performance the iteration of second circle for
avoiding the if
conditional
for (int px = x; px <= x + r; ++px) {
int dx = x - px, py = y;
for (; dx * dx + (py-y) * (py-y) <= r * r; ++py) {
/* (px, py), (px, y+y-py+r), (x+x-px+r, py)
& (x+x-px+r, y+y-py+r) are part of the circle.*/
}
}
well i think that other option is a binary search for upper limit:
int binarySearch(int R, int dx, int y){
int l=y, r=y+R;
while (l < r) {
int m = l + (r - l) / 2;
if(dx*dx + (y - m)*(y - m) > R*R) r = m - 1;
else if(dx*dx + (y - m)*(y - m) < R*R) l = m + 1;
else r = m;
}
if(dx*dx + (y - l)*(y - l) > R*R) --l;
return l;
}
for (int px = x; px <= x + r; ++px) {
int upperLimit = binarySearch(r, px-x, y);
for (int py = y; py <= upperLimit; ++py) {
/* (px, py), (px, y+y-py+r), (x+x-px+r, py)
& (x+x-px+r, y+y-py+r) are part of the circle.*/
}
}
The idea of binary search is to find the upper limit optimally, avoiding the if
condition and calculations within the for
cycle. For this, it is checked which is the largest integer that makes the distance between the current point and the radius within the circle.
PD: Sorry my English.