[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_frames는 frames.pdf를 생성해 전체 트리를 시각화해준다.
7. Day 4 체크리스트
- TF 프레임 트리 구조(부모-자식 관계)를 이해했다.
TransformBroadcaster로 동적 변환을 퍼블리시했다.Buffer.lookup_transform()으로 임의 두 프레임 간 변환을 조회했다.tf2_echo와view_frames로 TF 트리를 디버깅했다.
다음 글 예고
Day 5에서는 Nav2와 자율주행 스택을 다룬다. ROS2의 표준 내비게이션 프레임워크 Nav2의 구성 요소와 이동 명령을 보내는 방법을 정리한다.
This post is licensed under CC BY 4.0 by the author.