본문 바로가기

공부

SLAM 공부했다.

오우~ 대망의 SLAM이다. 그리고 이 강좌 (cs373)의 마지막 챕터이기도 하다.


SLAM(Simultaneous Localization And Mapping)은 지금까지 배웠던 모든 개념들이 한 번에 들어가 있는 것이라 해도 무관하다. 로봇을 어떤 장소에 던져 놓았다고 할 때, 이 로봇은 자기가 지금 어디 있는지 확인할 수 있는 방법이 없다.(GPS가 있다면 모르지만 이조차도 오차가 있기에 정확하게 알기는 힘들다.)


 계속 움직이며 기준이 되는 어떤 장소의 관찰을 한다면, 자신이 어디에 있는지 대충 알 수 있을 것이다. 이 과정이 Filter에 해당한다. 그럼, 로봇을 어디에 던져주면서, 어떤 임무를 수행하게 하기 위해서는 경로를 지정해 주어야 할 것이고 각 어떠한 움직임을 취하라는 명령도 해 주어야 한다. 이것들이 Search와 Control이었다. 


 그런데 사실, 그동안 우리가 보았던 것들은 모두 지도가 주어져 있고, 절대적인 좌표을 알 수 있다는 상황을 가정했었다. 그렇다면 만약 지도가 없을 때에는 어떻게 알 수 있을까? 이에 대한 문제를 해결하는 것이 SLAM의 과제이다. 


 본론으로 들어가기에 앞서서, 우리의 자동차 모델을 보다 유연하게 만들어 보자. 


 

 


 기존에 사용하던 모델은 noise의 영향으로 인해서 오른쪽과 같이 코너링을 할 때, 충돌이 발생할 우려가 있다. 우선적으로 이 문제를 고치고 시작하려 한다. 


 

 



 문제는 이거다. 왼쪽 그림과 같이 뚝뚝 끊긴 경로를 설정했는데, 실제로 로봇이 이렇게 갈 수 있을리 만무하다. 그래서 어느 지점부터는 다음 부분을 따르도록 switching을 시켜햐 하는데, 이 과정을 해결해야 함과 동시에, 직선 경로라서 다행이긴 하지만, Error를 계산하는 방법을 달리 해야 할 것이다. 경로와의 y거리가 아닌, 경로와의 최단 거리를 최소화하는쪽으로 Controll을 해야 한다. 


 직선과 점 사이의 거리 공식을 이용할 것인데, 간단히 말해서 벡터의 내적(inner product)가 사용되었다고 할 수 있다 그래서 식의 모양이 오른쪽 그림과 같은 것이다. CTE=~~~ 라고 쓰여 있는 부분이 실제 Error로 쓰일 부분이다. 여기서는 자세한 수학적 설명은 배제하고 싶어 일단은 상황에 따라 CTE를 잘 설계해야 함을 강조하고만 넘어가겠다. 


 아 , 그리고 문제 하단에는 이렇게 쓰여 있다.


