728x90

 파이썬 로보틱스 정리를 마친 후

다양한 개념들을 살펴보았으나

파이썬 알고리즘, 계산 이론등 하려다가 너무 벅찬것같아

한동안 손놓고 있었다.

 

 몇일 동안 내 공부가 손이 안잡히고 집중도 안되고

수식을 슈도코드를 어떻게 코드화 하는건지 모르겠고

OTL 하는 시간을 보내다가

 

 오늘은 제대로 푹 쉬고

이전부터 생각하던 파이썬 로보틱스 실제 구동 부분을 해보려고 한다.

일단 위치 추정알고리즘들은 칼만, 파티클, 히스토그램 기본적인 코드들은 돌려보았으니

 

 다음에는 슬램알고리즘들을 정리 후

경로 관련 알고리즘들알 다루어 보려고 한다.

 

 이번에 정리하면서 한 3~4시간 쯤 걸렸으나

모처럼 조금은 집중 잘되는 시간이었다.

 

 요즘에는 1시간 공부하고 2~3시간 놀게 되는 경우가 많다가

한동안은 아예 손놓고 예전 사진만 올리고 있었다.

 

 이번에 정리한 내용은 이전과는 달리

가능한 이론적인 내용과 구현 코드를 같이 설명하려고 했다.

 

 그런데 코드를 보기전에 이론을 다시 내용을 꺼내자고 하니

여러번 봤던 내용이긴 하지만 예전에 제대로 보지않아선지 동작하는 내용들을 제대로 본적이 없어선지

이론 해석이 잘안되는게 많았다.

 

 아마 이론 내용과 실제 파이썬으로 구현한 내용들이 조금 달라서 일수도 있고 ..

아무튼 이런 식으로 나머지 내용들도 조금씩 정리해보고

경로 계획 까지 순수한 파이썬 환경에서 정리를 마친다면,

 

 추후 ROS 가제보 상에서 터틀봇 같은걸로 실제 실험해보는걸 올려보고

멀리는 실제 간단한 로봇을 만들어 제어 실험같은걸 해보고 싶다.

 

 오늘은 여기까지 OTL

 

300x250

'그외 > 로그' 카테고리의 다른 글

내가 컴퓨터 공부를 시작하게 만든 다큐  (0) 2020.07.05
공부하기 싫어 feat. 조던 피터슨  (0) 2020.07.05
바콜로드 섬 어딘가  (0) 2020.07.03
싱가포르를 떠날때  (0) 2020.07.03
중동 어딘가  (0) 2020.07.03
728x90

지난번에는

연속 공간에서의 위치 추정 문제를 다루는

확장 칼만 필터와 파티클 필터를 이용한 위치 추정 시뮬레이션을 동작 시켜보았습니다.

 

 

이번에는 히스토그램 필터에 대해서 살펴보겠습니다.

히스토그램 필터는 파티클 필터처럼 가우시안 모델을 사용하지 않는 비모수적인 필터 중 하나로

상태 공간을 유한개의 영역들로 나누고, 사후 확률을 각 영역들의 확률 값으로 나타내는 방법입니다.

 

 이 작업이 이산 영역에 적용되면 이산 베이즈 필터, 연속 공간에서 적용이되면 히스토그램이라 부르개 됩니다.

이산 베이즈 필터를 본 후 연속 공간에 대한 히스토그램을 봅시다.

 

 우선 이산 베이즈 필터는 2장에서 살펴본 베이즈 필터의 적분을

유한 합에 대한 식으로 수정한 것으로 아래와 같습니다.

 

 

 이제 이 이산 베이즈 필터를 연속 상태 공간에서 근사 추론을 하는 도구로 사용을 할것이며, 이 필터를 히스토그램 필터라고 부릅니다.

 

 히스토그램은 연속 상태공간을 유한개의 많은 영역으로 분해하므로 아래와 같이 정리할수 있겠습니다.

 

 

 우선 처음에는 이산 베이즈 필터가 신뢰도 분포에 아무런 정보를 주지 못하므로, 각 영역에 대한 상태들이 균일 확률 분포가 되어 사후 확률은 상수 확률 밀도 함수가 됩니다.  (우항의 분모는 해당 영역의 크기)

 

 

 위 식에서 이산 베이즈 필터를 균일 분포를 사용하여 다음의 근사화 결과를 구할수 있습니다.

 

 

히스토그램에 대해 간단히 여기까지 살펴보고 히스토그램 필터 시뮬레이션 코드를 살펴보겠습니다.

 

 

 

# Parameters
SIM_TIME = 10.0  # simulation time [s]
DT = 0.1  # time tick [s]
MAX_RANGE = 10.0  # maximum observation range
MOTION_STD = 1.0  # standard deviation for motion gaussian distribution
RANGE_STD = 3.0  # standard deviation for observation gaussian distribution

# grid map param
XY_RESOLUTION = 0.5  # xy grid resolution
MIN_X = -15.0
MIN_Y = -5.0
MAX_X = 15.0
MAX_Y = 25.0

# simulation parameters
NOISE_RANGE = 2.0  # [m] 1σ range noise parameter
NOISE_SPEED = 0.5  # [m/s] 1σ speed noise parameter

show_animation = True

 

- 기본 파라미터

일단 시뮬레이션 시간은 10초

시간 간격은 0.1초

최대 측정 거리 10m

동작 모델 노이즈 가우시안 분포 표준편차 1.0

측정 모델 노이즈 가우시안 분포 표준편차 3.0

 

- 격자 지도 파라미터

지도 해상도는 0.5

x : -15 ~ 15

y : -5 ~ 25

 

- 시뮬레이션 파라미터

관측 노이즈 2.0

속도 노이즈 0.5

 

class GridMap:

    def __init__(self):
        self.data = None
        self.xy_resolution = None
        self.min_x = None
        self.min_y = None
        self.max_x = None
        self.max_y = None
        self.x_w = None
        self.y_w = None
        self.dx = 0.0  # movement distance
        self.dy = 0.0  # movement distance

일단 그리드맵 클래스 구조는 위와 같습니다.

 

일단 main함수에서 순서대로 따라가봅시다.

 

 

def main():
    print(__file__ + " start!!")

    # RF_ID positions [x, y]
    RF_ID = np.array([[10.0, 0.0],
                      [10.0, 10.0],
                      [0.0, 15.0],
                      [-5.0, 20.0]])

    time = 0.0

    xTrue = np.zeros((4, 1))
    grid_map = init_grid_map(XY_RESOLUTION, MIN_X, MIN_Y, MAX_X, MAX_Y)
    mx, my = calc_grid_index(grid_map)  # for grid map visualization

    while SIM_TIME >= time:
        time += DT
        print("Time:", time)

        u = calc_input()

        yaw = xTrue[2, 0]  # Orientation is known
        xTrue, z, ud = observation(xTrue, u, RF_ID)

        grid_map = histogram_filter_localization(grid_map, u, z, yaw)

        if show_animation:
            plt.cla()
            # for stopping simulation with the esc key.
            plt.gcf().canvas.mpl_connect(
                'key_release_event',
                lambda event: [exit(0) if event.key == 'escape' else None])
            draw_heat_map(grid_map.data, mx, my)
            plt.plot(xTrue[0, :], xTrue[1, :], "xr")
            plt.plot(RF_ID[:, 0], RF_ID[:, 1], ".k")
            for i in range(z.shape[0]):
                plt.plot([xTrue[0, :], z[i, 1]], [
                    xTrue[1, :], z[i, 2]], "-k")
            plt.title("Time[s]:" + str(time)[0: 4])
            plt.pause(0.1)

    print("Done")


if __name__ == '__main__':
    main()

 

메인 전체에서 루프 이전 내용들을 살펴보겠습니다.

 

def main():
    print(__file__ + " start!!")

    # RF_ID positions [x, y]
    RF_ID = np.array([[10.0, 0.0],
                      [10.0, 10.0],
                      [0.0, 15.0],
                      [-5.0, 20.0]])

    time = 0.0

    xTrue = np.zeros((4, 1))
    grid_map = init_grid_map(XY_RESOLUTION, MIN_X, MIN_Y, MAX_X, MAX_Y)
    mx, my = calc_grid_index(grid_map)  # for grid map visualization

 

중요한 부분만 짚고 넘어가면

 

1. 관측할 RF_ID 4개 위치를 초기화

2. 실제 로봇의 상태 공간 초기화

3. 그리드맵 초기화 

4. 그리드 인덱스 계산

 

일단 그리드맵 초기화 부분을 살펴보면

 

def init_grid_map(xy_resolution, min_x, min_y, max_x, max_y):
    grid_map = GridMap()

    grid_map.xy_resolution = xy_resolution
    grid_map.min_x = min_x
    grid_map.min_y = min_y
    grid_map.max_x = max_x
    grid_map.max_y = max_y
    grid_map.x_w = int(round((grid_map.max_x - grid_map.min_x)
                             / grid_map.xy_resolution))
    grid_map.y_w = int(round((grid_map.max_y - grid_map.min_y)
                             / grid_map.xy_resolution))

    grid_map.data = [[1.0 for _ in range(grid_map.y_w)]
                     for _ in range(grid_map.x_w)]
    grid_map = normalize_probability(grid_map)

    return grid_map

