Problem
I want to get all pixels that are within a circle of a given radius about a given point, where points can only have integer coordinates, i.e. pixels in a canvas.
So I want to obtain all points in the yellow area given (x, y) and r.
Approaches
The most efficient way I can think of is to loop through a square around (x, y) and check the Euclidean distance for each point:
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.
}
}
}
However, this means that this algorithm will check (r * 2)^2 * (4 - pi) / 4 pixels that are not part of the circle. dx * dx + dy * dy <= r * r, which seems rather expensive, is called redundantly almost 1 / 4 of the time.
Integrating something like what was proposed here might enhance performance:
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.
}
}
}
However as the author themselves pointed out, this is likely not going to be faster when most of the points are going to be inside of the circle (especially because of abs), which pi / 4 are in this case.
I was not able to find any resources on this question. I am looking specifically for a solution in C++ and not something in SQL.
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});
}
}
}
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, 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;
}
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.
Code
Based on the idea from #ScottHunter, I came up with the following algorithm:
#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;
}
}
Algorithm explained
This algorithm performs a minute number of checks. Specifically, it only checks in each row until the first point that is part of the circle is reached. Furthermore, it will skip points to the left of the previously identified point in the next row. Additionally, by using symmetry, only half of the rows (n/2 + 1/2 as we start at 0) are checked.
This is a visualization of the algorithm I created. The red outline indicates the square that would have previously been checked and the black pixels indicate the real circle (with the red pixel in the middle being the center). The algorithm checks points (marked blue) and loops through valid points (marked green).
As you can see, the number of blue pixels at the end is minute, i.e. there are only a few points being looped over that are not part of the circle. Additionally, notice that only the first green pixel each time needs a check, the others are only looped through, which is why they appear instantly.
Notes
The axes could easily be reversed, obviously.
This could be optimized by taking advantage of symmetry even more, i.e. that the rows are going to be the same as the columns (going through all rows is the same as going through all columns, left to right, up to down, vise versa, vise vera) and going down only a quarter of the rows from the center would be enough to determine exactly what points are going to be part of the circle. However, I feel like the minor performance bump this is going to give is not worth the additional code.
If someone wants to code it out, propose an edit to this answer.
Code with comments
#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;
}
}
The problem has a fixed complexity of O(n^2) where n is the radius of the circle. The same complexity as a square or any regular 2D shape
There is no getting past the fact that you can not reduce the number of pixels in a circle, even if you take advantage of the symmetry the complexity remains the same.
So ignoring complexity and looking for optimization.
In your question you state that the abs is a little too expensive per pixel (or 4th pixel)
Once per row is better than once per pixel
You can reduce it down to 1 square root per row. For a circle radius 256 that 128 square roots
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++;
}
}
To get more out of it you can crate a lookup table for the sqrt root calculations.
All integer
Alternatively you can use the variation on the bresenham line that replaces the square root with all integer math. However it is a mess and would not be of any benefit unless the device does not have a floating point unit.
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++
}
}
}
You can draw a square that fits inside the circle and it is pretty straightforward to find if the point falls in.
This will solve most points(2 * r^2) in O(1) time, instead of searching all (4 * r^2) points.
Edit: For the rest of the points, you don't need to loop all the other pixels. You need to loop for the 4 rectangles sized with dimensions [(2r/sqrt(2)), r-(r/sqrt(2))] on the 4 sides(north,east,south,west) of the square that is inside. It means that you never have to search for the squares on the corners. Since, it is completely symmetric, we can take the absolute values of input points and search if the point is inside half-squares on the positive side of the coordinate plane. Which means we only loop once instead of 4.
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
}
}
}
}
Overall complexity of the code is O((r-r/sqrt(2))* (r/sqrt(2))). Which is only looping for a half of a single rectangle(8 way symmetry) that is in between the inner square and cirle's outer border.
If I am having a convex polygon vertices, then my area calculation in image is not coming accurate by the standard formula.
(For simiplicity) if I am having 3x3 square, and the vertices are (1,1) (1,3) (3,3) (3,1)
by polygon area calculation method depicted here
and dividing the summation by 2 we get the Area.
So for the 3 x 3 data above, we'll get area as 4 instead of 9.
This is happening because vertices are not points but a pixel.
this is the corresponding code. The coordinates are cyclic.
int X[] = { 1, 1, 3, 3, 1};
int Y[] = { 1, 3, 3, 1, 1};
double Sum1 = 0;
double Sum2 = 0;
int numElements = 5;
for (int k = 0; k < numElements-1; k++)
{
Sum1 += X[k] * Y[k + 1];
Sum2 += Y[k] * X[k + 1];
}
double area = std::abs((double)(Sum1 - Sum2))/2;
For square, we can do +1 to width and height and get the area correct. But what about the irregular polygons in the image?
I hope the question makes sense.
If you don't want to work with pixel corners as vertices, consider next method (works for simple figures - all convex ones, some concave ones):
Add additional fake pixel at right side of every right-border pixel, at bottom side of every bottom-border pixel, at right-bottom of right-bottom corner pixel. Here gray pixels are initial, light blues ones - fake.
The area can be calculated in following steps:
1) fetching the pixels between the vertices
2) sorting the pixels by x coordinates (or y coordinates)
3) taking the difference between min and max y coordinates (or x) for a particular x (or y) value and adding one to the difference
4) summing up the total difference
NOTE: the area might vary (if there are slanted edges in the polygon) depending on the line drawing method chosen
int compare(const void * a, const void * b)
{
return (((Point*)a)->x() - ((Point*)b)->x());
}
double CalculateConvexHullArea(vector<int> ConvexHullX, vector<int> ConvexHullY)
{
float Sum1 = 0;
float Sum2 = 0;
std::vector<Point> FillPoints;
for (int k = 0; k < ConvexHullX.size() - 1; k++)
{
drawLine(ConvexHullX[k], ConvexHullX[k+1], ConvexHullY[k], ConvexHullY[k+1], FillPoints);
}
//sorting coordinates
qsort(FillPoints.data(), FillPoints.size(), sizeof(Point), compare);
double area = 0;
int startY = FillPoints[0].y(), endY = FillPoints[0].y();
int currX = FillPoints[0].x();
// traversing x and summing up diff of min and max Y
for (int cnt = 0; cnt < FillPoints.size(); cnt++)
{
if (FillPoints[cnt].x() == currX)
{
startY = startY > FillPoints[cnt].y() ? FillPoints[cnt].y() : startY;
endY = endY < FillPoints[cnt].y() ? FillPoints[cnt].y() : endY;
}
else
{
int diffY = endY - startY + 1;
area += diffY;
currX = FillPoints[cnt].x();
startY = endY = FillPoints[cnt].y();
}
}
return area + endY - startY + 1;
}