Skip to main content
Summary

미분이 쪼개는 것이라면, 적분은 다시 붙이는 것이다

지난 장에서 우리는 위치가 시간에 따라 얼마나 빨리 변하는지를 보며 속도를 구했다. 예를 들어 1초 전 위치가 2.0이고 지금 위치가 2.5라면, 로봇은 1초 동안 0.5m 움직인 셈이다. 이때 속도는 대략 0.5m/s라고 볼 수 있다.

이번 장에서는 방향을 반대로 바꿔 보자.

속도를 알고 있다면 위치를 어떻게 알 수 있을까?

로봇이 1m/s로 움직이고 있고, 그 상태가 0.1초 동안 유지되었다면 로봇은 그 짧은 시간 동안 0.1m 이동한다.

작은 이동거리 = 속도 × 시간 간격

이 작은 이동거리를 계속 더하면 전체 이동거리가 된다. 바로 이 감각이 적분이다. 적분은 어려운 기호로 시작할 필요가 없다. 로봇 입장에서 적분은 이렇게 말할 수 있다.

"아주 짧은 시간 동안 조금 움직였고, 그 조금을 계속 더했더니 여기까지 왔다."

로봇은 실제로도 이런 방식으로 자신의 위치를 추정한다. 바퀴가 얼마나 돌았는지, IMU가 어떤 가속도를 측정했는지, 제어 명령이 얼마 동안 지속되었는지를 이용해 작은 이동을 계속 누적한다. 문제는 이 누적이 강력하면서도 위험하다는 점이다. 잘 더하면 위치를 알 수 있지만, 작은 오차까지 함께 더하면 위치 추정이 점점 틀어진다.

이번 장에서는 Python으로 속도에서 위치를 만들고, 가속도에서 속도와 위치를 만드는 과정을 직접 확인해 보자.

적분과 이동거리

작은 이동을 누적한다는 것

1차원 직선 위를 움직이는 로봇을 생각해 보자. 로봇의 현재 위치를 x라고 하자. 로봇이 속도 v로 dt초 동안 움직이면 위치는 다음처럼 바뀐다.

x_new = x_old + v × dt

이 식은 단순하지만 로봇 시뮬레이션의 심장 같은 식이다. 앞 장에서 위치를 미분해 속도를 구했다면, 이번에는 속도를 적분해 위치를 구한다.

먼저 가장 쉬운 경우부터 보자. 로봇이 항상 1.5m/s로 움직인다고 하자. 시간 간격은 0.1초로 두겠다. 그러면 매 스텝마다 이동하는 거리는 다음과 같다.

1.5 × 0.1 = 0.15m

10번 반복하면 1초가 지나고, 로봇은 1.5m 이동한다. 100번 반복하면 10초가 지나고, 로봇은 15m 이동한다. 코드는 거의 우리가 방금 말한 그대로다.

import numpy as np
import matplotlib.pyplot as plt

v = 1.5       # m/s
dt = 0.1      # s
total_time = 10.0

steps = int(total_time / dt)

x = 0.0
positions = [x]
times = [0.0]

for i in range(steps):
    dx = v * dt
    x = x + dx

    positions.append(x)
    times.append((i + 1) * dt)

plt.figure(figsize=(8, 4))
plt.plot(times, positions, marker='o', markersize=3)
plt.xlabel('time (s)')
plt.ylabel('position x (m)')
plt.title('Position from constant velocity')
plt.grid(True)
plt.show()

그래프를 보면 위치가 시간에 따라 직선으로 증가한다. 속도가 일정하다는 것은 같은 시간마다 같은 거리만큼 이동한다는 뜻이다. 그래서 위치 그래프는 일정한 기울기를 가진 직선이 된다. 여기서 기울기가 바로 속도다.

중요한 점은 우리가 어떤 대단한 공식을 사용한 것이 아니라는 사실이다. 그저 v * dt를 계속 더했을 뿐이다. 하지만 이 반복이 곧 적분의 계산 버전이다.

직사각형을 쌓아 이동거리를 만든다

