Post

[ROS2 입문] Day 4: TF2와 좌표 변환 - 로봇 공간의 기하학

[ROS2 입문] Day 4: TF2와 좌표 변환 - 로봇 공간의 기하학

서론: 좌표계를 놓치면 로봇이 엉뚱한 곳을 본다

카메라가 물체를 발견했다. 그런데 그 좌표는 카메라 프레임 기준이다. 로봇 팔이 집으려면 로봇 베이스 기준 좌표가 필요하다. 이 변환을 체계적으로 관리하는 것이 TF2다.

1. 좌표 프레임 트리

ROS2는 모든 좌표 프레임을 트리 구조로 관리한다.

1
2
3
4
5
6
7
map
  └─ odom
       └─ base_link          ← 로봇 중심
            ├─ base_laser    ← 라이다
            ├─ camera_link   ← 카메라
            └─ arm_base      ← 매니퓰레이터 베이스
                 └─ arm_end_effector

부모 프레임에서 자식 프레임으로의 변환(Transform)이 트리를 구성한다. 임의의 두 프레임 간 변환은 트리를 따라 계산한다.

2. Transform 퍼블리시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import TransformStamped
from tf2_ros import TransformBroadcaster
import math

class CameraFramePublisher(Node):
    def __init__(self):
        super().__init__('camera_frame_pub')
        self.br = TransformBroadcaster(self)
        self.timer = self.create_timer(0.1, self.broadcast)

    def broadcast(self):
        t = TransformStamped()
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = 'base_link'      # 부모 프레임
        t.child_frame_id = 'camera_link'     # 자식 프레임

        # 카메라가 base_link 앞 0.2m, 위 0.3m에 위치
        t.transform.translation.x = 0.2
        t.transform.translation.y = 0.0
        t.transform.translation.z = 0.3

        # 회전 없음 (쿼터니언)
        t.transform.rotation.x = 0.0
        t.transform.rotation.y = 0.0
        t.transform.rotation.z = 0.0
        t.transform.rotation.w = 1.0

        self.br.sendTransform(t)

3. Transform 조회

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from tf2_ros import Buffer, TransformListener
from tf2_ros import LookupException, ConnectivityException, ExtrapolationException

class MyNode(Node):
    def __init__(self):
        super().__init__('tf_listener')
        self.tf_buffer = Buffer()
        self.tf_listener = TransformListener(self.tf_buffer, self)
        self.timer = self.create_timer(1.0, self.lookup)

    def lookup(self):
        try:
            # camera_link → base_link 변환 조회
            t = self.tf_buffer.lookup_transform(
                'base_link',       # 목표 프레임
                'camera_link',     # 소스 프레임
                rclpy.time.Time()  # 최신 변환
            )
            self.get_logger().info(
                f'x={t.transform.translation.x:.3f}')
        except (LookupException, ConnectivityException,
                ExtrapolationException) as e:
            self.get_logger().warn(str(e))

4. 정적 변환 (Static Transform)

고정된 변환(센서 마운트 위치 등)은 StaticTransformBroadcaster를 쓴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from tf2_ros import StaticTransformBroadcaster

class StaticFramePublisher(Node):
    def __init__(self):
        super().__init__('static_tf_pub')
        self.static_br = StaticTransformBroadcaster(self)

        t = TransformStamped()
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = 'base_link'
        t.child_frame_id = 'lidar_link'
        t.transform.translation.z = 0.5    # 50cm 위
        t.transform.rotation.w = 1.0

        self.static_br.sendTransform(t)

또는 CLI로 한 번만 퍼블리시:

1
2
3
4
ros2 run tf2_ros static_transform_publisher \
  --x 0 --y 0 --z 0.5 \
  --qx 0 --qy 0 --qz 0 --qw 1 \
  --frame-id base_link --child-frame-id lidar_link

5. URDF와 robot_state_publisher

URDF(Unified Robot Description Format)는 로봇의 링크와 조인트 구조를 XML로 정의한다. robot_state_publisher가 URDF를 읽어 TF 트리를 자동으로 퍼블리시한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- robot.urdf -->
<robot name="my_robot">
  <link name="base_link"/>
  <link name="arm_link">
    <visual>
      <geometry><box size="0.1 0.1 0.3"/></geometry>
    </visual>
  </link>
  <joint name="arm_joint" type="revolute">
    <parent link="base_link"/>
    <child link="arm_link"/>
    <origin xyz="0 0 0.1" rpy="0 0 0"/>
    <axis xyz="0 0 1"/>
    <limit lower="-1.57" upper="1.57" effort="10" velocity="1"/>
  </joint>
</robot>
1
2
ros2 run robot_state_publisher robot_state_publisher \
  --ros-args -p robot_description:="$(cat robot.urdf)"

6. TF 디버깅

1
2
3
4
5
6
7
8
# 특정 프레임 간 변환 출력
ros2 run tf2_ros tf2_echo base_link camera_link

# TF 트리 구조 확인 (PDF 생성)
ros2 run tf2_tools view_frames

# 현재 TF 트리 요약
ros2 topic echo /tf --no-arr | head -40

view_framesframes.pdf를 생성해 전체 트리를 시각화해준다.

7. Day 4 체크리스트

  1. TF 프레임 트리 구조(부모-자식 관계)를 이해했다.
  2. TransformBroadcaster로 동적 변환을 퍼블리시했다.
  3. Buffer.lookup_transform()으로 임의 두 프레임 간 변환을 조회했다.
  4. tf2_echoview_frames로 TF 트리를 디버깅했다.

다음 글 예고

Day 5에서는 Nav2와 자율주행 스택을 다룬다. ROS2의 표준 내비게이션 프레임워크 Nav2의 구성 요소와 이동 명령을 보내는 방법을 정리한다.

This post is licensed under CC BY 4.0 by the author.