(6-4-02)D* Lite 算法:D* Lite路径规划器(2)

6.4.3  路径规划可视化

文件astar.py实现了一个图形化界面的路径规划应用,使用Pygame库绘制网格,并通过A和D Lite算法进行路径规划。用户可以在设计模式下设置起点、终点和障碍物,然后在执行模式下运行路径规划算法,显示路径,用户还可以在步行模式下沿规划路径行进,并在途中添加障碍物来测试路径重新规划功能。

(1)定义Pygame窗口的基本参数,包括顶部菜单栏的高度百分比、窗口的宽度和高度,以及扫描范围和Pygame窗口的初始化设置。

TOP_MENU_HEIGHT = 0.025 # 百分比
WIN_WIDTH = 800
WIN_HEIGHT = WIN_WIDTH + int((WIN_WIDTH * (TOP_MENU_HEIGHT)))
# 主网格高度 = 主网格宽度 - (主网格宽度 * (顶部菜单栏高度的百分比))
SCAN_RANGE = 1
WIN = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
pygame.display.set_caption("A* Path Finding Algorithm")
pygame.font.init()

(2)定义颜色常量的RGB数值,用于在Pygame中绘制不同的图形和文本时使用。

RED = (200, 0, 0)          # 红色
GREEN = (0, 255, 0)        # 绿色
BLUE = (0, 255, 0)         # 蓝色(此处可能应为 (0, 0, 255))
YELLOW = (255, 255, 0)     # 黄色
WHITE = (255, 255, 255)    # 白色
BLACK = (0, 0, 0)          # 黑色
PURPLE = (128, 0, 128)     # 紫色
ORANGE = (255, 165, 0)     # 橙色
GREY = (128, 128, 128)     # 灰色
TURQUOISE = (64, 224, 208) # 青色
BROWN = (150, 75, 0)       # 棕色

(3)定义类Grid,用于创建和管理二维网格。通过初始化方法可以指定网格的行数、总宽度以及起始和结束的坐标位置。方法make_grid用于生成具有指定行数的网格,每个网格包含一个Spot对象。方法reset_grid用于重新生成空的网格,方法reset_search_area用于重置网格中所有开放、关闭或路径标记的点,方法get_grid用于返回当前网格,方法get_spot用于根据行列索引或点击位置返回对应的点对象,方法draw_grid用于在Pygame窗口中绘制网格的线条。

class Grid:
    def __init__(self, rows, width, start_coord, end_coord):
        self.gap = width // rows                # 计算每个格子的宽度
        self.width = width                      # 网格的总宽度
        self.rows = rows                        # 网格行数
        self.start_x, self.start_y = start_coord # 起始坐标的x和y位置
        self.end_x, self.end_y = end_coord      # 结束坐标的x和y位置
        self.grid = []                          # 网格初始化为空列表

    def make_grid(self):
        for i in range(self.rows):
            self.grid.append([])                                # 为每一行创建一个空列表
            for j in range(self.rows):
                spot = Spot(i, j, self.gap, self.rows,          # 创建网格中的每个点(Spot对象)
                            (self.start_x, self.start_y, self.end_x, self.end_y))
                self.grid[i].append(spot)                       # 将创建的点添加到网格的对应位置

    def reset_grid(self):
        self.grid = []  # 重置网格为一个空列表
        self.make_grid()  # 重新生成网格

    def reset_search_area(self):
        for row in self.grid:
            for spot in row:
                if spot.is_open() or spot.is_closed() or spot.is_path():
                    spot.reset()  # 重置网格中所有标记为开放、关闭或路径的点

    def get_grid(self):
        return self.grid  # 返回当前网格

    def get_spot(self, row=None, col=None, x=None, y=None):
        if row and col:
            return self.grid[row][col]  # 根据行和列索引返回对应的点对象
        elif x and y:
            x = x - self.start_x  # 转换为相对于网格起始位置的坐标
            y = y - self.start_y
            row = y // self.gap  # 计算出点击位置在网格中的行和列索引
            col = x // self.gap
            print(f"spot : [{row}][{col}]")  # 输出点击位置在网格中的行和列索引
            return self.grid[row][col]  # 返回对应的点对象
        else:
            pass
            #TODO: add exception  # TODO: 添加异常处理,暂未实现

    def draw_grid(self, win):
        for i in range(self.rows):
            pygame.draw.line(win, GREY, (self.start_x, self.start_y + (i * self.gap)),  # 在Pygame窗口中绘制网格的水平线
                             (self.end_x, self.start_y + (i * self.gap)))
        for j in range(self.rows):
            pygame.draw.line(win, GREY, (self.start_x + (j * self.gap), self.start_y),  # 在Pygame窗口中绘制网格的垂直线
                             (self.start_x + (j * self.gap), self.end_y)))

