【学习】A星寻路python可视化

一、算法原理

1.前言

作为启发式搜索中最容易理解的算法,A星算法其实就是A的最优解。

cdEfFe.png

寻路问题一般来讲,需要将地图划分如上图所示的格子地图,格子越小,精度就越高,能够简化搜索区域。对于每个格子,其大体上都有两种状态,分别是可以通过(walk)和不能通过(stop),最后计算出从起始点到终点需要走过哪些方格,就找到了路径。

2.搜索过程

将搜索区域再进行简化,视为一组二维节点,查找当前节点的相邻节点,依照下述规则进行搜索。

  • 从起点A开始搜索,并把其加入openlist中。openlist中存放待扩展节点,此时只有起点A。
  • 查看与点A相邻的所有节点,根据判别语句排除掉障碍、边界等无法到达的区域,将其中可达的节点放入openlist,并将A节点设置为他们的father节点。
  • 将A从openlist中移出,置入closelist中。closelist中存放的是扩展完毕的节点。
  • 随后从openlist中根据f值,挑选下一个需要扩展的点。

3.求f及排序

对于启发式搜索算法来说,通常用f表示该节点向目标节点前进一步的希望程度,对于A星算法来说$f=g+h$ ,其中g表示从起点 A 移动到指定方格的移动代价,h表示从指定的方格移动到终点 B 的估算成本。

举个例子:

cwsjFP.png

设定方格为单位大小,则对角线长度为根号2,为了方便计算用1.4代替,故该点的g=1.4,对于h来说,计算方法有很多,这里采用Manhattan作演示,h = 3+1 = 4。

f = g + h = 1.4 + 4 = 5.4

将openlist中节点的f进行排序,拥有最小f的节点就是估计的最优节点

4.继续搜索

  • 将openlist中f最小的节点移出,放入closelist中
  • 检查所有与其相邻的方格,将状态为walk的节点置入openlist中,并设置好father
  • 若某个节点已经存在openlist中,则比较g,若经由当前节点的g小,则更新那个节点father、f和g

二、核心代码实现

def FindPath(startRow,startCol,endRow,endCol):
    global mapRow,mapCol,nodes,openlist,closelist
    # 判断是否在地图范围内
    if startRow < 0 or startRow >= mapRow or startCol < 0 or startCol >= mapCol or endRow < 0 or endRow >= mapRow or endCol < 0 or endCol >= mapCol:
        print("开始或结束点在地图范围外!")
    
    # 获取起点终点位置格子对象 
    startNode = nodes[startRow,startCol]
    endNode = nodes[endRow,endCol]

    # 判断是不是障碍
    if startNode.type == 'stop' or endNode.type == 'stop':
        print("开始或结束点为障碍!")
        return
    
    # 清空openlist和closelist
    openlist.clear()
    closelist.clear()

    # 把起点放入closelist
    closelist.append(startNode)

    while True:
        # 从起点开始找寻周围的节点,并放入openlist中
        # 左上
        FindNearlyToOpenList(startNode.row - 1, startNode.column - 1, 1.4, startNode, endNode)
        # 上
        FindNearlyToOpenList(startNode.row - 1, startNode.column, 1.0, startNode, endNode)
        # 右上
        FindNearlyToOpenList(startNode.row - 1, startNode.column + 1, 1.4, startNode, endNode)
        # 左
        FindNearlyToOpenList(startNode.row, startNode.column - 1, 1.0, startNode, endNode)
        # 右
        FindNearlyToOpenList(startNode.row, startNode.column + 1, 1.0, startNode, endNode)
        # 左下
        FindNearlyToOpenList(startNode.row + 1, startNode.column - 1, 1.4, startNode, endNode)
        # 下
        FindNearlyToOpenList(startNode.row + 1, startNode.column, 1.0, startNode, endNode)
        # 右下
        FindNearlyToOpenList(startNode.row + 1, startNode.column + 1, 1.4, startNode, endNode)

        # time.sleep(0.04)

        # 将f值显示在window上
        ShowF()
        # time.sleep(1)
        # 如果openlist为空了还未找到终点,则证明死路
        if len(openlist) == 0:
            print("死路一条!")
            return
        # 选出openlist中,寻路消耗最小的结点
        openlist = sorted(openlist,key=lambda x:x.f)
        # 放入closelist
        closelist.append(openlist[0])
        # 最小的点作为新的起点
        startNode = openlist[0]
        openlist.pop(0)
        # 如果已经搜索到了终点,则得到最终结果
        if startNode == endNode:
            path = []
            path.append(endNode)
            while endNode.father != None:
                path.append(endNode.father)
                endNode = endNode.father
            # 反转
            path.reverse()
            return path
