# 복수 객체를 위한 프레임 트리 구성 기초

#### tf2에서 프레임 트리의 개념

ROS2에서 여러 물리 객체(로봇의 조인트, 센서, 기타 주변 환경 객체 등)를 동시에 다루려면, 각 객체의 좌표계를 통합적으로 관리하고 서로 변환(transform)할 수 있는 체계가 필요하다. 이를 위해 tf2 패키지가 제공된다. tf2는 기본적으로 각 객체 또는 링크(link)에 대한 좌표계를 정의하고, 이들 사이의 관계를 ‘트리(Tree)’ 형태로 구성하여 손쉽게 변환 연산을 수행하도록 돕는다.

tf2에서의 프레임 트리는 루트(root)에 해당하는 기준 프레임으로부터 자식(child) 프레임으로 연결되는 방향성 그래프로 이해할 수 있다. 여기서 프레임(frame)이란 좌표계이며, 이를 노드(node)로 보고 변환(transform)을 간선(edge)으로 볼 수 있다. 예를 들어 로봇의 바닥을 기준으로 한 프레임(base\_link)에서 로봇 팔의 끝단(end\_effector)까지 이어지는 변환 정보를 트리 형태로 관리하면, 중간 링크들을 순서대로 거쳐가며 임의의 프레임 간 상대 위치와 자세를 계산할 수 있다.

#### 여러 객체를 다룰 때의 고려사항

복수의 객체를 다룰 경우, 다음과 같은 사항들을 고려해야 한다.

1. **루트 프레임 선택**: 각 객체가 공통으로 참조할 수 있는 전역 좌표계를 선택하거나, 특정 로봇의 base\_link를 상위 프레임으로 잡아 다른 객체들을 연결할 수도 있다. 일반적으로 `map` 또는 `odom` 프레임을 가장 상위로 두는 경우가 많다.
2. **변환 방송주기**: 객체가 많을수록 각 객체가 보유한 변환 정보를 주기적으로 방송하는 주기가 늘어나므로, 주기의 적절한 설정이 중요하다. 변환 간의 의존관계가 복잡할수록 주기가 중요하게 작용한다.
3. **중복이나 충돌 방지**: 서로 다른 노드에서 같은 이름의 프레임을 사용할 경우 tf2 브로드캐스터가 데이터를 혼동할 수 있다. 프레임 명을 철저히 구분하고, 브로드캐스터에서 동일한 이름을 쓰지 않도록 주의해야 한다.

#### 부모 프레임과 자식 프레임의 관계

tf2에서 $A$ 프레임이 $B$ 프레임의 부모라고 할 때, 이는 “$B$ 프레임 좌표계가 $A$ 프레임에 대해 어떻게 표현되는가”를 의미한다. 변환은 다음과 같은 형태로 정의할 수 있다:

$$
^{A}\mathbf{T}*{B} =  \begin{bmatrix} \mathbf{R}*{A \rightarrow B} & \mathbf{p}\_{A \rightarrow B} \ \mathbf{0} & 1 \end{bmatrix}
$$

여기서 $\mathbf{R}*{A \rightarrow B}$는 $A$ 프레임 좌표계에서 본 $B$ 프레임의 회전 행렬, $\mathbf{p}*{A \rightarrow B}$는 위치 벡터다. 예컨대 로봇의 base\_link가 부모 프레임이고, 로봇 팔의 조인트가 자식 프레임이라고 하면, 로봇 팔 조인트 프레임에서의 위치와 자세를 base\_link에 대해 정의하는 것이다. 이때 실제로는 “base\_link → 로봇 팔 조인트” 순으로 변환을 쌓아가게 되며, 최종적으로는 base\_link를 기준으로 로봇 팔 조인트가 어디쯤 있는지를 계산할 수 있다.

#### TF 정보 전파 기초

tf2에서 각 프레임 사이의 관계는 브로드캐스터(transform broadcaster)에 의해 지속적으로 퍼블리시(publish)된다. 이를 수신(subscribe)한 tf2 버퍼(buffer)는 일정 시간 동안 변환 정보를 캐시(cache)하여 필요할 때 즉시 검색할 수 있도록 한다. 예를 들어 시점 $t$에 “어떤 프레임 $A$와 프레임 $B$의 변환 관계를 알고 싶다”고 요청하면, tf2 버퍼는 과거에 축적된 변환 트리를 추적해 $A \rightarrow B$ 변환을 계산해 준다.

