# tf2 Tools 활용 및 다양한 변환 예시

#### tf2 Tools 개요

tf2는 ROS2 환경에서 좌표계(Frames) 사이의 위치와 자세(Orientation)를 추적하고 변환(Transform)을 제공하기 위한 핵심 라이브러리다. 이를 효율적으로 사용하기 위해서는 여러 도구(tools)를 활용할 수 있는데, 이는 디버깅, 프레임 구조 확인, 실시간 관측 등을 가능하게 해준다. 대표적인 tf2 Tools로는 다음과 같은 것들이 있다.

* **tf2\_echo** 특정 두 프레임 사이의 변환 정보를 실시간으로 콘솔에 출력한다.
* **tf2\_monitor** 두 프레임 사이의 변환이 얼마나 업데이트되는지, 드롭이 있는지 등을 모니터링한다.
* **view\_frames** 현재 트랜스폼 트리(Transformation tree) 구조를 그래프로 표현하여 저장하고 확인한다.
* **rqt\_tf\_tree** GUI를 통해 현재 트랜스폼 트리를 직관적으로 확인하고 디버깅한다.

이러한 도구들은 소규모 로보틱스 프로젝트부터 대규모 자율주행까지 다양한 환경에서 tf2가 올바르게 동작하고 있는지 판단하는 데 큰 도움이 된다.

#### tf2\_echo의 활용

ROS2에서 제공하는 `tf2_echo` 노드는 특정 두 프레임(frame1, frame2) 간의 상대적인 위치와 자세를 쉽게 확인할 수 있게 해준다. 예를 들어, 로봇 베이스 프레임인 `base_link`에서 센서 프레임인 `camera_link`의 변환을 확인하고 싶다면, 다음과 같은 명령을 통해 콘솔에 실시간 출력할 수 있다.

```bash
ros2 run tf2_ros tf2_echo base_link camera_link
```

출력 예시는 보통 다음과 같은 구조로 나타난다.

```plaintext
At time 1672923276.123456789
- Translation: [x, y, z]
- Rotation: in Quaternion [x, y, z, w]
```

여기서 Translation과 Rotation(quaternion)은 각각 `base_link`에서 `camera_link` 프레임으로 가는 변환을 의미한다.

#### tf2\_monitor의 활용

`tf2_monitor`를 사용하면 특정 두 프레임 사이의 transform 업데이트 주기를 모니터링할 수 있다. 예를 들어, `map`과 `base_link` 프레임 사이의 정보가 얼마나 자주 업데이트되는지 확인하고 싶다면 다음과 같은 명령을 사용할 수 있다.

```bash
ros2 run tf2_ros tf2_monitor map base_link
```

이 명령어로 프레임 사이의 최소 지연, 최대 지연, 평균 지연, 패킷 드롭 여부 등을 확인할 수 있다. 이를 통해 TF의 대역폭 문제나 브로드캐스팅 주기 문제를 효율적으로 디버깅할 수 있다.

#### view\_frames를 이용한 트리 시각화

`view_frames` 도구는 ROS2에서 현재 broadcast되고 있는 모든 프레임의 구조를 .pdf 형태 등으로 시각화해 준다. 이 도구를 사용하기 위해서는 먼저 tf2가 서비스 중인 상태(즉, TF 트랜스폼 브로드캐스터가 동작 중)여야 한다.

```bash
ros2 run tf2_tools view_frames
```

위 명령어를 실행하면, 지정한 경로에 frames.pdf(혹은 frames.dot 등) 파일이 생성되며, 이 파일을 열어보면 트랜스폼 트리가 그래프로 나타난다. 프레임 간 연결 관계, 변환 여부, 브로드캐스팅 주기 등의 요소를 큰 그림에서 파악하기에 편리하다.

#### rqt\_tf\_tree를 통한 실시간 트리 확인

`rqt_tf_tree` 플러그인을 사용하면 GUI를 통해 현재 트랜스폼 트리를 실시간으로 확인할 수 있다.

```bash
ros2 run rqt_tf_tree rqt_tf_tree
```

이를 실행하면 노드와 프레임의 연결 관계가 GUI 상에 계층 구조로 표시된다. 또, 특정 프레임을 클릭하여 변환 정보 등을 확인할 수 있어, 개발 중 디버깅 시에 빠른 분석이 가능하다.

#### 3D 변환의 기초 수식