위에서 정의한 구조체 대로 그리드맵을 생성 후

 

지정한 파라미터들을 전달하고

 

x_w, y_w를 계산하는데

 

x축 경우만 살펴보면

15 - (-15) / 0.5 = 30/0.5 =60

 

x_w는 x 공간의 개수

y_w는 y 공간의 개수가 될것이고

 

grid_map.data는 모든 격자의 값이 1인 2차원 격자가 되겠습니다.

 

def normalize_probability(grid_map):
    sump = sum([sum(i_data) for i_data in grid_map.data])

    for ix in range(grid_map.x_w):
        for iy in range(grid_map.y_w):
            grid_map.data[ix][iy] /= sump

    return grid_map

 그 다음 오훌하는 확률 정규화 함수를 보면

 

모든 행의 합을 구하고

 

각 격자에 이 합을 나누면서

 

모든 격자는 균일 분포를 따르게 됩니다.

 

여기서 x, y 각각 60개 이므로 총 3,600개

 

하나의 격자는 1/3,600 크기를 가지게 됩니다.

 

이제 그리드 지도 초기화를 마치고 넘어가봅시다.

 

def main():
    print(__file__ + " start!!")

    # RF_ID positions [x, y]
    RF_ID = np.array([[10.0, 0.0],
                      [10.0, 10.0],
                      [0.0, 15.0],
                      [-5.0, 20.0]])

    time = 0.0

    xTrue = np.zeros((4, 1))
    grid_map = init_grid_map(XY_RESOLUTION, MIN_X, MIN_Y, MAX_X, MAX_Y)
    mx, my = calc_grid_index(grid_map)  # for grid map visualization

다시 메인으로 돌아와 보면

 

그리드 인덱스도 계산해줍시다

 

def calc_grid_index(grid_map):
    mx, my = np.mgrid[slice(grid_map.min_x - grid_map.xy_resolution / 2.0,
                            grid_map.max_x + grid_map.xy_resolution / 2.0,
                            grid_map.xy_resolution),
                      slice(grid_map.min_y - grid_map.xy_resolution / 2.0,
                            grid_map.max_y + grid_map.xy_resolution / 2.0,
                            grid_map.xy_resolution)]

    return mx, my

슬라이스 함수부터 보면

min_x 는 -15, resolution = 0.5

-15 - 0.25 = -15.25

-15.25 ~ 15.25간격을 0.5로 나눌것이고

 

30.5 / 0.5 = 61

길이가 61인 리스트가 반환될것이고

 

y도 있으니

길이가 61, 61인 리스트가 mgrid의 인자로 사용되겠습니다.

 

아래의 설명에 따르면 차원을 뻥튀기 해준다고하니

 

my, my는 둘다 61, 61차원의 값이 되겠습니다.

 

https://namyoungkim.github.io/python/numpy/2017/10/01/python_1/

 

mx 출력 결과

 

이제 그리드 인덱스 계산까지 마쳤으니 주 루프문을 다시 살펴보겠습니다.

 

    while SIM_TIME >= time:
        time += DT
        print("Time:", time)

        u = calc_input()

        yaw = xTrue[2, 0]  # Orientation is known
        xTrue, z, ud = observation(xTrue, u, RF_ID)

        grid_map = histogram_filter_localization(grid_map, u, z, yaw)

        if show_animation:
            plt.cla()
            # for stopping simulation with the esc key.
            plt.gcf().canvas.mpl_connect(
                'key_release_event',
                lambda event: [exit(0) if event.key == 'escape' else None])
            draw_heat_map(grid_map.data, mx, my)
            plt.plot(xTrue[0, :], xTrue[1, :], "xr")
            plt.plot(RF_ID[:, 0], RF_ID[:, 1], ".k")
            for i in range(z.shape[0]):
                plt.plot([xTrue[0, :], z[i, 1]], [
                    xTrue[1, :], z[i, 2]], "-k")
            plt.title("Time[s]:" + str(time)[0: 4])
            plt.pause(0.1)

 

여기서 시각화 하는 부분을 빼면

 

1. 입력 계산 calc_input

2. 관측 observation

3. 히스토그램 필터 위치추정 histogram_filter_localization

 

이렇게 3 파트로 나눌수 있겠습니다.

 

입력 계산을 살펴보겠습니다.

 

def calc_input():
    v = 1.0  # [m/s]
    yaw_rate = 0.1  # [rad/s]
    u = np.array([v, yaw_rate]).reshape(2, 1)
    return u

 

여기는 속도 기반 모델인지

 

2,1의 행렬로

 

첫재 값은 선속도, 둘때 값은 각속도를 넣어서 반환해 줍니다

 

다음으로 관측을 봅시다

 

def observation(xTrue, u, RFID):
    xTrue = motion_model(xTrue, u)

    z = np.zeros((0, 3))

    for i in range(len(RFID[:, 0])):

        dx = xTrue[0, 0] - RFID[i, 0]
        dy = xTrue[1, 0] - RFID[i, 1]
        d = math.hypot(dx, dy)
        if d <= MAX_RANGE:
            # add noise to range observation
            dn = d + np.random.randn() * NOISE_RANGE
            zi = np.array([dn, RFID[i, 0], RFID[i, 1]])
            z = np.vstack((z, zi))

    # add noise to speed
    ud = u[:, :]
    ud[0] += np.random.randn() * NOISE_SPEED

    return xTrue, z, ud

 

일단 관측하기전에

 

실제 상태에 노이즈 없는 입력을 주어

실제 이동 위치를 구하고

 

실제 이동 위치와 RFID 사이의 거리를 계산하겠습니다.

여기서 구한 거리 d가 관측 가능 거리에 속한다면

관측 노이즈 dn과 가우시안 분포를 곱하여 노이즈를 반영한 실제 측정치가 됩니다.

 

이 실제 측정치들을 모든 RFID에 구하여 z에 vstack으로 쌓아주면

아마 zi가 거리, rfid x, rfid y이므로 (1, 3)

zi가 4개이니

z는 (4, 3)d의 데이터가 될겁니다.

 

이제 실제 이동후 위치와 노이즈가 반영된 관측치, 노이즈가 반영된 입력치를 필터에 사용할수있도록 반환해줍시다.

 

    while SIM_TIME >= time:
        time += DT
        print("Time:", time)

        u = calc_input()

        yaw = xTrue[2, 0]  # Orientation is known
        xTrue, z, ud = observation(xTrue, u, RF_ID)

        grid_map = histogram_filter_localization(grid_map, u, z, yaw)

        if show_animation:
            plt.cla()
            # for stopping simulation with the esc key.
            plt.gcf().canvas.mpl_connect(
                'key_release_event',
                lambda event: [exit(0) if event.key == 'escape' else None])
            draw_heat_map(grid_map.data, mx, my)
            plt.plot(xTrue[0, :], xTrue[1, :], "xr")
            plt.plot(RF_ID[:, 0], RF_ID[:, 1], ".k")
            for i in range(z.shape[0]):
                plt.plot([xTrue[0, :], z[i, 1]], [
                    xTrue[1, :], z[i, 2]], "-k")
            plt.title("Time[s]:" + str(time)[0: 4])
            plt.pause(0.1)

 

이제 이 시뮬레이션의 핵심인

 

히스토그램 위치 추정 파트를 살펴보겠습니다.

 

def histogram_filter_localization(grid_map, u, z, yaw):
    grid_map = motion_update(grid_map, u, yaw)

    grid_map = observation_update(grid_map, z, RANGE_STD)

    return grid_map

 

히스토그램 필터는 모든 격자에 대해

 

아래의 이산 베이즈 필터처럼

 

동작 갱신과 관측 갱신이 수행됩니다.

 

우선 동작 갱신부터 살펴보겠습니다.

 

 

def motion_update(grid_map, u, yaw):
    grid_map.dx += DT * math.cos(yaw) * u[0]
    grid_map.dy += DT * math.sin(yaw) * u[0]

    x_shift = grid_map.dx // grid_map.xy_resolution
    y_shift = grid_map.dy // grid_map.xy_resolution

    if abs(x_shift) >= 1.0 or abs(y_shift) >= 1.0:  # map should be shifted
        grid_map = map_shift(grid_map, int(x_shift), int(y_shift))
        grid_map.dx -= x_shift * grid_map.xy_resolution
        grid_map.dy -= y_shift * grid_map.xy_resolution

    grid_map.data = gaussian_filter(grid_map.data, sigma=MOTION_STD)

    return grid_map

 

 우선 dx, dy를 계산하는데 초기화 당시에는 값을 넣어주지도 않았고,

첫번째 루프이므로 처음에는 0이 있었을 겁니다.

 

 노이즈 입력치 u와 각 yaw, 시간 간격 dt에 따라 이동 거리가 dx, dy에 들어갈 것이고

일단 yaw는 이동 전에 구했으므로 0

u는 (1.0, 0.1)^T이고, 시간 간격은 0.1이므로

dx, dy는 아무튼 매우 작은 수일 겂니다.

 

 x_shift, y_shift는 이 이동 거리 // 그리드 해상도이므로 

