학습 목표
- 좌표값에 Frame과 Timestamp가 필요한 이유를 설명할 수 있다.
- TF Tree의 부모·자식 관계와 Transform 방향을 정확히 읽을 수 있다.
map,odom,base_footprint,base_link의 역할을 구분할 수 있다.- Static·Dynamic Transform을 CLI와 Python으로 발행할 수 있다.
- Buffer·Listener로 측정 시각의 Transform을 조회하고 Message를 변환할 수 있다.
view_frames,tf2_echo,tf2_monitor, RViz2로 TF를 조사할 수 있다.- Lookup·Connectivity·Extrapolation 오류를 구분하여 진단할 수 있다.
- 실제 Robot의 Sensor, Odometry, Localization과 URDF 발행 책임을 설계할 수 있다.
1. 좌표만으로는 위치를 알 수 없다
(2.0, 0.0, 0.0)이라는 숫자만으로는 물체가 어디 있는지 알 수 없습니다. LiDAR 기준 앞 2m인지, Robot 기준 앞 2m인지, Map 원점 기준 X축 2m인지가 필요합니다.
같은 물체의 좌표
laser_frame 기준: (2.0, 0.0, 0.0)
base_link 기준: (2.2, 0.0, 0.3)
map 기준: (8.4, -1.7, 0.3)
Robot이 움직이면 언제 측정했는지도 필요합니다. Camera가 200ms 전에 촬영한 물체를 지금 Robot Pose로 변환하면 움직인 거리만큼 위치가 밀립니다.
의미 있는 공간 Data = 값 + frame_id + timestamp
TF2는 여러 Frame 사이의 Translation과 Rotation을 시간별로 보관하고, 필요한 시각에 여러 Transform을 연결하여 Data를 목표 Frame으로 바꿉니다.
2. ROS 좌표축 규약
REP-103의 Robot Body 좌표축은 다음과 같습니다.
+Z 위
│
│
└──── +X 앞
/
+Y 왼쪽
- 길이: meter
- 각도: radian
- 오른손 좌표계
- X: 앞, Y: 왼쪽, Z: 위
- Yaw: Z축 회전, Pitch: Y축 회전, Roll: X축 회전
Camera Optical Frame은 영상 처리 관례 때문에 축이 다릅니다.
camera_link: X 앞, Y 왼쪽, Z 위
camera_optical_frame: Z 앞, X 오른쪽, Y 아래
Driver Message의 header.frame_id가 실제 축 규약과 일치하지 않으면 TF 계산은 성공해도 Point Cloud와 영상 Detection이 회전하거나 반전됩니다.
3. Transform은 무엇을 표현하는가
Transform은 Child Frame이 Parent Frame 안에서 어디에 있고 어떻게 회전했는지를 나타냅니다.
header.frame_id = base_link # Parent
child_frame_id = laser_frame # Child
translation.x = 0.20 # Parent 기준 앞 20cm
translation.z = 0.15 # Parent 기준 위 15cm
rotation = Quaternion
이를 base_link → laser_frame Transform이라고 부릅니다. 그러나 lookup_transform(target, source, time)은 source Data를 target으로 표현하는 데 필요한 Transform을 반환합니다.
# laser_frame의 Point를 map 좌표로 표현하기 위한 Transform
transform = buffer.lookup_transform(
'map', # target
'laser_frame', # source
time,
)
화살표 이름과 조회 인자 때문에 혼동하기 쉽습니다. 질문을 문장으로 읽으십시오.
“laser_frame에서 표현된 Data를 map에서 표현하고 싶다.”
target=map, source=laser_frame
4. TF Tree의 구조
TF Graph는 일반적인 여러 Parent Graph가 아니라 Tree 구조를 지향합니다.
- 각 Child Frame의 Parent는 하나입니다.
- 두 Frame 사이에는 일관된 연결 경로가 있어야 합니다.
- 같은 Transform은 한 발행자가 책임져야 합니다.
- Cycle을 만들면 안 됩니다.
- 이름, 방향과 시간 기준이 System 전체에서 일치해야 합니다.
위 그림에서 base_link는 laser_frame, camera_link, imu_link의 Parent입니다. Sensor 장착 Transform은 보통 URDF와 robot_state_publisher가 제공합니다.
같은 odom → base_link를 Wheel Odometry Node와 EKF Node가 동시에 발행하면 값이 번갈아 적용되어 RViz의 Robot이 떨거나 순간 이동합니다.
5. map·odom·base_link를 분리하는 이유
Wheel Encoder와 IMU를 적분한 Local Motion입니다. 짧은 시간 동안 연속적이고 부드럽지만 Slip과 Bias가 누적되어 장기적으로 Drift합니다.
AMCL, SLAM이나 Localization System이 장기 오차를 보정합니다. 전역적으로 일관되지만 위치 추정 갱신 때 값이 불연속적으로 바뀔 수 있습니다.
base_link
Robot 본체에 고정된 기준 Frame입니다. Robot이 움직이면 odom과 map 기준 Pose가 변합니다.
map ──Localization 보정──> odom ──연속 Odometry──> base_link
| Frame | 성질 | 주 용도 |
|---|---|---|
map |
장기적으로 전역 일관, 불연속 보정 가능 | 경로 계획, 목적지, 지도 표시 |
odom |
연속적, 장기 Drift 가능 | Local 제어, 짧은 시간의 속도·궤적 |
base_link |
Robot 본체와 함께 이동 | Sensor와 Actuator 장착 기준 |
Localization이 map → base_link를 직접 발행하면 이미 존재하는 odom → base_link와 Parent 구조가 충돌합니다. 보통 Localization은 map → odom 보정 Transform을 담당합니다.
6. base_footprint와 Sensor Frame
base_footprint는 Robot 본체를 지면에 투영한 2D 기준입니다. base_link가 지면 위에 있고 Roll·Pitch가 변할 수 있을 때 Navigation의 평면 기준을 분리하는 데 유용합니다.
odom
└─ base_footprint 지면상의 X·Y·Yaw
└─ base_link 본체 높이·Roll·Pitch
├─ laser_frame
├─ camera_link
│ └─ camera_optical_frame
└─ imu_link
모든 Robot이 반드시 base_footprint를 가져야 하는 것은 아닙니다. 사용하는 Navigation Stack, URDF와 Frame 계약을 확인하고 중복 Frame을 만들지 않습니다.
7. Static과 Dynamic Transform
| 구분 | Topic | 예 | 발행 방식 |
|---|---|---|---|
| Static | /tf_static |
Sensor 장착 위치, 고정 본체 구조 | 한 번 발행, 늦은 Subscriber도 수신 |
| Dynamic | /tf |
Robot Pose, 회전 Joint | Timestamp와 함께 계속 발행 |
/tf_static은 Transient Local Durability를 사용하여 나중에 참가한 Listener도 Static Transform을 받을 수 있습니다. 고정 Sensor Transform을 /tf로 계속 보내면 Network와 CPU를 낭비하고 책임 구조도 흐려집니다.
반대로 움직이는 Joint를 Static으로 보내면 RViz에서 Sensor나 Link가 처음 위치에 고정됩니다.
ros2 topic info /tf --verbose
ros2 topic info /tf_static --verbose
ros2 topic echo /tf_static --once
8. CLI로 Static Transform 발행
학습용으로 base_link에서 앞 0.2m, 위 0.15m에 laser_frame을 만듭니다.
ros2 run tf2_ros static_transform_publisher \
--x 0.20 --y 0.0 --z 0.15 \
--roll 0.0 --pitch 0.0 --yaw 0.0 \
--frame-id base_link \
--child-frame-id laser_frame
배포판에 따라 위치 인자 형식도 지원되지만 명명 Option 형식이 읽기 쉽고 실수를 줄입니다. 설치된 Version의 문법을 확인합니다.
ros2 run tf2_ros static_transform_publisher --help
Quaternion으로 지정할 때 회전 없음은 (x=0, y=0, z=0, w=1)입니다. (0,0,0,0)은 유효한 단위 Quaternion이 아닙니다.
9. Python Static Transform Broadcaster
# tf_lab_py/static_sensor_broadcaster.py
import rclpy
from geometry_msgs.msg import TransformStamped
from rclpy.node import Node
from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster
class StaticSensorBroadcaster(Node):
def __init__(self):
super().__init__('static_sensor_broadcaster')
self.broadcaster = StaticTransformBroadcaster(self)
transform = TransformStamped()
transform.header.stamp = self.get_clock().now().to_msg()
transform.header.frame_id = 'base_link'
transform.child_frame_id = 'laser_frame'
transform.transform.translation.x = 0.20
transform.transform.translation.y = 0.0
transform.transform.translation.z = 0.15
transform.transform.rotation.x = 0.0
transform.transform.rotation.y = 0.0
transform.transform.rotation.z = 0.0
transform.transform.rotation.w = 1.0
self.broadcaster.sendTransform(transform)
self.get_logger().info('base_link → laser_frame 발행')
def main(args=None):
rclpy.init(args=args)
node = StaticSensorBroadcaster()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
실제 Robot에서는 Sensor 장착 위치를 URDF에 정의하고 robot_state_publisher가 발행하도록 하는 편이 구조 관리에 좋습니다. 같은 관계를 URDF와 별도 Static Publisher에서 중복 발행하지 마십시오.
10. Python Dynamic Transform Broadcaster
아래 Node는 odom → base_link가 원을 따라 움직이는 학습용 Transform을 20Hz로 발행합니다.
# tf_lab_py/moving_robot_broadcaster.py
import math
import rclpy
from geometry_msgs.msg import TransformStamped
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
class MovingRobotBroadcaster(Node):
def __init__(self):
super().__init__('moving_robot_broadcaster')
self.broadcaster = TransformBroadcaster(self)
self.theta = 0.0
self.timer = self.create_timer(0.05, self.tick)
def tick(self):
self.theta += 0.01
transform = TransformStamped()
transform.header.stamp = self.get_clock().now().to_msg()
transform.header.frame_id = 'odom'
transform.child_frame_id = 'base_link'
transform.transform.translation.x = math.cos(self.theta)
transform.transform.translation.y = math.sin(self.theta)
transform.transform.translation.z = 0.0
# 평면 Yaw를 Quaternion으로 변환
transform.transform.rotation.z = math.sin(self.theta / 2.0)
transform.transform.rotation.w = math.cos(self.theta / 2.0)
self.broadcaster.sendTransform(transform)
def main(args=None):
rclpy.init(args=args)
node = MovingRobotBroadcaster()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Dynamic Transform에는 측정 또는 추정 시각을 정확히 넣습니다. 단순히 Callback 수신 시각을 넣으면 Sensor 처리와 Network 지연이 숨겨질 수 있습니다.
11. URDF와 robot_state_publisher
Link와 Joint가 많은 Robot은 Transform을 Code에 하나씩 쓰지 않고 URDF로 모델링합니다.
<robot name="tf_lab_robot">
<link name="base_link"/>
<link name="laser_frame"/>
<joint name="base_to_laser" type="fixed">
<parent link="base_link"/>
<child link="laser_frame"/>
<origin xyz="0.20 0.0 0.15" rpy="0.0 0.0 0.0"/>
</joint>
</robot>
robot_state_publisher는 Fixed Joint를 /tf_static으로, 움직이는 Joint를 /joint_states 값에 따라 /tf로 발행합니다.
ros2 topic echo /robot_description --once
ros2 topic echo /joint_states
ros2 node info /robot_state_publisher
URDF의 Joint Parent·Child 방향, Origin 단위와 Joint State 이름이 실제 Hardware Driver와 일치해야 합니다.
12. TF Tree를 그림으로 탐색
ros2 run tf2_tools view_frames
명령은 몇 초간 Transform을 수집한 뒤 보통 frames.pdf를 생성합니다. 배포판에 따라 출력 파일과 Option이 다를 수 있으므로 --help를 확인합니다.
ros2 run tf2_tools view_frames --help
그림에서 확인할 항목:
- Tree가 한 덩어리로 연결되어 있는가?
map → odom → base_link순서가 맞는가?- Sensor Frame이 올바른 Parent 아래 있는가?
- 예상하지 않은 Frame 철자나 Prefix가 있는가?
- 발행 주기와 가장 오래된 Transform이 정상인가?
- 한 Child를 여러 Node가 발행하지 않는가?
view_frames결과는 관찰한 시간 구간의 Snapshot입니다. 간헐적 발행 충돌은 Log와 반복 측정도 함께 봐야 합니다.
13. tf2_echo로 두 Frame 직접 확인
ros2 run tf2_ros tf2_echo base_link laser_frame
ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo map base_link
첫 번째가 Target, 두 번째가 Source입니다. 출력의 Translation과 Rotation이 예상과 맞는지 확인합니다.
tf2_echo base_link laser_frame
→ laser_frame의 원점이 base_link에서 어디에 있는지 확인
tf2_echo laser_frame base_link
→ 반대 방향 Transform이므로 Translation과 Rotation이 역변환됨
단순 Translation만 있을 때 부호가 반대가 되는 것은 정상입니다. Rotation이 포함되면 역변환은 Translation 부호만 바꾸는 것보다 복잡합니다.
14. tf2_monitor로 지연과 발행 상태 확인
ros2 run tf2_ros tf2_monitor
ros2 run tf2_ros tf2_monitor map base_link
확인할 내용:
- Transform Chain이 연결되는가?
- 평균·최대 Delay가 Sensor 허용 범위 안인가?
- Broadcaster 이름과 발행률이 예상과 맞는가?
- 특정 Edge만 오래되거나 불규칙하지 않은가?
Sensor Message는 30Hz인데 odom → base_link가 1Hz라면 빠른 이동에서 과거·미래 Extrapolation과 표시 흔들림이 발생할 수 있습니다. 발행률은 무조건 높이는 것이 아니라 Robot 속도, Sensor 주기, Network와 지연 요구에 맞춥니다.
15. RViz2를 TF 탐색기로 사용
rviz2
- Global Options의 Fixed Frame을
map또는odom으로 지정합니다. TFDisplay를 추가합니다.- Frames, Names와 Axes를 켭니다.
- LaserScan, PointCloud2와 RobotModel을 함께 추가합니다.
| RViz 증상 | 가능한 원인 |
|---|---|
| “Fixed Frame does not exist” | Frame 미발행, 이름 오타 |
| RobotModel 일부가 안 보임 | URDF·Joint State·연결 누락 |
| Scan이 Robot과 떨어짐 | Sensor Static Transform 오류 |
| Scan이 벽에 번짐 | Timestamp·Odometry 지연·Motion Distortion |
| Robot이 두 위치를 오감 | 중복 Transform 발행 |
| Map은 보이지만 Robot이 없음 | map → odom 또는 odom → base_link 단절 |
RViz Fixed Frame을 바꾸는 것은 Data 자체를 변경하는 것이 아니라 모든 Display를 어느 Frame 기준으로 그릴지 바꾸는 것입니다.
16. Buffer와 TransformListener
Listener가 /tf와 /tf_static을 받아 Buffer에 시간별 Transform을 저장합니다. Application은 Buffer에 질문합니다.
from rclpy.duration import Duration
from tf2_ros import Buffer, TransformListener
self.tf_buffer = Buffer(cache_time=Duration(seconds=20.0))
self.tf_listener = TransformListener(self.tf_buffer, self)
최신 Transform을 조회하는 예입니다.
from rclpy.time import Time
transform = self.tf_buffer.lookup_transform(
'map',
'laser_frame',
Time(),
timeout=Duration(seconds=0.2),
)
Time()은 “Buffer에 있는 가장 최신의 공통 시각”을 요청할 때 사용합니다. Sensor Data를 정확히 변환할 때는 Message의 header.stamp를 사용합니다.
17. 왜 측정 Timestamp로 조회해야 하는가
Robot이 1m/s로 이동하고 Sensor 처리와 Network에 200ms가 걸렸다면 수신 시각 Pose로 변환할 때 약 0.2m의 공간 오차가 생길 수 있습니다.
공간 오차 ≈ Robot 속도 × 시간 오차
0.2 m ≈ 1.0 m/s × 0.2 s
from rclpy.time import Time
measurement_time = Time.from_msg(message.header.stamp)
transform = self.tf_buffer.lookup_transform(
'map',
message.header.frame_id,
measurement_time,
timeout=Duration(seconds=0.2),
)
현재 시각을 임의로 넣거나 header.stamp를 수신 시각으로 덮어쓰면 지연을 숨기기 때문에 Sensor Fusion과 지도 정합이 틀어질 수 있습니다.
18. PointStamped를 목표 Frame으로 변환
# tf_lab_py/point_to_map.py
import rclpy
from geometry_msgs.msg import PointStamped
from rclpy.duration import Duration
from rclpy.node import Node
from tf2_ros import Buffer, TransformListener, TransformException
import tf2_geometry_msgs # Geometry Message 변환 등록
class PointToMap(Node):
def __init__(self):
super().__init__('point_to_map')
self.buffer = Buffer(cache_time=Duration(seconds=20.0))
self.listener = TransformListener(self.buffer, self)
self.subscription = self.create_subscription(
PointStamped, 'detected_point', self.on_point, 10)
def on_point(self, message):
if not message.header.frame_id:
self.get_logger().warning('frame_id가 없는 Point 거절')
return
try:
transformed = self.buffer.transform(
message,
'map',
timeout=Duration(seconds=0.2),
)
self.get_logger().info(
f'map point=({transformed.point.x:.2f}, '
f'{transformed.point.y:.2f}, '
f'{transformed.point.z:.2f})')
except TransformException as error:
self.get_logger().warning(f'TF 변환 실패: {error}')
def main(args=None):
rclpy.init(args=args)
node = PointToMap()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Message의 Stamp와 Frame을 그대로 유지한 채 buffer.transform()을 사용하면 수동 행렬 계산과 Quaternion 순서 실수를 줄일 수 있습니다.
19. Transform 가용성을 비동기로 기다리기
Subscriber Callback에서 긴 Blocking 조회를 반복하면 다른 Callback을 지연시킬 수 있습니다. Data 흐름이 많다면 Message Filter 또는 비동기 Transform 조회를 검토합니다.
future = self.tf_buffer.wait_for_transform_async(
'map',
message.header.frame_id,
Time.from_msg(message.header.stamp),
)
운영 설계에서는 다음 정책도 필요합니다.
- TF가 늦으면 얼마나 기다릴 것인가?
- Timeout Data를 버릴 것인가, 별도 Queue에 둘 것인가?
- Queue 최대 길이와 Data 최대 나이는 얼마인가?
- 오래된 Detection을 Robot Command에 사용하지 않을 것인가?
무한히 기다리거나 Queue를 무제한 키우면 결국 오래된 Data로 판단하게 됩니다.
20. TF 예외를 의미별로 구분
| 예외 | 의미 | 주 원인 |
|---|---|---|
| LookupException | Frame 이름을 Buffer가 모름 | 오타, Broadcaster 미실행 |
| ConnectivityException | 두 Frame은 알지만 경로 없음 | Tree가 여러 조각으로 단절 |
| ExtrapolationException | 경로는 있지만 요청 시각 값 없음 | 미래 TF, 너무 오래된 Data, Clock 불일치 |
| TransformException | TF 관련 예외의 공통 Base | 공통 오류 처리에 사용 |
from tf2_ros import (
ConnectivityException,
ExtrapolationException,
LookupException,
)
try:
transform = self.tf_buffer.lookup_transform(...)
except LookupException as error:
self.get_logger().warning(f'Frame 이름·발행자 확인: {error}')
except ConnectivityException as error:
self.get_logger().warning(f'Tree 단절 확인: {error}')
except ExtrapolationException as error:
self.get_logger().warning(f'Stamp·Clock·지연 확인: {error}')
배포판별 Python 예외 Import 위치와 API는 설치된 tf2_ros 문서를 확인하십시오.
21. 과거·미래 Extrapolation 이해
과거 Extrapolation
요청 시각이 Buffer의 가장 오래된 Transform보다 과거입니다. Sensor 처리 지연이 너무 크거나 Bag Data의 Stamp가 현재 TF Buffer와 맞지 않을 수 있습니다.
미래 Extrapolation
Message Stamp가 최신 TF보다 앞서 있습니다. Sensor Clock이 빠르거나 TF가 늦게 도착했거나 서로 다른 Clock Source를 사용할 수 있습니다.
Buffer 보관 구간: [──────────────]
과거 요청: ● [──────────────] → past extrapolation
정상 요청: [──────●───────]
미래 요청: [──────────────] ● → future extrapolation
Timeout을 조금 준다고 Clock 불일치가 해결되지는 않습니다. 일시적 전송 순서 차이는 기다림으로 흡수할 수 있지만, 지속적인 시계 Offset과 잘못된 Stamp는 원인을 수정해야 합니다.
22. Simulation Time과 rosbag
Simulation과 Bag 재생에서는 /clock을 사용하는 Node의 use_sim_time을 일관되게 설정합니다.
ros2 topic echo /clock --once
ros2 param get /point_to_map use_sim_time
ros2 param set /point_to_map use_sim_time true
Launch의 공통 YAML 예시입니다.
/**:
ros__parameters:
use_sim_time: true
Bag에 Sensor Topic만 있고 /tf, /tf_static, /clock 또는 필요한 Odometry가 없으면 재생 시 변환을 복원할 수 없습니다.
ros2 bag record \
/scan /odom /tf /tf_static /clock
실제 기록 대상은 System과 Clock 구성에 맞게 정합니다. /tf_static도 함께 기록하여 Sensor 장착 관계를 재현합니다.
23. 일부러 TF Tree 끊어 보기
실험 A: 정상 Tree
# Terminal A
ros2 run tf2_ros static_transform_publisher \
--x 0 --y 0 --z 0 --yaw 0 --pitch 0 --roll 0 \
--frame-id map --child-frame-id odom
# Terminal B
ros2 run tf2_ros static_transform_publisher \
--x 1 --y 0 --z 0 --yaw 0 --pitch 0 --roll 0 \
--frame-id odom --child-frame-id base_link
# Terminal C
ros2 run tf2_ros tf2_echo map base_link
실험 B: 중간 Frame 이름 오타
Terminal B의 Parent를 odm으로 바꿉니다.
--frame-id odm --child-frame-id base_link
map → odom Tree와 odm → base_link Tree가 분리됩니다. view_frames와 tf2_echo map base_link로 Connectivity 문제를 확인합니다.
실험 C: 중복 발행
같은 odom → base_link를 서로 다른 값으로 두 Process에서 발행하고 RViz에서 떨림을 관찰합니다. 실제 Robot에서는 위험할 수 있으므로 Simulation에서만 수행하고 실험 후 Process를 모두 종료합니다.
24. 실제 Robot의 TF 발행 책임표
| Transform | 일반적인 책임 Node | Data 원천 |
|---|---|---|
map → odom |
AMCL·SLAM·Localization | Map과 Sensor 정합 |
odom → base_link |
EKF·Odometry | Encoder, IMU, Visual Odometry |
base_link → sensor |
robot_state_publisher | URDF Fixed Joint |
base_link → moving_link |
robot_state_publisher | /joint_states |
| Earth·Fleet 전역 Frame | Localization/Fleet 구성 | GNSS·Survey 기준 |
System마다 정확한 Frame 구조는 다를 수 있지만 Owner는 하나로 정합니다. Driver, EKF와 Localization의 “publish TF” Option을 무작정 모두 켜지 말고 책임표를 먼저 작성합니다.
25. Sensor별 실제 적용
LiDAR
LaserScan.header.frame_id와 base_link → laser_frame의 장착 위치가 정확해야 Scan이 Map의 벽과 맞습니다. LiDAR가 기울었는데 평면 Scan으로 가정하면 장애물 위치가 틀어집니다.
Camera
camera_link와 camera_optical_frame을 구분합니다. Object Detection의 3D Point나 Ray가 어느 Frame인지 명시합니다.
IMU
IMU 축 방향과 imu_link Transform이 실제 장착 방향과 일치해야 합니다. Orientation Convention, Gravity 제거와 Covariance도 함께 확인합니다.
Manipulator
base_link → shoulder → elbow → wrist → tool0 Chain은 /joint_states와 URDF로 갱신합니다. End Effector와 Tool Center Point의 Static Offset을 별도 Frame으로 둡니다.
26. 이름과 Prefix 설계
Frame 이름은 Topic Namespace와 완전히 같은 방식으로 자동 분리된다고 가정하면 안 됩니다. Multi-Robot에서는 URDF Prefix와 각 Stack의 Frame Parameter를 일관되게 설계합니다.
robot1/map
robot1/odom
robot1/base_link
robot1/laser_frame
또는 공유 map 아래 Robot별 odom Tree를 둘 수 있습니다. 어떤 전략이든 Navigation, Sensor Message, URDF와 RViz Fixed Frame이 같은 계약을 사용해야 합니다.
Frame 이름 앞 Slash, 대소문자, 철자와 Prefix를 혼용하지 않습니다.
27. TF 진단 표준 순서
1. Frame 이름·Fixed Frame 확인
↓
2. view_frames로 Tree 연결 확인
↓
3. tf2_echo로 문제 Edge·Chain 값 확인
↓
4. tf2_monitor로 지연·발행률·Owner 확인
↓
5. Message frame_id·stamp와 Clock 확인
↓
6. URDF·Publisher Option·중복 발행 확인
ros2 node list
ros2 run tf2_tools view_frames
ros2 run tf2_ros tf2_echo map base_link
ros2 run tf2_ros tf2_monitor map base_link
ros2 topic info /tf --verbose
ros2 topic info /tf_static --verbose
ros2 topic echo /tf_static --once
ros2 param get /problem_node use_sim_time
Application Code부터 수정하지 말고 Graph와 실제 Message에서 사실을 먼저 확인합니다.
28. 증상별 빠른 진단
| 증상 | 가능성이 큰 원인 | 첫 확인 |
|---|---|---|
| Fixed Frame 없음 | 이름 오류·Broadcaster 미실행 | view_frames |
| 두 Frame 연결 불가 | 중간 Transform 누락 | tf2_echo |
| Robot 떨림 | 중복 Publisher | /tf --verbose와 Owner |
| Sensor가 몸체와 떨어짐 | Static 장착값 오류 | URDF Origin·tf2_echo |
| Sensor 흔적이 벽에 번짐 | Stamp·Odometry 지연 | tf2_monitor |
| Future Extrapolation | TF 지연·Clock Offset | 최신 Stamp·/clock |
| Past Extrapolation | 처리 지연·Buffer 부족 | Data Age·Cache Time |
| Bag에서만 실패 | TF·Static·Clock 미기록 | ros2 bag info |
| Multi-Robot Frame 혼합 | Prefix·Namespace 계약 불일치 | Frame Tree 전체 |
| 방향이 90°·180° 틀림 | 축 규약·Quaternion 오류 | URDF RPY와 Axes |
29. 흔한 실수와 교정
- Frame 없는 숫자를 사용한다. Message의
frame_id를 필수 계약으로 둡니다. - 수신 시각으로 Sensor Stamp를 덮는다. 실제 측정 시각을 유지합니다.
lookup_transform의 Target·Source를 반대로 쓴다. 문장으로 읽어 확인합니다.- Quaternion을 모두 0으로 둔다. 회전 없음은
w=1입니다. - 고정 관계를
/tf로 계속 보낸다. URDF 또는 Static Broadcaster를 사용합니다. - 움직이는 관계를 Static으로 보낸다. Timestamp와 함께 Dynamic 발행합니다.
- 같은 Child를 여러 Node가 발행한다. Transform별 Owner를 하나로 정합니다.
- Timeout만 늘려 Clock 오류를 숨긴다. Stamp와 Time Source를 맞춥니다.
mapPose를 Local 제어에 직접 사용한다. 연속성이 필요한 제어에는odom을 검토합니다.- Bag에 Sensor만 기록한다. TF, Static TF와 관련 Clock·Odometry를 함께 기록합니다.
- Frame Prefix와 Node Namespace를 같다고 생각한다. 별도 이름 계약을 검증합니다.
- RViz에서 안 보이면 Sensor 고장으로 판단한다. Fixed Frame과 TF Chain부터 확인합니다.
30. 실습 체크리스트
- [ ] REP-103의 X·Y·Z 방향을 설명할 수 있다.
- [ ] Target·Source 순서로
tf2_echo를 실행했다. - [ ] CLI와 Python으로 Static Transform을 발행했다.
- [ ] Python으로 Dynamic Transform을 발행했다.
- [ ]
view_frames로 Tree 그림을 생성했다. - [ ]
tf2_monitor로 지연과 발행률을 확인했다. - [ ] RViz2에서 TF Axes와 RobotModel을 표시했다.
- [ ] Buffer와 Listener로 최신 Transform을 조회했다.
- [ ]
PointStamped를 측정 Timestamp 기준으로 변환했다. - [ ] Lookup·Connectivity·Extrapolation 오류를 재현했다.
- [ ]
use_sim_time과/clock을 확인했다. - [ ] Bag에
/tf와/tf_static을 함께 기록했다. - [ ] 중복 Broadcaster를 찾아 제거했다.
- [ ] 실제 Robot의 Transform Owner 표를 작성했다.
31. 정리
TF2는 좌표를 단순히 더하고 빼는 도구가 아니라 Robot 전체가 공간과 시간을 공유하게 하는 기반 System입니다. 모든 공간 Data는 Frame과 측정 Timestamp를 가져야 하며, TF Tree는 Parent·Child 관계와 발행 책임이 일관되어야 합니다.
map은 전역 일관성, odom은 지역 연속성, base_link는 Robot 본체 기준을 담당합니다. 고정 장착 관계는 /tf_static, 움직이는 관계는 Timestamp가 있는 /tf로 제공하며 URDF, Odometry와 Localization의 책임을 중복시키지 않습니다.
문제가 생기면 view_frames → tf2_echo → tf2_monitor → Message Stamp·Clock → Publisher Owner 순서로 좁히십시오. TF 오류의 많은 부분은 복잡한 수학보다 이름, 누락, 중복 발행과 시간 불일치에서 발생합니다.
- TF2좌표 변환 시스템
- ROS 2에서 여러 Frame 사이의 Translation과 Rotation을 시간별로 보관하고 연결해 주는 Library와 통신 체계입니다.
- Frame좌표계
- 공간의 위치와 방향을 표현하기 위한 원점과 X·Y·Z 축의 기준입니다.
- Transform좌표 변환
- Parent Frame에서 본 Child Frame의 Translation과 Rotation 관계입니다.
- Parent Frame부모 좌표계
- Child Frame의 위치와 방향을 표현하는 상위 기준 Frame입니다.
- Child Frame자식 좌표계
- 하나의 Parent Frame에 대한 Transform으로 연결되는 하위 Frame입니다.
- map지도 좌표계
- 장기적으로 전역 일관성을 제공하지만 Localization 보정으로 불연속 변화가 가능한 World-fixed Frame입니다.
- odom오도메트리 좌표계
- 짧은 시간에는 연속적이지만 Encoder와 IMU 오차가 누적되어 장기 Drift할 수 있는 World-fixed Frame입니다.
- base_link로봇 본체 좌표계
- Robot 본체에 고정되어 함께 움직이며 Sensor와 Link 장착의 중심 기준이 되는 Frame입니다.
- base_footprint지면 투영 좌표계
- Robot 본체를 지면 평면에 투영하여 2D Navigation 기준으로 사용하는 Frame입니다.
- Static Transform정적 좌표 변환
- Sensor 장착 위치처럼 시간에 따라 변하지 않아 /tf_static으로 제공하는 Transform입니다.
- Dynamic Transform동적 좌표 변환
- Robot Pose나 Joint처럼 시간에 따라 변해 Timestamp와 함께 /tf로 계속 발행하는 Transform입니다.
- TransformBroadcaster변환 발행기
- Node가 알고 있는 Dynamic Transform을 TF2 Network에 발행하는 객체입니다.
- TransformListener변환 수신기
- /tf와 /tf_static을 구독해 Transform Data를 Buffer에 채우는 객체입니다.
- TF Buffer변환 버퍼
- 최근 시간 구간의 Transform을 저장하고 특정 시각의 Frame 관계를 조회·연결하는 저장소입니다.
- LookupException프레임 조회 예외
- 요청한 Frame 이름을 Buffer가 알지 못할 때 발생하는 TF2 오류입니다.
- ConnectivityException연결 예외
- 두 Frame을 알고 있지만 TF Tree에서 서로 연결하는 경로가 없을 때 발생하는 오류입니다.
- ExtrapolationException시간 외삽 예외
- Frame 경로는 있지만 요청한 과거 또는 미래 시각의 Transform이 Buffer에 없을 때 발생합니다.
- view_framesTF 트리 시각화 도구
- 일정 시간 Transform을 수집하여 Frame 연결과 발행 상태를 그림으로 생성하는 TF2 진단 도구입니다.
- tf2_echo변환 확인 도구
- 지정한 Target과 Source Frame 사이의 최신 Translation과 Rotation을 반복 출력하는 명령입니다.
- tf2_monitor변환 지연 감시 도구
- TF Chain, Broadcaster, 발행률과 평균·최대 Delay를 조사하는 진단 도구입니다.
연습 문제
- 좌표값에
frame_id와 Timestamp가 모두 필요한 이유를 설명하세요. - REP-103의 Robot Body 좌표축과 Camera Optical 축을 비교하세요.
- Transform의 Parent·Child와
lookup_transform(target, source, time)의 의미를 설명하세요. - TF Tree에서 같은 Child를 두 Node가 발행하면 어떤 증상이 나타날 수 있나요?
map,odom,base_link의 역할과 연속성 차이를 설명하세요.- Localization이 일반적으로
map → odom을 발행하는 이유는 무엇인가요? /tf와/tf_static의 Data와 QoS 동작 차이는 무엇인가요?- 회전 없는 Quaternion 값과 모두 0인 Quaternion의 차이는 무엇인가요?
view_frames,tf2_echo,tf2_monitor의 역할을 각각 설명하세요.- Sensor Message를 최신 Transform이 아니라 측정 Stamp로 변환해야 하는 이유는 무엇인가요?
- LookupException, ConnectivityException과 ExtrapolationException의 차이는 무엇인가요?
- Future Extrapolation과 Past Extrapolation의 원인을 각각 두 가지씩 쓰세요.
- Simulation과 rosbag에서
use_sim_time,/clock,/tf_static을 확인해야 하는 이유는 무엇인가요? - URDF, Odometry, EKF와 Localization의 Transform 발행 책임을 어떻게 나눌 수 있나요?
- RViz에서 LaserScan이 벽을 따라 번질 때 Frame부터 Sensor 시간까지 진단 순서를 설명하세요.
COMMUNITY
강의 댓글
질문과 학습 경험을 함께 나눠보세요.댓글을 불러오는 중입니다.