(4)定义类Rectangle,用于创建和操作矩形对象。在初始化方法中,可以指定矩形的左上角坐标、宽度和高度,默认颜色为黑色。方法draw用于在传入的Pygame窗口中绘制矩形,使用了Pygame的pygame.draw.rect函数。方法draw_text用于在窗口中绘制文本,需要传入文本内容、字体大小、颜色和位置参数,使用了Pygame的文本渲染和绘制函数。

class Rectangle:
    def __init__(self, x, y, width, height):
        self.x = x                               # 矩形左上角 x 坐标
        self.y = y                               # 矩形左上角 y 坐标
        self.width = width                       # 矩形宽度
        self.height = height                     # 矩形高度
        self.color = BLACK                       # 矩形颜色,默认为黑色

    def draw(self, win):
        pygame.draw.rect(win, self.color, (self.x, self.y, self.width, self.height))
        # 在窗口中绘制矩形

    def draw_text(self, win, text, size, color, pos):
        # print(text, size, color, pos)
        font = pygame.font.SysFont("Comic Sans MS", size, True)
        text_surface = font.render(text, True, color)
        text_rect = text_surface.get_rect(topleft=pos)
        win.blit(text_surface, text_rect)
        # 在窗口中绘制文本,包括文本内容、大小、颜色和位置

(5)定义类TopBar,此类继承自类Rectangle,用于在Pygame窗口顶部创建一个带有模式和算法信息的条形框。在初始化方法__init__中定义了当前模式为"DESIGN",算法名称为空字符串,以及模式文本的位置、字体大小和颜色。方法update_mode用于更新当前模式并在窗口中重新绘制模式文本,显示当前的模式信息。

class TopBar(Rectangle):
    def __init__(self, x, y, width, height):
        self.current_mode = "DESIGN"                 # 当前模式,默认为设计模式
        self.alg = ""                                 # 算法名称,默认为空字符串
        self.mode_string_pos = (x+5,y+2)               # 模式文本位置偏移量
        # self.alg_string_pos = (x+200,y+2)              # 算法文本位置偏移量(未使用)
        self.mode_string_size = 10                     # 模式文本字体大小
        self.mode_string_color = WHITE                # 模式文本颜色,默认为白色
        super().__init__(x, y, width, height)              # 调用父类的初始化方法,设置矩形的位置和大小
    
    def update_mode(self, win, mode):
        self.current_mode = mode                           # 更新当前模式为传入的模式
        self.draw(win)                                     # 调用绘制方法刷新界面
        self.draw_text(win, f"MODE: {self.current_mode}", self.mode_string_size, self.mode_string_color, self.mode_string_pos)
        # 绘制更新后的模式文本到窗口中