def FindNearlyToOpenList(nodeRow,nodeCol,g,fatherNode,endNode):
    global mapRow,mapCol,nodes,openlist,closelist,findMethod
    # 边界判断
    if nodeRow < 0 or nodeRow >= mapRow or nodeCol < 0 or nodeCol >= mapCol:
        return
    node = nodes[nodeRow, nodeCol]
    # 判断其是否是障碍或者已经存在closelist之中
    if (node == None) or (node.type == 'stop') or (node in closelist):
        return

    # 如果当前节点已经存在openlist中
    if node in openlist:
        newg = fatherNode.g + g
        if newg > node.g:
            return
            
    # 记录父节点
    node.father = fatherNode
    # 计算g值
    node.g = fatherNode.g + g
    # h,这里用曼哈顿计算,常用的还有(欧式距离、对角线估价)
    if findMethod == "Manhattan":
        node.h = abs(endNode.row - node.row) + abs(endNode.column - node.column)
    elif findMethod == "Euclidean":
        node.h = ((endNode.row - node.row)**2 + (endNode.column - node.column)**2)**0.5

    # f
    node.f = node.g + node.h
    # 放入openlist
    openlist.append(node)

三、全部代码及可视化

import numpy as np
import random
import matplotlib.pyplot as plt
import tkinter as tk
import sys
import os
import time

# 地图宽高
mapRow = 0
mapCol = 0
# 初始化openlist与closelist
openlist = []
closelist = []
# 初始化格子容器
nodes = {}
# 当前功能
menuNode = -1
# 按钮字典
buttonNodes = {}
# 搜索起点,搜索终点坐标
startRow, startCol, endRow, endCol = 0, 0, 0, 0
# 按步执行参数(暂无用)
ispause = False
# 当前搜索方法,默认曼哈顿(Manhattan ),欧氏距离(Euclidean)
findMethod = "Manhattan"
# h的功能按钮
methodButton1, methodButton2 = 0, 0

# 定义格子类
class AStarNode:
    def __init__(self):
        # 格子坐标
        self.row = -1
        self.column = -1
        # 寻路消耗
        self.f = 0
        # 距离起点距离
        self.g = 0
        # 距离终点距离
        self.h = 0
        self.type = 'walk'
        # 声明父格子
        self.father = None
        # 状态码 0为初始,1为走过
        self.value = 0
        # 设置node坐标
    def setPos(self,row,column):
        self.row = row
        self.column = column
        # 设置node状态,walk可走,stop不可
    def setType(self,type):
        self.type = type

# 测试格子矩阵
def ShowNodes(w,h):
    global mapRow,mapCol,nodes,openlist,closelist
    for i in range(w):
        for j in range(h):
            print(nodes[i,j].type, end='\t')
        print()

# 控制台打印路径,用于测试
def ShowResultMap(w, h, pathlist):
    global nodes
    for p in pathlist:
        nodes[p.row,p.column].type = "gogo"
    for i in range(w):
        for j in range(h):
            print(nodes[i,j].type, end='\t')
        print()

# 初始化nodes(新)
def InitMap(w,h):
    # 初始化地图大小
    global mapRow, mapCol, nodes, openlist, closelist
    mapRow = w
    mapCol = h

    # 生成格子矩阵
    for i in range(w):
        for j in range(h):
            nodes[i,j] = AStarNode()
            # 初始化每个格子的位置
            nodes[i, j].setPos(i, j)
 
