Skip to content

Commit b19476f

Browse files
authored
Wavefront Coverage Path Planning (AtsushiSakai#351)
* First Commit of Wavefront Coverage Planner * Update wavefront_coverage_path_planner.py * fix CI / CodeFactor problem * Fix mypy error * update * Fix PyCodeStyle * Fix Code Scanning Warning and code styling problem * add simple unittest * followed second code review suggestions
1 parent 77f6277 commit b19476f

File tree

5 files changed

+283
-0
lines changed

5 files changed

+283
-0
lines changed
156 Bytes
Loading
150 Bytes
Loading
132 Bytes
Loading
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Distance/Path Transform Wavefront Coverage Path Planner
3+
4+
author: Todd Tang
5+
paper: Planning paths of complete coverage of an unstructured environment
6+
by a mobile robot - Zelinsky et.al.
7+
link: http://pinkwink.kr/attachment/[email protected]
8+
"""
9+
10+
import os
11+
import sys
12+
13+
import matplotlib.pyplot as plt
14+
import numpy as np
15+
16+
from scipy import ndimage
17+
18+
19+
do_animation = True
20+
21+
22+
def transform(
23+
gridmap, src, distance_type='chessboard',
24+
transform_type='path', alpha=0.01
25+
):
26+
"""transform
27+
28+
calculating transform of transform_type from src
29+
in given distance_type
30+
31+
:param gridmap: 2d binary map
32+
:param src: distance transform source
33+
:param distance_type: type of distance used
34+
:param transform_type: type of transform used
35+
:param alpha: weight of Obstacle Transform usedwhen using path_transform
36+
"""
37+
38+
nrows, ncols = gridmap.shape
39+
40+
if nrows == 0 or ncols == 0:
41+
sys.exit('Empty gridmap.')
42+
43+
inc_order = [[0, 1], [1, 1], [1, 0], [1, -1],
44+
[0, -1], [-1, -1], [-1, 0], [-1, 1]]
45+
if distance_type == 'chessboard':
46+
cost = [1, 1, 1, 1, 1, 1, 1, 1]
47+
elif distance_type == 'eculidean':
48+
cost = [1, np.sqrt(2), 1, np.sqrt(2), 1, np.sqrt(2), 1, np.sqrt(2)]
49+
else:
50+
sys.exit('Unsupported distance type.')
51+
52+
transform_matrix = float('inf') * np.ones_like(gridmap, dtype=np.float)
53+
transform_matrix[src[0], src[1]] = 0
54+
if transform_type == 'distance':
55+
eT = np.zeros_like(gridmap)
56+
elif transform_type == 'path':
57+
eT = ndimage.distance_transform_cdt(1-gridmap, distance_type)
58+
else:
59+
sys.exit('Unsupported transform type.')
60+
61+
# set obstacle transform_matrix value to infinity
62+
for i in range(nrows):
63+
for j in range(ncols):
64+
if gridmap[i][j] == 1.0:
65+
transform_matrix[i][j] = float('inf')
66+
is_visited = np.zeros_like(transform_matrix, dtype=bool)
67+
is_visited[src[0], src[1]] = True
68+
traversal_queue = [src]
69+
calculated = [(src[0]-1)*ncols + src[1]]
70+
71+
def is_valid_neighbor(i, j):
72+
return ni >= 0 and ni < nrows and nj >= 0 and nj < ncols \
73+
and not gridmap[ni][nj]
74+
75+
while traversal_queue != []:
76+
i, j = traversal_queue.pop(0)
77+
for k, inc in enumerate(inc_order):
78+
ni = i + inc[0]
79+
nj = j + inc[1]
80+
if is_valid_neighbor(ni, nj):
81+
is_visited[i][j] = True
82+
83+
# update transform_matrix
84+
transform_matrix[i][j] = min(
85+
transform_matrix[i][j],
86+
transform_matrix[ni][nj] + cost[k] + alpha*eT[ni][nj])
87+
88+
if not is_visited[ni][nj] \
89+
and ((ni-1)*ncols + nj) not in calculated:
90+
traversal_queue.append((ni, nj))
91+
calculated.append((ni-1)*ncols + nj)
92+
93+
return transform_matrix
94+
95+
96+
def wavefront(transform_matrix, start, goal):
97+
"""wavefront
98+
99+
performing wavefront coverage path planning
100+
101+
:param transform_matrix: the transform matrix
102+
:param start: start point of planning
103+
:param goal: goal point of planning
104+
"""
105+
106+
path = []
107+
nrows, ncols = transform_matrix.shape
108+
109+
def get_search_order_increment(start, goal):
110+
if start[0] >= goal[0] and start[1] >= goal[1]:
111+
order = [[1, 0], [0, 1], [-1, 0], [0, -1],
112+
[1, 1], [1, -1], [-1, 1], [-1, -1]]
113+
elif start[0] <= goal[0] and start[1] >= goal[1]:
114+
order = [[-1, 0], [0, 1], [1, 0], [0, -1],
115+
[-1, 1], [-1, -1], [1, 1], [1, -1]]
116+
elif start[0] >= goal[0] and start[1] <= goal[1]:
117+
order = [[1, 0], [0, -1], [-1, 0], [0, 1],
118+
[1, -1], [-1, -1], [1, 1], [-1, 1]]
119+
elif start[0] <= goal[0] and start[1] <= goal[1]:
120+
order = [[-1, 0], [0, -1], [0, 1], [1, 0],
121+
[-1, -1], [-1, 1], [1, -1], [1, 1]]
122+
else:
123+
sys.exit('get_search_order_increment: cannot determine \
124+
start=>goal increment order')
125+
return order
126+
127+
def is_valid_neighbor(i, j):
128+
is_i_valid_bounded = i >= 0 and i < nrows
129+
is_j_valid_bounded = j >= 0 and j < ncols
130+
if is_i_valid_bounded and is_j_valid_bounded:
131+
return not is_visited[i][j] and \
132+
transform_matrix[i][j] != float('inf')
133+
return False
134+
135+
inc_order = get_search_order_increment(start, goal)
136+
137+
current_node = start
138+
is_visited = np.zeros_like(transform_matrix, dtype=bool)
139+
140+
while current_node != goal:
141+
i, j = current_node
142+
path.append((i, j))
143+
is_visited[i][j] = True
144+
145+
max_T = float('-inf')
146+
i_max = (-1, -1)
147+
i_last = 0
148+
for i_last in range(len(path)):
149+
current_node = path[-1 - i_last] # get lastest node in path
150+
for ci, cj in inc_order:
151+
ni, nj = current_node[0] + ci, current_node[1] + cj
152+
if is_valid_neighbor(ni, nj) and \
153+
transform_matrix[ni][nj] > max_T:
154+
i_max = (ni, nj)
155+
max_T = transform_matrix[ni][nj]
156+
157+
if i_max != (-1, -1):
158+
break
159+
160+
if i_max == (-1, -1):
161+
break
162+
else:
163+
current_node = i_max
164+
if i_last != 0:
165+
print('backtracing to', current_node)
166+
path.append(goal)
167+
168+
return path
169+
170+
171+
def visualize_path(grid_map, start, goal, path, resolution):
172+
oy, ox = start
173+
gy, gx = goal
174+
px, py = np.transpose(np.flipud(np.fliplr((path))))
175+
176+
if not do_animation:
177+
plt.imshow(grid_map, cmap='Greys')
178+
plt.plot(ox, oy, "-xy")
179+
plt.plot(px, py, "-r")
180+
plt.plot(gx, gy, "-pg")
181+
plt.show()
182+
else:
183+
for ipx, ipy in zip(px, py):
184+
plt.cla()
185+
# for stopping simulation with the esc key.
186+
plt.gcf().canvas.mpl_connect(
187+
'key_release_event',
188+
lambda event: [exit(0) if event.key == 'escape' else None])
189+
plt.imshow(grid_map, cmap='Greys')
190+
plt.plot(ox, oy, "-xb")
191+
plt.plot(px, py, "-r")
192+
plt.plot(gx, gy, "-pg")
193+
plt.plot(ipx, ipy, "or")
194+
plt.axis("equal")
195+
plt.grid(True)
196+
plt.pause(0.1)
197+
198+
199+
def main():
200+
dir_path = os.path.dirname(os.path.realpath(__file__))
201+
img = plt.imread(os.path.join(dir_path, 'map', 'test.png'))
202+
img = 1 - img # revert pixel values
203+
204+
start = (43, 0)
205+
goal = (0, 0)
206+
207+
# distance transform wavefront
208+
DT = transform(img, goal, transform_type='distance')
209+
DT_path = wavefront(DT, start, goal)
210+
visualize_path(img, start, goal, DT_path, 1)
211+
212+
# path transform wavefront
213+
PT = transform(img, goal, transform_type='path', alpha=0.01)
214+
PT_path = wavefront(PT, start, goal)
215+
visualize_path(img, start, goal, PT_path, 1)
216+
217+
218+
if __name__ == "__main__":
219+
main()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import os
2+
import sys
3+
import matplotlib.pyplot as plt
4+
from unittest import TestCase
5+
6+
sys.path.append(os.path.dirname(
7+
os.path.abspath(__file__)) + "/../PathPlanning/WavefrontCPP")
8+
try:
9+
import wavefront_coverage_path_planner
10+
except ImportError:
11+
raise
12+
13+
wavefront_coverage_path_planner.do_animation = False
14+
15+
16+
class TestPlanning(TestCase):
17+
def wavefront_cpp(self, img, start, goal):
18+
num_free = 0
19+
for i in range(img.shape[0]):
20+
for j in range(img.shape[1]):
21+
num_free += 1 - img[i][j]
22+
23+
DT = wavefront_coverage_path_planner.transform(
24+
img, goal, transform_type='distance')
25+
DT_path = wavefront_coverage_path_planner.wavefront(DT, start, goal)
26+
self.assertEqual(len(DT_path), num_free) # assert complete coverage
27+
28+
PT = wavefront_coverage_path_planner.transform(
29+
img, goal, transform_type='path', alpha=0.01)
30+
PT_path = wavefront_coverage_path_planner.wavefront(PT, start, goal)
31+
self.assertEqual(len(PT_path), num_free) # assert complete coverage
32+
33+
def test_wavefront_CPP_1(self):
34+
img_dir = os.path.dirname(
35+
os.path.abspath(__file__)) + "/../PathPlanning/WavefrontCPP"
36+
img = plt.imread(os.path.join(img_dir, 'map', 'test.png'))
37+
img = 1 - img
38+
39+
start = (43, 0)
40+
goal = (0, 0)
41+
42+
self.wavefront_cpp(img, start, goal)
43+
44+
def test_wavefront_CPP_2(self):
45+
img_dir = os.path.dirname(
46+
os.path.abspath(__file__)) + "/../PathPlanning/WavefrontCPP"
47+
img = plt.imread(os.path.join(img_dir, 'map', 'test_2.png'))
48+
img = 1 - img
49+
50+
start = (10, 0)
51+
goal = (10, 40)
52+
53+
self.wavefront_cpp(img, start, goal)
54+
55+
def test_wavefront_CPP_3(self):
56+
img_dir = os.path.dirname(
57+
os.path.abspath(__file__)) + "/../PathPlanning/WavefrontCPP"
58+
img = plt.imread(os.path.join(img_dir, 'map', 'test_3.png'))
59+
img = 1 - img
60+
61+
start = (0, 0)
62+
goal = (30, 30)
63+
64+
self.wavefront_cpp(img, start, goal)

0 commit comments

Comments
 (0)