Skip to content

Commit 32ca6a7

Browse files
committed
initial commit
1 parent 366d4cb commit 32ca6a7

File tree

7 files changed

+550
-0
lines changed

7 files changed

+550
-0
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
QuadTree A*
2+
===========
3+
4+
Python programm showing A* path finding on quadtree representation of a 2D map.
5+
6+
```python
7+
python3 demo.py
8+
```
9+
10+
![demo](https://github.com/volkerp/quadtree_Astar/raw/master/screenshot.png "demo.py")

astar.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
""" generic implementation of A* path finding algorithm
2+
3+
(c) Volker Poplawski 2018
4+
"""
5+
# priority queue dictionary
6+
from pqdict import pqdict
7+
8+
9+
def astar(adjafunc, distfunc, heurfunc, start, goal):
10+
""" adjafunc: node -> List(nodes)
11+
distfunc: node, node -> double
12+
start: start node
13+
goal: end node. Terminate when reached.
14+
15+
Return dict node -> absolute dist from start, dict node -> path predessor node
16+
"""
17+
D = {start: 0} # final absolute distances
18+
P = {} # predecessors
19+
Q = pqdict({start: 0}) # fringe/frontier maps unexpanded node to estimated dist to goal
20+
21+
considered = 0 # count how many nodes have been considered on multiple paths
22+
23+
# keep expanding nodes from the fringe
24+
# until goal node is reached
25+
# or no more new nodes can be reached and fringe runs empty
26+
27+
for n, estimation in Q.popitems(): # pop node with min estimated costs from queue
28+
if n == goal: # reached goal node
29+
break # stop expanding nodes
30+
31+
for neighb in adjafunc(n): # for all neighbours/adjacent of current node n
32+
considered += 1
33+
dist = D[n] + distfunc(n, neighb) # calculate distance to neighbour: cost to current + cost reaching neighbour from current
34+
if neighb not in D or D[neighb] > dist: # if neighbour never visited or shorter using this way
35+
D[neighb] = dist # found (shorter) distance to neighbour
36+
Q[neighb] = dist + heurfunc(neighb, goal) # estimate distance from neighbour to goal
37+
P[neighb] = n # remember we reached neighbour via n
38+
39+
# expanding done: distance map D populated
40+
41+
if goal not in D: # goal node not in distance map
42+
return None, D, considered # no path to goal found
43+
44+
# build path from start to goal
45+
# by walking backwards on the predecessor map
46+
47+
path = [] # start with empty path
48+
n = goal # at the goal node
49+
50+
while n != start: # while not yet at the start node
51+
path.insert(0, n) # prepend node to path
52+
n = P[n] # get predecessor of node
53+
54+
path.insert(0, start) # dont forget the start node
55+
56+
return path, D, considered
57+
58+

demo.py

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
""" demonstration of A* routing on a quadtree representation of a 2D map
2+
3+
(c) Volker Poplawski 2018
4+
"""
5+
from tkinter import *
6+
from PIL import ImageTk, ImageDraw
7+
import mapgen
8+
import quadtree
9+
import astar
10+
import graph
11+
12+
13+
# square map height and width. power of 2. e.g 256, 512, 1024
14+
MAPSIZE = 512
15+
16+
17+
class MainObject:
18+
def run(self):
19+
self.mapimage = None
20+
self.quadtree = None
21+
self.startpoint = None
22+
self.drag_startp = False
23+
24+
self._setupgui()
25+
26+
self.root.mainloop()
27+
28+
29+
def _setupgui(self):
30+
self.root = Tk()
31+
self.root.title("QuadTree A*")
32+
33+
self.canvas = Canvas(self.root, bg='gray', width=MAPSIZE, height=MAPSIZE)
34+
self.canvas.pack(side=LEFT)
35+
36+
self.image_item = self.canvas.create_image((0, 0), anchor=NW)
37+
38+
rightframe = Frame(self.root)
39+
rightframe.pack(side=LEFT, fill=Y)
40+
41+
mapframe = Frame(rightframe, relief=SUNKEN, borderwidth=2)
42+
mapframe.pack(padx=5, pady=5)
43+
44+
label = Label(mapframe, text="Map", font=("Helvetica", 13))
45+
label.pack()
46+
47+
frame1 = Frame(mapframe)
48+
frame1.pack(fill=X, padx=4)
49+
50+
kernellbl = Label(frame1, text="Kernel Size")
51+
kernellbl.pack(side=LEFT, pady=4)
52+
53+
self.kernelsizevar = StringVar(self.root)
54+
self.kernelsizevar.set("7*7")
55+
kernelmenu = OptionMenu(frame1, self.kernelsizevar, "13*13", "11*11", "9*9", "7*7", "5*5", "3*3")
56+
kernelmenu.pack(fill=X, expand=True)
57+
58+
frame2 = Frame(mapframe)
59+
frame2.pack(fill=X, padx=4)
60+
61+
iterslbl = Label(frame2, text="Num Iterations")
62+
iterslbl.pack(side=LEFT, pady=4)
63+
64+
var = StringVar(self.root)
65+
var.set("40")
66+
self.iterspin = Spinbox(frame2, from_=0, to=100, textvariable=var)
67+
self.iterspin.pack(expand=True)
68+
69+
genbtn = Button(mapframe, text="Generate Map", command=self.onButtonGeneratePress)
70+
genbtn.pack(pady=2)
71+
72+
qtframe = Frame(rightframe, relief=SUNKEN, borderwidth=2)
73+
qtframe.pack(fill=X, padx=5, pady=5)
74+
75+
label = Label(qtframe, text="QuadTree", font=("Helvetica", 13))
76+
label.pack()
77+
78+
frame1 = Frame(qtframe)
79+
frame1.pack(fill=X, padx=4)
80+
81+
label = Label(frame1, text="Depth Limit")
82+
label.pack(side=LEFT, pady=4)
83+
84+
var = StringVar(self.root)
85+
var.set("100")
86+
self.limitspin = Spinbox(frame1, from_=2, to=100, textvariable=var)
87+
self.limitspin.pack(expand=True)
88+
89+
self.qtlabelvar = StringVar()
90+
label = Label(qtframe, fg='#FF8080', textvariable=self.qtlabelvar)
91+
label.pack()
92+
93+
quadtreebtn = Button(qtframe, text="Generate QuadTree", command=self.onButtonQuadTreePress)
94+
quadtreebtn.pack(pady=2)
95+
96+
astarframe = Frame(rightframe, relief=SUNKEN, borderwidth=2)
97+
astarframe.pack(fill=X, padx=5, pady=5)
98+
99+
label = Label(astarframe, text="Path", font=("Helvetica", 13))
100+
label.pack()
101+
102+
self.pathlabelvar = StringVar()
103+
label = Label(astarframe, fg='#0000FF', textvariable=self.pathlabelvar)
104+
label.pack()
105+
106+
self.astarlabelvar = StringVar()
107+
label = Label(astarframe, fg='#8080FF', textvariable=self.astarlabelvar)
108+
label.pack()
109+
110+
label = Label(rightframe, text="Instructions", font=("Helvetica", 13))
111+
label.pack()
112+
label = Label(rightframe, justify=LEFT, text=
113+
"Generate a random map.\n"
114+
"Black regions are impassable.\n"
115+
"Generate QuadTree on map.\n"
116+
"Set start position by dragging blue circle.\n"
117+
"Click anywhere on map to find a path.")
118+
label.pack(padx=14)
119+
120+
self.canvas.bind('<ButtonPress-1>', self.onMouseButton1Press)
121+
self.canvas.bind('<ButtonRelease-1>', self.onMouseButton1Release)
122+
self.canvas.bind('<B1-Motion>', self.onMouseMove)
123+
124+
125+
def onMouseButton1Press(self, event):
126+
if not self.quadtree:
127+
return
128+
129+
if self.startpoint in self.canvas.find_overlapping(event.x, event.y, event.x, event.y):
130+
self.drag_startp = True
131+
return
132+
133+
startx, starty, _, _ = self.canvas.coords(self.startpoint)
134+
start = self.quadtree.get(startx + 6, starty + 6)
135+
goal = self.quadtree.get(event.x, event.y)
136+
137+
adjacent = graph.make_adjacent_function(self.quadtree)
138+
path, distances, considered = astar.astar(adjacent, graph.euclidian, graph.euclidian, start, goal)
139+
140+
im = self.qtmapimage.copy()
141+
draw = ImageDraw.Draw(im)
142+
143+
self.astarlabelvar.set("Nodes visited: {} considered: {}".format(len(distances), considered))
144+
for tile in distances:
145+
fill_tile(draw, tile, color=(0xC0, 0xC0, 0xFF))
146+
147+
if path:
148+
self.pathlabelvar.set("Path Cost: {} Nodes: {}".format(round(distances[goal], 1), len(path)))
149+
for tile in path:
150+
fill_tile(draw, tile, color=(0, 0, 255))
151+
else:
152+
self.pathlabelvar.set("No Path found.")
153+
154+
self._updateimage(im)
155+
156+
157+
def onMouseButton1Release(self, event):
158+
self.drag_startp = False
159+
160+
161+
def onMouseMove(self, event):
162+
if self.drag_startp:
163+
self.canvas.coords(self.startpoint, event.x-6, event.y-6, event.x+6, event.y+6)
164+
165+
166+
def onButtonGeneratePress(self):
167+
ksize = int(self.kernelsizevar.get().split('*')[0])
168+
numiter = int(self.iterspin.get())
169+
170+
self.root.config(cursor="watch")
171+
self.root.update()
172+
self.mapimage = mapgen.generate_map(MAPSIZE, kernelsize=ksize, numiterations=numiter)
173+
self._updateimage(self.mapimage)
174+
self.quadtree = None
175+
self.qtlabelvar.set("")
176+
self.canvas.delete(self.startpoint)
177+
self.startpoint = None
178+
self.astarlabelvar.set("")
179+
self.pathlabelvar.set("")
180+
self.root.config(cursor="")
181+
182+
183+
def onButtonQuadTreePress(self):
184+
if not self.mapimage:
185+
return
186+
187+
depthlimit = int(self.limitspin.get())
188+
self.quadtree = quadtree.Tile(self.mapimage, limit=depthlimit)
189+
self.qtmapimage = self.mapimage.copy()
190+
draw = ImageDraw.Draw(self.qtmapimage)
191+
draw_quadtree(draw, self.quadtree, 8)
192+
self._updateimage(self.qtmapimage)
193+
194+
self.qtlabelvar.set("Depth: {} Nodes: {}".format(self.quadtree.depth(), self.quadtree.count()))
195+
self.astarlabelvar.set("")
196+
self.pathlabelvar.set("")
197+
198+
if not self.startpoint:
199+
pos = MAPSIZE//2
200+
self.startpoint = self.canvas.create_oval(pos-6, pos-6, pos+6, pos+6, fill='#2028FF', width=2)
201+
202+
203+
def _updateimage(self, image):
204+
self.imagetk = ImageTk.PhotoImage(image)
205+
self.canvas.itemconfig(self.image_item, image=self.imagetk)
206+
207+
208+
209+
210+
def draw_quadtree(draw, tile, maxdepth):
211+
if tile.level == maxdepth:
212+
draw_tile(draw, tile, color=(255, 110, 110))
213+
return
214+
215+
if tile.childs:
216+
for child in tile.childs:
217+
draw_quadtree(draw, child, maxdepth)
218+
else:
219+
draw_tile(draw, tile, color=(255, 110, 110))
220+
221+
222+
def draw_tile(draw, tile, color):
223+
draw.rectangle([tile.bb.x, tile.bb.y, tile.bb.x+tile.bb.w, tile.bb.y+tile.bb.h], outline=color)
224+
225+
226+
def fill_tile(draw, tile, color):
227+
draw.rectangle([tile.bb.x+1, tile.bb.y+1, tile.bb.x+tile.bb.w-1, tile.bb.y+tile.bb.h-1], outline=None, fill=color)
228+
229+
230+
231+
if __name__ == '__main__':
232+
o = MainObject()
233+
o.run()

graph.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
""" graph representation functions
2+
3+
(c) Volker Poplawski 2018
4+
"""
5+
import quadtree
6+
import math
7+
from mapgen import IMPASSABLE, PASSABLE
8+
9+
10+
def euclidian(start, end):
11+
dx, dy = start.center()[0] - end.center()[0], start.center()[1] - end.center()[1]
12+
return math.sqrt(dx**2 + dy**2)
13+
14+
15+
def manhatten(start, end):
16+
dx, dy = start.center()[0] - end.center()[0], start.center()[1] - end.center()[1]
17+
return dx + dy
18+
19+
20+
def neighbours(qt, tile):
21+
"""
22+
Return neighbour tiles for tile in quadtree.
23+
24+
There are more efficient ways to find neighbouring tiles in a quadtree!
25+
Here we simply intersect the whole quadtree with a slightly expanded bounding box
26+
of the query tile.
27+
"""
28+
neigh = []
29+
qt.intersect(quadtree.BoundingBox(tile.bb.x - 1, tile.bb.y -1, tile.bb.w + 2, tile.bb.h + 2), neigh)
30+
return neigh
31+
32+
33+
34+
def make_adjacent_function(quadtree):
35+
"""
36+
Return function suitable as adjacent function as parameter for call to A*
37+
38+
this wrapper function captures the quadtree in a closure of the adjacent function
39+
"""
40+
def adjacent(tile):
41+
a = []
42+
for neighbour in neighbours(quadtree, tile):
43+
assert neighbour.childs is None # must be leaf node
44+
if neighbour.color != IMPASSABLE:
45+
a.append(neighbour)
46+
47+
return a
48+
49+
return adjacent
50+
51+

mapgen.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
""" generate random terrain-like 2D image
2+
3+
(c) Volker Poplawski 2018
4+
"""
5+
from PIL import Image, ImageFilter
6+
import random
7+
8+
9+
IMPASSABLE = 0, 0, 0
10+
PASSABLE = 220, 220, 220
11+
12+
13+
def generate_map(size, kernelsize, numiterations):
14+
im = Image.new('RGB', (size, size), color=IMPASSABLE)
15+
16+
# init with random data
17+
for x in range(0, im.width):
18+
for y in range(0, im.height):
19+
im.putpixel((x, y), random.choice([IMPASSABLE, PASSABLE]))
20+
21+
# apply filter multiple times
22+
for i in range(numiterations):
23+
im = im.filter(ImageFilter.RankFilter(kernelsize, kernelsize**2 // 2))
24+
25+
return im
26+
27+
28+
29+

0 commit comments

Comments
 (0)