(6)定义类Spot,用于表示路径规划中的单个格子或节点。在初始化方法中,它接收行索引(row)、列索引(col)、格子的宽度(width)、总行数(total_rows)、网格的起始和结束坐标范围(grid_coord),以及可选的g值和rhs值。x和y属性被计算为网格中心点的坐标,通过给定的行列索引和格子宽度计算得出。默认情况下,颜色设置为白色,邻居节点列表初始化为空。

    def __init__(self, row, col, width, total_rows, grid_coord, g=None, rhs=None):
        self.row = row                                    # 行索引
        self.col = col                                    # 列索引
        self.g = g                                        # g值(路径成本)
        self.rhs = rhs                                    # rhs值
        ### cant be absolute X,Y
        self.x_start, self.y_start, self.x_end, self.y_end = grid_coord  # 网格的起始和结束坐标范围
        self.x = self.x_start + (col * width)             # 计算spot的x坐标
        self.y = self.y_start + (row * width)             # 计算spot的y坐标
        # self.x =  (row * width)
        # self.y =  (col * width)
        self.color = WHITE                                # 默认颜色为白色
        self.neighbors = []                               # 邻居节点列表
        self.width = width                                # spot的宽度
        self.total_rows = total_rows                      # 网格总行数

(7)定义类Spot的行为,用于设置和查询单个格子的状态和属性,通过不同的颜色表示不同的状态或类型,如起点、终点、障碍物、路径等。类Spot有多个方法,包括返回该格子的行和列索引、判断格子是否处于特定状态、重置格子的状态以及将格子标记为特定状态的功能。

class Spot:
    def get_pos(self):
        return self.row, self.col
        # 返回该spot的行和列索引

    def is_closed(self):
        return self.color == RED
        # 判断该spot是否被标记为关闭状态(红色)

    def is_open(self):
        return self.color == GREEN
        # 判断该spot是否被标记为开放状态(绿色)

    def is_barrier(self):
        return self.color == BLACK
        # 判断该spot是否被标记为障碍物(黑色)
    
    def is_object(self):
        return self.color == BROWN
        # 判断该spot是否被标记为物体(棕色)

    def is_start(self):
        return self.color == ORANGE
        # 判断该spot是否被标记为起点(橙色)

    def is_end(self):
        return self.color == TURQUOISE
        # 判断该spot是否被标记为终点(青色)

    def is_path(self):
        return self.color == PURPLE
        # 判断该spot是否被标记为路径中的一部分(紫色)

    def reset(self):
        self.color = WHITE
        # 重置该spot的颜色为默认的白色

    def make_start(self):
        self.color = ORANGE
        # 将该spot标记为起点(橙色)

    def make_closed(self):
        self.color = RED
        # 将该spot标记为关闭状态(红色)

    def make_open(self):
        self.color = GREEN
        # 将该spot标记为开放状态(绿色)

    def make_barrier(self):
        self.color = BLACK
        # 将该spot标记为障碍物(黑色)

    def make_object(self):
        self.color = BROWN
        # 将该spot标记为物体(棕色)

    def make_end(self):
        self.color = TURQUOISE
        # 将该spot标记为终点(青色)

    def make_path(self):
        self.color = PURPLE
        # 将该spot标记为路径中的一部分(紫色)

(8)定义类Spot中的update_neighbors方法,用于更新当前格子的邻居列表。通过检查当前格子上下左右四个方向相邻格子的状态,如果相邻格子不是障碍物或物体,就将其添加到当前格子的邻居列表中。这样可以帮助算法在路径搜索时遍历周围可通行的格子。

	def update_neighbors(self, grid):
		self.neighbors = []
		if self.row < self.total_rows - 1 and not grid[self.row + 1][self.col].is_barrier() and not grid[self.row + 1][self.col].is_object(): # DOWN
			self.neighbors.append(grid[self.row + 1][self.col])

		if self.row > 0 and not grid[self.row - 1][self.col].is_barrier() and not grid[self.row - 1][self.col].is_object(): # UP
			self.neighbors.append(grid[self.row - 1][self.col])

		if self.col < self.total_rows - 1 and not grid[self.row][self.col + 1].is_barrier() and not grid[self.row][self.col + 1].is_object(): # RIGHT
			self.neighbors.append(grid[self.row][self.col + 1])

		if self.col > 0 and not grid[self.row][self.col - 1].is_barrier() and not grid[self.row][self.col - 1].is_object(): # LEFT
			self.neighbors.append(grid[self.row][self.col - 1])