이동거리가 어느정도 커지기 전까지는 시프트가 일어나지는 않을것 같습니다.

 

 어느 정도 루프를 돌다보면 이동 거리도 커지고 지도가 시프트 해야 될 때가 올것 같습니다.

이번에는 map_shift 함수를 잠깐 살펴보면

 

def map_shift(grid_map, x_shift, y_shift):
    tmp_grid_map = copy.deepcopy(grid_map.data)

    for ix in range(grid_map.x_w):
        for iy in range(grid_map.y_w):
            nix = ix + x_shift
            niy = iy + y_shift

            if 0 <= nix < grid_map.x_w and 0 <= niy < grid_map.y_w:
                grid_map.data[ix + x_shift][iy + y_shift] =\
                    tmp_grid_map[ix][iy]

    return grid_map

 우선 그리드 맵의 데이터를 깊은 복사를 수행하네요.

맨 처음에 그리드 맵 데이터들을 균일 분포로 했으므로 각 영역들은 1/3,600의 값을 가지고 있을 겁니다.

 

 그리드 맵 초기화 함수에서 x_w, y_w는 60이었으니

이제 각 그리드 영역에 접근해서 값을 수정하는 작업을 수행할것 같습니다.

 

 일단 x_shift 1, y_shift도 1인 경우를 가정해서 생각해보겠습니다.

루프 안에서 nix, niy 값들은 모두 ix, iy보다 1씩 큰 값을 가지게 될겁니다.

 

 그리고 이 nix와 niy가 각각 x, y 축 범위 안에 속한다면

이전 위치의 값 (0, 0)을 (1,1)을 옮기도록 수행하게 됩니다.

* 조건문 안에 " =\(역슬래시) " 부분을 보고 햇갈렸는데 우항 값이 다음줄로 넘어가기때문에 다음줄의 값을 좌항에다 넣기 위해 =\를 사용한듯합니다.

 

일단 map_shift 함수는 계산한 x,y_shift에 따라 기존의 맵에 존재하던 값들을 시프트 연산을 해주는게 되겠습니다.

 

다시 모션 업데이트를 돌아오면

 

def motion_update(grid_map, u, yaw):
    grid_map.dx += DT * math.cos(yaw) * u[0]
    grid_map.dy += DT * math.sin(yaw) * u[0]

    x_shift = grid_map.dx // grid_map.xy_resolution
    y_shift = grid_map.dy // grid_map.xy_resolution

    if abs(x_shift) >= 1.0 or abs(y_shift) >= 1.0:  # map should be shifted
        grid_map = map_shift(grid_map, int(x_shift), int(y_shift))
        grid_map.dx -= x_shift * grid_map.xy_resolution
        grid_map.dy -= y_shift * grid_map.xy_resolution

    grid_map.data = gaussian_filter(grid_map.data, sigma=MOTION_STD)

    return grid_map

 

 dx, dy가 무한히 커지는걸 막기위해 지도만 시프트한후

다시 시프트 한 크기만큼 이동 거리를 줄여줍니다.

 

 마지막으로 가우시안 필터를 사용하게 되는데,

이동한 지도에 동작 노이즈만큼 가우시안 노이즈만 추가해주게 됩니다.

 

 이제 이동한 지도 값도 구하고 동작 노이즈도 반영하였으니

관측 갱신 파트를 살펴보겠습니다.

 

def observation_update(grid_map, z, std):
    for iz in range(z.shape[0]):
        for ix in range(grid_map.x_w):
            for iy in range(grid_map.y_w):
                grid_map.data[ix][iy] *= calc_gaussian_observation_pdf(
                    grid_map, z, iz, ix, iy, std)

    grid_map = normalize_probability(grid_map)

    return grid_map

 관측 갱신에서는 기존의 지도, 관측 데이터,

관측 노이즈 표준편차를 사용하여 계산됩니다.

 

 우선 z에 대해서 도는것을 보니 각각의 관측치 데이터를 가지고

전체 격자들의 값들을 변경시키는것 같습니다.

 

 루프문 맨 안에 보면 가우시안 관측 밀도 함수 계산값을 구하여 각 격자 값을 갱신시키고 있습니다.

가우시안 관측 확률 밀도 계산 함수를 살펴보겠습니다.

 

def calc_gaussian_observation_pdf(grid_map, z, iz, ix, iy, std):
    # predicted range
    x = ix * grid_map.xy_resolution + grid_map.min_x
    y = iy * grid_map.xy_resolution + grid_map.min_y
    d = math.hypot(x - z[iz, 1], y - z[iz, 2])

    # likelihood
    pdf = (1.0 - norm.cdf(abs(d - z[iz, 0]), 0.0, std))

    return pdf

 일단 z는 첫번째 rfid (10, 0)로

여기서 z는 관측 모델에서 구한 (10, 10, 0)으로 가정하겠습니다. (거리, x, y)

ix, iy는 1로 가정하고 살펴봅시다.

 

 x = 1 * 0.5 + (-15) = -14.5일 것이고, 

격자 1,1에서 첫번째 랜드마크 사이 거리 d를 구할것이고(예측 거리)

 

 cdf는 누적 정규 분포로 아래의 표를 참고하면 좋겠습니다.

 

https://datascienceschool.net/view-notebook/e6c0d4ff9f4c403c8587c7d394bc930a/

 

def calc_gaussian_observation_pdf(grid_map, z, iz, ix, iy, std):
    # predicted range
    x = ix * grid_map.xy_resolution + grid_map.min_x
    y = iy * grid_map.xy_resolution + grid_map.min_y
    d = math.hypot(x - z[iz, 1], y - z[iz, 2])

    # likelihood
    pdf = (1.0 - norm.cdf(abs(d - z[iz, 0]), 0.0, std))

    return pdf

 

 격자 (1,1)에서 예측 d는 첫번재 랜드마크와 매우 멀으므로

abs(예측 거리 - 실제 거리)는 매우 큰 값이 될것이고,

 

 누적 정규 분포에 따라 1에 가까운 값이 됩니다.

하지만 우도를 계산하는만큼 1 - (1에 가까운 값)이라면 0에 가까운 확률 밀도가 반환되고

결국 (1,1) 격자는 가중치가 매우 작아질겁니다.

 

 하지만 실제 첫번째 랜드마크와 가까운 격자라면

예측 거리 - 실제 거리는 매우 작은 값이 될것이며

높은 pdf 값을 반환한다고 볼수 있겠습니다.

 

 

def observation_update(grid_map, z, std):
    for iz in range(z.shape[0]):
        for ix in range(grid_map.x_w):
            for iy in range(grid_map.y_w):
                grid_map.data[ix][iy] *= calc_gaussian_observation_pdf(
                    grid_map, z, iz, ix, iy, std)

    grid_map = normalize_probability(grid_map)

    return grid_map

 다시 관측 갱신으로 돌아와 

확률 정규화 과정을 시키면

첫번째 루프의 히스토그램 필터 수행이 완료됩니다.

 

 

 

# Parameters
SIM_TIME = 20.0  # simulation time [s]
DT = 0.5  # time tick [s]
MAX_RANGE = 10.0  # maximum observation range
MOTION_STD = 1.0  # standard deviation for motion gaussian distribution
RANGE_STD = 3.0  # standard deviation for observation gaussian distribution

 

 파라미터를 조금 수정해서

동작시켜보겠습니다.

 

 

 

전체 코드 histogram_filter.py

"""
Histogram Filter 2D localization example
In this simulation, x,y are unknown, yaw is known.
Initial position is not needed.
author: Atsushi Sakai (@Atsushi_twi)
"""

import copy
import math
import matplotlib.pyplot as plt
import numpy as np
from scipy.ndimage import gaussian_filter
from scipy.stats import norm

# Parameters
SIM_TIME = 20.0  # simulation time [s]
DT = 0.5  # time tick [s]
MAX_RANGE = 10.0  # maximum observation range
MOTION_STD = 1.0  # standard deviation for motion gaussian distribution
RANGE_STD = 3.0  # standard deviation for observation gaussian distribution

# grid map param
XY_RESOLUTION = 0.5  # xy grid resolution
MIN_X = -15.0
MIN_Y = -5.0
MAX_X = 15.0
MAX_Y = 25.0

# simulation parameters
NOISE_RANGE = 2.0  # [m] 1σ range noise parameter
NOISE_SPEED = 0.5  # [m/s] 1σ speed noise parameter

show_animation = True

class GridMap:

    def __init__(self):
        self.data = None
        self.xy_resolution = None
        self.min_x = None
        self.min_y = None
        self.max_x = None
        self.max_y = None
        self.x_w = None
        self.y_w = None
        self.dx = 0.0  # movement distance
        self.dy = 0.0  # movement distance


def histogram_filter_localization(grid_map, u, z, yaw):
    grid_map = motion_update(grid_map, u, yaw)

    grid_map = observation_update(grid_map, z, RANGE_STD)

    return grid_map


