LPA*アルゴリズムを画像と文章で詳しく解説

A* アルゴリズムについては以前に見たことがあり、A* アルゴリズムの基本原理は知っていますが、A* アルゴリズムの欠点も明らかです。これはオフラインの経路計画アルゴリズムであり、経路を一度しか計画できませんが、後でパスを変更すると、有効になりません。この問題を解決するために、人々は D* アルゴリズムを開発しました。A* またはダイクストラ アルゴリズムと比較した D* アルゴリズムの最大の利点は、移動中にリアルタイムであることです: 当初計画された経路に障害物が現れた場合、現在の経路に対して新しい計画が作成されます。より短い反復で見つけることができます。しかし、より大きな問題は、障害物が空白の経路上に現れる状況にのみ対応でき、障害物が元々空白領域に変わった状況には対応できないことです。この状況に対応して、LPA* アルゴリズムが誕生しました。

LPA* アルゴリズムの基本原理:

LPA* は、各セルの開始距離の 2 つの推定値、g 値と rhs 値を維持します。g 値は、A* 検索の g 値に直接対応します。rhs 値は、g 値に基づく 1 ステップ先読みです。rhs 値は常に次の関係を満たします (g 値に基づく)。
ここに画像の説明を挿入します

開始セルの rhs 値は 0 です。他の場所の rhs 値は、隣接するグリッドの g 値と、周囲のグリッドからターゲット グリッドまで移動するコストの最小値です。したがって、各ラスターの g 値は、初期状態の rhs 値と等しくなります。この格子を局所的に整合性のある格子と呼びます。すべてのセルが局所的に一貫している場合、すべての g 値は対応する開始距離に等しいため、この概念は重要です。

マップ内の障害物が変化すると、グリッドの rhs 値が変化します。このとき、グリッドのg値とrhs値が不一致になります。g 値が rhs 値と一致しないラスターは、局所的に不一致なラスターと呼ばれます。ここにも 2 つの状況があります。

1. 元の障害物グリッドから障害物が削除されています。この状況を局所的過剰整合性と呼びます。つまり、
g ( s ) > rhs ( s ) g(s) > rhs(s)g ()>r h s ( s )

2. 新しい障害物が元の空白のグリッドに追加されます。この状況を局所的な一貫性の欠如と呼びます。つまり、
g ( s ) < rhs ( s ) g(s) < rhs(s)g ()<r h s ( s )

この2点は比較的理解しやすいです。もともと障害物である点の場合、rhs 値と g 値はもともと無限大です。障害物が除去されると rhs 値は更新されます。上式によれば、その値はある値でなければならないことがわかります (周囲のすべての点の g 値が無限でない限り)。同様に、本来は空白のグリッドの g 値は rhs 値と等しいはずですが、障害物になると rhs 値も無限大となり、局所不一致の条件が満たされます。

これら 2 つの状況に対して、アルゴリズムは 2 つの異なる処理方法に分割されます。
ここに画像の説明を挿入します
局所的に一貫した点については、g 値が rhs 値に更新され、同時に周囲の点情報も更新されます。局所的に矛盾している点については、その g 値が無限大に更新され、その周囲の点の情報が更新されます。ここでの UpdateVertex 関数の場合、元のペーパーは次のように定義されています。
ここに画像の説明を挿入します
簡単に言えば、アルゴリズムは 3 つの主要な手順で各ラスターを更新します。

1. このグリッドの rhs 値を更新します。その値は、周囲のすべてのグリッドのうち、このグリッドからターゲット グリッドまでの g 値と h 値の合計の最小値です。

2. このグリッドがセット U に属している場合は、U セットから削除します。これは、更新する必要がある各点がセット U から選択され、通過した点が最初に削除される必要があるためです。

3. この点の g 値が rhs 値と一致しない場合は、それを集合 U に再挿入します。挿入されたポイントの U 値は以前の値とは異なる必要があることに注意してください。ここでの U の値の計算は次の式に従います。

ここに画像の説明を挿入します
U は実際には 2 つの数値のセットであり、最初の桁は 1 ステップの予測値をもたらし、2 桁目はその g 値と rhs 値の小さい方の値であることがわかります。

簡単に言えば、LPA* の基本的な考え方は、まず A* アルゴリズムを使用して、各ラスターの 2 つの値 (g 値と rhs 値) を維持しながらパスを見つけることです。その後、移動中にマップの障害物が変化すると、その rhs 値が更新されます。このとき、g値とrhs値が不一致となり、このラスターは局所不一致ラスターとなります。次に、アルゴリズムは ComputeShortestPath 関数を使用して、これらのローカルで一貫性のないラスターを処理してローカルで一貫性を持たせ、その過程で新しいパスが見つかります。