복수 객체가 동시에 존재할 때, 각 객체(또는 해당 객체를 담당하는 노드)가 자신이 책임지는 변환 관계를 개별적으로 브로드캐스트한다. 예컨대 카메라 센서 A에 대한 프레임 트리와 카메라 센서 B에 대한 프레임 트리가 서로 다른 노드에서 전파되더라도, 동일한 상위 프레임($map$이나 $odom$ 등)으로 합류하여 결국 하나의 거대한 tf 트리를 형성하게 된다.

#### 코드 예시

ROS2에서 여러 객체(또는 링크)에 대한 변환을 방송하려면, 각 변환을 브로드캐스터를 통해 퍼블리시한다. 예시로서 각 객체마다 노드를 생성하고, TransformStamped 메시지를 발행하는 방식을 생각해볼 수 있다(여기서는 C++ 예시):

```cpp
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/transform_stamped.hpp>
#include <tf2/LinearMath/Quaternion.h>
#include <tf2_ros/transform_broadcaster.h>

class MultiObjectTFBroadcaster : public rclcpp::Node
{
public:
  MultiObjectTFBroadcaster() 
  : Node("multi_object_tf_broadcaster"),
    tf_broadcaster_(std::make_shared<tf2_ros::TransformBroadcaster>(this))
  {
    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(100),
      std::bind(&MultiObjectTFBroadcaster::broadcastTransforms, this)
    );
  }

private:
  void broadcastTransforms()
  {
    geometry_msgs::msg::TransformStamped transform_stamped;

    // 예: 부모 프레임 = "base_link", 자식 프레임 = "object_1"
    transform_stamped.header.stamp = this->now();
    transform_stamped.header.frame_id = "base_link";
    transform_stamped.child_frame_id = "object_1";

    // position
    transform_stamped.transform.translation.x = 1.0;
    transform_stamped.transform.translation.y = 0.5;
    transform_stamped.transform.translation.z = 0.0;

    // orientation
    tf2::Quaternion q;
    q.setRPY(0.0, 0.0, 0.0); 
    transform_stamped.transform.rotation.x = q.x();
    transform_stamped.transform.rotation.y = q.y();
    transform_stamped.transform.rotation.z = q.z();
    transform_stamped.transform.rotation.w = q.w();

    tf_broadcaster_->sendTransform(transform_stamped);

    // 필요 시 다른 객체의 변환도 동일한 방식으로 추가 발행
  }

  rclcpp::TimerBase::SharedPtr timer_;
  std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
};

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

이처럼 “object\_1”, “object\_2” 등 각 객체 프레임에 대해 메시지를 발행하면, tf2는 이를 취합하여 각 프레임 사이 관계를 트리 형태로 유지한다.

#### 프레임 트리 시각화

여러 객체가 브로드캐스팅한 tf를 RViz2 같은 시각화 도구를 통해 확인할 수 있다. RViz2에서 TF 플러그인을 켜면, 프레임들이 트리 모양으로 연결되는 것을 시각적으로 볼 수 있다. 이를 통해 객체 배치가 원하는 대로 되어 있는지, 혹은 프레임 네이밍이나 부모-자식 구조가 올바른지 등을 손쉽게 확인할 수 있다.

예시로, 다음과 같은 다이어그램을 통해 복수 객체가 “base\_link”를 기준으로 연결된 모습을 간략하게 표현할 수 있다:

{% @mermaid/diagram content="flowchart TB
A\[base\_link] --> B\[object\_1]
A\[base\_link] --> C\[object\_2]
A\[base\_link] --> D\[sensor\_frame]" %}

#### 스태틱 트랜스폼 활용

여러 객체 중 어떤 객체나 링크의 상대적 위치와 자세가 고정(불변)되어 있는 경우가 있다. 예를 들어, 특정 센서가 로봇 본체에 단단히 장착되어 있어서 해당 센서 프레임은 시간에 따라 변하지 않는다고 가정하자. 이 경우엔 매번 주기적으로 트랜스폼을 방송(broadcast)할 필요 없이, 스태틱 트랜스폼(static transform)을 활용하는 것이 효율적이다.

ROS2에서 스태틱 트랜스폼 브로드캐스터는 특정 노드에서 매번 메시지를 발행하는 대신, “변환이 고정적”이라고 시스템에 알리는 역할을 한다. 실제 구현에서는 다음과 같은 ROS2 내장 명령어나, 직접 C++/Python 노드를 작성하여 스태틱 트랜스폼 메시지를 발행할 수 있다.

```bash
ros2 run tf2_ros static_transform_publisher x y z roll pitch yaw parent_frame child_frame
```

위 명령을 통해 스태틱 트랜스폼을 한 번 발행해 두면, tf2는 이 정보를 전역적으로 유지하고 모든 노드에 공유한다. 예를 들어 카메라가 절대 위치를 바꾸지 않는 환경에서 카메라와 로봇 몸체(base\_link)의 상대 위치를 정해 놓고 싶다면, 이 스태틱 트랜스폼이 유효하다.

#### 동적 트랜스폼과의 차이

스태틱 트랜스폼과 달리, 로봇의 조인트나 주변 환경이 계속 움직이는 경우에는 동적 트랜스폼(dynamic transform)이 필요하다. 로봇의 바퀴가 회전하거나 로봇 팔 조인트가 움직이는 상황에서는, 매번 변환 행렬이 달라지기 때문에 정기적으로 tf2에 해당 정보를 전송해야 한다.

* **스태틱 트랜스폼**: 한 번 설정하면 시간이 지나도 변하지 않는 변환 관계.
* **동적 트랜스폼**: 주기적으로 바뀌는 변환 관계(모터, 조인트, 이동 객체 등).

복수 객체를 다룰 때, 어떤 것은 스태틱 트랜스폼으로 처리하고 어떤 것은 동적 트랜스폼으로 처리할지 적절히 구분하면 자원 사용 측면에서 유리하다. 예컨대 수십 개 이상의 센서가 전혀 움직이지 않는 상태라면, 모두 스태틱 트랜스폼으로 구성해도 무방하다.

#### 시간 동기화와 변환 시점

ROS2 tf2는 변환 정보를 시간 단위로 저장한다. 따라서 메시지 헤더의 타임스탬프(header.stamp)가 다른 변환들 간에도 정확히 일치하거나, 최소한 tf2 버퍼가 허용하는 일정 범위 안에 있어야 신뢰성 있게 변환을 계산할 수 있다. 예를 들어 시점 $t$에서 “frame A → frame B” 변환이 필요할 때, tf2는 $t$ 근방에서 수신한 변환 정보를 사용하거나 보간(interpolation)·외삽(extrapolation)을 수행한다.

만약 여러 객체가 각각 다른 주파수, 다른 타임스탬프를 발행하고 있다면, tf2가 변환 연산을 수행하기 어려워질 수 있다. 특히 “lookup\_transform” 함수 등을 호출했을 때, 적절한 시점에 해당하는 변환 정보를 찾지 못해 오류가 발생하거나, 정확성이 떨어질 가능성이 있다. 이 때문에 복수 객체 환경에서는 다음과 같은 방법으로 시간 동기화를 고려한다.

1. **동일한 클록(clock) 소스 사용**: ROS2는 일반적으로 시스템 시간(steady clock)을 사용하므로, 되도록 모든 노드는 같은 시간 기준을 사용한다.
2. **메시지 타임스탬프 정확성**: 센서 드라이버 등에서 발행되는 메시지의 타임스탬프가 실제 수집 시각과 최대한 일치하도록 설정한다.
3. **tf2 캐시 기간 조정**: tf2 버퍼가 오래된 트랜스폼을 얼마 동안 저장할지를 결정하는 파라미터를 조정해, 느린 주기로 변환을 주는 객체가 있어도 변환 정보가 유지되도록 한다.

#### TransformStamped 메시지 구조

복수 객체 환경에서 tf2를 활용하려면, “어떤 부모 프레임과 자식 프레임을 어떻게 연결할 것인가”를 명시해야 한다. ROS2에서 이를 위해 사용하는 메시지 타입이 `geometry_msgs/msg/TransformStamped`이다. 주된 필드는 다음과 같다.

* `header`: 메시지 발행 시점과 기준이 되는 프레임 ID를 명시한다.
  * `header.stamp`: 변환이 유효한 시점을 나타낸다.
  * `header.frame_id`: 변환의 부모 프레임 이름이다.
* `child_frame_id`: 변환의 자식 프레임 이름이다.
* `transform`: 실제 변환에 대한 정보(위치, 자세).
  * `transform.translation`: x, y, z 축 방향의 위치값.
  * `transform.rotation`: 사원수(quaternion) 형태의 회전값.

예컨대 `base_link`를 부모 프레임으로, `object_1`을 자식 프레임으로 정의했을 때 메시지를 채우는 과정은 이전 코드 예시에서 확인할 수 있다.

#### 다중 로봇 또는 중첩 프레임 트리

복수 객체를 다루는 기본 원리는 “단일 객체+프레임 트리”가 여러 개 동시에 존재한다고 보면 된다. 예컨대 로봇이 2대 이상 존재하는 환경에서, 각 로봇마다 base\_link를 루트로 하는 프레임 트리가 있고, 이를 상위 좌표계(예: `map`, `odom`)에 연결하여 최종적으로 단일 트리 구조로 합쳐질 수 있다.

이때 고려해야 할 사항은 다음과 같다.

1. **프레임 네이밍(prefix) 전략** 여러 로봇에 동일한 링크 이름(base\_link 등)이 존재할 수 있다. 이를 구분하기 위해 “robot1/base\_link”, “robot2/base\_link” 식으로 prefix를 붙여서 프레임 이름이 충돌하지 않도록 한다.
2. **공통 상위 프레임의 선택** 모든 로봇이 움직이는 환경에서, 서로 동일한 `odom` 프레임이나 `map` 프레임을 공유한다면, 각 로봇의 위치와 자세를 상호 변환할 수 있다. 만약 완전히 독립적인 좌표계를 쓰고 싶다면, 서로 다른 “world\_1”, “world\_2” 등으로 구분하여 사용하기도 한다.
3. **브로드캐스터 충돌 방지** 다중 로봇 시스템에서 잘못된 설정으로 인해 서로 다른 노드가 동일 부모-자식 관계를 서로 다른 값으로 브로드캐스트할 수 있다. 이러한 충돌을 막기 위해 로봇 별로 프레임 네이밍 규칙을 엄격하게 준수하고, 브로드캐스터 코드를 중복해서 작성하지 않는 것이 중요하다.

#### TF 트리 확인 및 디버깅

복수 객체 프레임 트리가 복잡해질수록, 각 프레임이 올바르게 연결되었는지 확인하고 디버깅하는 과정이 필수적이다. ROS2에는 다음과 같은 유용한 도구와 명령어가 있다.

* **RViz2의 TF 플러그인**: 프레임 간 상대 위치를 시각적으로 확인할 수 있다.
* **view\_frames**(tf2\_tools 패키지) tf2 데이터가 정상적으로 브로드캐스트되고 있는지, 노드 간에 충돌이나 루프(loop)가 없는지, 트리가 끊어지지는 않았는지 등의 점검에 활용된다.

```bash
ros2 run tf2_tools view_frames
```

이 명령으로 생성된 “frames.pdf” 같은 파일을 열어 보면, 현재 시스템에서 tf2가 파악하고 있는 전체 프레임 트리와 변환 관계를 살펴볼 수 있다.

#### 좌표 변환 연산 예시

복수 객체가 존재할 때, 특정 프레임 간의 좌표 변환이 필요해지는 경우가 흔하다. 예를 들어 “object\_1” 프레임에서 측정된 좌표값을 “base\_link” 프레임 기준으로 변환하거나, “camera\_1”에서 본 물체를 “map” 프레임 기준으로 표시해야 할 수 있다. 이때 tf2의 변환 연산은 대체로 다음과 같은 과정을 거친다.

**tf2 버퍼에서 해당 시점의 변환 관계 조회**: “lookup\_transform” 등의 함수를 사용하여, 원하는 시간 $t$에서의 “부모 프레임 → 자식 프레임” 변환 정보를 가져온다.

**포인트(point) 또는 벡터(vector)에 대한 좌표 변환**: 변환 행렬(또는 변환 쿼터니언+평행 이동)을 이용해, 점의 좌표나 방향 벡터를 다른 프레임 기준으로 재계산한다. 예를 들어 “object\_1”에서 관측한 점 $\mathbf{p}*{object\_1}$을 “base\_link” 좌표계에서 표현하는 $\mathbf{p}*\text{base\_link}$로 바꾸려면,

$$
\mathbf{p}*\text{base\_link} = \mathbf{R}*{\text{base\_link} \rightarrow \text{object\_1}} \cdot \mathbf{p}*\text{object\_1} + \mathbf{p}*{\text{base\_link} \rightarrow \text{object\_1}}
$$

와 같은 식을 사용한다(실제로는 “object\_1”을 부모 프레임으로 둔 경우, 행렬 곱의 순서를 유의해야 한다).

**시간 동기화 고려**: 실제 관측 시점과 tf2 버퍼가 보관 중인 변환 시점이 다를 수 있으므로, 보간(interpolation)을 사용하거나 적절한 시점 범위 내에서 변환을 요청해야 한다.

#### 확장: 2D SLAM, 3D SLAM 환경에서의 다중 객체

맵 프레임($map$)을 기준으로 SLAM이 동작하는 경우, 로봇 위치를 추정하는 노드는 “map → odom” 또는 “map → base\_link” 변환을 계속 업데이트한다. 동시에, 로봇 팔이나 센서의 내부 링크에 대한 변환은 “base\_link”에서 파생된 프레임 트리를 유지한다. 여기에 “object\_1”, “object\_2” 등이 추가되면, 결국 다음과 같은 트리 구조가 형성될 수 있다.

{% @mermaid/diagram content="flowchart TB
A\[map] --> O\[odom]
O\[odom] --> B\[base\_link]
B\[base\_link] --> S\[sensor\_frame]
B\[base\_link] --> R\[robot\_arm\_link]
B\[base\_link] --> Obj1\[object\_1]
B\[base\_link] --> Obj2\[object\_2]" %}

이 SLAM 환경에서 시간이 지날수록 “map → odom” 변환이 동적으로 바뀔 수 있으므로, TF 브로드캐스터나 SLAM 노드가 해당 정보를 계속 갱신하게 된다. 이때 “object\_1”의 위치를 “map” 기준 좌표로 알고 싶다면,

* “map → odom” 변환 (SLAM 노드에서 방송)
* “odom → base\_link” 변환 (일반적으로 스태틱하거나 동적일 수 있음)
* “base\_link → object\_1” 변환 (사용자 정의 동적/스태틱)

이 세 개 변환을 체인으로 연결하여 계산한다.

#### tf2 Echo와 프레임 단위 확인

복수 객체 프레임 트리가 의도대로 설정되었는지 개별적으로 검증하려면, “tf2 Echo” 명령어도 유용하다. 특정 부모 프레임과 자식 프레임 간의 변환을 지속적으로 모니터링할 수 있기 때문이다.

```bash
ros2 run tf2_ros tf2_echo parent_frame child_frame
```

위 명령을 실행하면 콘솔에 각 시점별로 “parent\_frame → child\_frame” 변환이 출력된다. 예컨대 “base\_link → object\_1” 변환이 실제로 어떻게 변화하는지, 회전(quaternion) 값과 위치(translation) 값이 시간에 따라 어떤 값을 가지는지 확인 가능하다.

여러 객체가 있을 때는 각각의 브로드캐스트가 제대로 동작하는지 확인하기 위해, “tf2\_echo” 명령어를 여러 번 실행하거나, RViz2에서 동적으로 위치가 바뀌는 모습을 관찰하면서 디버깅을 진행할 수 있다.

#### 복잡한 프레임 트리에서의 성능 고려

tf2는 실시간 변환 계산을 위해 내부 버퍼를 운용하는데, 복수 객체가 많아질수록 저장해야 할 변환 정보가 방대해질 수 있다. 또한 변환 주기가 빨라지면, TF 메시지의 송수신 빈도가 높아져 네트워크 및 CPU 사용량이 증가한다. 다음 사항을 고려하면 성능상의 문제를 완화할 수 있다.

1. **필요한 프레임만 브로드캐스트** 사용하지 않는 불필요한 프레임이나, 임시로만 쓰이는 테스트용 프레임을 오래 유지하는 것은 부하만 늘린다. 꼭 필요한 변환만 유지하는 것이 좋다.
2. **출력 주기(출판 주기) 최적화** 변환이 거의 변하지 않는 프레임이라면, 너무 높은 주기로 발행할 필요가 없다. 센서나 조인트처럼 빠른 동작이 필요 없는 경우에는 주기를 높게 설정하여 과도한 메시지 생성을 피한다.
3. **스태틱 트랜스폼 적극 활용** 앞서 언급했듯이, 절대 움직이지 않는 프레임들은 static\_transform\_publisher를 사용해 한 번만 선언하고, 이후 주기적 발행을 하지 않는 편이 좋다.
4. **tf2 버퍼 저장 시간 조정** tf2 버퍼가 너무 오랜 시간의 데이터를 저장하면 메모리 사용량이 커진다. 반면 너무 짧게 저장하면 과거 시점 변환을 필요로 할 때 에러가 발생할 수 있다. 실제 사용 시나리오에 맞춰 적절한 캐시 시간을 설정한다.

#### 다중 센서 융합(fusion)과 tf2

여러 객체(특히 센서)가 존재하는 로봇 시스템에서, 센서 융합(센서 퓨전)을 진행할 때도 tf2가 중추적인 역할을 한다. 예를 들어 카메라와 LiDAR가 각각 다른 위치와 자세로 장착되어 있을 때, 서로 다른 프레임으로 들어오는 관측 데이터를 일관된 월드 좌표계(예: “map”)에 투영하려면 각 센서 프레임 간 변환이 정확히 설정되어 있어야 한다.

* **카메라 센서**: “camera\_link” 등으로 불리는 프레임에서 2D 이미지를 전달.
* **LiDAR 센서**: “lidar\_link” 등에서 레이저 스캔(2D) 또는 포인트 클라우드(3D)를 전달.
* **통합**: tf2를 통해 “camera\_link”와 “lidar\_link”를 공통 프레임(“base\_link”, “map” 등)으로 환산하여, 두 센서 데이터를 하나의 좌표계에서 처리.

복수 객체 환경이라고 해서 로봇 본체만 제한적으로 다루는 것이 아니라, 별도의 고정형 LiDAR 스캐너나 외부 트래킹 시스템(모션 캡쳐 장치 등)이 등장할 수도 있다. 이 경우에도 “그러한 센서의 월드 좌표계”를 tf2 트리에 편입하면, 전체 시스템 차원에서 일관된 데이터 해석이 가능해진다.

#### 프레임 이름 규칙(Best Practice)

앞서 언급한 프레임 네이밍(prefix) 전략을 좀 더 일반화하면 다음과 같은 규칙을 들 수 있다.

1. **일반적인 루트 프레임** “map”, “odom”, “base\_link” 등 이미 관습적으로 쓰이는 이름은 가급적 표준을 따른다.
2. **객체 구분을 위한 접두사** 로봇이 여러 대일 때는 “robot1/”, “robot2/”처럼 prefix를 붙이거나, “robot1\_base\_link” 등으로 풀어서 이름을 만든다.
3. **센서명 규칙** 카메라가 여러 대면 “camera\_front”, “camera\_rear”, “camera\_left” 등 위치나 용도 정보를 네이밍에 반영한다.
4. **대소문자 및 밑줄/슬래시 사용** ROS2에서는 프레임 이름에 주로 소문자, 밑줄(“\_”), 슬래시(“/”)를 많이 쓴다. 공백이나 특수문자는 피하는 것이 권장된다.

#### 복수 객체 구성 예시 요약

마지막으로, 여러 객체(로봇, 센서, 기타 물체)가 함께 있는 환경에서 자주 쓰이는 tf2 구조를 간단히 요약해 보면, 다음과 같은 형태가 일반적이다.

{% @mermaid/diagram content="flowchart TB
M\[map]
O1\[odom\_robot1]
O2\[odom\_robot2]
R1\[robot1/base\_link]
R2\[robot2/base\_link]
R1C1\[robot1/camera\_link]
R2L1\[robot2/lidar\_link]

```
M --> O1
M --> O2
O1 --> R1
R1 --> R1C1
O2 --> R2
R2 --> R2L1" %}
```

* “map”을 전역 좌표계로 설정하고, 각 로봇별 “odom”이 여기에 붙는다.
* 로봇 내부에는 “base\_link”부터 시작해 센서 프레임, 조인트 프레임 등을 계층적으로 연결한다.
* 고정된 물체나 움직임이 없는 장애물은 스태틱 트랜스폼으로 연결할 수도 있다.