def calc_gaussian_observation_pdf(grid_map, z, iz, ix, iy, std):
    # predicted range
    x = ix * grid_map.xy_resolution + grid_map.min_x
    y = iy * grid_map.xy_resolution + grid_map.min_y
    d = math.hypot(x - z[iz, 1], y - z[iz, 2])

    # likelihood
    pdf = (1.0 - norm.cdf(abs(d - z[iz, 0]), 0.0, std))

    return pdf


def observation_update(grid_map, z, std):
    for iz in range(z.shape[0]):
        for ix in range(grid_map.x_w):
            for iy in range(grid_map.y_w):
                grid_map.data[ix][iy] *= calc_gaussian_observation_pdf(
                    grid_map, z, iz, ix, iy, std)

    grid_map = normalize_probability(grid_map)

    return grid_map


def calc_input():
    v = 1.0  # [m/s]
    yaw_rate = 0.1  # [rad/s]
    u = np.array([v, yaw_rate]).reshape(2, 1)
    return u


def motion_model(x, u):
    F = np.array([[1.0, 0, 0, 0],
                  [0, 1.0, 0, 0],
                  [0, 0, 1.0, 0],
                  [0, 0, 0, 0]])

    B = np.array([[DT * math.cos(x[2, 0]), 0],
                  [DT * math.sin(x[2, 0]), 0],
                  [0.0, DT],
                  [1.0, 0.0]])

    x = F @ x + B @ u

    return x


def draw_heat_map(data, mx, my):
    max_value = max([max(i_data) for i_data in data])
    plt.pcolor(mx, my, data, vmax=max_value, cmap=plt.cm.get_cmap("Blues"))
    plt.axis("equal")


def observation(xTrue, u, RFID):
    xTrue = motion_model(xTrue, u)

    z = np.zeros((0, 3))

    for i in range(len(RFID[:, 0])):

        dx = xTrue[0, 0] - RFID[i, 0]
        dy = xTrue[1, 0] - RFID[i, 1]
        d = math.hypot(dx, dy)
        if d <= MAX_RANGE:
            # add noise to range observation
            dn = d + np.random.randn() * NOISE_RANGE
            zi = np.array([dn, RFID[i, 0], RFID[i, 1]])
            z = np.vstack((z, zi))

    # add noise to speed
    ud = u[:, :]
    ud[0] += np.random.randn() * NOISE_SPEED

    return xTrue, z, ud


def normalize_probability(grid_map):
    sump = sum([sum(i_data) for i_data in grid_map.data])

    for ix in range(grid_map.x_w):
        for iy in range(grid_map.y_w):
            grid_map.data[ix][iy] /= sump

    return grid_map


def init_grid_map(xy_resolution, min_x, min_y, max_x, max_y):
    grid_map = GridMap()

    grid_map.xy_resolution = xy_resolution
    grid_map.min_x = min_x
    grid_map.min_y = min_y
    grid_map.max_x = max_x
    grid_map.max_y = max_y
    grid_map.x_w = int(round((grid_map.max_x - grid_map.min_x)
                             / grid_map.xy_resolution))
    grid_map.y_w = int(round((grid_map.max_y - grid_map.min_y)
                             / grid_map.xy_resolution))

    grid_map.data = [[1.0 for _ in range(grid_map.y_w)]
                     for _ in range(grid_map.x_w)]
    grid_map = normalize_probability(grid_map)

    return grid_map


def map_shift(grid_map, x_shift, y_shift):
    tmp_grid_map = copy.deepcopy(grid_map.data)

    for ix in range(grid_map.x_w):
        for iy in range(grid_map.y_w):
            nix = ix + x_shift
            niy = iy + y_shift

            if 0 <= nix < grid_map.x_w and 0 <= niy < grid_map.y_w:
                grid_map.data[ix + x_shift][iy + y_shift] =\
                    tmp_grid_map[ix][iy]

    return grid_map


def motion_update(grid_map, u, yaw):
    grid_map.dx += DT * math.cos(yaw) * u[0]
    grid_map.dy += DT * math.sin(yaw) * u[0]

    x_shift = grid_map.dx // grid_map.xy_resolution
    y_shift = grid_map.dy // grid_map.xy_resolution

    if abs(x_shift) >= 1.0 or abs(y_shift) >= 1.0:  # map should be shifted
        grid_map = map_shift(grid_map, int(x_shift), int(y_shift))
        grid_map.dx -= x_shift * grid_map.xy_resolution
        grid_map.dy -= y_shift * grid_map.xy_resolution

    grid_map.data = gaussian_filter(grid_map.data, sigma=MOTION_STD)

    return grid_map


def calc_grid_index(grid_map):
    mx, my = np.mgrid[slice(grid_map.min_x - grid_map.xy_resolution / 2.0,
                            grid_map.max_x + grid_map.xy_resolution / 2.0,
                            grid_map.xy_resolution),
                      slice(grid_map.min_y - grid_map.xy_resolution / 2.0,
                            grid_map.max_y + grid_map.xy_resolution / 2.0,
                            grid_map.xy_resolution)]
    return mx, my


def main():
    print(__file__ + " start!!")

    # RF_ID positions [x, y]
    RF_ID = np.array([[10.0, 0.0],
                      [10.0, 10.0],
                      [0.0, 15.0],
                      [-5.0, 20.0]])

    time = 0.0

    xTrue = np.zeros((4, 1))
    grid_map = init_grid_map(XY_RESOLUTION, MIN_X, MIN_Y, MAX_X, MAX_Y)
    mx, my = calc_grid_index(grid_map)  # for grid map visualization

    while SIM_TIME >= time:
        time += DT
        print("Time:", time)

        u = calc_input()

        yaw = xTrue[2, 0]  # Orientation is known
        xTrue, z, ud = observation(xTrue, u, RF_ID)

        grid_map = histogram_filter_localization(grid_map, u, z, yaw)

        if show_animation:
            plt.cla()
            # for stopping simulation with the esc key.
            plt.gcf().canvas.mpl_connect(
                'key_release_event',
                lambda event: [exit(0) if event.key == 'escape' else None])
            draw_heat_map(grid_map.data, mx, my)
            plt.plot(xTrue[0, :], xTrue[1, :], "xr")
            plt.plot(RF_ID[:, 0], RF_ID[:, 1], ".k")
            for i in range(z.shape[0]):
                plt.plot([xTrue[0, :], z[i, 1]], [
                    xTrue[1, :], z[i, 2]], "-k")
            plt.title("Time[s]:" + str(time)[0: 4])
            plt.pause(0.1)

    print("Done")


if __name__ == '__main__':
    main()

 

 

 

 

 

300x250
728x90

여기서는 파티클필터를 이용한 센서퓨전 위치 추정방법으로

 

파란 선은 실제 궤적

 

검은 선은 추측 항법으로 계산한 궤적

 

빨간선은 파티클 필터로 추정한 궤적입니다.

 

검은 점은 RFID 랜드마크로 로봇은 랜드마크까지의 거리를 측정하여 파티클 필터  위치추정에 사용합니다.

 

 

 

"""
Particle Filter localization sample
author: Atsushi Sakai (@Atsushi_twi)
"""

import math

import matplotlib.pyplot as plt
import numpy as np
from scipy.spatial.transform import Rotation as Rot

# Estimation parameter of PF
Q = np.diag([0.2]) ** 2  # range error
R = np.diag([2.0, np.deg2rad(40.0)]) ** 2  # input error

#  Simulation parameter
Q_sim = np.diag([0.2]) ** 2
R_sim = np.diag([1.0, np.deg2rad(30.0)]) ** 2

DT = 0.1  # time tick [s]
SIM_TIME = 20.0  # simulation time [s]
MAX_RANGE = 20.0  # maximum observation range

# Particle filter parameter
NP = 100  # Number of Particle
NTh = NP / 2.0  # Number of particle for re-sampling

show_animation = True


def calc_input():
    v = 1.0  # [m/s]
    yaw_rate = 0.1  # [rad/s]
    u = np.array([[v, yaw_rate]]).T
    return u


def observation(x_true, xd, u, rf_id):
    x_true = motion_model(x_true, u)

    # add noise to gps x-y
    z = np.zeros((0, 3))

    for i in range(len(rf_id[:, 0])):

        dx = x_true[0, 0] - rf_id[i, 0]
        dy = x_true[1, 0] - rf_id[i, 1]
        d = math.hypot(dx, dy)
        if d <= MAX_RANGE:
            dn = d + np.random.randn() * Q_sim[0, 0] ** 0.5  # add noise
            zi = np.array([[dn, rf_id[i, 0], rf_id[i, 1]]])
            z = np.vstack((z, zi))

    # add noise to input
    ud1 = u[0, 0] + np.random.randn() * R_sim[0, 0] ** 0.5
    ud2 = u[1, 0] + np.random.randn() * R_sim[1, 1] ** 0.5
    ud = np.array([[ud1, ud2]]).T

    xd = motion_model(xd, ud)

    return x_true, z, xd, ud


def motion_model(x, u):
    F = np.array([[1.0, 0, 0, 0],
                  [0, 1.0, 0, 0],
                  [0, 0, 1.0, 0],
                  [0, 0, 0, 0]])

    B = np.array([[DT * math.cos(x[2, 0]), 0],
                  [DT * math.sin(x[2, 0]), 0],
                  [0.0, DT],
                  [1.0, 0.0]])

    x = F.dot(x) + B.dot(u)

    return x