(9)定义、函数reverse_path,用于将从终点到起点的路径反转为从起点到终点的形式。设置一个字典path表示路径信息,以及起点current,函数通过迭代找到从起点到终点的完整路径,并将其反转后返回。

def reverse_path(path, current):
	end = current
	path_list = []
	reverse = {}
	while current in path:
		current = path[current]
		path_list.append(current)
	path_list.reverse()
	for pos in range(len(path_list)-1):
		reverse[path_list[pos]] = path_list[pos+1]
	reverse[path_list[len(path_list)-1]] = end
	return reverse	

(10)定义函数draw,用于在窗口中绘制网格和顶部菜单。函数draw根据当前模式mode和算法alg,从grid_list中获取设计网格,绘制网格中的每个格子,然后更新顶部菜单以显示当前模式和算法,最后刷新窗口以显示这些更新。

def draw(win, mode, alg, grid_list, top_menu, rows, width):
    win.fill(WHITE)
    # 如果当前模式是 "设计"
    design_grid = grid_list[0]
    grid = design_grid.get_grid()
    for row in grid:
        for spot in row:
            spot.draw(win)
    design_grid.draw_grid(win)

    top_menu.update_mode(win, mode)
    top_menu.update_alg(win, alg)
    pygame.display.update()

(11)定义函数get_clicked_spot_grid,用于确定在给定位置pos处点击的网格中的具体格子(spot)。函数遍历输入的grids列表,检查点击位置pos是否在每个网格的范围内。如果找到了包含点击位置的网格,则调用该网格的get_spot方法来获取并返回该位置的具体格子(spot)。如果没有找到匹配的网格,则返回None。

def get_clicked_spot_grid(pos, grids):
    x, y = pos
    clicked_grid = None
    for grid in grids:
        if (grid.start_x <= x <= grid.end_x) and (grid.start_y <= y <= grid.end_y):
            clicked_grid = grid
            break
    if clicked_grid:
        spot = clicked_grid.get_spot(x=x, y=y)
        return spot
    return None

(12)实现主函数main,使用库Pygame来实现一个路径规划和执行的可视化应用。在这个项目中,用户可以通过设计模式在网格上放置起点、终点和障碍物,并选择算法来寻找从起点到终点的路径。主要功能包括创建和管理网格、响应鼠标和键盘事件来放置或清除起点、终点和障碍物,以及执行路径规划算法(A和D Lite)来计算路径并进行路径跟踪。在路径执行模式下,用户还可以通过空格键逐步移动路径或通过回车键实时执行D* Lite算法的移动和扫描过程。