tf2가 처리하는 변환은 크게 평행이동(translation)과 회전(rotation)으로 분류된다.

* 평행이동 벡터: $\mathbf{t} = (t\_x, t\_y, t\_z)$
* 회전: 일반적으로 사원수(quaternion) 혹은 회전 행렬을 사용한다.

회전 행렬 $\mathbf{R}$은 직교 행렬로, 3차원에서 다음과 같은 특성을 갖는다.

$$
\mathbf{R} \in \mathbb{R}^{3 \times 3}, \quad \mathbf{R}^\mathsf{T} \mathbf{R} = \mathbf{I}
$$

로봇의 한 프레임에서 다른 프레임으로의 좌표 변환은 보통 다음과 같이 표현할 수 있다.

$$
\begin{bmatrix} \mathbf{p}' \ 1 \end{bmatrix} = \begin{bmatrix} \mathbf{R} & \mathbf{t} \ \mathbf{0}^\mathsf{T} & 1 \end{bmatrix} \begin{bmatrix} \mathbf{p} \ 1 \end{bmatrix}
$$

여기서 $\mathbf{p}$는 원래 프레임에서의 점 좌표, $\mathbf{p}'$는 변환된 프레임에서의 점 좌표를 의미한다. 결과적으로 4×4 동차변환행렬(Homogeneous transformation)을 통해 평행이동과 회전을 한 번에 표현할 수 있다.

#### 좌표계 표현 방식과 tf2 내부 구조

tf2는 ROS2에서 여러 좌표계를 동시에 다룰 수 있게 해주는 핵심 라이브러리다. 내부적으로 트랜스폼 트리를 관리하며, 사용자가 요청하는 두 프레임 사이의 변환을 계산해 제공한다. 이를 위해 tf2는 다음과 같은 구조적 특징을 가진다.

* **트랜스폼(Transform) 메시지** ROS2에서 트랜스폼은 `geometry_msgs/msg/TransformStamped` 메시지를 통해 표현된다. 해당 메시지는 3차원 평행이동(translation)과 사원수(quaternion) 회전(rotation) 정보를 포함한다.
* **브로드캐스터(TransformBroadcaster)** 한 노드가 특정 프레임 간 변환 정보를 생성해 네트워크에 퍼블리시하면, tf2의 다른 노드들이 이를 활용할 수 있다. 이를 담당하는 객체가 바로 `TransformBroadcaster`다.
* **리스너(Buffer 및 TransformListener)** 트랜스폼을 요청하는 노드는 tf2 버퍼(Buffer)에 이미 저장된 트랜스폼 정보를 활용하거나, 새로운 트랜스폼이 수신되면 이를 자동으로 업데이트한다. tf2는 내부적으로 여러 시간(Time)에 해당하는 트랜스폼을 캐싱하며, 쿼리 시점(Time)과 가장 근접한 변환을 반환해 준다.
* **시간 동기화** tf2는 메시지에 타임스탬프를 부여하여, 특정 시점의 프레임 간 변환을 재현할 수 있게 해준다. 즉, 과거 시점의 위치 관계가 필요하면 해당 시점의 트랜스폼을 적절히 보간하거나 외삽하여 사용할 수 있다.

#### tf2 Tools를 활용한 디버깅 단계별 시나리오

아래는 tf2 Tools를 활용하여 좌표계 문제를 해결해 나가는 일반적인 단계별 시나리오다.

1. tf2\_echo
   * 일단 특정 두 프레임 사이에 올바른 변환 값이 출력되는지 간단히 확인한다.
   * 예: `map -> base_link` 간 변환이 주기적으로 업데이트되는지, 제대로 계산되는지.
2. tf2\_monitor
   * 변환 주기가 불규칙하거나 누락(드롭)이 있는지 모니터링한다.
   * 업데이트 주기를 확인해 실제 시스템(예: 센서 주기, 제어 주기)과 동기화 상태가 맞는지 검증한다.
3. view\_frames
   * 전체 트랜스폼 트리를 PDF 등으로 시각화해서 구조적 오류(원형 참조, 불필요한 프레임 다중 연결 등)가 없는지 확인한다.
4. rqt\_tf\_tree
   * GUI로 트랜스폼 트리를 보며 실시간으로 디버깅한다.
   * 특정 프레임(예: 센서 링크)을 클릭해 트랜스폼의 세부 정보를 확인하고, 의도한 값과 다른지를 신속히 파악할 수 있다.