적분을 그림으로 설명할 때 흔히 "면적"이라는 말을 쓴다. 속도-시간 그래프에서 아래쪽 면적이 이동거리라는 이야기다. 처음에는 조금 이상하게 들릴 수 있다. 속도가 왜 면적이 되고, 그 면적이 왜 거리가 될까?

단위를 보면 훨씬 자연스럽다.

속도 단위: m/s
시간 단위: s
속도 × 시간: (m/s) × s = m

즉 속도에 시간을 곱하면 초 단위가 사라지고 미터가 남는다. 로봇이 실제로 이동한 거리다.

시간을 아주 잘게 나누면 각 구간에서 속도를 거의 일정하다고 볼 수 있다. 그러면 각 구간의 이동거리는 작은 직사각형의 면적처럼 계산된다.

작은 면적 = 속도 × 작은 시간
작은 이동거리 = v × dt
전체 이동거리 = 작은 이동거리들의 합

Python으로 이 직사각형 누적을 조금 더 명확히 표현해 보자.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.1
times = np.arange(0.0, 10.0 + dt, dt)

velocity = np.ones_like(times) * 1.5

# 각 시간 구간의 작은 이동거리
small_distances = velocity[:-1] * dt

# 작은 이동거리를 누적해 위치 계산
positions = np.zeros_like(times)
positions[1:] = np.cumsum(small_distances)

plt.figure(figsize=(8, 4))
plt.plot(times, velocity, label='velocity (m/s)')
plt.fill_between(times, velocity, alpha=0.2)
plt.xlabel('time (s)')
plt.ylabel('velocity (m/s)')
plt.title('Area under velocity graph means distance')
plt.grid(True)
plt.legend()
plt.show()

print('final position:', positions[-1])

여기서 np.cumsum은 누적합을 계산한다. 예를 들어 [0.1, 0.1, 0.1]을 cumsum하면 [0.1, 0.2, 0.3]이 된다. 로봇 위치 계산에서는 이 누적합이 매우 자주 등장한다. 작은 이동을 하나씩 더해 현재 위치를 만드는 것이기 때문이다.

변하는 속도에서도 똑같이 더한다

현실의 로봇은 항상 같은 속도로 움직이지 않는다. 출발할 때는 천천히 움직이다가 점점 빨라지고, 목표 근처에서는 다시 느려진다. 바퀴가 미끄러지거나 제어 명령이 바뀌면 속도도 계속 변한다.

그렇다고 적분의 감각이 달라지는 것은 아니다. 속도가 변해도 각 순간의 작은 이동거리를 계산해서 더하면 된다.

이번에는 속도가 시간에 따라 변하는 로봇을 만들어 보자. 예를 들어 다음과 같은 속도를 생각해 보자.

v = 0.5 + 0.2 × t

처음에는 0.5m/s로 움직이고, 시간이 지날수록 조금씩 빨라지는 로봇이다.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.1
total_time = 10.0
times = np.arange(0.0, total_time + dt, dt)

velocity = 0.5 + 0.2 * times

positions = np.zeros_like(times)

for i in range(len(times) - 1):
    dx = velocity[i] * dt
    positions[i + 1] = positions[i] + dx

plt.figure(figsize=(8, 4))
plt.subplot(2, 1, 1)
plt.plot(times, velocity)
plt.ylabel('velocity (m/s)')
plt.title('Changing velocity')
plt.grid(True)

plt.subplot(2, 1, 2)
plt.plot(times, positions)
plt.xlabel('time (s)')
plt.ylabel('position (m)')
plt.title('Position by integration')
plt.grid(True)

plt.tight_layout()
plt.show()

print('final position:', positions[-1])

위치 그래프를 보면 더 이상 직선이 아니다. 시간이 지날수록 속도가 커지기 때문에 위치 그래프의 기울기도 점점 커진다. 로봇이 처음에는 느리게 가다가 나중에는 더 빠르게 멀어지는 모습이다.

