1

Can you provide an efficient algorithm for drawing a circle(ish) shape in a grid of arbitrary position and radius?

. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . o O o . . . . . . . . . . . . . . . . . . . .
. . . . O O O O O . . . . . . . . . . . . . . . . . . .
. . . o O O O O O o . . . . . . . . . . . . . . . . . .
. . . O O O O O O O . . . . . . . . . . o O o . . . . .
. . . o O O O O O o . . . . . . . . . o O O O o . . . .
. . . . O O O O O . . . . . . . . . . O O O O O . . . .
. . . . . o O o . . . . . . . . . . . o O O O o . . . .
. . . . . . . . . . . . . . . . . . . . o O o . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .

I'm using this for pathfinding. It is a lower-res abstraction of a more finely-resolved graphic field. These shapes serve as blocks to avoid.

Keep in mind that I want to be able to use this to quickly index a 2d array of where the blocks are located.

score = self.map[x][y]

So "drawing" the circle will be akin to setting values as blocked:

self.map[x][y] = PATH_COST_PROX1

Drawing the field looks like this:

def printme(self):
    """ Print the map to stdout in ASCII."""
    for y in reversed(range(self.ymax)):
        for x in range(self.xmax):
            if self.map[x][y] >= PATH_COST_PROX0:
                print 'O',
            elif self.map[x][y] >= PATH_COST_PROX1:
                print 'o',
            else:
                print '.',
        print ''

EDIT: Here was my original (shameful) attempt. I made circles by hand on a grid and just jotted down the points that were added by each increase in radius. It isn't a terrible idea, but the accepted answer is much more elegant.

COVER_MAP = [
    [(0,0)],
    [(0,1),(1,0),(0,-1),(-1,0)],
    [(1,1),(1,-1),(-1,-1),(-1,1)],
    [(0,2),(2,0),(0,-2),(-2,0)],
    [(1,2),(2,1),(2,-1),(1,-2),(-1,-2),(-2,-1),(-2,1),(-1,2)],
    [(0,3),(2,2),(3,0),(2,-2),(0,-3),(-2,-2),(-3,0),(-2,2)],
    [(1,3),(3,1),(3,-1),(1,-3),(-1,-3),(-3,-1),(-3,1),(-1,3)]
]

def set_blocked(self, p, radius):
    """
    Set the blocked state of a coordinate. Takes an integer value that
    represents the cost of the block
    """
    #radius = radius * 2
    if radius > len(COVER_MAP)-1:
        radius=len(COVER_MAP)-1
    #print "point:",p," radius:",radius
    (cx,cy) = p
    for i in range(len(COVER_MAP)):
        for j in range(len(COVER_MAP[i])):
            (rx,ry) = COVER_MAP[i][j]
            x = cx + rx
            y = cy + ry
            if x >= 0 and x < self.xmax and y >= 0 and y < self.ymax:
                if i < radius:
                    self.map[x][y] = PATH_COST_PROX0
                elif i == radius:
                    self.map[x][y] = PATH_COST_PROX1
                elif i == radius + 1:
                    self.map[x][y] = PATH_COST_PROX2
                elif i == radius + 2:
                    self.map[x][y] = PATH_COST_PROX3
                elif i == radius + 3:
                    self.map[x][y] = PATH_COST_PROX4

Mine does have the advantage of being able to make a fuzzy ring of lessened cost around the original circle, something that the memorization algorithm below doesn't have but could be adapted to provide.