def FindPath(startRow,startCol,endRow,endCol):
    global mapRow,mapCol,nodes,openlist,closelist
    # 判断是否在地图范围内
    if startRow < 0 or startRow >= mapRow or startCol < 0 or startCol >= mapCol or endRow < 0 or endRow >= mapRow or endCol < 0 or endCol >= mapCol:
        print("开始或结束点在地图范围外!")
    
    # 获取起点终点位置格子对象 
    startNode = nodes[startRow,startCol]
    endNode = nodes[endRow,endCol]

    # 判断是不是障碍
    if startNode.type == 'stop' or endNode.type == 'stop':
        print("开始或结束点为障碍!")
        return
    
    # 清空openlist和closelist
    openlist.clear()
    closelist.clear()

    # 把起点放入closelist
    closelist.append(startNode)

    while True:
        # 从起点开始找寻周围的节点,并放入openlist中
        # 左上
        FindNearlyToOpenList(startNode.row - 1, startNode.column - 1, 1.4, startNode, endNode)
        # 上
        FindNearlyToOpenList(startNode.row - 1, startNode.column, 1.0, startNode, endNode)
        # 右上
        FindNearlyToOpenList(startNode.row - 1, startNode.column + 1, 1.4, startNode, endNode)
        # 左
        FindNearlyToOpenList(startNode.row, startNode.column - 1, 1.0, startNode, endNode)
        # 右
        FindNearlyToOpenList(startNode.row, startNode.column + 1, 1.0, startNode, endNode)
        # 左下
        FindNearlyToOpenList(startNode.row + 1, startNode.column - 1, 1.4, startNode, endNode)
        # 下
        FindNearlyToOpenList(startNode.row + 1, startNode.column, 1.0, startNode, endNode)
        # 右下
        FindNearlyToOpenList(startNode.row + 1, startNode.column + 1, 1.4, startNode, endNode)

        # time.sleep(0.04)

        # 将f值显示在window上
        ShowF()
        # time.sleep(1)
        # 如果openlist为空了还未找到终点,则证明死路
        if len(openlist) == 0:
            print("死路一条!")
            return
        # 选出openlist中,寻路消耗最小的结点
        openlist = sorted(openlist,key=lambda x:x.f)
        # 放入closelist
        closelist.append(openlist[0])
        # 最小的点作为新的起点
        startNode = openlist[0]
        openlist.pop(0)
        # 如果已经搜索到了终点,则得到最终结果
        if startNode == endNode:
            path = []
            path.append(endNode)
            while endNode.father != None:
                path.append(endNode.father)
                endNode = endNode.father
            # 反转
            path.reverse()
            return path


def FindNearlyToOpenList(nodeRow,nodeCol,g,fatherNode,endNode):
    global mapRow,mapCol,nodes,openlist,closelist,findMethod
    # 边界判断
    if nodeRow < 0 or nodeRow >= mapRow or nodeCol < 0 or nodeCol >= mapCol:
        return
    node = nodes[nodeRow, nodeCol]
    # 判断其是否是障碍或者已经存在closelist之中
    if (node == None) or (node.type == 'stop') or (node in closelist):
        return

    # 如果当前节点已经存在openlist中
    if node in openlist:
        newg = fatherNode.g + g
        if newg > node.g:
            return
            
    # 记录父节点
    node.father = fatherNode
    # 计算g值
    node.g = fatherNode.g + g
    # h,这里用曼哈顿计算,常用的还有(欧式距离、对角线估价)
    if findMethod == "Manhattan":
        node.h = abs(endNode.row - node.row) + abs(endNode.column - node.column)
    elif findMethod == "Euclidean":
        node.h = ((endNode.row - node.row)**2 + (endNode.column - node.column)**2)**0.5

    # f
    node.f = node.g + node.h
    # 放入openlist
    openlist.append(node)

# 检测当前是否可以寻路
def CheckMap():
    # 有起点和终点,则可以寻路
    global nodes
    checkStart = False
    checkEnd = False
    for bt in buttonNodes.values():
        if bt['bg'] == 'green':
            checkStart = True
        if bt['bg'] == 'red':
            checkEnd = True
    if checkEnd and checkStart:
        return True

def ShowF():
    global openlist, closelist,buttonNodes
    for node in openlist:
        buttonNodes[node.row, node.column]['text'] = str(round(node.f, 3))
    for node in closelist:
        buttonNodes[node.row, node.column]['text'] = str(round(node.f, 3))

def ChangeMethod(method,temp):
    global findMethod,methodButton1,methodButton2
    findMethod = method
    if temp == 1:
        methodButton1['bg'] = 'DeepPink'
        methodButton2['bg'] = '#6495ED'
    elif temp == 2:
        methodButton2['bg'] = 'DeepPink'
        methodButton1['bg'] = '#6495ED'

# 清除f和颜色
def ClearMap():
    global openlist, closelist, buttonNodes, nodes, mapRow, mapCol
    # 清除按钮上的f和颜色
    for btKey in buttonNodes.keys():
        buttonNodes[btKey]['bg'] = 'white'
        buttonNodes[btKey]['text'] = ''
    # 初始化nodes
    for i in range(mapRow):
        for j in range(mapCol):
            nodes[i,j] = AStarNode()
            # 默认均为walk
            nodes[i,j].setType('walk')
            # 初始化每个格子的位置
            nodes[i, j].setPos(i, j)