여기서 한 가지 중요한 계산 습관을 익혀야 한다. positions[i + 1]을 계산할 때 우리는 velocity[i]를 사용했다. 즉 현재 구간의 시작점 속도를 사용해 다음 위치를 예측했다. 이런 방식은 간단하고 직관적이지만, dt가 크면 오차가 커질 수 있다.

시간 간격 dt를 바꾸어 비교해 보자.

import numpy as np

def integrate_position(dt):
    total_time = 10.0
    times = np.arange(0.0, total_time + dt, dt)
    velocity = 0.5 + 0.2 * times

    positions = np.zeros_like(times)
    for i in range(len(times) - 1):
        positions[i + 1] = positions[i] + velocity[i] * dt

    return positions[-1]

for dt in [1.0, 0.5, 0.1, 0.01]:
    final_x = integrate_position(dt)
    print('dt =', dt, 'final position =', final_x)

일반적으로 dt가 작을수록 실제 연속적인 움직임에 더 가까워진다. 로봇 시뮬레이션에서 시간 간격이 중요한 이유다. 너무 큰 dt를 사용하면 움직임이 뚝뚝 끊겨 보이고 계산도 거칠어진다. 반대로 너무 작은 dt를 사용하면 계산량이 많아진다. 그래서 시뮬레이션에서는 정확도와 계산 속도 사이에서 적당한 균형을 찾는다.

2D 로봇의 위치 적분

지금까지는 1차원 직선 위의 위치 x만 다루었다. 하지만 우리가 앞에서 만든 로봇은 2D 평면 위에서 움직인다. 2D에서도 원리는 같다. 다만 위치와 속도가 벡터가 된다.

로봇의 위치가 [x, y]이고 속도가 [vx, vy]라면 작은 이동은 다음과 같다.

작은 이동 벡터 = 속도 벡터 × dt
새 위치 = 이전 위치 + 작은 이동 벡터

코드로는 더 간단하다.

position = position + velocity * dt

이번에는 2D 평면에서 로봇이 처음에는 오른쪽으로 움직이다가, 시간이 지날수록 위쪽 속도도 커지는 예제를 보자.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.1
total_time = 12.0
times = np.arange(0.0, total_time + dt, dt)

position = np.array([0.0, 0.0])
positions = [position.copy()]

for t in times[:-1]:
    vx = 0.8
    vy = 0.1 * t
    velocity = np.array([vx, vy])

    position = position + velocity * dt
    positions.append(position.copy())

positions = np.array(positions)

plt.figure(figsize=(6, 6))
plt.plot(positions[:, 0], positions[:, 1], marker='o', markersize=2)
plt.scatter(positions[0, 0], positions[0, 1], c='green', s=80, label='start')
plt.scatter(positions[-1, 0], positions[-1, 1], c='red', s=80, label='end')
plt.axis('equal')
plt.xlabel('x (m)')
plt.ylabel('y (m)')
plt.title('2D position integration')
plt.grid(True)
plt.legend()
plt.show()

처음에는 거의 오른쪽으로 움직이지만, 시간이 지날수록 위쪽 속도 vy가 커지면서 궤적이 점점 휘어진다. 로봇이 곡선 경로를 따라가는 것처럼 보인다. 하지만 계산 자체는 여전히 단순하다. 매 순간 속도 벡터에 dt를 곱해 작은 이동 벡터를 만들고, 그 벡터를 현재 위치에 더했을 뿐이다.

이 방식은 앞으로 경로 추종, 시뮬레이션, 위치 추정에서 계속 사용된다. 로봇의 움직임은 결국 작은 시간 간격마다 상태를 업데이트하는 일의 반복이다.

가속도를 적분하면 속도가 된다

이제 한 단계 더 들어가 보자. 속도를 알면 위치를 계산할 수 있었다. 그렇다면 가속도를 알면 무엇을 계산할 수 있을까?

가속도는 속도의 변화다. 따라서 가속도에 시간 간격을 곱하면 작은 속도 변화량이 된다.

작은 속도 변화 = 가속도 × 시간 간격
새 속도 = 이전 속도 + 작은 속도 변화

즉 가속도를 적분하면 속도가 된다. 그리고 그 속도를 다시 적분하면 위치가 된다.

