659.25 TransformBroadcaster의 rclpy 사용법
1. 개요
TransformBroadcaster는 tf2_ros 패키지에서 제공하는 핵심 클래스로서, 동적 좌표 변환(dynamic transform)을 /tf 토픽으로 발행하는 기능을 담당한다. Python 클라이언트 라이브러리인 rclpy를 통하여 TransformBroadcaster를 활용하면, 로봇 시스템 내에서 시간에 따라 변화하는 좌표 프레임 간의 관계를 실시간으로 발행할 수 있다. 본 절에서는 tf2_ros.TransformBroadcaster의 rclpy 기반 사용법을 체계적으로 기술한다.
2. 필수 모듈 및 의존성 설정
2.1 Python 임포트
TransformBroadcaster를 rclpy 환경에서 사용하기 위해서는 다음의 모듈을 임포트하여야 한다.
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
rclpy는 ROS2 Python 클라이언트 라이브러리이며, tf2_ros.TransformBroadcaster는 동적 변환 발행자 클래스이다. geometry_msgs.msg.TransformStamped는 좌표 변환 메시지의 데이터 구조를 제공한다.
쿼터니언 변환을 위하여 추가적으로 다음의 모듈을 임포트할 수 있다.
from tf_transformations import quaternion_from_euler
tf_transformations 패키지가 설치되어 있지 않은 경우, scipy를 사용한 대안적 방법도 가능하다.
from scipy.spatial.transform import Rotation
2.2 package.xml 의존성
package.xml 파일에 다음의 의존성을 선언하여야 한다.
<exec_depend>rclpy</exec_depend>
<exec_depend>tf2_ros_py</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>tf_transformations</exec_depend>
2.3 setup.py 설정
Python 패키지의 setup.py에서 진입점(entry point)을 등록한다.
entry_points={
'console_scripts': [
'dynamic_tf_broadcaster = my_robot_tf.dynamic_tf_broadcaster:main',
],
},
3. TransformBroadcaster 클래스의 인터페이스
3.1 클래스 생성
tf2_ros.TransformBroadcaster는 rclpy.node.Node 인스턴스를 인자로 받아 생성된다.
self.tf_broadcaster = TransformBroadcaster(self)
여기서 self는 rclpy.node.Node를 상속한 클래스의 인스턴스이다.
3.2 주요 메서드
TransformBroadcaster의 핵심 메서드는 sendTransform()이다. 이 메서드는 단일 TransformStamped 메시지 또는 TransformStamped 메시지의 리스트를 인자로 받는다.
# 단일 변환 발행
self.tf_broadcaster.sendTransform(transform_stamped)
# 복수 변환 발행
self.tf_broadcaster.sendTransform([transform_stamped_1, transform_stamped_2])
단일 변환과 복수 변환 모두 동일한 sendTransform() 메서드를 통하여 처리된다. 복수 변환 발행 시 모든 변환은 하나의 tf2_msgs/TFMessage 메시지에 포함되어 /tf 토픽으로 전송된다.
4. 기본 사용 패턴
4.1 노드 클래스 내부에서의 초기화
가장 일반적인 사용 패턴은 rclpy.node.Node를 상속한 사용자 정의 클래스 내부에서 TransformBroadcaster를 멤버 변수로 선언하고 초기화하는 것이다.
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
from tf_transformations import quaternion_from_euler
import math
class DynamicTFBroadcaster(Node):
def __init__(self):
super().__init__('dynamic_tf_broadcaster')
self.tf_broadcaster = TransformBroadcaster(self)
self.timer = self.create_timer(0.1, self.broadcast_timer_callback) # 10Hz
def broadcast_timer_callback(self):
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = 'robot_base'
# 병진 성분 설정
t.transform.translation.x = 1.0
t.transform.translation.y = 2.0
t.transform.translation.z = 0.0
# 회전 성분 설정 (쿼터니언)
q = quaternion_from_euler(0, 0, 1.57) # Roll, Pitch, Yaw (라디안)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
self.tf_broadcaster.sendTransform(t)
self.get_clock().now().to_msg()를 통하여 현재 ROS 시간을 builtin_interfaces.msg.Time 메시지 형식으로 변환한다. use_sim_time 파라미터가 활성화된 경우, 시뮬레이션 시간이 반환된다.
4.2 main 함수 구성
def main(args=None):
rclpy.init(args=args)
node = DynamicTFBroadcaster()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
rclpy.spin()을 통하여 노드를 실행하면, 타이머 콜백이 주기적으로 호출되어 좌표 변환을 발행한다. KeyboardInterrupt 예외를 처리하여 Ctrl+C에 의한 안전한 종료를 보장한다.
5. TransformStamped 메시지 구성
5.1 헤더 (Header) 설정
TransformStamped 메시지의 header 필드는 변환의 시간 정보와 부모 프레임 식별자를 포함한다.
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'parent_frame'
stamp 필드에는 변환이 유효한 시각을 설정한다. Python에서는 self.get_clock().now()가 rclpy.time.Time 객체를 반환하므로, .to_msg() 메서드를 호출하여 메시지 형식으로 변환하여야 한다.
5.2 프레임 식별자 설정
t.header.frame_id = 'world' # 부모 프레임
t.child_frame_id = 'sensor_link' # 자식 프레임
frame_id는 부모 프레임(parent frame)을, child_frame_id는 자식 프레임(child frame)을 지정한다. 이 두 필드는 변환 트리(transform tree)에서 프레임 간 계층 관계를 정의한다.
5.3 병진 (Translation) 설정
t.transform.translation.x = 0.5 # 미터 단위
t.transform.translation.y = 0.0
t.transform.translation.z = 0.3
병진 벡터는 부모 프레임 원점에서 자식 프레임 원점까지의 변위를 나타내며, 단위는 미터(m)이다.
5.4 회전 (Rotation) 설정
회전은 쿼터니언(quaternion)으로 표현한다. tf_transformations 패키지의 quaternion_from_euler() 함수를 활용하면 오일러 각(Euler angle)으로부터 쿼터니언을 편리하게 생성할 수 있다.
from tf_transformations import quaternion_from_euler
q = quaternion_from_euler(roll, pitch, yaw) # 라디안 단위
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
quaternion_from_euler() 함수는 Roll-Pitch-Yaw 순서의 오일러 각을 입력으로 받아 [x, y, z, w] 순서의 쿼터니언 배열을 반환한다. 인자의 단위는 라디안(radian)이다.
5.4.1 scipy를 사용한 대안적 방법
tf_transformations 패키지가 사용 불가능한 환경에서는 scipy를 활용할 수 있다.
from scipy.spatial.transform import Rotation
r = Rotation.from_euler('xyz', [roll, pitch, yaw], degrees=False)
q = r.as_quat() # [x, y, z, w] 순서
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
회전이 없는 경우에는 항등 쿼터니언(identity quaternion)을 설정한다.
t.transform.rotation.x = 0.0
t.transform.rotation.y = 0.0
t.transform.rotation.z = 0.0
t.transform.rotation.w = 1.0
6. 복수 변환의 동시 발행
하나의 노드에서 여러 좌표 변환을 동시에 발행하여야 하는 경우, 리스트를 사용하여 복수의 변환을 일괄 전송할 수 있다.
def broadcast_multiple_transforms(self):
transforms = []
t1 = TransformStamped()
t1.header.stamp = self.get_clock().now().to_msg()
t1.header.frame_id = 'base_link'
t1.child_frame_id = 'left_wheel'
t1.transform.translation.x = 0.0
t1.transform.translation.y = 0.15
t1.transform.translation.z = 0.0
t1.transform.rotation.w = 1.0
transforms.append(t1)
t2 = TransformStamped()
t2.header.stamp = self.get_clock().now().to_msg()
t2.header.frame_id = 'base_link'
t2.child_frame_id = 'right_wheel'
t2.transform.translation.x = 0.0
t2.transform.translation.y = -0.15
t2.transform.translation.z = 0.0
t2.transform.rotation.w = 1.0
transforms.append(t2)
self.tf_broadcaster.sendTransform(transforms)
이 방식은 네트워크 효율성 측면에서 유리하다. 복수의 변환이 하나의 TFMessage 메시지에 포함되므로, 개별적으로 발행하는 것에 비하여 통신 오버헤드가 감소한다.
7. 구독자 콜백에서의 변환 발행
외부 센서 데이터나 토픽 메시지를 수신한 후, 해당 데이터를 기반으로 좌표 변환을 발행하는 패턴이 빈번하게 사용된다.
from nav_msgs.msg import Odometry
class OdomTFBroadcaster(Node):
def __init__(self):
super().__init__('odom_tf_broadcaster')
self.tf_broadcaster = TransformBroadcaster(self)
self.odom_sub = self.create_subscription(
Odometry,
'odom',
self.odom_callback,
10
)
def odom_callback(self, msg):
t = TransformStamped()
t.header.stamp = msg.header.stamp
t.header.frame_id = 'odom'
t.child_frame_id = 'base_link'
t.transform.translation.x = msg.pose.pose.position.x
t.transform.translation.y = msg.pose.pose.position.y
t.transform.translation.z = msg.pose.pose.position.z
t.transform.rotation = msg.pose.pose.orientation
self.tf_broadcaster.sendTransform(t)
이 예제에서는 오도메트리(odometry) 메시지를 수신할 때마다 odom 프레임에서 base_link 프레임으로의 동적 변환을 발행한다. 변환의 타임스탬프는 수신한 메시지의 타임스탬프를 그대로 사용하여 시간적 일관성을 보장한다.
8. 파라미터를 활용한 동적 구성
rclpy의 파라미터(parameter) 메커니즘을 활용하면, 변환 발행의 동작을 런타임에 동적으로 구성할 수 있다.
class ParameterizedTFBroadcaster(Node):
def __init__(self):
super().__init__('parameterized_tf_broadcaster')
# 파라미터 선언
self.declare_parameter('parent_frame', 'world')
self.declare_parameter('child_frame', 'robot_base')
self.declare_parameter('publish_frequency', 10.0)
self.declare_parameter('x_offset', 0.0)
self.declare_parameter('y_offset', 0.0)
self.declare_parameter('z_offset', 0.0)
self.tf_broadcaster = TransformBroadcaster(self)
frequency = self.get_parameter('publish_frequency').value
period = 1.0 / frequency
self.timer = self.create_timer(period, self.timer_callback)
def timer_callback(self):
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = self.get_parameter('parent_frame').value
t.child_frame_id = self.get_parameter('child_frame').value
t.transform.translation.x = self.get_parameter('x_offset').value
t.transform.translation.y = self.get_parameter('y_offset').value
t.transform.translation.z = self.get_parameter('z_offset').value
t.transform.rotation.w = 1.0
self.tf_broadcaster.sendTransform(t)
이 패턴을 사용하면 런치 파일이나 명령행에서 프레임 이름, 발행 주기, 변위 값 등을 유연하게 설정할 수 있다.
9. 발행 주기와 타이머 구성
동적 변환은 반드시 일정한 주기로 발행되어야 한다. tf2_ros.Buffer는 캐시 기간(cache duration) 내에 유효한 변환만을 조회하므로, 발행이 중단되면 변환 조회 시 LookupException이 발생한다.
# 100ms 주기 (10Hz)
self.timer = self.create_timer(0.1, self.broadcast_timer_callback)
타이머 주기의 선택은 응용의 요구 사항에 의존한다. 일반적으로 다음의 기준을 고려한다.
| 응용 유형 | 권장 발행 주기 | 비고 |
|---|---|---|
| 저속 이동 로봇 | 10–20 Hz | 실내 서비스 로봇 등 |
| 고속 이동 로봇 | 20–50 Hz | 자율 주행 차량 등 |
| 로봇 팔 관절 | 50–100 Hz | 매니퓰레이터 관절 변환 |
| 고정밀 응용 | 100 Hz 이상 | 진동 보상 등 |
Python은 C++에 비하여 실행 속도가 느리므로, 100Hz 이상의 고주기 발행이 필요한 경우에는 rclcpp 기반 구현을 고려하여야 한다.
10. 전체 예제: 회전하는 좌표 프레임 발행
다음은 시간에 따라 z축 주위로 회전하면서 원형 궤적을 따르는 자식 프레임을 발행하는 전체 예제이다.
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
from tf_transformations import quaternion_from_euler
import math
class RotatingFrameBroadcaster(Node):
def __init__(self):
super().__init__('rotating_frame_broadcaster')
self.tf_broadcaster = TransformBroadcaster(self)
self.timer = self.create_timer(0.05, self.timer_callback) # 20Hz
self.angle = 0.0
self.get_logger().info('Rotating frame broadcaster started.')
def timer_callback(self):
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = 'rotating_frame'
# 원형 궤적 상의 병진
radius = 2.0
t.transform.translation.x = radius * math.cos(self.angle)
t.transform.translation.y = radius * math.sin(self.angle)
t.transform.translation.z = 0.0
# z축 회전
q = quaternion_from_euler(0, 0, self.angle)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
self.tf_broadcaster.sendTransform(t)
self.angle += 0.02 # 약 0.02 라디안/프레임
if self.angle > 2.0 * math.pi:
self.angle -= 2.0 * math.pi
def main(args=None):
rclpy.init(args=args)
node = RotatingFrameBroadcaster()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
이 예제에서 rotating_frame은 world 프레임의 원점을 중심으로 반지름 2.0미터의 원형 궤적을 따라 이동하면서, 동시에 z축 주위로 회전한다.
11. QoS 정책
TransformBroadcaster는 내부적으로 /tf 토픽에 대한 발행자(publisher)를 생성하며, 기본 QoS(Quality of Service) 프로파일은 다음과 같다.
| QoS 항목 | 기본값 |
|---|---|
| Reliability | RELIABLE |
| Durability | VOLATILE |
| History | KEEP_LAST |
| Depth | 100 |
동적 변환의 /tf 토픽은 VOLATILE 내구성(durability)을 사용한다. 이는 발행자가 연결되기 이전의 메시지를 구독자가 수신하지 않음을 의미한다.
12. rclcpp와 rclpy 구현의 주요 차이점
| 항목 | rclcpp (C++) | rclpy (Python) |
|---|---|---|
| 생성자 인자 | *this 또는 shared_from_this() | self |
| 타임스탬프 취득 | this->get_clock()->now() | self.get_clock().now().to_msg() |
| 쿼터니언 생성 | tf2::Quaternion::setRPY() | quaternion_from_euler() |
| 메모리 관리 | std::unique_ptr 또는 std::shared_ptr | Python 가비지 컬렉션 |
| 실행 성능 | 고성능, 저지연 | 상대적으로 높은 지연 |
| 적합한 용도 | 고주기 발행, 실시간 제어 | 프로토타이핑, 저주기 발행 |
Python 구현에서는 .to_msg() 호출이 필수적이라는 점에 특히 유의하여야 한다. self.get_clock().now()의 반환값은 rclpy.time.Time 객체이므로, TransformStamped 메시지의 stamp 필드에 직접 대입할 수 없다.
13. 예외 처리 및 주의사항
13.1 프레임 ID 유효성
frame_id와 child_frame_id는 빈 문자열이어서는 안 된다. 빈 프레임 ID로 변환을 발행하면 경고(warning) 메시지가 출력되며, 변환 트리의 무결성이 훼손될 수 있다.
13.2 순환 변환 방지
변환 트리는 비순환 방향 그래프(Directed Acyclic Graph, DAG) 구조를 유지하여야 한다. 순환 구조가 형성되면 변환 조회 시 무한 루프에 빠질 위험이 있으므로, 프레임 간 관계를 사전에 설계하여야 한다.
13.3 타임스탬프의 일관성
발행하는 변환의 타임스탬프는 단조 증가(monotonically increasing)하여야 한다. 과거 시간의 타임스탬프를 가진 변환을 발행하면, tf2_ros.Buffer의 보간(interpolation) 로직에서 예기치 않은 결과가 발생할 수 있다.
13.4 GIL(Global Interpreter Lock)의 영향
Python의 GIL로 인하여 rclpy 기반 노드에서는 고주기 발행 시 성능 제약이 발생할 수 있다. 특히 콜백 함수 내에서 복잡한 연산을 수행하는 경우, 타이머의 실제 발행 주기가 설정값보다 느려질 수 있다. 이러한 경우 MultiThreadedExecutor의 사용을 고려하거나, 연산 집약적인 부분을 rclcpp로 분리하는 것이 권장된다.
14. 런치 파일에서의 실행
TransformBroadcaster 노드를 런치 파일에서 실행하는 방법은 다음과 같다.
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='my_robot_tf',
executable='dynamic_tf_broadcaster',
name='dynamic_tf_broadcaster',
output='screen',
parameters=[{
'parent_frame': 'world',
'child_frame': 'robot_base',
'publish_frequency': 10.0,
}],
),
])
런치 파일의 parameters 필드를 통하여 노드의 파라미터를 설정할 수 있다.
15. 참고 자료
- ROS2 공식 문서, “Writing a tf2 broadcaster (Python)”, https://docs.ros.org/en/humble/Tutorials/Intermediate/Tf2/Writing-A-Tf2-Broadcaster-Py.html
- tf2_ros API 문서, https://docs.ros2.org/latest/api/tf2_ros/
- tf_transformations 패키지, https://github.com/DLu/tf_transformations
- REP 103 – Standard Units of Measure and Coordinate Conventions, https://www.ros.org/reps/rep-0103.html
- REP 105 – Coordinate Frames for Mobile Platforms, https://www.ros.org/reps/rep-0105.html
- ROS2 Humble Hawksbill