def gauss_likelihood(x, sigma):
    p = 1.0 / math.sqrt(2.0 * math.pi * sigma ** 2) * \
        math.exp(-x ** 2 / (2 * sigma ** 2))

    return p


def calc_covariance(x_est, px, pw):
    """
    calculate covariance matrix
    see ipynb doc
    """
    cov = np.zeros((3, 3))
    n_particle = px.shape[1]
    for i in range(n_particle):
        dx = (px[:, i:i + 1] - x_est)[0:3]
        cov += pw[0, i] * dx @ dx.T
    cov *= 1.0 / (1.0 - pw @ pw.T)

    return cov


def pf_localization(px, pw, z, u):
    """
    Localization with Particle filter
    """

    for ip in range(NP):
        x = np.array([px[:, ip]]).T
        w = pw[0, ip]

        #  Predict with random input sampling
        ud1 = u[0, 0] + np.random.randn() * R[0, 0] ** 0.5
        ud2 = u[1, 0] + np.random.randn() * R[1, 1] ** 0.5
        ud = np.array([[ud1, ud2]]).T
        x = motion_model(x, ud)

        #  Calc Importance Weight
        for i in range(len(z[:, 0])):
            dx = x[0, 0] - z[i, 1]
            dy = x[1, 0] - z[i, 2]
            pre_z = math.hypot(dx, dy)
            dz = pre_z - z[i, 0]
            w = w * gauss_likelihood(dz, math.sqrt(Q[0, 0]))

        px[:, ip] = x[:, 0]
        pw[0, ip] = w

    pw = pw / pw.sum()  # normalize

    x_est = px.dot(pw.T)
    p_est = calc_covariance(x_est, px, pw)

    N_eff = 1.0 / (pw.dot(pw.T))[0, 0]  # Effective particle number
    if N_eff < NTh:
        px, pw = re_sampling(px, pw)
    return x_est, p_est, px, pw


def re_sampling(px, pw):
    """
    low variance re-sampling
    """

    w_cum = np.cumsum(pw)
    base = np.arange(0.0, 1.0, 1 / NP)
    re_sample_id = base + np.random.uniform(0, 1 / NP)
    indexes = []
    ind = 0
    for ip in range(NP):
        while re_sample_id[ip] > w_cum[ind]:
            ind += 1
        indexes.append(ind)

    px = px[:, indexes]
    pw = np.zeros((1, NP)) + 1.0 / NP  # init weight

    return px, pw


def plot_covariance_ellipse(x_est, p_est):  # pragma: no cover
    p_xy = p_est[0:2, 0:2]
    eig_val, eig_vec = np.linalg.eig(p_xy)

    if eig_val[0] >= eig_val[1]:
        big_ind = 0
        small_ind = 1
    else:
        big_ind = 1
        small_ind = 0

    t = np.arange(0, 2 * math.pi + 0.1, 0.1)

    # eig_val[big_ind] or eiq_val[small_ind] were occasionally negative
    # numbers extremely close to 0 (~10^-20), catch these cases and set the
    # respective variable to 0
    try:
        a = math.sqrt(eig_val[big_ind])
    except ValueError:
        a = 0

    try:
        b = math.sqrt(eig_val[small_ind])
    except ValueError:
        b = 0

    x = [a * math.cos(it) for it in t]
    y = [b * math.sin(it) for it in t]
    angle = math.atan2(eig_vec[big_ind, 1], eig_vec[big_ind, 0])
    rot = Rot.from_euler('z', angle).as_matrix()[0:2, 0:2]
    fx = rot.dot(np.array([[x, y]]))
    px = np.array(fx[0, :] + x_est[0, 0]).flatten()
    py = np.array(fx[1, :] + x_est[1, 0]).flatten()
    plt.plot(px, py, "--r")


def main():
    time = 0.0

    # RF_ID positions [x, y]
    rf_id = np.array([[10.0, 0.0],
                      [10.0, 10.0],
                      [0.0, 15.0],
                      [-5.0, 20.0]])

    # State Vector [x y yaw v]'
    x_est = np.zeros((4, 1))
    x_true = np.zeros((4, 1))

    px = np.zeros((4, NP))  # Particle store
    pw = np.zeros((1, NP)) + 1.0 / NP  # Particle weight
    x_dr = np.zeros((4, 1))  # Dead reckoning

    # history
    h_x_est = x_est
    h_x_true = x_true
    h_x_dr = x_true

    while SIM_TIME >= time:
        time += DT
        u = calc_input()

        x_true, z, x_dr, ud = observation(x_true, x_dr, u, rf_id)

        x_est, PEst, px, pw = pf_localization(px, pw, z, ud)

        # store data history
        h_x_est = np.hstack((h_x_est, x_est))
        h_x_dr = np.hstack((h_x_dr, x_dr))
        h_x_true = np.hstack((h_x_true, x_true))

        if show_animation:
            plt.cla()
            # for stopping simulation with the esc key.
            plt.gcf().canvas.mpl_connect(
                'key_release_event',
                lambda event: [exit(0) if event.key == 'escape' else None])

            for i in range(len(z[:, 0])):
                plt.plot([x_true[0, 0], z[i, 1]], [x_true[1, 0], z[i, 2]], "-k")
            plt.plot(rf_id[:, 0], rf_id[:, 1], "*k")
            plt.plot(px[0, :], px[1, :], ".r")
            plt.plot(np.array(h_x_true[0, :]).flatten(),
                     np.array(h_x_true[1, :]).flatten(), "-b")
            plt.plot(np.array(h_x_dr[0, :]).flatten(),
                     np.array(h_x_dr[1, :]).flatten(), "-k")
            plt.plot(np.array(h_x_est[0, :]).flatten(),
                     np.array(h_x_est[1, :]).flatten(), "-r")
            plot_covariance_ellipse(x_est, PEst)
            plt.axis("equal")
            plt.grid(True)
            plt.pause(0.001)


if __name__ == '__main__':
    main()
300x250
728x90

 

확장 칼만 필터 위치추정 Extended Kalman Filter Localization

 

이번에는 확장 칼만필터를 이용한 센서 퓨전 위치 추정 방법에 대해 살펴보겠습니다.

 

다음 그림에서 파란 선은 실제 궤적이고, 검은 선은 추측 항법 dead reckoning으로 계산한 궤적입니다..

 

녹색 점은 GPS로 관측한 값이고 빨간 선은 EKF로 추종한 궤적이 됩니다.

 

빨간 타원은 EKF의 추정 공분산이 됩니다.

 

 

 

 

 

필터 설계

 

이 시뮬레이션에서는 로봇은 시간 t에 대개 4개의 상태 벡터를 가지고 있습니다.

 

x, y는 2차원 상 x-y 좌표이며,

 

$\phi$는 방위, v는 속도를 의미합니다.

 

 

이 코드 상에서

 

xEst는 상태 벡터를 의미하며

 

$P_t$는 이 상태의 공분산 행렬

 

Q는 동작 노이즈의 공분산 행렬

 

R은 시간 t에 대한 측정 노이즈의 공분산 행렬이 됩니다.

 

로봇은 속도와 자이로 센서를 가지고 있으며

 

입력 벡터는 각 시간에 대해 다음과 같습니다.

 

그리고 로봇은 GNSS 센서를 가지고 있어 매 시간마다 x-y상 위치를 알수 있습니다.

 

 

이러한 입력과 관측 벡터는 센서 노이즈를 가지고 있는데

 

코드 상에서 observation 함수는 입력과 관측 벡터에 대해 노이즈를 추가하는 역활을 합니다.

 

 

동작 모델 Motion model

 

다음은 로봇 모델로

이에 따라 동작 모델은 다음과 같습니다.

 

 

여기서 dt는 시간 주기를 의미합니다.

 

이에 대한 자코비안 행렬은 다음과 같습니다.

 

 

관측 모델 Observation Model

 

로봇은 GPS로부터 x-y 위치 정보를 받으므로

 

GPS 관측 모델은 다음과 같습니다.

 

 

이에 대한 자코비안 행렬은

 

 

확장 칼만 필터

확장 칼만 필터를 이용한 위치 추정 과정을 정리하면

-- 예측 과정 --

-- 갱신 과정 --

 

"""
Extended kalman filter (EKF) localization sample
author: Atsushi Sakai (@Atsushi_twi)
"""

import math

import matplotlib.pyplot as plt
import numpy as np

# Covariance for EKF simulation
Q = np.diag([
    0.1,  # variance of location on x-axis
    0.1,  # variance of location on y-axis
    np.deg2rad(1.0),  # variance of yaw angle
    1.0  # variance of velocity
]) ** 2  # predict state covariance
R = np.diag([1.0, 1.0]) ** 2  # Observation x,y position covariance

#  Simulation parameter
INPUT_NOISE = np.diag([1.0, np.deg2rad(30.0)]) ** 2
GPS_NOISE = np.diag([0.5, 0.5]) ** 2

DT = 0.1  # time tick [s]
SIM_TIME = 20.0  # simulation time [s]

show_animation = True