#### Euler 각과 사원수 변환

회전은 사원수(Quaternion)나 오일러(Euler) 각으로 표현할 수 있다. ROS2 tf2 내에서는 기본적으로 사원수를 쓰지만, 오일러 각과 상호 변환이 필요할 수도 있다.

**오일러 각**: 일반적으로 roll, pitch, yaw 3개의 회전으로 표현한다. 순서는 Z-Y-X (또는 다른 표준)에 의해 정의되며, 다음과 같은 3×3 회전 행렬을 얻을 수 있다.

$$
\mathbf{R}\_{z,y,x}(\psi, \theta, \phi) = \mathbf{R}\_z(\psi) \mathbf{R}\_y(\theta) \mathbf{R}\_x(\phi)
$$

**사원수(Quaternion)**: 사원수는 ${x, y, z, w}$ 형태로, 단위 사원수($\lVert q \rVert = 1$)로 사용된다. $tf2$에서는 사원수를 표준 회전 표현으로 채택한다. 오일러 각에서 사원수로 변환하는 공식은 다음과 같다.

$$
w = \cos\left(\frac{\phi}{2}\right)\cos\left(\frac{\theta}{2}\right)\cos\left(\frac{\psi}{2}\right)  + \sin\left(\frac{\phi}{2}\right)\sin\left(\frac{\theta}{2}\right)\sin\left(\frac{\psi}{2}\right)
\\
x = \sin\left(\frac{\phi}{2}\right)\cos\left(\frac{\theta}{2}\right)\cos\left(\frac{\psi}{2}\right)  - \cos\left(\frac{\phi}{2}\right)\sin\left(\frac{\theta}{2}\right)\sin\left(\frac{\psi}{2}\right)
\\
y = \cos\left(\frac{\phi}{2}\right)\sin\left(\frac{\theta}{2}\right)\cos\left(\frac{\psi}{2}\right)  + \sin\left(\frac{\phi}{2}\right)\cos\left(\frac{\theta}{2}\right)\sin\left(\frac{\psi}{2}\right)
\\
z = \cos\left(\frac{\phi}{2}\right)\cos\left(\frac{\theta}{2}\right)\sin\left(\frac{\psi}{2}\right)  - \sin\left(\frac{\phi}{2}\right)\sin\left(\frac{\theta}{2}\right)\cos\left(\frac{\psi}{2}\right)
$$

여기서 $\phi, \theta, \psi$는 각각 roll, pitch, yaw에 해당한다.

#### tf2로 다양한 변환 적용하기

tf2는 여러 노드 간 트랜스폼을 쉽게 주고받을 수 있도록 설계되어 있으므로, 다음과 같이 자유도가 많은(예: 로봇팔, UAV 등) 시스템에서도 효과적으로 사용할 수 있다.

1. **조인트(Join) 변환** 로봇팔에서는 여러 조인트(Servo)의 회전에 따라 변환이 연쇄적으로 일어난다. 각 조인트마다 고유한 프레임을 두고, tf2로 연결함으로써 전체 로봇팔 끝단(Effector) 위치를 쉽게 계산할 수 있다.
2. **비전 센서(Camera) 좌표** 카메라, LiDAR와 같은 센서는 고유한 프레임을 갖는다. 이를 로봇 본체(예: base\_link)와 tf2로 연결하면, 센서 데이터(영상, 점군 등)가 실제 월드 좌표계(예: map)로 매핑되거나, 반대로 로봇 좌표계로 변환되어 처리될 수 있다.
3. **다중 로봇** 여러 대의 로봇이 각각 독립된 트랜스폼 트리를 갖고 있을 때, 서로의 위치 관계를 한정된 글로벌 프레임(예: world)으로 통합할 수 있다. 예컨대, 한 로봇이 SLAM으로 추정한 `map` 프레임과 다른 로봇의 `map` 프레임을 정렬(Alignment)하여 하나의 월드 프레임으로 통합할 수 있다.

#### 고정 변환과 동적 변환

tf2에서 다루는 변환(Transform)은 크게 \*\*고정 변환(static transform)\*\*과 \*\*동적 변환(dynamic transform)\*\*으로 나눌 수 있다.

**고정 변환(Static Transform)**:

* 두 프레임 사이의 관계가 시간에 따라 변하지 않는 경우, 예를 들어 로봇의 베이스 링크와 실제 물리적으로 고정된 센서(카메라 등) 사이의 변환이다.
* 이러한 경우에는 `static_transform_publisher`를 사용하면 된다. ROS2에서 static transform은 트랜스폼에 대한 TF 토픽을 계속해서 퍼블리시하지 않고, 한 번만 퍼블리시해도 tf2가 이를 내부적으로 관리한다.
* 예:

```bash
ros2 run tf2_ros static_transform_publisher 0 0 1 0 0 0 base_link camera_link
```

위 명령은 base\_link에서 camera\_link 로의 위치 편차가 $x=0, y=0, z=1$, 회전은 $roll=0, pitch=0, yaw=0$임을 선언하며, 이는 변하지 않는다고 가정한다.

1. **동적 변환(Dynamic Transform)**
   * 시간에 따라 계속 변하는 프레임 간 변환 정보가 필요한 경우, 예컨대 이동 로봇의 `odom -> base_link` 변환 또는 로봇 팔의 조인트 변환 등이 이에 해당한다.
   * 이때는 `TransformBroadcaster`를 사용하여 일정 주기로 변환을 퍼블리시해야 한다.

#### TransformBroadcaster 예시(Python)

다음은 Python에서 `TransformBroadcaster`를 사용해 주기적으로 동적 변환을 퍼블리시하는 예시다.

```python
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
import math
import tf_transformations

class DynamicTransformPublisher(Node):
    def __init__(self):
        super().__init__('dynamic_tf_publisher')
        self.br = TransformBroadcaster(self)
        self.timer = self.create_timer(0.1, self.timer_callback)  # 10Hz
        self.theta = 0.0

    def timer_callback(self):
        t = TransformStamped()
        
        # 타임스탬프와 프레임 정의
        t.header.stamp = self.get_clock().now().to_msg()
        t.header.frame_id = 'odom'
        t.child_frame_id = 'base_link'

        # 이동 변환 (예: 원을 그리면서 움직이는 좌표)
        radius = 1.0
        self.theta += 0.1
        x = radius * math.cos(self.theta)
        y = radius * math.sin(self.theta)
        t.transform.translation.x = x
        t.transform.translation.y = y
        t.transform.translation.z = 0.0

        # 회전 변환 (예: yaw로만 회전한다고 가정)
        q = tf_transformations.quaternion_from_euler(0, 0, self.theta)
        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.br.sendTransform(t)

def main(args=None):
    rclpy.init(args=args)
    node = DynamicTransformPublisher()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    rclpy.shutdown()

if __name__ == '__main__':
    main()
```

* 핵심 포인트
  * `TransformBroadcaster` 객체(`self.br`)를 통해 매 주기마다 `TransformStamped` 메시지를 생성해 보내면, tf2가 `odom -> base_link` 변환을 추적할 수 있게 된다.
  * `$tf_transformations.quaternion_from_euler$` 함수를 통해 roll, pitch, yaw(오일러 각)를 사원수로 변환하여 `transform.rotation` 필드에 할당한다.

#### StaticTransformBroadcaster 예시(C++)

고정 변환을 C++로 퍼블리시하는 방법 예시:

```cpp
#include <memory>
#include <string>
#include <rclcpp/rclcpp.hpp>
#include <tf2_ros/static_transform_broadcaster.h>
#include <geometry_msgs/msg/transform_stamped.hpp>

using std::placeholders::_1;

class StaticTransformPublisher : public rclcpp::Node
{
public:
  StaticTransformPublisher()
  : Node("static_tf_publisher")
  {
    broadcaster_ = std::make_shared<tf2_ros::StaticTransformBroadcaster>(this);
    
    geometry_msgs::msg::TransformStamped static_transform;
    static_transform.header.stamp = now();
    static_transform.header.frame_id = "base_link";
    static_transform.child_frame_id = "camera_link";
    
    static_transform.transform.translation.x = 0.0;
    static_transform.transform.translation.y = 0.0;
    static_transform.transform.translation.z = 1.0;

    // 회전 없는 예시 (즉, Quaternion은 단위 회전)
    static_transform.transform.rotation.x = 0.0;
    static_transform.transform.rotation.y = 0.0;
    static_transform.transform.rotation.z = 0.0;
    static_transform.transform.rotation.w = 1.0;

    broadcaster_->sendTransform(static_transform);
  }

private:
  std::shared_ptr<tf2_ros::StaticTransformBroadcaster> broadcaster_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<StaticTransformPublisher>());
  rclcpp::shutdown();
  return 0;
}
```