가속도 → 적분 → 속도 → 적분 → 위치

이 구조는 IMU를 사용하는 로봇에서 매우 중요하다. IMU는 관성 측정 장치로, 보통 가속도와 각속도를 측정한다. 가속도를 이용하면 속도와 위치를 추정할 수 있을 것 같지만, 곧 보게 되듯이 오차 누적 문제가 매우 심각하다.

먼저 이상적인 가속도 데이터를 만들어 보자. 로봇이 처음 3초 동안 1.0m/s²로 가속하고, 그 다음 4초 동안은 가속하지 않고, 마지막 3초 동안 -1.0m/s²로 감속한다고 하자.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.01
total_time = 10.0
times = np.arange(0.0, total_time + dt, dt)

acceleration = np.zeros_like(times)
acceleration[(times >= 0.0) & (times < 3.0)] = 1.0
acceleration[(times >= 3.0) & (times < 7.0)] = 0.0
acceleration[(times >= 7.0) & (times <= 10.0)] = -1.0

velocity = np.zeros_like(times)
position = np.zeros_like(times)

for i in range(len(times) - 1):
    velocity[i + 1] = velocity[i] + acceleration[i] * dt
    position[i + 1] = position[i] + velocity[i] * dt

plt.figure(figsize=(8, 7))

plt.subplot(3, 1, 1)
plt.plot(times, acceleration)
plt.ylabel('a (m/s^2)')
plt.title('Acceleration')
plt.grid(True)

plt.subplot(3, 1, 2)
plt.plot(times, velocity)
plt.ylabel('v (m/s)')
plt.title('Velocity from acceleration')
plt.grid(True)

plt.subplot(3, 1, 3)
plt.plot(times, position)
plt.xlabel('time (s)')
plt.ylabel('x (m)')
plt.title('Position from velocity')
plt.grid(True)

plt.tight_layout()
plt.show()

print('final velocity:', velocity[-1])
print('final position:', position[-1])

그래프를 차례로 보면 움직임의 이야기가 보인다. 처음 3초 동안 로봇은 점점 빨라진다. 3초에서 7초까지는 가속도가 0이므로 속도가 일정하게 유지된다. 7초 이후에는 음의 가속도가 걸려 로봇이 점점 느려진다.

이 예제에서 우리가 한 일은 두 번의 누적이다. 첫 번째 누적으로 가속도에서 속도를 만들고, 두 번째 누적으로 속도에서 위치를 만들었다. 그래서 이를 2단계 적분이라고 부를 수 있다.

업데이트 순서가 결과를 바꿀 수 있다

방금 코드에서 위치를 업데이트할 때 velocity[i]를 사용했다.

velocity[i + 1] = velocity[i] + acceleration[i] * dt
position[i + 1] = position[i] + velocity[i] * dt

이 방식은 현재 속도로 먼저 이동하고, 그다음 속도를 바꾼다고 해석할 수 있다. 그런데 다음처럼 새로 계산된 속도 velocity[i + 1]를 사용해 위치를 업데이트할 수도 있다.

velocity[i + 1] = velocity[i] + acceleration[i] * dt
position[i + 1] = position[i] + velocity[i + 1] * dt

두 방식은 dt가 작으면 비슷한 결과를 내지만, dt가 크면 차이가 보일 수 있다. 이것은 적분을 컴퓨터로 근사할 때 생기는 자연스러운 문제다. 실제 시간은 연속적으로 흐르지만, 컴퓨터는 시간을 잘라서 계산한다. 그래서 각 구간 안에서 속도나 가속도를 어떻게 대표할지 선택해야 한다.

가장 단순한 방법은 지금처럼 한쪽 끝의 값을 사용하는 것이다. 조금 더 나은 방법으로는 구간의 시작 속도와 끝 속도의 평균을 사용할 수 있다.

v_next = velocity[i] + acceleration[i] * dt
v_avg = 0.5 * (velocity[i] + v_next)
position[i + 1] = position[i] + v_avg * dt
velocity[i + 1] = v_next

