로봇 · 기본
좌표·센서·구동기의 기초
차동 구동 기구학 - 바퀴 속도와 로봇 속도
정기구학·역기구학, 오도메트리 적분, 오차 누적
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서는 이동과 회전을 하나의 동차 변환 행렬로 표현하는 법을 다뤘다. 이번 장은 그 변환이 실제로 어디서 나오는지, 즉 두 바퀴의 회전 속도가 로봇 전체의 이동 속도와 회전 속도로 어떻게 합쳐지는지를 다룬다. 2바퀴 차동 구동(differential drive) 로봇을 기준으로 정기구학과 역기구학을 세우고, 그 결과를 시간에 대해 적분해서 로봇의 위치를 추정하는 오도메트리(odometry)까지 구현한다.
- 왼쪽·오른쪽 바퀴 속도로부터 로봇의 직진 속도와 회전 속도를 구하는 정기구학 공식을 세운다.
- 원하는 로봇 속도를 바퀴 속도 명령으로 바꾸는 역기구학 공식을 세운다.
- 바퀴 속도를 시간에 대해 적분해서 위치와 방향을 추정하는 오도메트리를 구현한다.
- 바퀴 속도 측정에 작은 계통오차가 있을 때 오도메트리 결과가 어떻게 벌어지는지 확인한다.
문제 상황
차동 구동 로봇에 "앞으로 40cm 이동한 뒤 제자리에서 90도 돌고 다시 20cm 이동하라"는 명령을 내렸다. 로봇은 왼쪽·오른쪽 모터에 속도 명령만 받을 수 있고, 자기 위치를 직접 알려주는 센서는 없다. 개발자는 두 바퀴 속도를 어떻게 조합해야 "40cm 직진"이나 "90도 회전" 같은 명령이 되는지부터 정해야 하고, 명령을 실행한 뒤에는 로봇이 지금 어디에 있다고 판단할지도 계산해야 한다. 이 두 계산이 바로 정기구학과 오도메트리이며, 여기서 조금이라도 어긋나면 로봇이 실제로는 다른 위치에 가 있는데도 소프트웨어는 정확한 위치에 있다고 믿는 상황이 생긴다.
정기구학과 역기구학
왼쪽 바퀴 속도를 v_l, 오른쪽 바퀴 속도를 v_r(둘 다 m/s), 두 바퀴 중심 사이의 거리를 L이라 하자. 두 바퀴가 같은 속도면 로봇은 그대로 직진하고, 속도가 다르면 느린 쪽 바퀴를 중심으로 도는 원호를 그린다. 로봇 중심의 직진 속도 v는 두 바퀴 속도의 평균이고, 회전 속도 w는 두 바퀴 속도 차이를 L로 나눈 값이다.
이 관계를 정리하면 다음과 같다. v_r이 v_l보다 크면 w가 양수가 되어 로봇이 왼쪽으로(반시계 방향으로) 돈다고 정한다. 이는 x축을 로봇의 초기 정면, y축을 왼쪽으로 두는 일반적인 평면 좌표계 관례와 맞는다.
| 구분 | 입력 | 출력 | 공식 |
|---|---|---|---|
| 정기구학 | 바퀴 속도 v_l, v_r | 로봇 속도 v, w | v=(v_l+v_r)/2, w=(v_r-v_l)/L |
| 역기구학 | 로봇 속도 v, w | 바퀴 속도 v_l, v_r | v_l=v-wL/2, v_r=v+wL/2 |
역기구학은 정기구학을 뒤집어 풀기만 하면 나온다. 제어 루프가 "직진 0.2m/s, 회전 0.5rad/s로 움직여라"라고 결정하면, 그 값을 역기구학에 넣어 각 모터에 보낼 속도 명령을 만든다.
오도메트리 적분과 오차 누적
순간 속도를 위치로 적분하기
정기구학으로 구한 v, w는 그 순간의 속도일 뿐이다. 로봇의 위치 (x, y)와 방향 θ를 알려면 이 속도를 시간에 대해 적분해야 한다. θ(t) = θ0 + w·t이므로, dx/dt = v·cos θ(t)를 0부터 dt까지 적분하면 다음 식이 나온다.
w가 0이 아닐 때: x' = x0 + (v/w)·[sin(θ0+w·dt) - sin θ0], y' = y0 - (v/w)·[cos(θ0+w·dt) - cos θ0], θ' = θ0 + w·dt. w가 0이면 로봇은 직선으로만 움직이므로 x' = x0 + v·dt·cos θ0, y' = y0 + v·dt·sin θ0로 단순해진다. 이 두 식을 구간마다 반복 적용하는 것이 오도메트리다.
오차는 왜 누적되는가
오도메트리는 바퀴 속도 "측정값"을 적분한 결과이지, 로봇의 실제 위치를 직접 재는 것이 아니다. 만약 바퀴 지름을 실제보다 조금 크게 알고 있거나 트랙 폭 L을 잘못 재면, 매 구간마다 같은 방향으로 조금씩 틀린 값이 더해진다. 이런 계통오차(systematic error)는 이동 거리나 회전각에 비례해서 꾸준히 쌓이기 때문에, 직진 구간에서는 몇 밀리미터 차이였던 것이 회전을 몇 번 거치고 나면 방향 자체가 눈에 띄게 틀어진다. 반면 바퀴 미끄러짐처럼 방향이 매번 랜덤한 오차는 서로 상쇄되는 경향이 있어 상대적으로 천천히 쌓인다.
완성 코드
아래 프로그램은 정기구학·역기구학을 확인한 뒤, 직진-회전-직진으로 이루어진 세 구간을 참값(오차 없음)과 3% 계통오차가 섞인 추정값 두 가지로 오도메트리 적분해서 비교한다. 표준 라이브러리인 math와 dataclasses만 사용하며 난수를 쓰지 않으므로 실행할 때마다 같은 결과가 나온다. 파일명은 diff_drive_odometry.py로 저장한다.
"""차동 구동 로봇의 정기구학·역기구학과 오도메트리 적분 예제."""
import math
from dataclasses import dataclass
TRACK_WIDTH = 0.2 # m, 왼쪽·오른쪽 바퀴 중심 사이 거리
@dataclass
class Pose:
x: float
y: float
theta: float # rad
def forward_kinematics(v_l: float, v_r: float) -> tuple[float, float]:
v = (v_l + v_r) / 2.0
w = (v_r - v_l) / TRACK_WIDTH
return v, w
def inverse_kinematics(v: float, w: float) -> tuple[float, float]:
v_l = v - w * TRACK_WIDTH / 2.0
v_r = v + w * TRACK_WIDTH / 2.0
return v_l, v_r
def integrate_pose(pose: Pose, v: float, w: float, dt: float) -> Pose:
if abs(w) < 1e-9:
x = pose.x + v * dt * math.cos(pose.theta)
y = pose.y + v * dt * math.sin(pose.theta)
theta = pose.theta
else:
theta = pose.theta + w * dt
x = pose.x + (v / w) * (math.sin(theta) - math.sin(pose.theta))
y = pose.y - (v / w) * (math.cos(theta) - math.cos(pose.theta))
return Pose(x, y, theta)
def run_phase(pose: Pose, v_l: float, v_r: float, duration: float, bias: float = 1.0) -> Pose:
v, w = forward_kinematics(v_l * bias, v_r * bias)
return integrate_pose(pose, v, w, duration)
def print_pose(label: str, pose: Pose) -> None:
deg = math.degrees(pose.theta)
print(f"{label}: x={pose.x:.4f} m, y={pose.y:.4f} m, theta={deg:.4f} deg")
def main() -> None:
print("=== 기구학 변환 확인 ===")
v, w = forward_kinematics(0.2, 0.3)
print(f"정기구학: v_l=0.20, v_r=0.30 -> v={v:.4f} m/s, w={w:.4f} rad/s")
v_l, v_r = inverse_kinematics(0.3, 0.4)
print(f"역기구학: v=0.30, w=0.40 -> v_l={v_l:.4f} m/s, v_r={v_r:.4f} m/s")
phases = [
(0.2, 0.2, 2.0),
(-0.1, 0.1, math.pi / 2),
(0.2, 0.2, 1.0),
]
print("\n=== 오도메트리 적분: 참값 vs 3% 계통오차 ===")
true_pose = Pose(0.0, 0.0, 0.0)
est_pose = Pose(0.0, 0.0, 0.0)
for i, (v_l, v_r, dt) in enumerate(phases, start=1):
true_pose = run_phase(true_pose, v_l, v_r, dt, bias=1.0)
est_pose = run_phase(est_pose, v_l, v_r, dt, bias=1.03)
print(f"-- 구간 {i} --")
print_pose("참값", true_pose)
print_pose("추정값", est_pose)
dx = est_pose.x - true_pose.x
dy = est_pose.y - true_pose.y
dtheta = math.degrees(est_pose.theta) - math.degrees(true_pose.theta)
error = math.hypot(dx, dy)
print("\n=== 최종 오차 ===")
print(f"위치 오차: {error:.4f} m")
print(f"방향 오차: {dtheta:.4f} deg")
if __name__ == "__main__":
main()
줄별 해설
TRACK_WIDTH는 두 바퀴 "중심" 사이의 거리다. 로봇 외곽 폭이 아니라 실제로 바닥에 닿는 바퀴 접지점 사이 거리를 재야 한다.
Pose는 x, y, theta 세 값을 묶은 자료형이다. theta는 항상 라디안 단위로 저장하고, 출력할 때만 math.degrees로 변환한다.
forward_kinematics와 inverse_kinematics는 앞서 표로 정리한 두 공식을 그대로 옮긴 것이다. 인자 순서(v_l이 먼저, v_r이 나중)를 지키는 것이 중요한데, 뒤에 "실무에서 자주 틀리는 것"에서 이 부분을 다시 짚는다.
integrate_pose는 w가 0에 가까운지 먼저 확인한다. w가 정확히 0이면 (v/w) 계산에서 0으로 나누게 되므로, 아주 작은 값(1e-9)을 기준으로 직진 공식과 회전 포함 공식을 나눈다. 회전이 섞인 경우에는 앞서 유도한 정확한 호 적분 공식을 그대로 쓴다.
run_phase는 명령한 바퀴 속도(v_l, v_r)에 bias를 곱한 뒤 정기구학과 오도메트리 적분을 한 번에 실행한다. bias=1.0이면 오차 없는 참값, bias=1.03이면 두 바퀴 속도가 실제보다 3% 높게 측정된 상황을 흉내 낸다. 두 바퀴에 똑같이 곱하므로 직진 구간에서는 속도만 틀리고, 회전 구간에서는 각속도만 틀린다.
main의 phases 리스트는 "직진 2초 → 제자리 회전(90도가 되는 시간만큼) → 직진 1초" 세 구간을 정의한다. 같은 phases를 bias만 다르게 주어 참값과 추정값을 나란히 적분하고, 마지막에 두 최종 위치의 차이를 위치 오차와 방향 오차로 출력한다.
실행 결과
터미널에서 다음과 같이 실행한다.
python3 diff_drive_odometry.py
출력은 다음과 같다.
=== 기구학 변환 확인 ===
정기구학: v_l=0.20, v_r=0.30 -> v=0.2500 m/s, w=0.5000 rad/s
역기구학: v=0.30, w=0.40 -> v_l=0.2600 m/s, v_r=0.3400 m/s
=== 오도메트리 적분: 참값 vs 3% 계통오차 ===
-- 구간 1 --
참값: x=0.4000 m, y=0.0000 m, theta=0.0000 deg
추정값: x=0.4120 m, y=0.0000 m, theta=0.0000 deg
-- 구간 2 --
참값: x=0.4000 m, y=0.0000 m, theta=90.0000 deg
추정값: x=0.4120 m, y=0.0000 m, theta=92.7000 deg
-- 구간 3 --
참값: x=0.4000 m, y=0.2000 m, theta=90.0000 deg
추정값: x=0.4023 m, y=0.2058 m, theta=92.7000 deg
=== 최종 오차 ===
위치 오차: 0.0062 m
방향 오차: 2.7000 deg
1구간(직진)이 끝난 시점에 이미 x가 0.4000과 0.4120으로 갈라지고, 2구간(회전)에서는 각도가 90도와 92.7도로 벌어진다. 3구간을 지나면 이 두 오차가 합쳐져 최종 위치가 6mm 넘게 어긋난다. 바퀴 속도 측정 오차 자체는 3%로 작았지만, 여러 구간을 거치며 위치 오차로 누적된 것을 볼 수 있다.
실무에서 자주 틀리는 것
트랙 폭을 로봇 외곽 폭으로 재는 실수
바퀴가 로봇 몸체보다 안쪽에 달려 있는데도 몸체 전체 폭을 L로 쓰면 w 계산이 항상 같은 비율로 틀어진다.
# 잘못된 예: 로봇 겉면 폭을 그대로 사용
TRACK_WIDTH = 0.24
# 고친 예: 왼쪽 바퀴 중심에서 오른쪽 바퀴 중심까지 실측
TRACK_WIDTH = 0.20
각속도 단위(rad/s와 deg/s) 혼동
w는 항상 rad/s로 다뤄야 하는데, 사람이 이해하기 쉬운 도(degree) 값을 변환 없이 그대로 적분식에 넣는 실수가 흔하다.
# 잘못된 예: 도(deg) 값을 라디안 연산에 그대로 사용
w_deg = 30.0
theta += w_deg * dt
# 고친 예: 라디안으로 변환한 뒤 사용
w_rad = math.radians(30.0)
theta += w_rad * dt
회전이 섞인 구간을 매 스텝 단순 직진 공식으로 적분
회전 중에는 x, y 변화가 cos θ, sin θ에 비선형으로 의존한다. theta를 갱신하기 전의 값으로 이동량을 계산하면 dt가 클수록 오차가 커진다.
# 잘못된 예: 갱신 전 theta로 이동량을 계산
x += v * math.cos(theta) * dt
y += v * math.sin(theta) * dt
theta += w * dt
# 고친 예: 정확한 호 적분 공식을 쓰는 integrate_pose를 호출
pose = integrate_pose(pose, v, w, dt)
forward_kinematics 인자 순서를 뒤바꿔 정의
함수를 정의할 때 v_r을 먼저 받도록 실수하면, 호출부는 그대로인데 로봇이 명령과 반대로 돈다.
# 잘못된 예: 인자 순서가 뒤바뀜
def forward_kinematics(v_r: float, v_l: float):
...
# 고친 예: v_l을 먼저, v_r을 나중에
def forward_kinematics(v_l: float, v_r: float):
...
한눈에 보기
| 오차 종류 | 대표 원인 | 시간에 따른 변화 | 대응 방법 |
|---|---|---|---|
| 계통오차 | 트랙 폭·바퀴 지름 보정값 오류 | 이동·회전량에 비례해 꾸준히 누적 | 실측으로 보정값 검증, 주기적 재보정 |
| 랜덤오차 | 바퀴 미끄러짐, 노면 요철 | 평균이 0에 가까워 상대적으로 느리게 누적 | 필터링과 외부 센서로 보정 |
정기구학은 바퀴 속도를 로봇 속도로, 역기구학은 그 반대로 바꾼다는 점, 그리고 오도메트리는 그 속도를 적분한 "추정값"일 뿐 실제 위치를아니라는 점을 기억해야 한다는 것으로 이번 장을 마무리한다.
연습 문제
- TRACK_WIDTH를 실제보다 10% 작게 설정하면, 같은 v_l, v_r에 대해 계산된 w가 실제보다 크게 나오는지 작게 나오는지 이유와 함께 설명하라.
- v_l=0.15 m/s, v_r=0.25 m/s, TRACK_WIDTH=0.18 m일 때 v와 w를 구하라.
- 로봇이 v=0.3 m/s, w=-0.5 rad/s로 2초 동안 움직였다. θ0=0이라 할 때, 정확한 호 적분 공식으로 최종 pose (x, y, θ)를 구하라.
- 계통오차는 시간이 지날수록 위치 오차가 누적되지만, 랜덤오차는 상대적으로 덜 누적된다고 했다. 그 이유를 직관적으로 설명하라.
정답과 해설
1번. w = (v_r - v_l) / L 식에서 L이 분모다. 실제 L보다 10% 작은 값(0.9·L)을 넣으면 나누는 값이 작아지므로 계산된 w는 실제보다 약 11%(1/0.9배) 크게 나온다. 로봇은 실제로 그만큼 덜 돌았는데도 소프트웨어는 더 많이 돈 것으로 판단하게 된다.
2번. v = (0.15 + 0.25) / 2 = 0.2000 m/s, w = (0.25 - 0.15) / 0.18 = 0.5556 rad/s.
3번. θ' = 0 + (-0.5)·2 = -1.0000 rad. v/w = 0.3 / (-0.5) = -0.6. x' = -0.6·(sin(-1.0) - sin(0)) = -0.6·(-0.841471) = 0.5049 m. y' = -(-0.6)·(cos(-1.0) - cos(0)) = 0.6·(0.540302 - 1) = -0.2758 m. 정리하면 최종 pose는 x≈0.5049 m, y≈-0.2758 m, θ≈-1.0000 rad(약 -57.2958도)이다.
4번. 계통오차는 매 구간마다 같은 방향으로 값이 밀리기 때문에 구간을 거칠수록 오차가 한 방향으로 계속 더해진다. 반면 랜덤오차는 이번 구간에서 양(+)으로 틀렸다가 다음 구간에서 음(-)으로 틀리는 식으로 부호가 들쭉날쭉하므로, 여러 구간을 합산하면 서로 상쇄되는 부분이 생겨 계통오차보다 느리게 쌓인다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.