* 핵심 포인트
  * `StaticTransformBroadcaster`는 반복적으로 퍼블리시하지 않아도 한 번 보내면 tf2가 계속 기억한다.
  * 정적인 변환을 매번 퍼블리시하는 것은 불필요하므로, 시스템 부담을 줄일 수 있다.

#### tf2에서 시간(Time) 다루기

tf2는 변환 정보를 $t\_0, t\_1, \dots$ 시간축을 따라 축적한다. 특정 시점의 트랜스폼이 필요한 경우 tf2 Buffer는 해당 시점과 가장 가까운(또는 동일한) 트랜스폼을 반환하게 된다. 이를 위해 tf2는 내부적으로 다음과 같은 방식을 사용한다.

1. Buffer
   * tf2가 모든 트랜스폼을 무작정 저장하는 것은 아니며, 일정 기간 동안만 정보를 캐싱한다. 파라미터에 따라 최대 캐싱 시간은 조정 가능하다.
2. lookupTransform
   * `lookupTransform(target_frame, source_frame, time_point)`와 같은 함수를 통해 특정 시점(`time_point`)에 대한 `source_frame` $\to$ `target_frame` 변환을 요청할 수 있다.
   * 과거 또는 미래 시점 변환이 필요하면 tf2가 내부적으로 보간(Interpolation) 또는 외삽(Extrapolation)하여 반환한다.
   * 예: 센서 데이터의 타임스탬프가 $t\_s$라면, `map` $\to$ `sensor_frame` 변환도 $t\_s$ 시점 기준으로 요청하여 정확한 월드 좌표계에서의 센서 위치를 얻을 수 있다.

#### 보간(Interpolation)과 외삽(Extrapolation)

* **보간(Interpolation)** 요청한 시점이 tf2가 갖고 있는 실제 트랜스폼 측정치(예: 브로드캐스트)들의 사이 시간에 해당할 때, tf2는 두 변환 사이를 적절히 보간하여 반환한다.
* **외삽(Extrapolation)** 보유하고 있는 최신 측정치 이후 시점의 변환을 요청하는 경우 발생하며, 데이터 유효 시간을 벗어난다고 판단되면 에러를 반환하거나 일정 범위 내에서 추정(Extrapolate)한다.

이런 메커니즘을 통해 tf2는 시간과 좌표계를 함께 다루며, 다양한 시나리오(로봇 센서 Fusion, SLAM, 조인트 모션 추정 등)에서 정확도 높은 3D 변환을 제공한다.

#### 다양한 변환 조합 사례

tf2는 여러 개의 프레임 간 변환을 실시간으로 추적할 수 있기 때문에, 복잡한 로봇 시스템에서도 전체 좌표계를 일관성 있게 관리할 수 있다. 다음은 일반적으로 많이 접할 수 있는 변환 조합 사례들이다.

**이동 로봇 + 센서 융합**:

* 자율주행 로봇에서 흔히 사용되는 구조는 다음과 같다.

$$
\texttt{map} \quad \longrightarrow \quad \texttt{odom} \quad \longrightarrow \quad \texttt{base\_link} \quad \longrightarrow \quad \texttt{sensor\_link}
$$

* $\texttt{map}$은 전역 지도를 의미하고, $\texttt{odom}$은 로봇의 주행 누적 정보를 추정하는 국소 좌표계다.
* 로봇 자체의 본체 프레임($\texttt{base\_link}$) 아래에 추가적으로 센서($\texttt{lidar\_link}$, $\texttt{camera\_link}$) 등이 달려 있어, 각각의 변환이 체인 형태로 연결된다.
* tf2를 통해 $\texttt{map} \to \texttt{sensor\_link}$ 변환을 직통으로 요청할 수 있으며, 내부적으로는 위 체인을 따라 변환을 자동으로 합성해 준다.

**로봇 팔(Kinematic Chain)**:

* 멀티 조인트 로봇 팔은 각 조인트마다 회전·슬라이딩 변환이 있으며, 이들이 순차적으로 연결되어 최종적으로 그리퍼(gripper) 등 끝단 이펙터의 위치/자세가 결정된다.
* 전형적인 예:

$$
\texttt{base\_link} \quad \rightarrow \quad \texttt{shoulder\_link} \quad \rightarrow \quad \texttt{elbow\_link} \quad \rightarrow \quad \texttt{wrist\_link} \quad \rightarrow \quad \texttt{end\_effector\_link}
$$

* tf2를 통해 이러한 변환 정보를 퍼블리시하면, 3D 시뮬레이터나 RViz 등에서 실시간으로 로봇 팔의 동작 상태를 확인할 수 있다.

**드론(UAV) 좌표계**:

* 드론은 6자유도(6 DOF)로 움직일 수 있으므로, 오일러 각 혹은 사원수를 활용한 회전 정보가 매우 중요하다.
* $\texttt{world}$ 프레임(또는 $\texttt{map}$) 아래에 $\texttt{drone\_base}$ 프레임이 존재하며, 드론 내부에 $\texttt{imu\_link}$, $\texttt{camera\_link}$ 등이 연결될 수 있다. 드론의 빠른 자세 변화에도 tf2가 시점별 변환을 적절히 보간해 주기 때문에, 센서 융합이나 영상처리에 용이하다.

#### tf2와 좌표계 변환 시 주의사항

다음과 같은 사항들을 숙지해 두면 tf2를 사용할 때 발생할 수 있는 오류를 방지할 수 있다.

1. 프레임 명명 규칙
   * 프레임 이름이 겹치거나 모호하게 지어지면 추후 디버깅이 어렵다. 로봇마다 고유한 접두어(prefix)를 사용하거나, 기능별로 명확한 naming convention을 설정한다.
2. 브로드캐스트 주기와 동기화
   * 동적 변환을 퍼블리시할 때, 지나치게 높은 주기로 퍼블리시하면 네트워크 부하가 발생할 수 있다. 로봇 상태 업데이트 주기와 비슷하거나 조금 여유 있는 수준(예: 10Hz, 30Hz 등)으로 조정한다.
3. 회전 변환(Euler vs. Quaternion) 불일치
   * 로봇 공학에서 $\texttt{roll}, \texttt{pitch}, \texttt{yaw}$로 표현할 때와, 사원수(Quaternion)로 표현할 때의 관용 차이가 있을 수 있다.
   * tf2 내부에는 사원수가 표준이므로, 오일러 각으로 각도 연산을 수행한 뒤 최종 퍼블리시 시점에는 사원수로 변환하여 송신한다.
4. 시간 축과 TF 데이터 유효성
   * tf2는 시간 정보를 기반으로 변환을 관리하므로, 센서 데이터의 타임스탬프와 TF 타임스탬프가 크게 어긋나면 변환을 제대로 얻을 수 없다.
   * ROS2 메시지 타임스탬프를 반드시 올바르게 설정해야 하며, 시뮬레이션 환경(예: Gazebo)에서도 시뮬레이션 타임(sim time)을 활용한다면 tf2와 동일하게 세팅해야 한다.

#### 다양한 시뮬레이션·시각화 연동

* RViz2
  * RViz2에서 TF 플러그인을 활성화하면, 현재 사용 중인 트랜스폼 목록을 자동으로 가져와 시각화한다.
  * 로봇 모델(URDF)과 결합하면, 움직이는 로봇을 3D 뷰로 디버깅할 수 있다.
* Gazebo
  * Gazebo 시뮬레이션에서 각 링크(Link)와 조인트(Joint)를 정의하면, ROS2를 통해 TF가 자동으로 퍼블리시된다. 이를 RViz2에서 구독해 실제 하드웨어 없이도 로봇 모션을 확인할 수 있다.
* rqt\_graph
  * tf2 관련 토픽(예: /tf, /tf\_static 등) 및 노드 간 연결 상태를 시각적으로 볼 수 있다.

#### mermaid를 통한 TF 트리 예시

아래는 간단한 TF 트리 구조를 다이어그램으로 표현한 예시다.

{% @mermaid/diagram content="flowchart LR
A\[map] --> B\[odom]
B --> C\[base\_link]
C --> D\[laser\_link]
C --> E\[camera\_link]" %}

* $\texttt{map} \to \texttt{odom}$ : 동적으로 업데이트될 수 있는 전역-국소 변환
* $\texttt{odom} \to \texttt{base\_link}$ : 로봇 주행 상태에 따른 변환
* $\texttt{base\_link} \to \texttt{laser\_link}$, $\texttt{camera\_link}$ : 로봇 본체와 센서 사이의 변환(고정 또는 동적)
