TIKI-MINI로 시작하는 ROS2 실무
목차

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가 대신 해 줍니다.

odom 계산 노드localization 노드robot_state_publisher/tf/tf_static

둥근 상자 = 노드 · 사각형 = 토픽 · 화살표 = 데이터 방향

TF 변환은 두 토픽으로 퍼집니다.

종류토픽예특징
동적 변환/tfodom → base_link, 움직이는 관절계속 갱신되어 발행
정적 변환/tf_staticbase_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 (map)+y (map)xymapxybase_linkxysensor감지된 점
map 기준 로봇(base_link)의 위치와 방향
base_link 기준 센서의 위치와 방향
센서가 본 점 (센서 프레임)

같은 점, 세 프레임의 좌표

프레임점의 좌표 (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 프레임 이름과는 다르지만 부모에서 자식으로 이어지는 구조와, 연결선 위에 발행하는 노드와 갱신 주기가 적히는 형식은 같습니다.

rqt_tf_tree 화면. odom_combined에서 base_footprint, base_link, 바퀴 링크로 이어지는 TF 트리

한눈에 정리

  1. 좌표를 말할 때는 항상 어느 프레임 기준인지가 필요하고, TF2가 프레임 사이 변환을 계산해 준다.
  2. ROS의 규칙은 x 앞, y 왼쪽, z 위, 길이 미터, 각도 라디안이고 반시계 방향이 +다.
  3. base_link(로봇 본체), odom(연속적이지만 드리프트), map(전역적이지만 점프 가능)이 map → odom → base_link로 이어진다. 프레임마다 부모는 하나다.
  4. 움직이는 변환은 /tf, 변하지 않는 변환은 /tf_static으로 발행된다.
  5. 확인은 ros2 run tf2_tools view_frames, ros2 run tf2_ros tf2_echo <프레임1> <프레임2>로 한다.
  6. 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으로 빌드하는지 다룹니다.