def MenuClick(i):
    global mapRow,mapCol,nodes,menuNode,buttonNodes,startRow,startCol,endRow,endCol,findMethod
    # 0 开始
    # 1 设置障碍
    # 2 设置起点
    # 3 设置终点
    # 4 按步显示结果
    # 5 重置
    if i == 0 and CheckMap():
        menuNode = 0
        pathlist = FindPath(startRow, startCol, endRow, endCol)
        # 画出路径
        if pathlist is not None:
            for p in pathlist:
                if buttonNodes[p.row, p.column]['bg'] == 'white':
                    buttonNodes[p.row, p.column]['bg'] = 'Orange'
            # print(nodes[5,1].f)
    elif i == 1:
        menuNode = 1
    elif i == 2:
        menuNode = 2
    elif i == 3:
        menuNode = 3
    # elif i == 4:
    #     menuNode = 4
    elif i == 4:
        menuNode = 4
        ClearMap()
        # 重启程序
        # python = sys.executable
        # os.execl(python, python, *sys.argv)

def CreateGUI():
    global mapRow,mapCol,nodes,buttonNodes,methodButton1,methodButton2
    
    window = tk.Tk()
    window.title('my window')
    window.geometry('1920x1027')

    tk.Button(window,text='开始',width=11,height=5,bg='#6495ED',command=lambda : MenuClick(i = 0)).grid(row=0,column=0,padx=0,pady=0)
    tk.Button(window,text='障碍',width=11,height=5,bg='#6495ED',command=lambda : MenuClick(i = 1)).grid(row=0,column=1,padx=0,pady=0)
    tk.Button(window,text='起点',width=11,height=5,bg='#6495ED',command=lambda : MenuClick(i = 2)).grid(row=0,column=2,padx=0,pady=0)
    tk.Button(window,text='终点',width=11,height=5,bg='#6495ED',command=lambda : MenuClick(i = 3)).grid(row=0,column=3,padx=0,pady=0)
    # tk.Button(window,text='下一步',width=11,height=5,bg='#6495ED',command=lambda : MenuClick(i = 4)).grid(row=0,column=4,padx=0,pady=0)
    tk.Button(window,text='重置',width=11,height=5,bg='#6495ED',command=lambda : MenuClick(i = 4)).grid(row=0,column=4,padx=0,pady=0)
    methodButton1 = tk.Button(window, text='欧氏距离', width=11, height=5, bg='#6495ED', command=lambda: ChangeMethod(method="Euclidean", temp=1))
    methodButton1.grid(row=0, column=5, padx=0, pady=0)
    methodButton2 = tk.Button(window, text='曼哈顿距离', width=11, height=5, bg='DeepPink', command=lambda: ChangeMethod(method="Manhattan", temp=2))
    methodButton2.grid(row=0,column=6,padx=0,pady=0)
    for i in range(mapRow):
        # width +1加9像素
        # height +1加20像素
        for j in range(mapCol):
            buttonNodes[i,j] = tk.Button(window,text='',width=11,height=5,bg='white',command=lambda i=i,j=j: SetButton(i,j))
            buttonNodes[i,j].grid(row=1+i,column=j,padx=0,pady=0)
    
    
    # if pathlist is not None:
    #     for p in pathlist:
    #         buttonNodes[p.row,p.column]['bg'] = 'red'

    window.mainloop()

def SetButton(i,j):
    global menuNode,buttonNodes,mapRow,mapCol,nodes,startRow,startCol,endRow,endCol
    if menuNode == -1:
        return
    elif menuNode == 1:
        # 如果设置过障碍,则清除
        if buttonNodes[i,j]['bg'] == 'black':
            nodes[i,j].type = 'walk'
            buttonNodes[i,j]['bg'] = 'white'
        else:
            nodes[i,j].type = 'stop'
            buttonNodes[i,j]['bg'] = 'black'
    elif menuNode == 2:
        # 如果设置过起点,则清除
        for buttonKey,buttonValue in buttonNodes.items():
            if buttonValue['bg'] == 'green':
               buttonNodes[buttonKey]['bg'] = 'white'
               break
        buttonNodes[i,j]['bg'] = 'green'
        startRow,startCol = i,j
        nodes[i,j].type = 'walk'
    elif menuNode == 3:
        # 如果设置过终点,则清除
        for buttonKey,buttonValue in buttonNodes.items():
            if buttonValue['bg'] == 'red':
               buttonNodes[buttonKey]['bg'] = 'white'
        buttonNodes[i,j]['bg'] = 'red'
        endRow,endCol = i,j
        nodes[i,j].type = 'walk'

InitMap(8, 17)
ShowNodes(8, 17)
# pathlist = FindPath(1, 0, 5, 6)
print("_________________________________")
CreateGUI()
posted @ 2022-01-18 19:34  小拳头呀  阅读(639)  评论(0编辑  收藏  举报