평균 속도를 사용하면 일정한 가속도 구간에서 더 자연스러운 결과를 얻는다. 물론 이 책에서는 처음부터 복잡한 수치해석 방법을 깊게 파고들지는 않는다. 지금 중요한 것은 하나다. 컴퓨터 속 적분은 "작은 양을 더하는 근사 계산"이며, 시간 간격과 업데이트 방식에 따라 결과가 달라질 수 있다.

바퀴 로봇의 전진 속도 적분

지금까지는 속도 벡터를 직접 알고 있다고 가정했다. 하지만 실제 2D 이동 로봇에서는 로봇의 방향과 전진 속도를 함께 사용해 위치를 업데이트하는 경우가 많다.

로봇의 상태를 [x, y, theta]라고 하자. 여기서 theta는 로봇이 바라보는 방향이다. 로봇이 자기 앞쪽으로 속도 v만큼 움직이면 월드 좌표계에서의 속도 성분은 다음과 같다.

vx = v × cos(theta)
vy = v × sin(theta)

그리고 각속도 omega가 있으면 방향도 업데이트한다.

theta_new = theta_old + omega × dt

이제 아주 간단한 바퀴 로봇 적분 시뮬레이션을 만들어 보자. 로봇은 계속 앞으로 가면서 조금씩 회전한다. 그러면 궤적은 원호처럼 휘어진다.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.05
total_time = 12.0
times = np.arange(0.0, total_time + dt, dt)

x = 0.0
y = 0.0
theta = 0.0

v = 1.0        # forward speed (m/s)
omega = 0.4    # angular speed (rad/s)

poses = []

for t in times:
    poses.append([x, y, theta])

    x = x + v * np.cos(theta) * dt
    y = y + v * np.sin(theta) * dt
    theta = theta + omega * dt

poses = np.array(poses)

plt.figure(figsize=(6, 6))
plt.plot(poses[:, 0], poses[:, 1])
plt.scatter(poses[0, 0], poses[0, 1], c='green', s=80, label='start')
plt.scatter(poses[-1, 0], poses[-1, 1], c='red', s=80, label='end')
plt.axis('equal')
plt.xlabel('x (m)')
plt.ylabel('y (m)')
plt.title('Integrating robot motion')
plt.grid(True)
plt.legend()
plt.show()

이 식은 앞으로 여러 번 다시 등장한다. 로봇이 목표를 향해 움직일 때도, 경로를 따라갈 때도, 시뮬레이터 안에서 로봇의 다음 위치를 계산할 때도 이 구조를 사용한다.

x += v * cos(theta) * dt
y += v * sin(theta) * dt
theta += omega * dt

이 세 줄은 매우 작아 보이지만, 로봇의 시간을 앞으로 한 칸 밀어 주는 엔진이다.

오차도 함께 적분된다

이제 적분의 어두운 면을 살펴보자. 적분은 작은 값을 계속 더한다. 그런데 측정값에 작은 오차가 들어 있다면 어떻게 될까? 안타깝게도 오차도 같이 더해진다.

예를 들어 실제 속도는 1.0m/s인데 센서가 항상 1.01m/s라고 측정한다고 하자. 오차는 겨우 0.01m/s다. 매우 작아 보인다. 하지만 100초 동안 누적하면 위치 오차는 다음과 같다.

0.01m/s × 100s = 1m

작은 속도 오차가 1미터의 위치 오차로 자란다. 로봇이 좁은 복도에서 움직이거나 물체를 집어야 하는 상황이라면 1미터는 엄청난 차이다.

코드로 확인해 보자.

import numpy as np
import matplotlib.pyplot as plt

dt = 0.1
total_time = 100.0
times = np.arange(0.0, total_time + dt, dt)

true_velocity = np.ones_like(times) * 1.0
measured_velocity = np.ones_like(times) * 1.01

true_position = np.zeros_like(times)
estimated_position = np.zeros_like(times)

for i in range(len(times) - 1):
    true_position[i + 1] = true_position[i] + true_velocity[i] * dt
    estimated_position[i + 1] = estimated_position[i] + measured_velocity[i] * dt

