# tf2 구조와 동작 원리

#### tf2란 무엇인가

ROS2에서 좌표 변환(Transform)을 다루기 위한 핵심 라이브러리는 **tf2**이다. 이는 서로 다른 노드들이 주고받는 좌표계 사이의 변환을 일관성 있게 유지하도록 도와주는 “분산” 시스템으로, 다양한 노드에서 동시에 발생하는 좌표 변환 요청을 효율적으로 처리한다. 예를 들어 로봇 조인트(링크) 간 상대적인 위치, 카메라와 센서 간의 좌표계, 그리고 지도(world) 좌표계에 대한 로봇 위치 등을 쉽게 연결해 준다.

#### 좌표계와 변환(Transform)

tf2는 ‘좌표계(Frame)’들을 트리(Tree) 형태로 구성하고, 각 노드는 자기 부모(parent) 좌표계와의 상대적인 변환 정보를 유지한다. 변환은 일반적으로 3차원에서의 회전과 병진(translation)으로 표현되며, 이를 수학적으로 나타내면 다음과 같은 4×4 동차 좌표(homogeneous coordinate) 행렬로 표현할 수 있다.

$$
\mathbf{M} = \begin{bmatrix}
r\_{11} & r\_{12} & r\_{13} & t\_x \\
r\_{21} & r\_{22} & r\_{23} & t\_y \\
r\_{31} & r\_{32} & r\_{33} & t\_z \\
0 & 0 & 0 & 1
\end{bmatrix}
$$

여기서 $r\_{ij}$는 회전 성분, $t\_x, t\_y, t\_z$는 병진 성분을 뜻한다. 회전은 쿼터니언(quaternion)이나 오일러(Euler) 각도로도 표현할 수 있지만, tf2 내부적으로는 쿼터니언을 주로 사용한다.

#### tf2의 핵심 컴포넌트

tf2의 구조는 크게 다음 세 가지 요소를 중심으로 동작한다.

1. **`TransformBroadcaster`**
   * 특정 좌표계 간의 변환을 주기적으로 브로드캐스팅(방송)하는 역할을 한다.
   * 예: “odom” 좌표계 대비 “base\_link” 좌표계, “base\_link” 대비 “camera\_link” 좌표계 등.
2. **`TransformListener`**
   * 브로드캐스팅된 변환 정보를 받아서 내부의 버퍼에 저장한다.
   * 예: “base\_link”에서 “camera\_link”로 좌표를 변환해야 할 때, 해당 정보를 빠르게 얻을 수 있도록 준비해둔다.
3. **Buffer (tf2::BufferCore)**
   * 변환 정보를 시간(time)과 함께 관리하는 캐시(cache)로, 과거 정보도 일정 시간 동안 유지하여 과거 시점의 변환도 조회할 수 있다.
   * 이 버퍼 덕분에 센서 데이터의 타이밍이 어긋나도, 메시지의 타임스탬프(timestamp)에 맞는 적절한 변환을 찾아 적용할 수 있다.

#### 트리 구조의 장점

tf2에서 각 좌표계를 트리 구조로 구성하면, 트리의 어떤 두 노드(좌표계) 간에도 유일한 경로가 성립한다. 예를 들어, “map → odom → base\_link → camera\_link”와 같은 경로가 있다면, “map”과 “camera\_link” 사이의 변환은 경로상 변환 행렬들의 곱으로 구할 수 있다.

아래는 예시로 “Frame A → Frame B → Frame C” 형태로 연결되는 단순 트리를 나타낸 다이어그램이다.