Wes Modes
  • 2,024
  • 2
  • 22
  • 40
  • 1
    What have you tried? You might want to provide a minimal code example http://www.sscce.org/ – Jakob Bowyer Feb 24 '14 at 11:50
  • [try Bresenham's algorithm](http://en.wikipedia.org/wiki/Midpoint_circle_algorithm), [this SO post discusses how to expand it to filled circles](http://stackoverflow.com/questions/1201200/fast-algorithm-for-drawing-filled-circles) – Kevin Feb 24 '14 at 11:52
  • 1
    I'm sorry to say, Numpy and I have not yet been introduced. – Wes Modes Feb 24 '14 at 18:53

2 Answers2

1

I suspect the fastest way to do this uses memoization (not to be confused with "memorization"). Here is an example of generating discs up to a radius of 20 pixels. If you want circles or hollow discs instead of filled discs, you need to specify a width for them and include x_sq + y_sq >= (k_r_sq - width) in the if statement.

According to time.time() (you can use time.perf_counter() if you have python 3.3 or higher), it takes me 3 microseconds to load each disc's coordinate set, but that doesn't take into account whatever calculation you may want to do on that disc.

Hope this helps.

import time
max_radius = 20    

i0 = time.time()
class DiscTemplate:
    def __init__(self, max_r):
        self.memos = []
        for k_r in range(1, max_r + 1):
            k_r_sq = k_r ** 2
            self.memos.append([])
            for x in range(-max_r, max_r + 1):
                x_sq = x ** 2
                for y in range(-max_r, max_r + 1):
                    y_sq = y ** 2
                    if x_sq + y_sq <= k_r_sq:
                        self.memos[k_r - 1].append((x,y))

        self.max_r = max_r

    def get_disc(self, r):
        return self.memos[r - 1]
test = DiscTemplate(20)
i1 = time.time()

print("time to make class:", i1 - i0)

t0 = time.time()
disc = test.get_disc(2)
t1 = time.time()

print("time to produce disc:", t1 - t0)
print("Disc coordinates: \n", disc)
andreipmbcn
  • 209
  • 1
  • 4
  • If I understand this properly, you "predraw" each of the circles, remember a list of points for each radius, and then iterate over the point list to render them. This is great. Thanks, @andreipmbcn. – Wes Modes Feb 24 '14 at 18:37
  • Tell me more about the algorithm you are using to fill the circle. Or better yet, drop in a few in-line comments to your code? – Wes Modes Feb 24 '14 at 18:59
  • The algorithm I proposed to predraw the circles is very basic and slow - find all the points in a square for which the distance to the circle's center is between a minimum and a maximum, then draw them. The Bresenham circle algorithm is much faster for pre-drawing. Nevertheless, memoization itself works irrespective of the algorithm you use. I believe memoization itself is faster than re-using the Bresenham algorithm for every new circle, and I can test this if you like. – andreipmbcn Feb 26 '14 at 12:40
0

I would try something like this :

  1. Find the enclosing rectangle of the circle you want to draw :

    top_left = (cx + radius*cos(3*pi/4), cy + radius*sin(3*pi/4)) # (cx, cy) center of the circle
    width, height = 2*radius, 2*radius
    
  2. For each point of this rectangle test the distance to the center and set the corresponding char if this distance is less than the radius

Edit : numpy

The following code will give you the points in a circle. Here you don't have any loop in Python, so efficiency will be maximal.

import numpy as np

#precomputed (depends on the dimensions of your grid)
x = np.arange(50) # x values
y = np.arange(50) # y values
xx, yy = np.meshgrid(x, y) # combine both vectors

# for this particular circle
cx, cy, r = 10, 10, 5

condition = (xx-cx)**2 + (yy-cy)**2 <= r**2
points = xx[condition] + 1j*yy[condition] # list of points in the circle (stored as complex)
Kiwi
  • 2,698
  • 16
  • 15
  • Why would you reinvent the wheel? There are tons of algorithms for drawing primitives, most are far better than this. – Kevin Feb 24 '14 at 11:59
  • What I understood is that the goal is not to get a well anti-aliased drawing. Moreover, it can be achieved with numpy routines in a very efficient way. IHMO `premature optimization is the root of all evil` – Kiwi Feb 24 '14 at 12:30
  • It has nothing to do with premature optimization, it's about selecting the right algorithm for the purpose, which should be one of the first steps of designing an application. The OP asked for `an efficient algorithm for drawing a circle(ish) shape`. Your algorithm will work, but it's not efficient at all. – Kevin Feb 24 '14 at 12:32
  • So I guess I should have said medium-efficient. It would be run once for each object in each frame to calculate pathfinding, but not multiple times per frame. So it is important that it be reasonably efficient, but doesn't need to be blindingly fast. That said, I'm not sure iterating over all of the spaces in the enclosing rectangle is reasonably efficient. O(n^2) – Wes Modes Feb 24 '14 at 18:28
  • While the memorizing algorithm above is just a hair more efficient, the idea of precalculating the circles and using the memorized list of points is very quick in practice. Something like O(n). I'll also note, that your technique could be used and the results memorized for similar results. – Wes Modes Feb 24 '14 at 18:44
  • Also, the solution -- while iterating over the field -- avoids costly cos, sin, and sqrts. – Wes Modes Feb 24 '14 at 19:02
  • You really should use `numpy` if you don't want efficiency issues, whatever the method. – Kiwi Feb 24 '14 at 19:05
  • Thanks for the numpy example. Just learning the wonders of it. – Wes Modes Mar 01 '14 at 08:34