アルゴリズム全体の全体的な原理フローチャートは次のとおりです。
ここに画像の説明を挿入します

たくさん話した後、簡単な例を通してこの問題を見てみましょう。
ここに画像の説明を挿入します上記のシーンがあるとします。左下隅の薄緑色が開始点、青いグリッドが終了点、赤が障害物です。最初に、A* アルゴリズムを使用して、次のようにパスを計画します。
ここに画像の説明を挿入します同時に、次のように各グリッドの g 値と rhs 値を取得します。
ここに画像の説明を挿入します

左上隅が g 値、右上隅が rsh 値です。g 値が inf であるのに rsh 値が特定の値である点がいくつかあることに注意してください。これは、ある点を更新すると、その周囲の点の rsh 値は更新されますが、g 値は更新されないためです。この時点までは更新されます。そして、A* は方向性があると言え、すべての点が通過されるわけではないため、この問題が発生します。ただし、アルゴリズムの実際の使用には影響しません。

部分整合性処理フロー:

(2, 2) にある障害物を取り除くとします。まず、アルゴリズムはポイントの rhs 値を更新します。その値の更新は、記事の上部にある式に従います。したがって、rhs 値は 1.41 になります。
ここに画像の説明を挿入します

上記の変更された点はセット U に入れられ、不一致点として処理されます。次に、アルゴリズムは ComputeShortestPath 関数に入り、これらの不一致の g 値と rhs 値を更新します。更新時に点を抽出するロジックは、まず集合 U 内の最小値 (min(g(s), rhs(s)) + h(s)) を持つ点を選択することです。したがって、最初に取得される点は (2, 2) であり、この点は局所的過整合条件を満たすため、その g 値と rhs 値が更新されます。次に、その周囲のすべての隣接点の値が更新されます:
ここに画像の説明を挿入しますこのプロセスでは 2 つの点が変更されます。最初の変更は点 (2, 2) の g 値であり、rhs 値と一致するように変更されます。このとき、点は局所的に一致する点に戻り、U から削除されます。同時に、前の点 (3, 2) の rhs 値は、g 値よりも小さい 2.41 に変更されるため、点は局所的に不一致になります。点、集合 U に参加します。

次に、アルゴリズムは U から点 (3, 2) を再度取得し、ComputeShortestPath 関数を実行します。
ここに画像の説明を挿入しますこのプロセスでは、点 (3, 2) が集合 U から削除され、点 (3, 3) が集合に追加されます。 。次に、アルゴリズムは U から点 (3, 3) を再度取得し、ComputeShortestPath 関数を実行します。
ここに画像の説明を挿入します

この処理では、集合 U から点 (3, 3) が削除され、集合に点 (3, 4) が追加されます。次に、アルゴリズムは U から点 (3, 4) を再度取得し、ComputeShortestPath 関数を実行します。

ここに画像の説明を挿入しますこの処理では、点 (3, 4) が集合 U から削除され、点 (3, 5) が集合 U に追加されます。次に、アルゴリズムは再び U から点 (3, 5) を取得し、ComputeShortestPath 関数を実行します。
ここに画像の説明を挿入しますこのプロセスでは、点 (3, 5) が集合 U から削除され、点 (3, 6)、点 (4, 5) 、ポイント (4、6) がセットに追加されます。次に、アルゴリズムは再び U から点 (4, 5) を取得し、ComputeShortestPath 関数を実行します。
ここに画像の説明を挿入します
このプロセスでは、点 (4, 5) が集合 U から削除され、点 (5, 5)、点 (5, 6)コレクションに参加してください。次に、アルゴリズムは U から点 (4, 6) を再度取得し、ComputeShortestPath 関数を実行します。
ここに画像の説明を挿入しますこのプロセスでは、点 (4, 6) は集合 U から削除されますが、周囲の点の rhs 値は削除されています。更新されるため、このステップではセットに新しいポイントは追加されません。次に、アルゴリズムは U から点 (5, 5) を再度取得し、ComputeShortestPath 関数を実行します。つまり、
ここに画像の説明を挿入します
集合 U から点 (5, 5) を削除し、点 (6, 6) を集合に追加します。アルゴリズムは再び U から点 (5, 6) を取得し、ComputeShortestPath 関数を実行します。
ここに画像の説明を挿入しますセット U から点 (5, 6) を削除します。アルゴリズムは再び U から点 (6, 6) を取得し、ComputeShortestPath 関数を実行します。機能:
ここに画像の説明を挿入します現時点では、これらのポイントはローカルの一貫性を再び復元しています。このプロセスにはまだローカルに不整合なポイントがあることに注意してください。たとえば、(3, 6) は更新されていません。しかし、この時点で ComputeShortestPath 関数は満たされなくなったため、アルゴリズムが飛び出します。この点は再度更新する必要はありません。