{% @mermaid/diagram content="graph LR
A((Frame A)) --> B((Frame B))
B --> C((Frame C))" %}

* “Frame A” 좌표계에서 “Frame B” 좌표계로 변환: $^{A}\mathbf{T}\_{B}$
* “Frame B” 좌표계에서 “Frame C” 좌표계로 변환: $^{B}\mathbf{T}\_{C}$
* “Frame A”에서 “Frame C”로 가는 전체 변환:

$$
^{A}\mathbf{T}*{C}  = ; ^{A}\mathbf{T}*{B} \times ; ^{B}\mathbf{T}\_{C}
$$

이처럼 단순히 두 변환을 곱해 줌으로써, 우리가 원하는 어떤 두 좌표계 사이도 쉽게 잇는 것이 tf2 동작의 핵심 원리 중 하나다.

#### 시간 정보의 활용

tf2는 ROS2 메시지의 타임스탬프를 적극 활용한다. 예를 들어 센서 측정 시점과 로봇 상태(조인트 정보, odom 등)를 브로드캐스트하는 시점이 어긋날 수 있으나, tf2는 같은 시간대(time stamp)에 해당하는 변환을 적절히 추출해 준다.

**타임 트래블(Time Travel)**: 과거 변환 조회

$$
\mathbf{p}*\text{sensor}(t\_1)  \xrightarrow\[\text{과거 변환}]{t\_1}  \mathbf{p}*\text{world}(t\_1)
$$

센서가 $t\_1$ 시점에 측정한 값이 어떤 월드 좌표계에서의 좌표와 대응되는지, 과거 변환 정보를 조회하여 해결할 수 있다.

**퓨전(Fusion)**: 여러 센서 데이터 동기화

서로 다른 시점에 측정된 여러 센서 데이터를 하나의 기준 시점으로 맞추어 결합해야 할 때, tf2의 시간 기반 캐시를 사용하면 합리적으로 조합할 수 있다.

#### tf2 내부 동작 과정

tf2는 크게 **(1)** 변환 정보 생성(브로드캐스트) → **(2)** 변환 정보 수신(리스닝) → **(3)** 변환 정보 캐싱(Buffer)에 따른 조회(lookup) 과정으로 요약할 수 있다. 이 과정이 매우 빠르고 반복적으로 수행되어, 여러 노드가 동시에 다양한 변환을 요청해도 안정적으로 응답할 수 있다.

1. **TransformBroadcaster**가 일정 간격으로 특정 변환 정보를 브로드캐스트한다.
2. **TransformListener**가 모든 브로드캐스트 메시지를 수신하고, 이를 내부 **Buffer**에 적절한 타임스탬프와 함께 저장한다.
3. 변환을 요청하는 노드는 **lookupTransform(…)** 등의 API로 Buffer에서 필요한 시점의 변환 정보를 얻는다.

#### TransformBroadcaster의 작동

* **주기적 업데이트**: 변환 정보는 주로 로봇 조인트각이나 odom, IMU 등에서 주어지는 동적인 값에 따라 달라진다. 예를 들어 로봇이 움직이면 “odom → base\_link” 변환이 계속 바뀌게 된다. 이 정보를 필요한 주기로 “tf” 토픽을 통해 방송한다.
* **ROS2 인터페이스**: C++에서는 `tf2_ros::TransformBroadcaster` 클래스를, Python에서는 `tf2_ros.TransformBroadcaster`를 사용한다.
* **메시지 포맷**: 브로드캐스터가 송신하는 메시지는 주로 `geometry_msgs/msg/TransformStamped` 형식을 사용한다. 여기에는 $t\_x, t\_y, t\_z$ 병진(translation)과 quaternion($x, y, z, w$) 회전 정보, 그리고 부모/자식 좌표계 이름, 타임스탬프가 담긴다.

```cpp
// C++ 예시 (간단 예제 코드 스니펫)
geometry_msgs::msg::TransformStamped transform_stamped;
transform_stamped.header.stamp = this->now();
transform_stamped.header.frame_id = "odom";
transform_stamped.child_frame_id = "base_link";
transform_stamped.transform.translation.x = ...;
transform_stamped.transform.translation.y = ...;
transform_stamped.transform.translation.z = ...;
transform_stamped.transform.rotation.x = ...;
transform_stamped.transform.rotation.y = ...;
transform_stamped.transform.rotation.z = ...;
transform_stamped.transform.rotation.w = ...;

tf_broadcaster_->sendTransform(transform_stamped);
```

위 코드를 통해 방송된 변환 정보는 ROS2 네트워크 상에서 “/tf” 토픽(또는 “/tf\_static”) 등을 통해 모든 Listener에게 전달된다.

#### TransformListener의 작동

* **버퍼에 저장**: TransformListener는 **Buffer** 객체와 결합되어, 들어오는 모든 변환 정보를 시간 순으로 관리한다. 이를 통해 나중에 특정 시점의 변환을 조회할 수 있다.
* **Static vs Dynamic**: 변환 중에 고정된(transform이 바뀌지 않는) 좌표 변환은 “static\_transform\_publisher” 또는 static 브로드캐스트로 관리한다. 이러한 변환은 “/tf\_static” 토픽으로 별도 송신되어, 주기적으로 재전송할 필요가 없다. 대표적인 예로 카메라와 LiDAR 센서의 상대 위치(링크)가 바뀌지 않는 경우가 있다.
* **비동기 처리**: tf2는 ROS 콜백과 별도의 내부 쓰레드를 통해 변환 정보를 수신하고 저장하기 때문에, 실제 사용자 코드가 동작하는 동안에도 버퍼에 변환이 축적된다.

#### BufferCore의 역할

* **시간별 변환 저장**: BufferCore는 각 프레임 쌍(예: “odom” ↔ “base\_link”)에 대해 시간 축 상에 변환 정보를 기록한다. 이때 사용되는 자료구조는 일반적으로 “프레임 그래프 + 시간에 따른 변환 목록” 형태다.
* 변환 인터폴레이션(Interpolation): 만약 사용자가 특정 시점 $t$의 변환을 요청했는데, Buffer에 저장된 정보가 $t\_1 < t < t\_2$ 같은 형태로만 존재하면, $t$ 시점의 변환을 $t\_1$과 $t\_2$ 사이를 보간(interpolate)하여 근사적으로 구해준다.
  * 예: $t\_1=2.0s$ 시점 변환과 $t\_2=2.02s$ 시점 변환 사이에 $t=2.01s$를 요청하면, 회전 및 병진 벡터를 선형 또는 SLERP(Spherical Linear Interpolation) 방식으로 보간한다.
* **extrapolation 방지**: 요청된 시점이 Buffer에 기록된 시간 범위를 벗어나면 “ExtrapolationException”을 발생시켜 경고한다. 과도하게 과거(또는 미래) 시점에 대한 변환을 요청하면 잘못된 변환을 적용할 위험이 있기 때문이다.

#### Transform Lookup 예시

tf2는 “lookupTransform” 함수를 통해 원하는 시점에 임의의 부모-자식 프레임 변환을 얻어온다. 예를 들어 “base\_link” → “camera\_link” 변환을 가져오려면, 내부적으로는 다음 과정을 거친다.

1. “base\_link”에서 상위 프레임(예: “odom”)까지의 변환들을 단계별로 찾는다.
2. “camera\_link”에서 상위 프레임(예: “base\_link”)까지의 변환을 역연산으로 취합하거나, 더 높은 공통조상(common ancestor)까지 추적해 합성한다.
3. 최종적으로 두 프레임 사이의 변환 행렬을 곱해 얻는다.

이를 간단히 나타내면

$$
^{\text{base\_link}}\mathbf{T}\_{\text{camera\_link}}
=====================================================

\Bigl( ^{\text{base\_link}}\mathbf{T}*{\text{odom}} \Bigr)^{-1}
\times
\Bigl( ^{\text{camera\_link}}\mathbf{T}*{\text{odom}} \Bigr)
$$

와 같이 내부적으로 계산될 수도 있다(트리 구조에 따라 다를 수 있음).

#### TF 메타정보

* **Parent-Child Relationship**: 매 순간 “child\_frame\_id”는 그 상위 프레임 “frame\_id”에 대해 정의된다. 로봇 모델에서 각 링크가 부모 링크와 자식 링크로 연결되는 것과 유사하다.
* **타임스탬프**: 메시지에 포함된 $t$ 시점은 곧 “이 변환이 유효한 시간대”를 의미한다. tf2는 이 정보를 바탕으로 일정 시간 동안 버퍼에 변환을 저장하고 관리한다(ROS 파라미터로 설정 가능).

#### 주요 예외 처리(TransformExceptions)

tf2에서 변환을 요청할 때, 내부적으로 여러 단계의 검증 과정을 거친다. 트리 연결 관계, 시점(time stamp) 유효성 등이 만족되지 않으면 예외가 발생한다. C++ API 기준으로 예외 클래스는 `tf2::TransformException`을 상속받는 형태이며, 대표적인 종류는 다음과 같다.

1. **LookupException**
   * 요청한 프레임을 찾을 수 없거나, 프레임들 간의 관계가 TF 트리 상에서 전혀 연결되지 않았을 때 발생한다.
   * 예: “map” 좌표계와 “some\_unknown\_frame” 좌표계가 서로 연결되지 않은 상태에서 변환을 요청한 경우.
2. **ExtrapolationException**
   * 시간 범위를 벗어나는 변환을 요청할 때 발생한다. 예컨대, 이미 TF 버퍼가 오래된 시간 정보는 폐기했는데, 그 과거 시점을 요청하거나, 아직 미래 시점(예측 불가능) 변환을 요구한 경우에 생긴다.
   * 예: 버퍼가 최근 10초 치만 유지하는 설정인데, 20초 전 시점의 변환을 요구하는 경우.
3. **ConnectivityException**
   * 트리 상에서 부모-자식 프레임이 이론적으로 연결되었다고 해도, 특정 시점에서 불완전한 연결(예: 임시적으로 브로드캐스팅이 끊긴 구간)로 인해 경로를 못 찾는 경우이다.
   * 예: 센서 데이터를 브로드캐스트하는 노드가 일시적으로 정지해, 프레임이 끊겨 있으면 발생 가능.
4. **InvalidArgumentException**
   * 잘못된 인자 값(예: 음수 시간, 불가능한 쿼터니언 등)을 사용하거나 API 계약에 맞지 않는 입력이 들어왔을 때 발생한다.

개발 시에는 이러한 예외를 적절히 `try{ ... } catch(...){ ... }`로 처리해야 하며, 메시지 로그를 통해 어떤 예외가 발생했는지 확인하는 것이 중요하다.

#### 비동기 변환 처리: waitForTransform

tf2에서 변환을 “즉시” 가져오려 할 경우, 트리나 시간 정보가 아직 준비되지 않아 예외가 발생할 수 있다. 이를 완화하기 위해, 특정 시간까지 변환이 유효해지기를 기다리는 `waitForTransform` 인터페이스가 제공된다.

사용 예:

```cpp
if (buffer.waitForTransform("target_frame", "source_frame", time, tf2::durationFromSec(0.5))) {
    // 변환이 준비됨
    geometry_msgs::msg::TransformStamped ts;
    ts = buffer.lookupTransform("target_frame", "source_frame", time);
    // 변환 사용
} else {
    // 타임아웃. 변환이 준비되지 않음
}
```

주의 사항:

* `waitForTransform` 후에 곧바로 `lookupTransform`을 호출해도, 실제로는 시점이 약간의 차이가 있어 여전히 예외가 발생할 수 있다.
* tf2 노드 특성상, 무한정 대기하는 구조는 동시성 문제를 일으킬 수도 있으니, 적절한 타임아웃을 설정하고 실패 시 재시도 혹은 대안을 마련해야 한다.

#### TransformableCallbacks (콜백 기반 변환)

tf2는 특정 프레임 쌍이 특정 시점에 연결될 때까지 “알림(Callback)”을 받도록 하는 기능도 제공한다(주로 ROS1 tf에서 지원되었으나, ROS2에서도 유사하게 구현 가능).

동작 흐름:

* 사용자 코드가 “A 프레임에서 B 프레임으로 $t$ 시점의 변환이 준비되면, 콜백 함수를 호출해 달라”는 식으로 등록한다.
* 내부적으로 tf2가 계속 트리를 모니터링하다가 변환이 가능해지는 순간, 콜백 함수를 실행한다.

이러한 방식을 사용하면, 변환이 준비되지 않아 발생하는 예외를 앱 레벨에서 매번 확인하지 않아도 된다는 장점이 있다. 다만 구현 복잡도와 디버깅 측면에서 주의를 기울여야 한다.

#### TF2 툴과 디버깅

tf2 환경에서 좌표계 변환이 제대로 이루어지고 있는지 확인하기 위해, 여러 가지 도구(tools)가 제공된다.

**tf2\_echo**:

* CLI(커맨드 라인)에서 두 프레임 사이의 변환 정보를 실시간 출력해 준다.
* 예:

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

위 명령을 통해 “base\_link” ↔ “camera\_link”간 변환 값(병진, 회전, 타임스탬프 등)을 즉시 확인할 수 있다.

**tf2\_monitor**:

* 수신된 변환 정보의 통계(주파수, 누락 여부, 지연 등)를 보여주는 툴이다.
* 예:

```bash
ros2 run tf2_tools tf2_monitor
```

출력 결과에는 “highest transform rate”나 “most delayed transform” 같은 정보가 요약되어 나타난다.

**view\_frames** (ROS1 시절의 rqt\_tf\_tree, ROS2에서는 유사 툴 사용):

* TF 트리 구조를 그래프로 시각화해 준다.
* 예:

```bash
ros2 run tf2_tools view_frames
```

실행 후 생성된 PDF나 그래프 이미지를 통해 트리 구조와 연결 관계, 현재 브로드캐스팅 중인 프레임들을 한눈에 파악할 수 있다.

**rviz2 TF 플러그인**:

* RViz2에서 TF 데이터를 시각적으로 확인 가능하다.
* “Displays” 목록에서 “TF”를 추가하면, 각 프레임의 좌표축이 화면에 표시되어, 실제 로봇 모델과 겹쳐서 변환 상태를 실시간으로 살펴볼 수 있다.

#### 네임스페이스와 멀티 로봇

tf2는 동일한 토픽(주로 “/tf”, “/tf\_static”)을 사용하기 때문에, 여러 대의 로봇이 있을 경우 좌표계 이름 충돌이 발생할 수 있다. 이때는 ROS2 네임스페이스를 적극 활용하여 충돌을 방지한다.

* **멀티 로봇 예**: 로봇 A는 “/robot1/base\_link” 프레임, 로봇 B는 “/robot2/base\_link” 프레임처럼 구분.
* **별도 브로드캐스트 토픽**: 만약 서로 전혀 상관없는 여러 로봇 간 TF를 동시에 사용하지 않아도 된다면, 독립된 노드 네임스페이스나 토픽 리맵을 통해 메시지 혼선을 줄일 수 있다.

#### 성능 최적화와 주의 사항

* **필요 이상으로 높은 브로드캐스트 주기 지양**: 예를 들어, 변환이 거의 변하지 않는 고정된 프레임을 높은 주기로 송신하면, 불필요한 네트워크 부담이 생긴다.
* **버퍼 크기와 시간 관리**: tf2가 저장하는 시간 범위를 과하게 늘리면 메모리를 많이 사용한다. 로봇 환경에서 10초\~30초 정도 유지가 일반적이나, 실제 애플리케이션에 맞춰 조정해야 한다.
* **Thread Safety**: tf2 내부는 멀티쓰레드로 동작하기 때문에, 동시에 여러 변환 조회가 들어오면 잠금(lock)을 사용한다. 따라서 지나치게 빈번한 조회는 시스템 부하를 높일 수 있다.