error = estimated_position - true_position

plt.figure(figsize=(8, 6))

plt.subplot(2, 1, 1)
plt.plot(times, true_position, label='true position')
plt.plot(times, estimated_position, '--', label='estimated position')
plt.ylabel('position (m)')
plt.grid(True)
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(times, error, color='red')
plt.xlabel('time (s)')
plt.ylabel('error (m)')
plt.title('Position error grows over time')
plt.grid(True)

plt.tight_layout()
plt.show()

print('final error:', error[-1])

그래프를 보면 처음에는 두 위치가 거의 붙어 있다. 하지만 시간이 지나면서 추정 위치가 실제 위치보다 조금씩 앞서 나간다. 이처럼 시간이 지날수록 추정값이 실제값에서 서서히 벗어나는 현상을 드리프트라고 한다.

드리프트는 로봇공학에서 매우 중요한 문제다. 바퀴 엔코더만 믿고 계속 위치를 적분하면 바퀴 미끄러짐 때문에 위치가 틀어진다. IMU 가속도만 두 번 적분해 위치를 구하면 더 빠르게 틀어진다. 그래서 로봇은 여러 센서를 함께 사용하고, 확률적인 추정 방법으로 오차를 줄이려고 한다. 이 책의 뒤쪽에서 배울 칼만 필터와 센서 퓨전은 바로 이런 문제를 해결하기 위한 도구다.

가속도 오차는 더 무섭다

속도 오차를 한 번 적분하면 위치 오차가 생긴다. 그렇다면 가속도 오차는 어떨까?

가속도는 먼저 속도로 적분되고, 그 속도가 다시 위치로 적분된다. 즉 가속도 오차는 두 번 누적된다. 그래서 아주 작은 가속도 편향도 시간이 지나면 위치 추정에 큰 문제를 만든다.

이번에는 실제 가속도가 0인 로봇을 생각해 보자. 로봇은 사실 정지해 있다. 그런데 가속도 센서가 아주 작은 편향 때문에 항상 0.02m/s²를 측정한다고 하자. 센서 입장에서는 거의 0처럼 보이는 작은 값이다. 하지만 이 값을 믿고 적분하면 어떤 일이 생길까?

import numpy as np
import matplotlib.pyplot as plt

dt = 0.01
total_time = 30.0
times = np.arange(0.0, total_time + dt, dt)

true_acceleration = np.zeros_like(times)
measured_acceleration = np.ones_like(times) * 0.02

true_velocity = np.zeros_like(times)
true_position = np.zeros_like(times)

estimated_velocity = np.zeros_like(times)
estimated_position = np.zeros_like(times)

for i in range(len(times) - 1):
    true_velocity[i + 1] = true_velocity[i] + true_acceleration[i] * dt
    true_position[i + 1] = true_position[i] + true_velocity[i] * dt

    estimated_velocity[i + 1] = estimated_velocity[i] + measured_acceleration[i] * dt
    estimated_position[i + 1] = estimated_position[i] + estimated_velocity[i] * dt

plt.figure(figsize=(8, 6))

plt.subplot(2, 1, 1)
plt.plot(times, estimated_velocity, label='estimated velocity')
plt.axhline(0.0, color='black', linewidth=1, linestyle='--')
plt.ylabel('velocity (m/s)')
plt.grid(True)
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(times, estimated_position, color='red', label='estimated position')
plt.axhline(0.0, color='black', linewidth=1, linestyle='--')
plt.xlabel('time (s)')
plt.ylabel('position (m)')
plt.grid(True)
plt.legend()

plt.tight_layout()
plt.show()

print('estimated final velocity:', estimated_velocity[-1])
print('estimated final position:', estimated_position[-1])

실제 로봇은 가만히 있는데, 추정 결과는 로봇이 점점 빨라지고 점점 멀리 이동했다고 말한다. 이것이 가속도 적분의 위험함이다. 가속도에 아주 작은 편향이 있어도 속도는 직선적으로 틀어지고, 위치는 더 빠르게 휘어지며 틀어진다.

