|
| 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() |
0 commit comments