ここでの判定について、論文では次のように定義されています。
ここに画像の説明を挿入します
つまり、点 (3, 6) については、その値は [9.41, 6.41] となるはずです。9.41 は rhs(s)+h(s) の値です。終点の値は 8.82 で、9.41 より小さいため、点 (3, 6) がどれほど最適化されていても、現在のパスより短くすることはできないため、更新する必要はありません。

最後に、次のように更新されたパスを取得できます。
ここに画像の説明を挿入します

ローカルでの一貫性の欠如の処理フロー:

部分的に一貫したプロセスを確認した後、部分的に一貫性のないプロセスを見てみましょう。初期マップを変更し、(2, 2) 点が最初は空白であると想定し、マップ パスを取得します。

ここに画像の説明を挿入します
次に、障害物として (2, 2) を追加します。このとき、点の rhs 値は inf なので、その点をセット U に追加して、ComputeShortestPath 関数を実行します。

ここに画像の説明を挿入しますこのとき、点 (3, 2) の rhs 値は 3 に変更され、その点は局所的に不一致な点となり、集合 U に追加されます。同時に、点 (2, 2) は集合 U から削除されます。 ComputeShortestPath 関数のアルゴリズムに従って、
ここに画像の説明を挿入します最初に点 (3, 2) の g 値を更新します。この点は局所的に矛盾した点であるため、その g 値は inf に直接更新される必要があります。同時に点 (3, 2) の周りの点の右辺値を更新します。

ここに画像の説明を挿入します次に、点 (3, 3) の g 値を更新し、点 (3, 3) の周囲の点の rhs 値を更新します。

ここに画像の説明を挿入します次に、点 (3, 2) の g 値を更新し、点 (4, 3) の周囲の点の rhs 値を更新します。このとき、その点は過整合点であり、次に従って処理されます。前の過剰な一貫性のあるポイント: このようにして、ポイント (2
ここに画像の説明を挿入します, 2) のローカル一貫性がポイント (3, 2) で復元され、ポイント (3, 4) が更新されます。

ここに画像の説明を挿入しますその後、ポイント (3、3) を処理します。
ここに画像の説明を挿入します
次に、ポイント (3、5) を処理
ここに画像の説明を挿入します
します。
ここに画像の説明を挿入します次に、ポイント (3、4) を処理します。 次に、ポイント (4、5) を処理します。
ここに画像の説明を挿入します次に、ポイント (4、6) を
ここに画像の説明を挿入します処理します。 , 5):
ここに画像の説明を挿入します次にポイント (5, 6) を処理:
ここに画像の説明を挿入します次にポイント (6, 6) を処理:

ここに画像の説明を挿入します
反復処理の一部を省略 (1,5)》(5,1)》(5,2)》(3,5)》(5,3)》(4,5)》(5,4)》(6 , 2)》(6,3)》(4,6)》(6,4)》(5,5)》(5,6)》(6,5)》(6,6)、最終結果は:
ここに画像の説明を挿入します

まとめ

一般的に言えば、LPA* アルゴリズムの考え方は、2 つのリストを使用して各ラスターの値を維持することです。2 つの値が矛盾する場合、そのラスターはローカルに矛盾していると呼ばれ、各ラスターの値はそれに応じて更新されます。 ComputeShortestPath関数に渡す オリジナルの場合 障害物がなくなった状態を局所過整合点と呼び、扱いやすくする 障害となる元の空白点を局所過整合点と呼ぶ 処理状況もう少し複雑になりますが、まず、点を局所的整合点に変更し、局所的整合点の処理方法に従って再度処理を行い、最終的に点の整合性をとります。 、一貫性のないポイントの走査原理は、A* アルゴリズムの走査方法と同様になり、ポイントの精度が向上します。

コード:

import os
import sys
import math
import matplotlib.pyplot as plt