이 현상을 보면 "IMU로 가속도를 측정해서 두 번 적분하면 위치를 알 수 있지 않을까?"라는 생각이 왜 조심스러운지 알 수 있다. 이론적으로는 가능하지만, 실제로는 센서 오차와 중력 보정, 자세 오차, 노이즈 때문에 금방 드리프트가 생긴다. 그래서 IMU는 단독으로 완벽한 위치 센서가 되기 어렵고, 다른 센서와 함께 쓰이는 경우가 많다.

노이즈가 있는 속도 적분하기

이번에는 항상 한쪽으로 치우친 편향이 아니라, 랜덤하게 흔들리는 노이즈를 넣어 보자. 센서값은 매 순간 조금씩 튄다. 어떤 순간에는 실제보다 크게 나오고, 어떤 순간에는 작게 나온다.

노이즈는 평균적으로 0이라면 어느 정도 서로 상쇄될 수 있다. 하지만 짧은 시간에는 추정값을 흔들고, 편향이 섞이면 드리프트로 이어진다.

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(0)

dt = 0.1
total_time = 60.0
times = np.arange(0.0, total_time + dt, dt)

true_velocity = np.ones_like(times) * 1.0
noise = np.random.normal(loc=0.0, scale=0.05, size=len(times))
measured_velocity = true_velocity + noise

true_position = np.zeros_like(times)
estimated_position = np.zeros_like(times)

for i in range(len(times) - 1):
    true_position[i + 1] = true_position[i] + true_velocity[i] * dt
    estimated_position[i + 1] = estimated_position[i] + measured_velocity[i] * dt

plt.figure(figsize=(8, 6))

plt.subplot(2, 1, 1)
plt.plot(times, measured_velocity, alpha=0.7, label='measured velocity')
plt.plot(times, true_velocity, linewidth=2, label='true velocity')
plt.ylabel('velocity (m/s)')
plt.grid(True)
plt.legend()

plt.subplot(2, 1, 2)
plt.plot(times, true_position, label='true position')
plt.plot(times, estimated_position, '--', label='estimated position')
plt.xlabel('time (s)')
plt.ylabel('position (m)')
plt.grid(True)
plt.legend()

plt.tight_layout()
plt.show()

print('final error:', estimated_position[-1] - true_position[-1])

np.random.normal은 정규분포를 따르는 랜덤 노이즈를 만든다. 여기서는 평균이 0이고 표준편차가 0.05인 노이즈를 속도에 더했다. 아직 확률분포를 자세히 배우지는 않았지만, 지금은 "센서값이 실제값 주변에서 흔들린다" 정도로 이해하면 충분하다.

실행할 때마다 노이즈 모양이 달라지면 최종 위치 오차도 달라진다. 그래서 로봇 위치 추정에서는 단순히 하나의 값만 믿기보다 "이 추정이 얼마나 불확실한가"도 함께 생각해야 한다. 이 흐름은 확률과 센서 오차를 다룰 때 다시 이어진다.

적분을 함수로 정리하기

지금까지 여러 코드에서 같은 패턴이 반복되었다. 속도 배열이 있으면 위치 배열을 만들고, 가속도 배열이 있으면 속도 배열을 만든다. 이런 반복은 함수로 정리해 두면 좋다.

먼저 1차원 데이터를 적분하는 간단한 함수를 만들자.

import numpy as np

def integrate_1d(values, dt, initial=0.0):
    result = np.zeros(len(values))
    result[0] = initial

    for i in range(len(values) - 1):
        result[i + 1] = result[i] + values[i] * dt

    return result

이 함수는 values에 들어 있는 값을 시간에 따라 누적한다. values가 속도라면 결과는 위치가 되고, values가 가속도라면 결과는 속도가 된다.

dt = 0.1
times = np.arange(0.0, 10.0 + dt, dt)

velocity = 0.5 + 0.2 * times
position = integrate_1d(velocity, dt, initial=0.0)