def calc_input():
    v = 1.0  # [m/s]
    yawrate = 0.1  # [rad/s]
    u = np.array([[v], [yawrate]])
    return u


def observation(xTrue, xd, u):
    xTrue = motion_model(xTrue, u)

    # add noise to gps x-y
    z = observation_model(xTrue) + GPS_NOISE @ np.random.randn(2, 1)

    # add noise to input
    ud = u + INPUT_NOISE @ np.random.randn(2, 1)

    xd = motion_model(xd, ud)

    return xTrue, z, xd, ud


def motion_model(x, u):
    F = np.array([[1.0, 0, 0, 0],
                  [0, 1.0, 0, 0],
                  [0, 0, 1.0, 0],
                  [0, 0, 0, 0]])

    B = np.array([[DT * math.cos(x[2, 0]), 0],
                  [DT * math.sin(x[2, 0]), 0],
                  [0.0, DT],
                  [1.0, 0.0]])

    x = F @ x + B @ u

    return x


def observation_model(x):
    H = np.array([
        [1, 0, 0, 0],
        [0, 1, 0, 0]
    ])

    z = H @ x

    return z


def jacob_f(x, u):
    """
    Jacobian of Motion Model
    motion model
    x_{t+1} = x_t+v*dt*cos(yaw)
    y_{t+1} = y_t+v*dt*sin(yaw)
    yaw_{t+1} = yaw_t+omega*dt
    v_{t+1} = v{t}
    so
    dx/dyaw = -v*dt*sin(yaw)
    dx/dv = dt*cos(yaw)
    dy/dyaw = v*dt*cos(yaw)
    dy/dv = dt*sin(yaw)
    """
    yaw = x[2, 0]
    v = u[0, 0]
    jF = np.array([
        [1.0, 0.0, -DT * v * math.sin(yaw), DT * math.cos(yaw)],
        [0.0, 1.0, DT * v * math.cos(yaw), DT * math.sin(yaw)],
        [0.0, 0.0, 1.0, 0.0],
        [0.0, 0.0, 0.0, 1.0]])

    return jF


def jacob_h():
    # Jacobian of Observation Model
    jH = np.array([
        [1, 0, 0, 0],
        [0, 1, 0, 0]
    ])

    return jH


def ekf_estimation(xEst, PEst, z, u):
    #  Predict
    xPred = motion_model(xEst, u)
    jF = jacob_f(xEst, u)
    PPred = jF @ PEst @ jF.T + Q

    #  Update
    jH = jacob_h()
    zPred = observation_model(xPred)
    y = z - zPred
    S = jH @ PPred @ jH.T + R
    K = PPred @ jH.T @ np.linalg.inv(S)
    xEst = xPred + K @ y
    PEst = (np.eye(len(xEst)) - K @ jH) @ PPred
    return xEst, PEst


def plot_covariance_ellipse(xEst, PEst):  # pragma: no cover
    Pxy = PEst[0:2, 0:2]
    eigval, eigvec = np.linalg.eig(Pxy)

    if eigval[0] >= eigval[1]:
        bigind = 0
        smallind = 1
    else:
        bigind = 1
        smallind = 0

    t = np.arange(0, 2 * math.pi + 0.1, 0.1)
    a = math.sqrt(eigval[bigind])
    b = math.sqrt(eigval[smallind])
    x = [a * math.cos(it) for it in t]
    y = [b * math.sin(it) for it in t]
    angle = math.atan2(eigvec[bigind, 1], eigvec[bigind, 0])
    rot = np.array([[math.cos(angle), math.sin(angle)],
                    [-math.sin(angle), math.cos(angle)]])
    fx = rot @ (np.array([x, y]))
    px = np.array(fx[0, :] + xEst[0, 0]).flatten()
    py = np.array(fx[1, :] + xEst[1, 0]).flatten()
    plt.plot(px, py, "--r")


def main():
    time = 0.0

    # State Vector [x y yaw v]'
    xEst = np.zeros((4, 1))
    xTrue = np.zeros((4, 1))
    PEst = np.eye(4)

    xDR = np.zeros((4, 1))  # Dead reckoning

    # history
    hxEst = xEst
    hxTrue = xTrue
    hxDR = xTrue
    hz = np.zeros((2, 1))

    while SIM_TIME >= time:
        time += DT
        u = calc_input()

        xTrue, z, xDR, ud = observation(xTrue, xDR, u)

        xEst, PEst = ekf_estimation(xEst, PEst, z, ud)

        # store data history
        hxEst = np.hstack((hxEst, xEst))
        hxDR = np.hstack((hxDR, xDR))
        hxTrue = np.hstack((hxTrue, xTrue))
        hz = np.hstack((hz, z))

        if show_animation:
            plt.cla()
            # for stopping simulation with the esc key.
            plt.gcf().canvas.mpl_connect('key_release_event',
                    lambda event: [exit(0) if event.key == 'escape' else None])
            plt.plot(hz[0, :], hz[1, :], ".g")
            plt.plot(hxTrue[0, :].flatten(),
                     hxTrue[1, :].flatten(), "-b")
            plt.plot(hxDR[0, :].flatten(),
                     hxDR[1, :].flatten(), "-k")
            plt.plot(hxEst[0, :].flatten(),
                     hxEst[1, :].flatten(), "-r")
            plot_covariance_ellipse(xEst, PEst)
            plt.axis("equal")
            plt.grid(True)
            plt.pause(0.001)


if __name__ == '__main__':
    main()

 

 

 

 

300x250
728x90

확률 생성 법칙 probabilistic generative laws

제 1법칙

 신뢰도는 상태 $x_t$로 나타내며, 이 값은 모든 이전 상태와 측정, 그리고 제어 입력이 주어질 때 구할수 있는데, 이 조건부 확률은 수학적으로 정리하면 다음과 같습니다.

 

1. $z_t$는 측정

2. $u_t$ 동작 명령

3. $x_t$는 시간 t에 대한 로봇이나 주위 환경의 상태(위치, 속도 등)

 

 만약 우리가 상태 $x_{t-1}$과 $u_t$를 알고 있다면, 상태 $x_{0:t-2}$와 $z_{1:t-1}$는 조건부 독립 성질에 따라 아는게 크게 중요하지는 않습니다. 상태 $x_{t-1}$는 $x_t$와 $z_{1:t-1}$, $u_{1:t1}$ 사이 조건부 독립을 나타내는데 사용됩니다. 이를 정리하면 다음과 같습니다.

 

2법칙

 $x_t$가 완전하다면

 

 여기서 $x_t$가 완전하다는것의 의미는 과거의 상태, 측정, 동작 명령이 미래를 예측하는데 필요한 정보를 주지않는 경우를 말합니다.

 $x_{0:t-1}$, $z_{1:t-1}$, $u_{1:t}$는 조건부 독립인 $x_t$가 주어질때 $z_t$와 같습니다.

 

이 필터는 아래의 두 가지의 파트로 이루어 집니다.

 

p($x_t$ | $x_{t-1}$, $u_t$) -> 상태 전이 확률 state trasition probability

p($z_t$ | $x_t$) -> 측정 확률

 

 

 조건부 의존과 조건부 독립의 예제

 

- 독립인 경우와 조건부 의존하는 경우

2개의 양면 동전이 있다고 합시다.

A - 첫번째 동전이 앞면을

B - 두 번째 동전이 앞면을

C - 두 동전이 같은 경우

 

 A와 B만 보면 독립이지만, C가 주어진 경우라면 A와 B는 조건부 독립이 됩니다. C라는 사실을 알고 있다면 첫번째 동전의 결과가 다른 것의 결과도 알려주기 때문입니다.

 

- 의존하는 경우와 조건부 독립인 경우

 어느 상자에 2개의 동전이 있는데 1개는 일반적인 동전이고 하나는 둘다 동일한 가짜 동전이라고 합시다(P(H) = 1). 동전을 하나 고르고 던지는 것을 2번 해보면 이 사건들을 다음과 같이 정의할 수 있습니다.

 

A = 첫 코인이 앞면이 나온 경우

B = 두 번째 동전이 앞면이 나오는 경우

C = 일반 동전을 골른 경우

 

 A가 발생한 경우, 우리가 일반 코인 보다는 가짜 코인을 고른 가능성이 더 크다고 볼 수 있습니다. 이는 B가 발생할 조건부 확률을 증가시키는데, A와 B는 의존 관계임을 의미합니다. 하지만 C라는 사실이 주어질 때는 A와 B는 서로 독립이 됩니다.

 

베이즈 법칙 bayes rule

사후 확률 posterior P(B | A) = $\frac{우도 Likelihood P(A|B) * 사전 확률 prior P(B)} {주변 확률 분포 Marginal P(A)}$

여기서

- 사후 확률 : A라는 사건 일어났을때, 사건 B가 일어날 확률

- 우도 : 사건 B가 일어났을때, A인 확률

- 사전 확률 : 사건 B가 일어날 확률

- 주변 확률 : 사건 A가 일어날 확률

 

예제 : 

여성 중 1%는 유방암을 가지고 99%는 그렇지 않습니다. 유방함 검사로 80%의 경우 유방암을 찾아내지만 20%는 놓치게 됩니다. 유방암 검사에서 9.6%는 유방암이 존재하지 않지만 유방암이 있다고 잘못 찾아냅니다.

 