class LPAStar:
    def __init__(self, s_start, s_goal, heuristic_type,xI, xG):
        self.xI, self.xG = xI, xG
        self.x_range = 51  # size of background
        self.y_range = 31
        self.motions = [(-1, 0), (-1, 1), (0, 1), (1, 1),
                        (1, 0), (1, -1), (0, -1), (-1, -1)]  # feasible input set
        self.obs = self.obs_map()
        self.s_start, self.s_goal = s_start, s_goal
        self.heuristic_type = heuristic_type
        self.u_set = self.motions
        self.obs = self.obs
        self.x = self.x_range
        self.y = self.y_range

        self.g, self.rhs, self.U = {
    
    }, {
    
    }, {
    
    }

        for i in range(self.x_range):
            for j in range(self.y_range):
                self.rhs[(i, j)] = float("inf")
                self.g[(i, j)] = float("inf")

        self.rhs[self.s_start] = 0
        self.U[self.s_start] = self.CalculateKey(self.s_start)
        self.visited = set()
        self.count = 0

        self.fig = plt.figure()

    def obs_map(self):
        """
        Initialize obstacles' positions
        :return: map of obstacles
        """

        x = 51
        y = 31
        obs = set()

        for i in range(x):
            obs.add((i, 0))
        for i in range(x):
            obs.add((i, y - 1))

        for i in range(y):
            obs.add((0, i))
        for i in range(y):
            obs.add((x - 1, i))

        for i in range(10, 21):
            obs.add((i, 15))
        for i in range(15):
            obs.add((20, i))

        for i in range(15, 30):
            obs.add((30, i))
        for i in range(16):
            obs.add((40, i))

        return obs
    def update_obs(self, obs):
        self.obs = obs


    def plot_grid(self, name):
        obs_x = [x[0] for x in self.obs]
        obs_y = [x[1] for x in self.obs]

        plt.plot(self.xI[0], self.xI[1], "bs")
        plt.plot(self.xG[0], self.xG[1], "gs")
        plt.plot(obs_x, obs_y, "sk")
        plt.title(name)
        plt.axis("equal")
    def plot_path(self, path, cl='r', flag=False):
        path_x = [path[i][0] for i in range(len(path))]
        path_y = [path[i][1] for i in range(len(path))]

        if not flag:
            plt.plot(path_x, path_y, linewidth='3', color='r')
        else:
            plt.plot(path_x, path_y, linewidth='3', color=cl)

        plt.plot(self.xI[0], self.xI[1], "bs")
        plt.plot(self.xG[0], self.xG[1], "gs")

        plt.pause(0.01)


    def run(self):
        self.plot_grid("Lifelong Planning A*")

        self.ComputeShortestPath()
        self.plot_path(self.extract_path())
        self.fig.canvas.mpl_connect('button_press_event', self.on_press)

        plt.show()

    def on_press(self, event):
        x, y = event.xdata, event.ydata
        if x < 0 or x > self.x - 1 or y < 0 or y > self.y - 1:
            print("Please choose right area!")
        else:
            x, y = int(x), int(y)
            print("Change position: s =", x, ",", "y =", y)

            self.visited = set()
            self.count += 1

            if (x, y) not in self.obs:
                self.obs.add((x, y))
                self.UpdateVertex((x, y))
            else:
                self.obs.remove((x, y))
                self.UpdateVertex((x, y))

            self.update_obs(self.obs)

            #for s_n in self.get_neighbor((x, y)):
            #    self.UpdateVertex(s_n)

            self.ComputeShortestPath()
            
            plt.cla()
            self.plot_grid("Lifelong Planning A*")
            self.plot_visited(self.visited)
            self.plot_path(self.extract_path())
            self.fig.canvas.draw_idle()

    def ComputeShortestPath(self):
        while True:
            s, v = self.TopKey()
            if v >= self.CalculateKey(self.s_goal) and \
                    self.rhs[self.s_goal] == self.g[self.s_goal]:
                break

            self.U.pop(s)
            self.visited.add(s)
            
            if self.g[s] > self.rhs[s]:

                # Condition: over-consistent (eg: deleted obstacles)
                # So, rhs[s] decreased -- > rhs[s] < g[s]
                self.g[s] = self.rhs[s]
            else:

                # Condition: # under-consistent (eg: added obstacles)
                # So, rhs[s] increased --> rhs[s] > g[s]
                self.g[s] = float("inf")
                self.UpdateVertex(s)

            for s_n in self.get_neighbor(s):
                self.UpdateVertex(s_n)

    def UpdateVertex(self, s):
        """
        update the status and the current cost to come of state s.
        :param s: state s
        """

        if s != self.s_start:

            # Condition: cost of parent of s changed
            # Since we do not record the children of a state, we need to enumerate its neighbors
            self.rhs[s] = min(self.g[s_n] + self.cost(s_n, s)
                              for s_n in self.get_neighbor(s))

        if s in self.U:
            self.U.pop(s)

        if self.g[s] != self.rhs[s]:

            # Condition: current cost to come is different to that of last time
            # state s should be added into OPEN set (set U)
            self.U[s] = self.CalculateKey(s)
            #print(self.U[s])

    def TopKey(self):
        """
        :return: return the min key and its value.
        """

        s = min(self.U, key=self.U.get)

        return s, self.U[s]

    def CalculateKey(self, s):

        return [min(self.g[s], self.rhs[s]) + self.h(s),
                min(self.g[s], self.rhs[s])]

    def get_neighbor(self, s):
        """
        find neighbors of state s that not in obstacles.
        :param s: state
        :return: neighbors
        """

        s_list = set()

        for u in self.u_set:
            s_next = tuple([s[i] + u[i] for i in range(2)])
            if s_next not in self.obs:
                s_list.add(s_next)

        return s_list

    def h(self, s):
        """
        Calculate heuristic.
        :param s: current node (state)
        :return: heuristic function value
        """

        heuristic_type = self.heuristic_type  # heuristic type
        goal = self.s_goal  # goal node

        if heuristic_type == "manhattan":
            return abs(goal[0] - s[0]) + abs(goal[1] - s[1])
        else:
            return math.hypot(goal[0] - s[0], goal[1] - s[1])

    def cost(self, s_start, s_goal):
        """
        Calculate Cost for this motion
        :param s_start: starting node
        :param s_goal: end node
        :return:  Cost for this motion
        :note: Cost function could be more complicate!
        """

        if self.is_collision(s_start, s_goal):
            return float("inf")

        return math.hypot(s_goal[0] - s_start[0], s_goal[1] - s_start[1])

    def is_collision(self, s_start, s_end):
        if s_start in self.obs or s_end in self.obs:
            return True

        if s_start[0] != s_end[0] and s_start[1] != s_end[1]:
            if s_end[0] - s_start[0] == s_start[1] - s_end[1]:
                s1 = (min(s_start[0], s_end[0]), min(s_start[1], s_end[1]))
                s2 = (max(s_start[0], s_end[0]), max(s_start[1], s_end[1]))
            else:
                s1 = (min(s_start[0], s_end[0]), max(s_start[1], s_end[1]))
                s2 = (max(s_start[0], s_end[0]), min(s_start[1], s_end[1]))

            if s1 in self.obs or s2 in self.obs:
                return True

        return False

    def extract_path(self):
        """
        Extract the path based on the PARENT set.
        :return: The planning path
        """

        path = [self.s_goal]
        s = self.s_goal

        for k in range(100):
            g_list = {
    
    }
            for x in self.get_neighbor(s):
                if not self.is_collision(s, x):
                    g_list[x] = self.g[x]
            s = min(g_list, key=g_list.get)
            path.append(s)
            if s == self.s_start:
                break

        return list(reversed(path))

    def plot_path(self, path):
        px = [x[0] for x in path]
        py = [x[1] for x in path]
        plt.plot(px, py, linewidth=2)
        plt.plot(self.s_start[0], self.s_start[1], "bs")
        plt.plot(self.s_goal[0], self.s_goal[1], "gs")

    def plot_visited(self, visited):
        color = ['gainsboro', 'lightgray', 'silver', 'darkgray',
                 'bisque', 'navajowhite', 'moccasin', 'wheat',
                 'powderblue', 'skyblue', 'lightskyblue', 'cornflowerblue']

        if self.count >= len(color) - 1:
            self.count = 0

        for x in visited:
            plt.plot(x[0], x[1], marker='s', color=color[self.count])


def main():
    x_start = (5, 5)
    x_goal = (45, 25)

    lpastar = LPAStar(x_start, x_goal, "Euclidean",x_start,x_goal)
    lpastar.run()


if __name__ == '__main__':
    main()

初期状態では、計画結果は次のようになります。
ここに画像の説明を挿入します
障害物の除去:
ここに画像の説明を挿入します
新しい障害物の追加:
ここに画像の説明を挿入します
参考:
1. 「生涯計画 Aアルゴリズム (LPA ): 生涯計画 A*
2. 「LPA* 経路探索アルゴリズムの紹介と完全なコード
3.「経路計画アルゴリズム

おすすめ

転載: blog.csdn.net/YiYeZhiNian/article/details/133102751