acceleration = np.ones_like(times) * 0.3
velocity_from_acc = integrate_1d(acceleration, dt, initial=0.0)
position_from_acc = integrate_1d(velocity_from_acc, dt, initial=0.0)

2D 벡터도 비슷하게 처리할 수 있다. positions가 (시간 개수, 2) 모양의 배열이 되도록 만들면 된다.

import numpy as np

def integrate_vectors(vectors, dt, initial):
    result = np.zeros_like(vectors)
    result[0] = initial

    for i in range(len(vectors) - 1):
        result[i + 1] = result[i] + vectors[i] * dt

    return result


dt = 0.1
times = np.arange(0.0, 8.0 + dt, dt)

velocities = np.zeros((len(times), 2))
velocities[:, 0] = 1.0
velocities[:, 1] = 0.2 * np.sin(times)

positions = integrate_vectors(velocities, dt, initial=np.array([0.0, 0.0]))

이렇게 함수를 만들어 두면 앞으로 로봇의 이동, 센서값 누적, 경로 시뮬레이션을 훨씬 깔끔하게 작성할 수 있다. 수학적으로는 적분이지만, 코드에서는 결국 "배열을 따라가며 이전 값에 작은 변화를 더하는 함수"가 된다.

적분할 때 기억해야 할 것들

적분은 로봇공학에서 너무 자주 등장하기 때문에, 지금 단계에서 몇 가지 감각을 단단히 잡아 두는 것이 좋다.

  • 속도 × 시간 간격은 작은 이동거리다.

  • 작은 이동거리를 계속 더하면 위치가 된다.

  • 가속도 × 시간 간격은 작은 속도 변화다.

  • 가속도를 적분하면 속도, 속도를 적분하면 위치가 된다.

  • 측정 오차도 함께 누적되므로 시간이 지날수록 드리프트가 생길 수 있다.

특히 마지막이 중요하다. 적분은 로봇에게 기억을 만들어 준다. 지금까지 얼마나 움직였는지 누적해서 현재 위치를 추정하게 해 준다. 하지만 잘못된 기억도 함께 쌓인다. 바퀴가 조금 미끄러진 일, 센서가 살짝 치우쳐 측정한 일, 시간 간격이 정확하지 않았던 일들이 모두 위치 추정에 흔적을 남긴다.

그래서 실제 로봇은 한 가지 정보만 믿지 않는다. 바퀴 엔코더로 이동을 추정하고, IMU로 회전과 가속도를 보고, 카메라나 LiDAR로 주변 환경을 확인한다. 그리고 이 정보들을 잘 섞어 더 믿을 만한 위치를 만든다.

작은 적분기가 로봇을 움직인다

이번 장의 핵심은 단순하다.

x = x + v * dt
v = v + a * dt

이 두 줄을 이해하면 로봇 움직임의 절반은 이미 손에 들어온 셈이다. 위치, 속도, 가속도는 따로 떨어진 개념이 아니다. 시간이라는 실 위에 차례로 연결된 값들이다. 위치를 시간으로 쪼개 보면 속도가 보이고, 속도를 시간으로 쪼개 보면 가속도가 보인다. 반대로 가속도를 시간에 따라 누적하면 속도가 되고, 속도를 누적하면 위치가 된다.

다음에 로봇이 화면 위에서 부드럽게 움직이는 모습을 보게 된다면, 그 안에서 작은 덧셈이 수백 번, 수천 번 반복되고 있다고 생각해 보자. 로봇은 한 번에 목적지로 순간이동하지 않는다. 아주 짧은 시간 동안 조금 움직이고, 다시 조금 움직이고, 또 조금 움직인다. 그 작은 이동들이 이어져 궤적이 되고, 궤적이 모여 로봇의 행동이 된다.

다음 장에서는 이 움직임을 목표 지점들의 모음인 웨이포인트와 연결한다. 로봇은 단순히 움직이는 것을 넘어, 정해진 경로를 따라가야 한다. 이제 우리는 그 경로를 시간에 따라 어떻게 지나갈지 생각할 준비가 되었다.

책 목록

로봇을 위한 수학과 Python: 좌표에서 시뮬레이터까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기