베이즈 정리로 이를 식으로 정리하면 다음과 같이 정리할 수 있습니다.

- Pr(A|X) - 양성 (X)인데, 실제로 암을 가진 (A) 확률을 위 식을 이용해서 구하면 7.8%가 됩니다.

- Pr(X|A) - 암을 가진 경우 (A), 양성인 (X) 경우 이는 참 긍정으로 80%가 됩니다.

- Pr(A) - 암이 있는 경우 1%

- Pr(not A) - 암이 없는 경우 99%

- Pr(X | not A) - 암이 없는데 (~A) 양성 (X)이 된 경우, 이는 거짓 긍정 false positive으로 9.6%가 됩니다.

 

 

베이즈 필터 알고리즘

기본 필터 알고리즘은 $x_t$에 대해 다음과 같이 정리 할 수 있습니다.

 

예측 단계

 첫 번째 단계는 베이즈 정리 bayes theorem을 이용하여 사전확률을 구하는데 이를 예측 과정 prediction step이라 합니다. 신뢰도 $\bar{bel}(x_t)$는 시간 t일때 측정 $z_t$를 반영하기 이전으로 이 단계에서는 동작이 발생함에 따라 기존의 가우시안과 공분산이 더해져 기존 정보를 잃어버리게 됩니다. 이 방정식의 RHS(right hand side) 우항은 사전 확률 계산을 위해서 전체 확률 법칙 law of total probabily를 사용합니다.

 

갱신 단계

 다음은 갱신 단계로 correction or update step 시간 t에서의 측정 $z_t$를 통합하여 로봇의 신뢰도를 계산하는데, 로봇이 어디에 있는지 센서 정보로 반영하며 구하며 여기서 가우시안 분포들을 곱하게 됩니다. 가우시안의 곱 연산 결과로 평균은 사이에 위치하고, 공분산은 작아지게 됩니다.

 

 

 

복도를 따라 이동하며 문을 감지하는 센서를 가진 로봇.

 위 그림은 베이즈 필터로 로봇의 위치를 추정하는 예시로서 로봇은 복도가 어떻게 생겼는지는(지도) 알지만 자신의 위치를 모르는 상태 입니다.

 

1. 맨 처음, 로봇은 지도상에서 어디에 위치하는지 모르므로 지도 전체에 동일한 확률들을 배정합니다.

2. 첫 센서 결과로 문이 존재하는것을 알았습니다. 로봇은 지도가 어떻게 되어있는지 알고는 있으나 문이 3개가 있으므로 어디에 있는지는 아직 알 수 없습니다. 그래서 각각의 문 위치에다가 같은 확률들이 배정됩니다.

3.  로봇이 앞으로 전진하면서 예측과정이 발생하는데 일부 정보를 잃어버리므로 4번째 다이어그램 처럼 가우시안의 분산이 커지게 됩니다. 마지막으로 신뢰도 분포는 이전 단계서의 사후확률과 동작후 상태를 컨볼루션한 결과로 평균이 우측으로 이동하게 됩니다.

4. 다시 측정 결과를 반영을 하는데 문이 존재한다면, 세 문들 앞에서 모두 문이 존재할 확률들은 같으므 5번째 다이어그램에서 계산한 사후확률을 구할 수 있습니다. 이 사후확률은 이전 사후확률과 측정의 컨벌루션 결과이며 로봇이 2번째 문 근처에 있음을 알 수 있습니다. 이때 분산은 작아지며 확률은 커지게 됩니다.

5. 이러한 동작과 갱신 과정이 반복되면 로봇은 6번째 다이어그램처럼 주위 환경에 대해 자신의 위치를 추정할 수 있게 됩니다.

 

 

베이즈 필터와 칼만 필터 구조

 칼만 필터의 기본 구조와 컨셉은 베이즈 필터와 동일합니다. 다만 차이점은 칼만 필터의 수학적인 표현법인데, 칼만 필터는 가우시안을 이용한 베이지안 필터가 아닙니다. 베이즈 필터를 칼만 필터에 적용하기 위해서 가우시안을 히스토그램이 아닌 방법으로 나타낼건데, 베이즈 필터의 기본 공식은 다음과 같습니다.

 

$\bar{x}$는 사전 확률

$\iota$는 사전 확률 $\bar{x}$가 주어질때 측정에 대한 우도

$f_x$($\cdot$)은 속도를 이용해서 다음 위치를 예측할때 사용하는 모델이 됩니다.

*는 컨볼루션을 의미합니다.

 

 

칼만 게인

위에서 x는 사후확률이며 $\iota$와 $\bar{x}$는 가우시안이 됩니다.

 

그러므로 이 사후확률의 평균은 다음과 같이 정리할 수 있습니다.

 다음과 같은 형태로 사전확률과 측정치를 스캐일링을 할수 있는데

 

 여기서 분모가 정규화 시키므로 가중치를 하나에 합치며 K = $W_1$로 정리하면 다음과 같습니다.

 

칼만 게인에서의 분산은 다음과 같습니다.

 

K는 칼만게인이라 하며 측정치 $\mu_z$와 예측 평균 $\bar{\mu}$ 사이 값을 얼마나 반영할지 스케일링하는데 사용됩니다.

 

 

 

칼만 필터 - 단변수와 다변수

예측 단계

 

갱신 단계

 

 

레퍼런스

1. 로저 라베의 칼만 필터 관련 저장소

github.com/rlabbe/Kalman-and-Bayesian-Filters-in-Python

2. 세바스찬 스런의 probabilistic robotics

 

 

300x250
728x90

 

왜 확률 밀도 함수 PDF로 신뢰도 belief를 나타내는데 사용하는가?

 로봇이 동작하는 환경은 확률론적이기 때문입니다. 로봇과 주위 환경은 (시간에 대한 함수인)결정론적인 모델이 될수 없는데, 현실 세계의 센서들은 에러를 포함하고 있어 그 센서에 대한 평균과 분산으로 나타냅니다. 그래서 앞으로 평균과 분산을 이용한 모델을 다루게 될 것 입니다.

 

확률 변수의 기대값이란 무엇인가? expectation of a random variable

 기대값은 확률의 평균 뿐만이 아니라

 연속 영역에서의 형태로도 나타낼 수 있습니다.

 

 

import numpy as np
import random
x=[3,1,2]
p=[0.1,0.3,0.4]
E_x=np.sum(np.multiply(x,p))
print(E_x)

 

 

멀티모달보다 유니모달로 신뢰도를 나타내는 경우 이점은 무엇인가?

What is the advantage of representing the belief as a unimodal as opposed to multimodal?

 움직이는 자동차의 위치를 두 가지 확률로 나타내는건 햇갈리므로 효율적이지는 않습니다.

 

분산, 공분산, 상관계수

분산

 분산은 데이터가 퍼진 정도를 의미하며, 평균은 데이터가 얼마나 많은지를 나타내지는 않습니다. 

 

x=np.random.randn(10)
np.var(x)

 

공분산

 다변수 분포 multivariate distribution인 경우에 사용되는데, 예를 들면 2차원 공간에서의 로봇은 위치를 나타내는데 x, y 값을 사용할겁니다. 이를 나타내기 위해서 평균이 x, y인 정규 분포가 사용됩니다.

 

다변수 분포에서 평균 $\mu$는 아래의 행렬로 나타낼수 있는데

 

이처럼 분산도 표현할 수 있습니다.

 

 하지만 중요한 점은 모든 변수들은 그 값에 대해 분산을 가지고 있는 점인데, 각각의 확률 변수들이 어떻게 서로 다른 형태 존재하도록 가능해 집니다. 역시 이 두 데이터셋에는 그들이 얼마나 관련되어있는지를 의미하는 상관 계수와도 관련 있습니다.

 

 예를 들자면 보통 키가 증가하면 몸무게도 증가하는데, 이러한 변수들은 서로 상관관계를 가진다고 할 수 있습니다. 이 값들은 한 변수가 커지면 다른것도 커지므로 양의 상관 관계가 가진것이 됩니다.

 

 이제 다변수 정규분포의 공분산을 공분산 행렬 covariance matrix를 사용해서 다음과 같이 나타내겠습니다.

 

 

대각 성분 diagonal : 각 변수들의 분산

비대각 성분 off-diagonal : $i_{th}$ 변수와 $j_{th}$ 변수의 공분산

 

 

x=np.random.random((3,3))
np.cov(x)

 

 

 

 

가우시안 Gaussian

중심 극한 이론 Central Limit Thorem

 이 이론에 따르면, 독립 확률 변수 n개 샘플의 평균은 샘플 사이즈를 늘릴수록 정규 분포의 형태가 되는 경향이 있습니다.(일반적으로 n >= 30) 

import matplotlib.pyplot as plt
import random
a=np.zeros((100,))
for i in range(100):
    x=[random.uniform(1,10) for _ in range(1000)]
    a[i]=np.sum(x,axis=0)/1000
plt.hist(a)

 