Note: the formula for CTE should actually take the square root of the denominator, i.e. (Ry dx - Rx dy) / sqrt(dx^2 + dy^2), but if you implement it as shown in lecture it will work well enough to pass the grader.


 분모에 나누어주는 항에 루트를 씌워야 하는 것이 원칙이긴 하나, 연산의 최소화를 위해서 하지 않은 듯 하다. 연습 문제의 답이 포함된 코드를 남겨 놓겠다. 500줄 정도 되다 보니 그냥 답까지 한 번에 올린다. 




        estimate = filter.get_position()
 
        ### ENTER CODE HERE
        delta_x = spath[index + 1][0- spath[index][0]
        delta_y = spath[index + 1][1- spath[index][1]
        R_x = estimate[0- spath[index][0]
        R_y = estimate[1- spath[index][1]
        u = (R_x * delta_x + R_y * delta_y) / (delta_x * delta_x + delta_y * delta_y)
        cte = (R_y * delta_x - R_x * delta_y) / (delta_x * delta_x + delta_y * delta_y)
        if u > 1.0:
            index += 1


 핵심은 위 부분이다. 현재 위치로 추정되는 좌표를 받고, Error를 새로 계산한 다음, 이를 이용했다. 다음으로는 gain값들을 조절하면서, 최적의 경우를 찾아 보시라고 한다. 583-586의 값들을 조정하면서 step을 최소로 하되, collision이 생기지는 않도록 해 보시라.






 만약, 로봇이 가진 지도가 없어서 당장 자신이 어디 있는지도 모르다고 할 때, 알 수 있는 정보는 자신이 얼마나 움직였는지에 대한 벡터와, 지금 감지할 수 있는 물체들의 정보 뿐이다. 그런데 이들만 가지고로, 로봇은 충분히 지도를 만들어 낼 수 있다!! 다시 말하지만, 이것이 SLAM이고 그 중에서 가장 이해하기 쉬운 Graph SLAM에 대해서 배워 보려고 한다.


 


왼쪽 그림에서 로봇이 10만큼 움직였다고 했을 때, 실제로 정확히 가로로만 10만큼 움직여 주었다면 너무나 고맙겠지만, 고질적인 불확실성 때문에 이는 거의 불가하다. 그래서 우리는 지금까진 불확실성을 다루기 위해서 로봇의 위치를 가우시안 확률로 나타내었다. x-y좌표에 있으므로 각각 좌표에 하나씩 확률식을 세우고, 이들을 곱한다.


 우리가 하고 싶은 것은 주어진 x0 y0 값들과 움직인 양을 가지고 지금 로봇이 어디 있는지 알고 싶은 것이다. 계속 움직일 때 마다 가우시안들이 곱해질 것이고 그 식에서 최대 값을 가지는 곳을 찾는 것과 동일하다고 할 수 있다. Graph SLAM이 하는 일은 이 확률을 계속해서 새로고침하는 것이라 할 수 있다. 


 Graph SLAM을 짜면서 우리가 알고 있는 내용은 

1. 초기 위치

2. 직전 위치에서 얼마나 움직였는지(정확하진 않음)

3. Landmark들을 관찰함으로 인해서 알 수 있는 measurement 값들 


이것들을 이용해서 지금 자신이 어디에 있는지, 그리고 Landmark들이 어떤 위치에 있는지를 알아내는 것이 목표이다. 

다만, 지금의 예제에서는 Landmark 개수를 한정지어서 완벽한 Mapping을 한다고는 할 수 없겠다. 보통 visual SLAM을 하게 된다면, edge같은 feature들을 이용해서 보다 사실적인 지도를 만들고 이것이 일반적으로 SLAM이라고 했을 때 생각하는 이미지인데, 그 기초 과정이라고 할 수 있다.


 그럼 이 Constraint들을 실직적으로 어떻게 써먹는지 알아보자. 본질적으로 이들은 모두 '벡터'이다. 수없이 많은 벡터들이 계속해서 주어질 것이고, 추가가 될 때마다 이들을 일일이 계산하는 것도 성가실 테므로, 우리는 행렬을 사용하자!!


 위와 같이 모든 position과 landmark들을 추가한 거대한 행렬을 만들었다. 그리고 이제 조건들을 추가해줄 것인데, motion를 고려하는 것에 대해서 먼저 알아보자. x1 = x0 + 5라는 조건이 주어졌을 때, 이를 두 가지로 변형할 수 있다.


x1 - x0 = 5

x0 - x1 = -5


행렬을 만들면서 모든 변수를 추가했기 때문에 x1 - x0 = 5 이렇게만 추가해 주는 것이 아니라. x0의 관점에서도 변경해 주어야 한다. 그리고 오른편의 긴 column 벡터에 대해서도 5만 추가하는 것이 아니라 변형된 식의 -5를 고려해야 한다. 그래서 위 그림처럼 1 -1 , -1 1 이러한 모양을 가지게 되는 것이다. 이번에는 x1와 x2의 관계를 추가해 보자. 



 이렇게 할 수 있다. 여기서 알 수 있는 점은 행렬을 diagonal한 모양을 가지게 되고, 연속된 2가지 변수(x0 & x1 / x1 & x2)들에 대해서 조건을 추가했기 때문에, 정사각형 모양이 대각 방향으로 내려오면서 (x_t, x_t), (x_t, x_t+1) / (x_t+1, x_t), (x_t+1, x_t+1)에 1 -1 / -1 1이 더해진다고 볼 수 있다. value들에 대해서도 (x_t, x_t+1)에 -value / value이 이렇게 차례로 한칸씩 내려오면서 더해질 것이다. 사실 그림으로 설명하는 것이 좋은데, 그래서 동영상을 첨부한다. ㅠㅠ




 이번에는 Landmark들에 대한 조건을 추가해 보자!! x1에서 Landmark0가 관찰되었다고 했을 때, x1과 Landmark가 관여된 모든 요소들이 변경되어야 한다. 하나에 조건에 대해서 (x1, x1) (x1, L0) (L0, x1) (L0, L0)이 네 가지 요소들에 변화가 생긴다는 것이다. 


  


x1에서 관측된 L0가 9의 거리를 가지고 있었다고 한다면, x1 + 9 = L0라는 뜻이다. 그래서 이를 변형하여


x1 - L0 = -9

L0 - x1 = 9


이 두 식을 얻을 수 있을 것이고, 이제 행렬의 각 요소에 이에 해당되는 값들을 추가할 수 있다. 오른쪽 그림과 같이 말이다. Landmark를 measurement할 때마다, 그리고 어떠한 motion을 취할 때마다 행렬의 어떤 부분에 수정이 이루어 지는지 이제 알게 되었다. 그럼 아래 문제를 풀어 보자.



 선으로 연결된 부분은 우리가 조건을 알고 있다는 뜻이다. 결국 x0-x1 / x1-x2 / x0-L / x2-L이 네 가지 조건을 알고 있다고 했을 때, 행렬에서 0을 가지는 부분이 어디일까? 라는 문제였다. 사진에서 빨간 0이 있는 부분은 우리가 건들지 못하는 부분이다. 커다란 행렬을 사용하는데 거의 모든 부분을 건드린다는 걸 보여주고 싶었나 보다.



 그런데 우리가 이 행렬을 정의한 이유는 단순히 계산의 편의를 위해서만이 아니다. 정방 행렬을 Omega, 세로로 긴 행렬을 Xi라고 했을 때, Omega(inverse) * Xi를 하면 모든 요소들의 추정된 위치를 알 수 있게 된다!! 그럼 실제로 그러한지 예제를 통해서 알아보자. 




 예제는 위 경우를 가지고 진행할 것이다. x0가 3이었으니 결과는 -3 2 5 이러한 값이 나와야 할 텐데 실제로 그러한지 한 번 코드를 짜 보자는 것이다. 코드는 다음과 같다.



def doit(initial_pos, move1, move2):
    Omega = matrix([[100], [000], [000]])
    xi = matrix([[initial_pos], [0], [0]])
    Omega += matrix([[1-10], [-110], [000]])
    xi += matrix([[-move1], [move1], [0]])
    Omega += matrix([[000], [01-1], [0-11]])
    xi += matrix([[0], [-move2], [move2]])
 
    #Omega.show()
    #xi.show()
    mu = Omega.inverse() * xi
    #mu.show()
    return mu
 
doit(-353)


 위 부분이 핵심인데, 앞서 설명했으므로 한 번 읽어보면 이해가 갈 것이다. 


이번에는 그럼 Landmark를 추가해 보자! 



 로봇의 처음 위치는 -3이었고 Landmark는 10만큼 떨어져 있었다고 한다.

 로봇이 5만큼 움직였고 이 위치에서 Landmark는 5만큼 떨어져 있었다고 한다.

 로봇이 또 3만큼 움직였고 이번에는 Landmark가 2만큼 떨어져 있었다고 한다.

 그럼 이 상황에서 Landmark의 위치는??


 -3 + 10 = -3 + 5 + 5 = -3 + 5 + 3 + 2 = 7


 모든 정황들이 Landmark의 위치가 7이라고 말하고 있다. 왜 같은 말을 이렇게 여러번 하는지 이해를 못할 수도 있다. 그런데, 실제로는 noise가 있어서 같은 값이 계속 나오는 것이 아니다. 나중에 볼 것이다. 본론으로 돌아가서, 우리가 만들고 있는 행렬에 이러한 요소를 추가시켜 준다면, 위와 같은 여러 번의 연산을 일일이 짜주지 않아도 행렬의 역행렬 연산을 통해서 손쉽게 Landmark를 추정할 수 있다. 그럼 이제 Landmark 요소를 추가시켜 보자!!!



 갑자기 코드가 나왔지만 실제로 볼 부분은 아래이다. 


    Omega = matrix([[1.00.00.0],
                    [0.00.00.0],
                    [0.00.00.0]])
                    
    Xi = matrix([[initial_pos],
                 [0.0],
                 [0.0]])
 
    Omega += matrix([[1.0-1.00.0],
                     [-1.01.00.0],
                     [0.00.00.0]])
    Xi += matrix([[-move1],
                  [move1],
                  [0.0]])
    
    Omega += matrix([[0.00.00.0],
                     [0.01.0-1.0],
                     [0.0-1.01.0]])
    Xi += matrix([[0.0],
                  [-move2],
                  [move2]])


 여기까지는 위에서 했었다. 이 때는 Landmark는 생각도 하지 않고 motion만 고려했었는데 그러다 보니 행렬의 크기가 커져야 함을 알 수 있다. 그래서 expand라는 함수를 사용해서 행렬의 크기를 키워 주었다.


    Omega = Omega.expand(44, [012], [012])
    Xi = Xi.expand(41, [012], [0])


그리고 이제 Landmark 요소들을 추가해 줄 것이다. 위 그림을 보면 x0에서 10만큼 떨어져 있었는데 이를 식으로 나타내면 x0 + 10 = L 이렇게 된다. 이를 motion에서 해 주었던 것과 똑같이 x0 - L = -10 / L - x0 = 10 이렇게 바꾸어서 행렬에 추가할 것이다. 그 부분이 아래이다.


    Omega += matrix([[100-1],
                     [0000],
                     [0000],
                     [-1001]])
    Xi += matrix([[-10],
                  [0],
                  [0],
                  [10]])
    Omega += matrix([[0000],
                     [010-1],
                     [0000],
                     [0-101]])
    Xi += matrix([[0],
                  [-5],
                  [0],
                  [5]])
    Omega += matrix([[0000],
                     [0000],
                     [001-1],
                     [00-11]])
    Xi += matrix([[0],
                  [0],
                  [-2],
                  [2]])

 

 원시적인 Graph SLAM이 완성되었다!! 이 원리만 알고 있으면 좌표계를 확장하고 더 많은 움직임, 더 많은 Landmark들을 추가만 해 주는 것은 단순 작업일 뿐이다. 


 앞서 살짝 이야기했는데 여러 위치에서 Landmark가 보일 수는 있지만, 추정된 Landmark의 위치들은 다를 수 있다. 이번에는 이 noise를 추가해 보는 작업을 할 것이다. 



 measurement의 불확실성에 대한 이야기이다. 우리가 measurement를 할 때마다 위의 가우시안 폼의 수식들이 계속해서 곱해지게 된다. 그런데, 이 과정을 수행하면서, 모든 measurement를 똑같이 신뢰하기보다. 어떤 특정한measurement 값은 확신을 가지고 있다고 하자, 그랬을 때, 해당하는 value들에 weight(수학적으로 더 이야기하자면 1/sigma)를 곱해줌으로서 이에 해당하는 결과를 얻을 수 있게 된다. 여기서는 *5를 해주기로 했다. 코드로 나타내면 아래와 같다. 


1
2
3
4
5
6
7
8
    Omega += matrix([[0.0, 0.0, 0.0, 0.0],
                     [0.0, 0.0, 0.0, 0.0],
                     [0.0, 0.0, 5.0, -5.0],
                     [0.0, 0.0, -5.0, 5.0]])
    Xi    += matrix([[0.0],
                     [0.0],
                     [-Z2 * 5.0],
                     [Z2 * 5.0]])
cs


필요한 부분만 추가를 했다. 그럼 이번에는 일반적인 경우로 확장을 해서 임의의 data가 들어왔을 때, 2-D SLAM 구현하는 코드를 보이겠다. data는 이렇게 들어온다. 


1
2
3
test_data1 = [[[[1, 19.457599255548065, 23.8387362100849], [2, -13.195807561967236, 11.708840328458608], [3, -30.0954905279171, 15.387879242505843]], [-12.2607279422326, -15.801093326936487]], [[[2, -0.4659930049620491, 28.088559771215664], [4, -17.866382374890936, -16.384904503932]], [-12.2607279422326, -15.801093326936487]], [[[4, -6.202512900833806, -1.823403210274639]], [-12.2607279422326, -15.801093326936487]], [[[4, 7.412136480918645, 15.388585962142429]], [14.008259661173426, 14.274756084260822]], [[[4, -7.526138813444998, -0.4563942429717849]], [14.008259661173426, 14.274756084260822]], [[[2, -6.299793150150058, 29.047830407717623], [4, -21.93551130411791, -13.21956810989039]], [14.008259661173426, 14.274756084260822]], [[[1, 15.796300959032276, 30.65769689694247], [2, -18.64370821983482, 17.380022987031367]], [14.008259661173426, 14.274756084260822]], [[[1, 0.40311325410337906, 14.169429532679855], [2, -35.069349468466235, 2.4945558982439957]], [14.008259661173426, 14.274756084260822]], [[[1, -16.71340983241936, -2.777000269543834]], [-11.006096015782283, 16.699276945166858]], [[[1, -3.611096830835776, -17.954019226763958]], [-19.693482634035977, 3.488085684573048]], [[[1, 18.398273354362416, -22.705102332550947]], [-19.693482634035977, 3.488085684573048]], [[[2, 2.789312482883833, -39.73720193121324]], [12.849049222879723, -15.326510824972983]], [[[1, 21.26897046581808, -10.121029799040915], [2, -11.917698965880655, -23.17711662602097], [3, -31.81167947898398, -16.7985673023331]], [12.849049222879723, -15.326510824972983]], [[[1, 10.48157743234859, 5.692957082575485], [2, -22.31488473554935, -5.389184118551409], [3, -40.81803984305378, -2.4703329790238118]], [12.849049222879723, -15.326510824972983]], [[[0, 10.591050242096598, -39.2051798967113], [1, -3.5675572049297553, 22.849456408289125], [2, -38.39251065320351, 7.288990306029511]], [12.849049222879723, -15.326510824972983]], [[[0, -3.6225556479370766, -25.58006865235512]], [-7.8874682868419965, -18.379005523261092]], [[[0, 1.9784503557879374, -6.5025974151499]], [-7.8874682868419965, -18.379005523261092]], [[[0, 10.050665232782423, 11.026385307998742]], [-17.82919359778298, 9.062000642947142]], [[[0, 26.526838150174818, -0.22563393232425621], [4, -33.70303936886652, 2.880339841013677]], [-17.82919359778298, 9.062000642947142]]]
test_data2 = [[[[0, 26.543274387283322, -6.262538160312672], [3, 9.937396825799755, -9.128540360867689]], [18.92765331253674, -6.460955043986683]], [[[0, 7.706544739722961, -3.758467215445748], [1, 17.03954411948937, 31.705489938553438], [3, -11.61731288777497, -6.64964096716416]], [18.92765331253674, -6.460955043986683]], [[[0, -12.35130507136378, 2.585119104239249], [1, -2.563534536165313, 38.22159657838369], [3, -26.961236804740935, -0.4802312626141525]], [-11.167066095509824, 16.592065417497455]], [[[0, 1.4138633151721272, -13.912454837810632], [1, 8.087721200818589, 20.51845934354381], [3, -17.091723454402302, -16.521500551709707], [4, -7.414211721400232, 38.09191602674439]], [-11.167066095509824, 16.592065417497455]], [[[0, 12.886743222179561, -28.703968411636318], [1, 21.660953298391387, 3.4912891084614914], [3, -6.401401414569506, -32.321583037341625], [4, 5.034079343639034, 23.102207946092893]], [-11.167066095509824, 16.592065417497455]], [[[1, 31.126317672358578, -10.036784369535214], [2, -38.70878528420893, 7.4987265861424595], [4, 17.977218575473767, 6.150889254289742]], [-6.595520680493778, -18.88118393939265]], [[[1, 41.82460922922086, 7.847527392202475], [3, 15.711709540417502, -30.34633659912818]], [-6.595520680493778, -18.88118393939265]], [[[0, 40.18454208294434, -6.710999804403755], [3, 23.019508919299156, -10.12110867290604]], [-6.595520680493778, -18.88118393939265]], [[[3, 27.18579315312821, 8.067219022708391]], [-6.595520680493778, -18.88118393939265]], [[], [11.492663265706092, 16.36822198838621]], [[[3, 24.57154567653098, 13.461499960708197]], [11.492663265706092, 16.36822198838621]], [[[0, 31.61945290413707, 0.4272295085799329], [3, 16.97392299158991, -5.274596836133088]], [11.492663265706092, 16.36822198838621]], [[[0, 22.407381798735177, -18.03500068379259], [1, 29.642444125196995, 17.3794951934614], [3, 4.7969752441371645, -21.07505361639969], [4, 14.726069092569372, 32.75999422300078]], [11.492663265706092, 16.36822198838621]], [[[0, 10.705527984670137, -34.589764174299596], [1, 18.58772336795603, -0.20109708164787765], [3, -4.839806195049413, -39.92208742305105], [4, 4.18824810165454, 14.146847823548889]], [11.492663265706092, 16.36822198838621]], [[[1, 5.878492140223764, -19.955352450942357], [4, -7.059505455306587, -0.9740849280550585]], [19.628527845173146, 3.83678180657467]], [[[1, -11.150789592446378, -22.736641053247872], [4, -28.832815721158255, -3.9462962046291388]], [-19.841703647091965, 2.5113335861604362]], [[[1, 8.64427397916182, -20.286336970889053], [4, -5.036917727942285, -6.311739993868336]], [-5.946642674882207, -19.09548221169787]], [[[0, 7.151866679283043, -39.56103232616369], [1, 16.01535401373368, -3.780995345194027], [4, -3.04801331832137, 13.697362774960865]], [-5.946642674882207, -19.09548221169787]], [[[0, 12.872879480504395, -19.707592098123207], [1, 22.236710716903136, 16.331770792606406], [3, -4.841206109583004, -21.24604435851242], [4, 4.27111163223552, 32.25309748614184]], [-5.946642674882207, -19.09548221169787]]] 
 
cs


 data는 위와 같은 형식으로 구성되어 있다.


1. 로봇이 모든 Landmark들을 볼 수는 없다고 가정을 한다. (볼 수 있는 range가 있다.) measurement를 먼저 하고 motion을 취한다고 가정한다.

2. 1, 2, 3, 4 이렇게 Landmark들에는 이름이 붙어 있으며(사실상 이 부분에서 완벽한 SLAM은 아니라고 생각을 한다.),  볼 수 있는 Landmark들 까지의 x-y 거리가 data로 들어온다.

3. Landmark들을 한 위치에서 다수 볼 수 있기에 data가 주르륵 들어오는데, 마지막으로는 자기가 움직인 x-y 거리가 data로 들어온다.

4. 계속 움직이면서 계속계속 이런 data들을 받는다. 


 이러한 상황에서 모든 요소를 포함하고 있는 행렬들을 만들고 이들의 연산을 통해서 모든 position을 estimate하고 Landmark들의 위치도 estimate하라!!! 아래는 정답이다. 



으아아아 길다. 길어 ㅠ 2부로 나누어서 포스트 하겠다.

 

'공부' 카테고리의 다른 글

RTOS 시스템 mbed os - thread 정리  (0) 2019.01.25
mbed os cli 개발 환경 구축 (python3)  (0) 2019.01.22
백준 - 1009번  (0) 2019.01.16
파이썬 - 리스트 중간 수정  (0) 2019.01.16
Control 공부했다. - PID Control  (0) 2019.01.08