1. 이론
1.9좌표계와 TF2, URDF
왜 좌표계가 여러 개인가
LiDAR가 "앞쪽 1.5m에 장애물이 있다"고 알려 줍니다. 그런데 이 "앞쪽"은 LiDAR 기준입니다. LiDAR는 로봇 앞쪽 위에 달려 있고, 로봇은 방 안 어딘가에서 어느 방향을 보고 있습니다. 장애물을 피해 이동하려면 이 점이 로봇 기준으로는 어디인지, 방(지도) 기준으로는 어디인지를 알아야 합니다.
위치를 말할 때는 기준이 필요합니다. 이 기준 좌표계를 프레임(frame) 이라고 합니다. 로봇에는 센서마다, 바퀴마다, 로봇 본체마다, 그리고 지도마다 프레임이 있습니다. 프레임 사이의 위치 관계는 TF2라는 시스템이 계산합니다.
축과 단위 규칙 (REP-103)
ROS는 REP-103이라는 규약으로 축과 단위를 통일합니다. 모든 패키지가 이를 따르기 때문에 다른 사람이 만든 노드와 값을 그대로 주고받을 수 있습니다.
| 항목 | 규칙 |
|---|---|
| 좌표계 | 오른손 좌표계 |
| 축 방향 | x 앞, y 왼쪽, z 위 |
| 길이 | 미터 (m) |
| 각도 | 라디안 (rad), 도(degree)가 아님 |
| 회전 | x축 둘레 roll, y축 둘레 pitch, z축 둘레 yaw |
| 시간, 속도 | 초, m/s, rad/s |
- 위에서 내려다볼 때 반시계 방향 회전이 + 입니다 (z축이 위를 향하므로).
Twist의angular.z가 + 이면 왼쪽으로 돕니다. 1.3장에서 본 그대로입니다. - 카메라 광학 프레임만은 예외로 z 앞, x 오른쪽, y 아래를 쓰며, 이름에
_optical이 붙습니다.
로봇의 기본 프레임 (REP-105)
이동 로봇에서 거의 항상 등장하는 세 프레임이 있습니다.
| 프레임 | 의미 | 특징 |
|---|---|---|
base_link | 로봇 본체에 고정된 프레임 | 로봇이 움직이면 같이 움직임. 센서 위치의 기준 |
odom | 바퀴 엔코더 등으로 계산한 연속적인 기준 | 값이 부드럽게 변하고 점프하지 않지만, 시간이 지나면 오차가 쌓임(드리프트) |
map | 전체 공간에서 전역적으로 일정한 기준 | 오차가 쌓이지 않지만, 위치 보정 때 값이 갑자기 점프할 수 있음 |
그래서 짧은 시간의 부드러운 제어에는 odom을, 장기간의 정확한 위치에는 map을 씁니다. 세 프레임은 map → odom → base_link 순서로 이어집니다. 이 연결은 항상 이 방향입니다. map과 base_link를 직접 연결하지 않는 이유는 프레임마다 부모가 하나뿐이기 때문입니다.
odom→base_link변환은 바퀴 속도(오도메트리)를 계산하는 노드가 발행합니다.map→odom변환 발행은 위치 추정(localization)이나 SLAM 노드가 맡습니다. 지도가 없는 상황이면map이 없을 수도 있습니다.
TF 트리와 정적/동적 변환
모든 프레임은 부모-자식 관계의 나무(TF tree)로 이어집니다. 임의의 두 프레임 사이 관계는 나무를 따라 올라갔다 내려오며 변환을 곱해서 계산합니다. 그래서 laser에서 본 점을 map으로 바꾸는 일을 TF2가 대신 해 줍니다.
둥근 상자 = 노드 · 사각형 = 토픽 · 화살표 = 데이터 방향
TF 변환은 두 토픽으로 퍼집니다.
| 종류 | 토픽 | 예 | 특징 |
|---|---|---|---|
| 동적 변환 | /tf | odom → base_link, 움직이는 관절 | 계속 갱신되어 발행 |
| 정적 변환 | /tf_static | base_link → laser (센서 장착 위치) | 값이 변하지 않으므로 한 번만 발행 |
/tf_static은 앞 장에서 배운 transient local QoS를 씁니다. 그래서 한 번만 발행해도 나중에 켠 노드도 받아 갑니다.
정적 변환은 static_transform_publisher로 직접 발행할 수 있습니다. 아래 예는 laser 프레임을 base_link에서 앞으로 0.1m, 위로 0.2m 자리에 둡니다. 값은 설명용이며 실제 TIKI-MINI의 센서 위치가 아닙니다.
ros2 run tf2_ros static_transform_publisher \
--x 0.1 --y 0 --z 0.2 --yaw 0 --pitch 0 --roll 0 \
--frame-id base_link --child-frame-id laser
터미널로 TF 확인하기
ros2 run tf2_tools view_frames # 현재 TF 트리를 frames_날짜_시각.pdf로 저장 (Graphviz 필요)
ros2 run tf2_ros tf2_echo map base_link # 첫 번째 프레임 기준으로 본 두 번째 프레임의 위치와 방향 출력
ros2 topic echo /tf # TF 메시지 원본 보기
view_frames는 몇 초 동안/tf를 듣고 프레임 연결을 그림(PDF)으로 만듭니다. 파일 이름은frames_2026-10-15_12.30.45.pdf처럼 실행한 시각이 붙고,-o 이름을 주면 날짜 없이이름.pdf가 됩니다. PDF는dot으로 만들기 때문에sudo apt install graphviz가 먼저 필요합니다. 트리가 끊겨 있거나 부모가 둘인 문제를 찾을 때 씁니다.tf2_echo는 두 프레임 사이의 이동(Translation)과 회전(Rotation)을 계속 출력합니다. 회전은 쿼터니언과 롤/피치/요 값이 함께 나옵니다.
해보기: 한 점이 프레임마다 다른 좌표를 가진다
아래 도구는 위에서 내려다본 평면입니다. 로봇(base_link)의 위치와 방향을 map 기준으로 움직이고, 센서가 붙은 위치와 방향도 바꿀 수 있습니다. 센서가 본 점(거리와 방향)은 그대로인데, 프레임마다 좌표가 달라지는 것을 표에서 확인하세요.
같은 점, 세 프레임의 좌표
| 프레임 | 점의 좌표 (x, y) [m] | 계산 |
|---|---|---|
| sensor | (1.88, 0.68) | 거리와 방향에서: x = r·cos(θ), y = r·sin(θ) (θ = 0.349 rad) |
| base_link | (2.18, 0.68) | 센서 방향 φ = 0.000 rad만큼 회전한 뒤 센서 위치 (0.30, 0.00)를 더함: (1.88, 0.68) + (0.30, 0.00) |
| map | (3.23, 2.92) | 로봇 방향 yaw = 0.698 rad만큼 회전한 뒤 로봇 위치 (2.00, 1.00)를 더함: (1.23, 1.92) + (2.00, 1.00) |
빨간 선이 x축(앞), 초록 선이 y축(왼쪽)입니다. 각도는 위에서 봤을 때 반시계 방향이 +입니다. 로봇 크기는 그림용 가상의 값이며 TIKI-MINI의 크기가 아닙니다. 이 도구는 2차원(평면)만 다룹니다.
변환은 두 단계의 회전 후 이동입니다. 점 p를 센서 프레임에서 base_link로 바꾸는 식은 다음과 같습니다.
p_base = R(센서 방향 φ) · p_sensor + (센서 위치)
p_map = R(로봇 방향 yaw) · p_base + (로봇 위치)
R(a) = [ cos a -sin a ]
[ sin a cos a ]
직접 해 보기
- 로봇을 yaw = 0, 위치 (0, 0)으로 두고, 센서 오프셋을 0으로 하면 세 프레임의 좌표가 어떻게 되나요?
- 로봇 위치만 움직이면
sensor와base_link좌표는 변하나요,map좌표는 변하나요? 이유를 설명해 보세요. - yaw를 90도로 하고 센서 점을 앞(방향 0도) 2m에 두세요.
map에서는 어느 방향에 있나요? - 장애물은 가만히 있는데 로봇이 움직이면, 센서가 본 좌표와
map좌표 중 어느 쪽이 변해야 할까요? 로봇을 움직이면서 거리와 방향 슬라이더를 조절해map좌표를 거의 일정하게 유지해 보세요.
URDF: 로봇의 모양과 관절을 기술하는 파일
URDF(Unified Robot Description Format) 는 로봇의 구조를 XML로 적은 파일입니다. 크게 두 가지로 이루어집니다.
- 링크(link): 로봇의 한 덩어리 부품. 모양(visual), 충돌 영역(collision), 무게 정보(inertial)를 담습니다.
- 조인트(joint): 두 링크를 잇는 연결. 부모 링크, 자식 링크, 연결 위치(origin), 종류가 들어갑니다.
링크 하나가 프레임 하나에 대응하고, 조인트가 TF 트리의 부모-자식 연결이 됩니다. robot_state_publisher 노드가 URDF를 읽고 조인트로부터 TF를 발행하는데, 고정 조인트는 /tf_static으로 보내고 움직이는 조인트는 /joint_states 값으로 계산해 /tf로 보냅니다.
아래는 본체, 센서, 바퀴 하나로 이루어진 설명용 최소 예제입니다. 숫자는 임의의 값입니다.
<?xml version="1.0"?>
<robot name="mini_example">
<!-- 링크: 본체 (프레임 이름 = base_link) -->
<link name="base_link">
<visual>
<geometry>
<box size="0.4 0.25 0.1"/> <!-- 길이(x) 폭(y) 높이(z), 미터 -->
</geometry>
</visual>
</link>
<!-- 링크: 센서 -->
<link name="laser">
<visual>
<geometry>
<cylinder radius="0.03" length="0.04"/>
</geometry>
</visual>
</link>
<!-- 조인트: base_link 에 laser 를 고정 -->
<joint name="laser_joint" type="fixed">
<parent link="base_link"/>
<child link="laser"/>
<origin xyz="0.1 0 0.07" rpy="0 0 0"/> <!-- 위치(m), 회전(rad) -->
</joint>
<!-- 링크: 바퀴 -->
<link name="left_wheel">
<visual>
<origin rpy="1.5708 0 0"/> <!-- 원통을 눕혀 바퀴 모양으로 -->
<geometry>
<cylinder radius="0.05" length="0.02"/>
</geometry>
</visual>
</link>
<!-- 조인트: 계속 회전하는 바퀴 -->
<joint name="left_wheel_joint" type="continuous">
<parent link="base_link"/>
<child link="left_wheel"/>
<origin xyz="0 0.14 -0.03" rpy="0 0 0"/> <!-- 왼쪽(+y)에 위치 -->
<axis xyz="0 1 0"/> <!-- y축 둘레로 회전 -->
</joint>
</robot>
읽는 요령은 다음과 같습니다.
<link name="...">의 이름이 곧 TF 프레임 이름입니다.- 조인트의
origin은 부모 링크 기준으로 자식 링크가 어디에 있는지(xyz, 미터)와 어떻게 회전했는지(rpy, 라디안)입니다. REP-103 규칙이 그대로 적용됩니다. - 조인트 종류는 자주 쓰는 것만 보면
fixed(고정),revolute(각도 제한 있는 회전),continuous(제한 없는 회전),prismatic(직선 이동)입니다. - 바퀴는 y축(왼쪽)을 축으로 돌므로 로봇이 앞으로(x) 굴러갑니다.
URDF와 xacro
바퀴가 네 개이거나 같은 센서를 여러 개 붙일 때 URDF를 그대로 복사해 쓰면 길고 실수하기 쉽습니다. xacro는 URDF에 변수, 수식, 매크로를 더한 XML 확장 형식입니다. xacro 파일을 처리하면 일반 URDF가 나옵니다.
| 형식 | 확장자 | 특징 |
|---|---|---|
| URDF | .urdf | 순수 XML. 짧고 단순한 로봇에 적합. 반복을 줄일 수 없음 |
| xacro | .urdf.xacro 또는 .xacro | 변수, 수식, 매크로, 파일 포함 가능. 실제 로봇 모델은 대부분 이 형식 |
xacro robot.urdf.xacro > robot.urdf # xacro를 일반 URDF로 변환
실제 TF 트리 보기
실행 중인 로봇의 TF 트리는 rqt_tf_tree로 볼 수 있습니다. 아래는 다른 로봇(PR2)의 예로, odom_combined에서 base_footprint, base_link, 그 아래 바퀴 프레임으로 이어진 모습입니다. 이 교재의 TIKI-MINI 프레임 이름과는 다르지만 부모에서 자식으로 이어지는 구조와, 연결선 위에 발행하는 노드와 갱신 주기가 적히는 형식은 같습니다.
한눈에 정리
- 좌표를 말할 때는 항상 어느 프레임 기준인지가 필요하고, TF2가 프레임 사이 변환을 계산해 준다.
- ROS의 규칙은 x 앞, y 왼쪽, z 위, 길이 미터, 각도 라디안이고 반시계 방향이 +다.
base_link(로봇 본체),odom(연속적이지만 드리프트),map(전역적이지만 점프 가능)이map→odom→base_link로 이어진다. 프레임마다 부모는 하나다.- 움직이는 변환은
/tf, 변하지 않는 변환은/tf_static으로 발행된다. - 확인은
ros2 run tf2_tools view_frames,ros2 run tf2_ros tf2_echo <프레임1> <프레임2>로 한다. - URDF는 링크와 조인트로 로봇 구조를 적은 XML이고, 반복이 많으면 xacro를 쓴다.
확인 문제
Q1. 로봇이 x = 2, y = 0, yaw = 90도로 서 있습니다. 센서가 base_link 기준 앞(x) 1m의 점을 봤다면, 이 점의 map 기준 좌표는 얼마인가요? (센서는 base_link 원점에 있다고 가정)
yaw 90도이면 로봇의 앞(x)이 map의 +y 방향입니다. 점은 로봇 원점에서 그 방향으로 1m이므로 (2, 1) 입니다. 계산: R(90도)·(1, 0) = (0, 1), 여기에 (2, 0)을 더합니다.
Q2. odom 프레임은 부드럽게 변하지만 오차가 쌓이고, map 프레임은 오차가 쌓이지 않지만 점프할 수 있습니다. 두 프레임이 각각 더 적합한 용도는 무엇인가요?
odom은 짧은 시간 부드럽게 움직여야 하는 제어나 지역적인 판단에 맞습니다. map은 오랫동안 위치가 일관돼야 하는 전역 경로 계획에 적합합니다.
Q3. 센서 장착 위치처럼 바뀌지 않는 변환을 /tf로 계속 발행하지 않고 /tf_static으로 발행하는 이유는 무엇인가요?
값이 변하지 않으므로 반복 발행할 필요가 없기 때문입니다. /tf_static은 transient local QoS를 사용해 한 번만 발행해도 나중에 시작한 노드가 받을 수 있습니다.
다음 장
다음 장에서는 지금까지 만든 코드를 어떻게 패키지로 묶고 워크스페이스에서 colcon으로 빌드하는지 다룹니다.