def main(win, win_size, top_menu_height):
    ROWS = 50
    start = None
    end = None
    planned_path = None
    current = None
    k_m = 0
    queue = []
    g_score = {}
    rhs_score = {}
    mode = "DESIGN"
    alg = "NONE"
    width, height = win_size
    start_coord_grid = (0, top_menu_height)
    end_coord_grid = (width, height)
    grid_width = width
    design_grid = Grid(ROWS, grid_width, start_coord_grid, end_coord_grid)    
    design_grid.make_grid()
    grids = [design_grid]
    top_menu = TopBar(0, 0, width, top_menu_height)
    run = True
    while run:
        draw(win, mode, alg, grids, top_menu, ROWS, width)
        if mode == "DESIGN":
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    run = False
                if pygame.mouse.get_pressed()[0]: # 左键
                    pos = pygame.mouse.get_pos()
                    spot = get_clicked_spot_grid(pos, grids)
                    if not start and spot != end:
                        start = spot
                        start.make_start()
                    elif not end and spot != start:
                        end = spot
                        end.make_end()
                    elif spot != end and spot != start:
                        spot.make_barrier()
                elif pygame.mouse.get_pressed()[2]: # 右键
                    pos = pygame.mouse.get_pos()
                    spot = get_clicked_spot_grid(pos, grids)
                    spot.reset()
                    if spot == start:
                        start = None
                    elif spot == end:
                        end = None
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE and start and end:
                        upd_grid = grids[0].get_grid()
                        for row in upd_grid:
                            for spot in row:
                                spot.update_neighbors(upd_grid)                        
                        mode = "EXECUTION"
                        alg = "a-star"
                    if event.key == pygame.K_RETURN and start and end:
                        upd_grid = grids[0].get_grid()
                        for row in upd_grid:
                            for spot in row:
                                spot.update_neighbors(upd_grid)        
                        mode = "EXECUTION"
                        alg = "d-star-lite"
                    if event.key == pygame.K_c:
                        start = None
                        end = None
                        planned_path = None
                        current = None
                        mode = "DESIGN"
                        grids[0].reset_grid()
        if mode == "EXECUTION":
            if alg == "a-star":
                planned_path = a_star(lambda: draw(win, mode, alg, grids, top_menu, ROWS, width), upd_grid, start, end)
                planned_path = reverse_path(planned_path, end)
                print("alg execution ended! ")
                mode = "WALK"
                end.make_end()
                current = start
            elif alg == "d-star-lite":
                last = start
                current = start
                print("running D*")
                queue, k_m = d_star_lite(lambda: draw(win, mode, alg, grids, top_menu, ROWS, width), upd_grid, queue, start, end, k_m)
                print("FINISHED running D*")
                mode = "WALK"
                start.make_start()
                end.make_end()
        if mode == "WALK":
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    run = False
                if pygame.mouse.get_pressed()[0]: # 左键
                    pos = pygame.mouse.get_pos()                    
                    spot = get_clicked_spot_grid(pos, grids)
                    if spot != end and spot != start and not spot.is_barrier() and not spot.is_path():
                        spot.make_object()
                        upd_grid = grids[0].get_grid()
                        for row in upd_grid:
                            for spot in row:
                                spot.update_neighbors(upd_grid)    
                elif pygame.mouse.get_pressed()[2]: # 右键
                    pos = pygame.mouse.get_pos()
                    spot = get_clicked_spot_grid(pos, grids)
                    if spot.is_object():
                        spot.reset()                    

                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE and planned_path:                        
                        next = planned_path[current]
                        if next != end:
                            if next.is_object():
                                start, current = current, start
                                start.make_start()
                                current.reset()                            
                                current = None
                                planned_path = None
                                grids[0].reset_search_area()
                                upd_grid = grids[0].get_grid()
                                for row in upd_grid:
                                    for spot in row:
                                        spot.update_neighbors(upd_grid)                        
                                mode = "EXECUTION"
                            else:
                                current = next
                                current.make_path()
                    
                    if event.key == pygame.K_RETURN:
                        print(f"current position {current.get_pos()} ")
                        next, k_m = move_and_rescan(lambda: draw(win, mode, alg, grids, top_menu, ROWS, width), queue, current, end, SCAN_RANGE, k_m)
                        print(f"next position {next.get_pos()} ")
                        current = next
                        current.make_path()
                        

                    if event.key == pygame.K_c:
                        start = None
                        end = None
                        planned_path = None
                        current = None
                        mode = "DESIGN"
                        grids[0].reset_grid()
                        
    pygame.quit()
main(WIN, (WIN_WIDTH, WIN_HEIGHT), int(WIN_WIDTH * (TOP_MENU_HEIGHT)))

运行后使用鼠标左键点击网格来放置起点、终点和障碍物,使用鼠标右键点击网格来清除已放置的元素(起点、终点、障碍物)。按下空格键执行选择的路径规划算法(A* 或 D* Lite),按下回车键实时执行 D* Lite 算法的移动和扫描过程,按下 c 键清除所有放置的元素并回到设计模式。可视化如图6-3所示。

图6-3  执行效果

  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值