가우시안 분포 gaussian distribution

 가우시안은 연속 확률 분포 중 하나로 평균 $\mu$와 분산 $\sigma^2$ 두개의 파라미터로 나타내며 정의는 아래와 같습니다.

 이는 평균 $\mu$과 표준편차 $\sigma$의 함수로서 종 모양이 되는 정규 분포의 특성을 나타냅니다.

import matplotlib.mlab as mlab
import math
import scipy.stats

mu = 0
variance = 5
sigma = math.sqrt(variance)
x = np.linspace(mu - 5*sigma, mu + 5*sigma, 100)
plt.plot(x,scipy.stats.norm.pdf(x, mu, sigma))
plt.show()

 

 

가우시안의 성질 Gaussian Properties

곱 multiplication

 베이즈 필터 bayes filter의 측정 갱신 measurement update에서 그 알고리즘은 사전 확률 P($X_t$)와 측정 확률 P($Z_t$ | $X_t$)를 다음의 사후 확률을 구하기 위해 곱하여야 합니다.

 

 여기서 분자 P(Z | X), P(X)는 N($\bar{\mu}$, $\bar{\sigma}^{1}$), N($\bar{\mu}$, $\bar{\sigma}^{2}$)을 따르는 가우시안이 됩니다.

곱 연산으로 구한 새로운 평균은

새로운 분산은

import matplotlib.mlab as mlab
import math
mu1 = 0
variance1 = 2
sigma = math.sqrt(variance1)
x1 = np.linspace(mu1 - 3*sigma, mu1 + 3*sigma, 100)
plt.plot(x1,scipy.stats.norm.pdf(x1, mu1, sigma),label='prior')

mu2 = 10
variance2 = 2
sigma = math.sqrt(variance2)
x2 = np.linspace(mu2 - 3*sigma, mu2 + 3*sigma, 100)
plt.plot(x2,scipy.stats.norm.pdf(x2, mu2, sigma),"g-",label='measurement')


mu_new=(mu1*variance2+mu2*variance1)/(variance1+variance2)
print("New mean is at: ",mu_new)
var_new=(variance1*variance2)/(variance1+variance2)
print("New variance is: ",var_new)
sigma = math.sqrt(var_new)
x3 = np.linspace(mu_new - 3*sigma, mu_new + 3*sigma, 100)
plt.plot(x3,scipy.stats.norm.pdf(x3, mu_new, var_new),label="posterior")
plt.legend(loc='upper left')
plt.xlim(-10,20)
plt.show()

 

덧셈 Addition

 동작 단계 motion step에서는 확률을 더하게 되는데, 여기서 사용되는 두 가우시안인 신뢰도들이 합하여야 합니다. 아래는 두 확률의 덧셈 예시가 됩니다.

import matplotlib.mlab as mlab
import math
mu1 = 5
variance1 = 1
sigma = math.sqrt(variance1)
x1 = np.linspace(mu1 - 3*sigma, mu1 + 3*sigma, 100)
plt.plot(x1,scipy.stats.norm.pdf(x1, mu1, sigma),label='prior')

mu2 = 10
variance2 = 1
sigma = math.sqrt(variance2)
x2 = np.linspace(mu2 - 3*sigma, mu2 + 3*sigma, 100)
plt.plot(x2,scipy.stats.norm.pdf(x2, mu2, sigma),"g-",label='measurement')


mu_new=mu1+mu2
print("New mean is at: ",mu_new)
var_new=(variance1+variance2)
print("New variance is: ",var_new)
sigma = math.sqrt(var_new)
x3 = np.linspace(mu_new - 3*sigma, mu_new + 3*sigma, 100)
plt.plot(x3,scipy.stats.norm.pdf(x3, mu_new, var_new),label="posterior")
plt.legend(loc='upper left')
plt.xlim(-10,20)
plt.show()

 

이번 예시는 이변수 가우시안 분포 bivariate gaussian distribution를 시각화하는 예제로 2차원 표면에서 나오는 결과를 3차원으로 사영한 결과를 보여줍니다. 타원에서 가장 깊은 곳은 가장 높은 부분으로 주어진 (x,y)의 최대 확률이 됩니다.

#Example from:
#https://scipython.com/blog/visualizing-the-bivariate-gaussian-distribution/
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

# Our 2-dimensional distribution will be over variables X and Y
N = 60
X = np.linspace(-3, 3, N)
Y = np.linspace(-3, 4, N)
X, Y = np.meshgrid(X, Y)

# Mean vector and covariance matrix
mu = np.array([0., 1.])
Sigma = np.array([[ 1. , -0.5], [-0.5,  1.5]])

# Pack X and Y into a single 3-dimensional array
pos = np.empty(X.shape + (2,))
pos[:, :, 0] = X
pos[:, :, 1] = Y

def multivariate_gaussian(pos, mu, Sigma):
    """Return the multivariate Gaussian distribution on array pos.

    pos is an array constructed by packing the meshed arrays of variables
    x_1, x_2, x_3, ..., x_k into its _last_ dimension.

    """

    n = mu.shape[0]
    Sigma_det = np.linalg.det(Sigma)
    Sigma_inv = np.linalg.inv(Sigma)
    N = np.sqrt((2*np.pi)**n * Sigma_det)
    # This einsum call calculates (x-mu)T.Sigma-1.(x-mu) in a vectorized
    # way across all the input variables.
    fac = np.einsum('...k,kl,...l->...', pos-mu, Sigma_inv, pos-mu)

    return np.exp(-fac / 2) / N

# The distribution on the variables X, Y packed into pos.
Z = multivariate_gaussian(pos, mu, Sigma)

# Create a surface plot and projected filled contour plot under it.
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_surface(X, Y, Z, rstride=3, cstride=3, linewidth=1, antialiased=True,
                cmap=cm.viridis)

cset = ax.contourf(X, Y, Z, zdir='z', offset=-0.15, cmap=cm.viridis)

# Adjust the limits, ticks and view angle
ax.set_zlim(-0.15,0.2)
ax.set_zticks(np.linspace(0,0.2,5))
ax.view_init(27, -21)

plt.show()

 

 

 

300x250
728x90

오랜만에 다시 로봇 공학을 공부하려고 한다.

 

이번에 보는건

 

로봇 공학 알고리즘을 파이썬으로 구현한 오픈소스로

 

https://atsushisakai.github.io/PythonRobotics/

 

위 링크에서 다운받아 사용하면 되고

 

https://pythonrobotics.readthedocs.io/en/latest/modules/appendix.html

 

 

이 다큐먼트를 참고하면서 내용을 정리해나가려고한다.

 

 

지난번에살펴본 소형 무인 비행체는 비행체을 수학적으로 모델링하고 매트랩, 시뮬링크로 제어하는 과정이었다면

 

 

이번에는 확률론, 필터, 선형대수 등을 이용해서

 

 

실제 로봇에 사용되는 알고리즘들을 학습해 나가는 과정이 된다.

 

 

그동안 로봇 공학을 공부할때 쓰런의 probabilisitc robotics 위주로 학습하였지만

 

 

의사 코드와 설명을 봐도 이해하기 힘든 점이 많았고,

 

 

매트랩으로 구현 한 코드들이 사용하기 되게 불편했었던것 같았다.

 

 

 

마지막으로 봤을때 겨우 fast slam까지는 어떻게든 이해하고 보긴했지만 그리드나 레이케스팅, 그리고 스램 이후 파트도

 

 

진도 나가긴 해야하는데, 아무리 slam kr이나 다른 분들의 자료를 정리해나가면서 봤지만 잘 이해하기가 힘들더라

 

 

이번에는 쓰런의 책이 아닌 파이선 로보틱스로 시작하려고 한다.

 

 

 

 

이 오픈소스는

 

자율주행에 주로 사용되는 로봇 공학 알고리즘들의 모음으로

 

특징

 

1. 각 알고리즘들의 기본 원리를 이해하기 쉽게 작성하였고

 

2. 자주 사용하는 알고리즘들로 이루어져있으며

 

3. 의존관계를 최소화 시켰다.

 

자세한 내용들은 다음의 논문에 서술했다고 한다.

 

[1808.10703] PythonRobotics: a Python code collection of robotics algorithms

 

 

요구사항

- python 3.8.x

- numpy

- scipy

- matplotlib

- pandas

- cvxpy

 

 

여기서 사용하는 알고리즘들은 크게 다음과 같이 정리할수 있겠다.

 

1. localization 위치 추정

2. mapping 지도 작성

3. slam 동시적 위치 추정 및 지도 작성

4. path planing 경로 계획

5. path trackking 경로 추종

6. arm navigation 매니퓰레이터 제어

7. aerial navigation 항공 비행

 

 

우선 다큐먼트 사이트에서 칼만 필터 기초 부분부터 정리해보려고 하는데

 

베이즈 필터를 공부할때마다 식겁한 적이 많았다.

 

https://pythonrobotics.readthedocs.io/en/latest/modules/appendix.html

 

 

 

쓰런 책에서 가장 먼저 나오는 위치 추정의 원리를 나타내는 그림인데

 

여전히 belief distribution 신뢰도 분포 개념이 이해될것 같으면서도 아닌 오락가락한게 지긋지긋하게 나온다.

300x250

+ Recent posts