AEGLOSLAB ROS 2 Jazzy 대전

제5부 좌표·모델

5.1 tf2 아키텍처 — 버퍼·리스너·브로드캐스터

5.1.1 이 장에서 배우는 것 / 선수 지식

로봇은 공간 안의 물체이고, 여러 부분으로 이루어져 각자 움직인다. 카메라가 본 점, 라이다가 측정한 거리, 바퀴가 돈 각도 — 이 값들은 전부 서로 다른 좌표계에서 나온다. "카메라 화면의 저 사람이 로봇 발밑 기준 어디인가"를 답하려면 좌표계 간 관계를 알아야 하고, 그 관계는 시간에 따라 변한다.

tf2는 이 문제를 푸는 ROS의 표준 답이다. 그리고 ROS 2에서 가장 많은 시간을 소모하는 하위 시스템이기도 하다 — 문제가 생겼을 때 원인이 좌표계인지 시간인지 네트워크인지 가리기 어렵기 때문이다.

이 장은 tf2의 구조를 세운다. 실전 함정은 다음 장 5.2에서 집중적으로 다룬다.

배우는 것:

  • tf2가 푸는 문제 — 왜 단순한 변수 저장으로는 안 되는가.
  • 아키텍처 — 브로드캐스터·토픽·리스너·버퍼의 역할 분담.
  • 트리 제약 — 단일 부모·비순환·연결성이 왜 필수인가.
  • 시간 축 — 버퍼가 이력을 갖는 이유와 보간.
  • 정적 변환 — /tf_static이 별도로 존재하는 이유.
  • REP 105 — map·odom·base_link 규약.
  • API — 조회·대기·변환 적용.
  • QoS·CLI 실측(5.1.13) — /tf·/tf_static이 실제로 쓰는 QoS와 static_transform_publisher의 두 인자 방식.

선수 지식: 1.4(토픽), 1.6.3.3(TRANSIENT_LOCAL), 1.10(시간), 1.1.11(프레임 이름에 슬래시 금지). 회전 표현·동차변환 같은 수학은 자매서 robotics(RB-21~RB-24)가 다루며, 여기서는 ROS 2에서 어떻게 배선하는가에 집중한다(BK-04).

정직: tf2 API·동작은 ros2/geometry2 저장소에 근거한다. 버퍼 기본 캐시 시간·/tf·/tf_static QoS 기본값·static_transform_publisher 인자 형식·정적 브로드캐스터의 중복 호출 동작은 Jazzy 컨테이너에서 실행 확인했다(아래 각 절에 실행 확인으로 표시). 나머지 서술은 저장소 코드 기준이며 배포판에 따라 세부가 다를 수 있다(목차 3).

5.1.2 tf2가 푸는 문제

단순한 접근이 왜 실패하는가:

// ❌ 변환을 전역 변수에 저장?
Eigen::Isometry3d camera_to_base;    // 언제 기준인가?

문제가 셋이다.

문제설명
분산변환을 아는 노드와 필요한 노드가 다르다. 바퀴 오도메트리는 드라이버가, 관절 각도는 robot_state_publisher가 안다
시간로봇이 움직이면 변환이 계속 바뀐다. "지금"이 아니라 "그 데이터가 측정된 시점"의 변환이 필요하다
연쇄camera_optical → camera_link → base_link → odom → map처럼 여러 단계를 거쳐야 한다

두 번째가 특히 중요하다. 30 Hz 카메라 이미지를 50 ms 뒤에 처리한다면, 그 사이 로봇은 움직였다. 이미지가 촬영된 시각의 변환을 써야 하며, 그러려면 과거 변환을 보관하고 있어야 한다.

$$p_{\text{map}} = T_{\text{map} \leftarrow \text{odom}}(t) \cdot T_{\text{odom} \leftarrow \text{base}}(t) \cdot T_{\text{base} \leftarrow \text{cam}}(t) \cdot p_{\text{cam}}$$

여기서 모든 $T$가 같은 시각 $t$의 값이어야 한다. tf2가 하는 일이 이것이다 — 분산된 변환을 모으고, 시간별로 보관하고, 연쇄를 계산한다.

5.1.3 아키텍처

odom 발행 노드 TransformBroadcaster /tf 토픽 tf2_msgs/TFMessage robot_state_ publisher 5.5 정적 변환 발행 /tf_static TRANSIENT_LOCAL TransformListener (구독) Buffer 트리 + 시간별 이력 lookupTransform (소비 노드)
구성 요소역할
TransformBroadcaster변환을 /tf에 발행
StaticTransformBroadcaster변하지 않는 변환을 /tf_static에 발행
/tf·/tf_static일반 토픽. 전역 이름(1.1.4)
TransformListener두 토픽을 구독해 버퍼를 채운다
Buffer트리 구조 + 시간별 이력을 유지. 조회를 처리

핵심 통찰: tf2는 특별한 미들웨어가 아니라 토픽 두 개 위의 라이브러리다. 그래서 1.4~1.6의 토픽 지식이 그대로 적용된다 — QoS 불일치면 안 오고, 유실되면 공백이 생기고, 늦게 기동된 노드는 과거를 못 본다.

발행 측:

#include "tf2_ros/transform_broadcaster.h"
#include "tf2/LinearMath/Quaternion.h"

class OdomNode : public rclcpp::Node {
public:
  OdomNode() : Node("odom_node") {
    tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(*this);
  }

  void publish_odom(double x, double y, double theta, const rclcpp::Time & stamp) {
    geometry_msgs::msg::TransformStamped t;
    t.header.stamp = stamp;              // ★ 측정 시각 (1.10.6)
    t.header.frame_id = "odom";          // 부모
    t.child_frame_id = "base_link";      // 자식
    t.transform.translation.x = x;
    t.transform.translation.y = y;
    t.transform.translation.z = 0.0;

    tf2::Quaternion q;
    q.setRPY(0, 0, theta);
    t.transform.rotation = tf2::toMsg(q);

    tf_broadcaster_->sendTransform(t);
  }
private:
  std::unique_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster_;
};

수신 측:

#include "tf2_ros/buffer.h"
#include "tf2_ros/transform_listener.h"

class ConsumerNode : public rclcpp::Node {
public:
  ConsumerNode() : Node("consumer") {
    tf_buffer_ = std::make_unique<tf2_ros::Buffer>(this->get_clock());
    tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
  }

  void on_scan(const sensor_msgs::msg::LaserScan::SharedPtr msg) {
    try {
      auto t = tf_buffer_->lookupTransform(
          "base_link",              // 대상 프레임 (to)
          msg->header.frame_id,     // 원본 프레임 (from)
          msg->header.stamp,        // ★ 데이터 시각
          rclcpp::Duration::from_seconds(0.1));   // 타임아웃
      // t 를 사용해 변환
    } catch (const tf2::TransformException & ex) {
      RCLCPP_WARN(get_logger(), "변환 실패: %s", ex.what());
      return;      // 반드시 처리 — 예외를 묵살하지 말 것
    }
  }
private:
  std::unique_ptr<tf2_ros::Buffer> tf_buffer_;
  std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
};

함정: TransformListener를 지역 변수로 만들면 즉시 소멸해 버퍼가 채워지지 않는다. 2.8.8의 rclpy 참조 유지 문제와 같은 성질이며, C++에서도 발생한다. 반드시 멤버로 보관한다.

함정: lookupTransform은 예외를 던진다. try로 감싸지 않으면 노드가 종료된다. 그런데 예외를 포착해 로그만 남기고 넘어가는 것도 위험하다 — 변환이 계속 실패하는데 조용히 동작하는 것처럼 보인다. THROTTLE 로그(1.11.5)와 함께 실패율을 진단 토픽으로 내는 것이 좋다(6.9).

5.1.4 트리 제약

tf2의 프레임 관계는 트리여야 한다. 그래프가 아니다.

map odom base_link laser_link camera_link camera_optical_frame wheel_left wheel_right
제약의미위반 시
단일 부모각 프레임의 부모는 하나뿐두 노드가 같은 자식을 발행하면 충돌
비순환순환이 없어야 한다조회가 무한 루프·오류
연결성두 프레임 사이에 경로가 있어야 조회 가능Could not find a connection 오류

"단일 부모"가 실무에서 가장 자주 깨진다. 두 노드가 같은 child_frame_id를 발행하면 tf2는 나중 것으로 덮어쓰거나 번갈아 쓰며, 결과가 흔들린다.

# 같은 프레임을 누가 발행하는지 확인
ros2 run tf2_ros tf2_monitor
# 또는 특정 프레임
ros2 run tf2_ros tf2_monitor odom base_link

전형적 충돌 사례:

사례설명
오도메트리 노드 두 개휠 오도메트리와 시각 오도메트리가 둘 다 odom → base_link
AMCL과 SLAM 동시 실행둘 다 map → odom
시뮬레이터와 실제 드라이버둘 다 발행
launch 파일 중복 포함같은 노드가 두 번 뜸

견해: 오도메트리 소스가 여럿이면 융합 노드를 두는 것이 표준적 방법이다(robot_localization — 7.6). 각 소스는 토픽으로 발행하고, 융합 노드 하나만 tf를 발행한다. "누가 tf를 발행하는가"를 시스템 설계에서 명확히 정하고 문서화하라 — 이것이 tf 문제의 상당수를 예방한다.

왜 트리인가: 두 프레임 사이의 경로가 유일해야 변환이 일의적으로 정해진다. 그래프라면 경로마다 다른 답이 나올 수 있고(측정 오차 때문에 실제로 다르다), 어느 것을 택할지 결정할 수 없다. 여러 경로의 정보를 융합하는 것은 추정 문제이며 tf2의 역할이 아니다 — 그것은 SLAM·상태 추정의 영역이다(7.6·8.2).

5.1.5 시간 축과 보간

버퍼는 각 변환의 이력을 시간순으로 보관한다.

odom → base_link 이력 t=1.00: (x=0.0, θ=0.0) t=1.05: (x=0.1, θ=0.02) t=1.10: (x=0.2, θ=0.04) 조회: t=1.07 선형 보간 (x≈0.14, θ≈0.028)

보간이 자동으로 일어난다. 요청한 시각이 두 샘플 사이면 tf2가 보간한다 — 위치는 선형, 회전은 구면 선형 보간(slerp).

보간의 전제와 한계:

항목설명
전제두 샘플 사이에 부드럽게 움직였다
샘플 간격이 크면오차가 커진다 — 고속 회전에서 특히
외삽은 하지 않는다이력 범위 밖은 오류(5.2의 주제)

캐시 시간:

// 기본값보다 길게 (bag 재생·느린 처리 파이프라인)
tf_buffer_ = std::make_unique<tf2_ros::Buffer>(
    this->get_clock(), tf2::durationFromSec(30.0));
캐시 시간트레이드오프
짧다메모리 적음. 오래된 데이터 조회 실패
길다과거 조회 가능. 메모리·조회 비용 증가

기본 캐시 시간은 10초다(실행 확인, Jazzy). tf2/buffer_core.hpp의 BUFFER_CORE_DEFAULT_CACHE_TIME = std::chrono::seconds(10)이며 tf2_ros::Buffer 생성자가 이 값을 기본 인자로 받는다. 실무에서는 파이프라인 지연에 맞춰 조정한다. 처리 지연이 큰 인지 파이프라인(7.8)이나 bag 재생 시 시간 점프(1.10.4)를 다룬다면 늘리는 것을 검토한다.

5.1.6 정적 변환 — 왜 별도인가

변하지 않는 변환(로봇 몸체에 볼트로 고정된 센서)을 계속 발행하는 것은 낭비다.

# CLI로 정적 변환 발행
ros2 run tf2_ros static_transform_publisher \
  --x 0.1 --y 0 --z 0.2 --roll 0 --pitch 0 --yaw 0 \
  --frame-id base_link --child-frame-id laser_link
# 옵션 방식이 정식이다. --frame-id·--child-frame-id 는 필수 인자다.
#include "tf2_ros/static_transform_broadcaster.h"

static_broadcaster_ = std::make_shared<tf2_ros::StaticTransformBroadcaster>(this);
geometry_msgs::msg::TransformStamped t;
t.header.stamp = this->now();
t.header.frame_id = "base_link";
t.child_frame_id = "laser_link";
t.transform.translation.x = 0.1;
t.transform.rotation.w = 1.0;
static_broadcaster_->sendTransform(t);    // 한 번만 호출

/tf_static의 특수성:

성질설명
QoSTRANSIENT_LOCAL + RELIABLE, depth 100(실행 확인, 1.6.3.3) — 늦게 기동된 노드도 받는다
발행 횟수한 번이면 충분
시간 처리버퍼가 모든 시각에 유효한 것으로 취급
누적여러 발행자의 변환이 함께 보관된다

"모든 시각에 유효"가 중요하다. 정적 변환은 스탬프와 무관하게 조회되므로, 과거 시각으로 조회해도 성공한다. 이것이 시간 관련 오류를 크게 줄인다.

함정: 정적 변환이 정말 정적인지 확인하라. 팬틸트 위의 카메라, 접히는 팔, 교체 가능한 툴은 정적이 아니다. 정적으로 발행해 두고 실제로는 움직이면 조용한 오차가 된다.

같은 브로드캐스터로 여러 번 호출하면 어떻게 되나(실행 확인, Jazzy): StaticTransformBroadcaster는 내부에 net_message_라는 누적 메시지를 들고 있다. child_frame_id가 다르면 누적되고(두 번 호출 후 /tf_static을 받으면 둘 다 들어 있다), 같으면 대체된다(한 건만 남는다). 즉 "먼저 보낸 것이 사라진다"는 흔한 걱정은 사실이 아니다.

함정: 그래도 여러 정적 변환은 한 번의 호출에 벡터로 보내는 편이 낫다 — 호출할 때마다 누적된 전체가 다시 발행되므로, 프레임이 수십 개면 같은 데이터를 반복 전송하게 된다.

std::vector<geometry_msgs::msg::TransformStamped> transforms = {t1, t2, t3};
static_broadcaster_->sendTransform(transforms);

5.1.7 REP 105 — 표준 프레임 규약

생태계 전체가 이 규약을 전제한다. Nav2·SLAM·robot_localization이 이 이름과 의미를 기대한다.

earth (선택) map odom base_link 센서 · 부품 프레임들
프레임의미성질
earth지구 고정(다중 map 연결용)거의 안 씀
map세계 고정 기준점프한다(재측위 시). 장기적으로 드리프트 없음
odom오도메트리 기준연속적(점프 없음). 장기적으로 드리프트
base_link로봇 본체 기준로봇에 고정

map과 odom을 나눈 이유가 이 규약의 핵심이다:

요구만족하는 프레임
연속성(제어·지역 계획에 필수)odom — 절대 점프하지 않는다
전역 정확성(장기 항법에 필수)map — 재측위로 보정된다

둘 다 만족하는 프레임은 만들 수 없다. 전역 보정은 필연적으로 점프를 만들고, 점프하지 않으려면 드리프트를 감수해야 한다. 그래서 두 개를 두고 용도를 나눈다.

  • 지역 제어·장애물 회피는 odom 기준으로 — 점프하면 로봇이 급격히 움직인다.
  • 전역 목표·지도상 위치는 map 기준으로.

누가 무엇을 발행하는가:

변환발행자
map → odom측위 노드(AMCL·SLAM — 8.2·8.3)
odom → base_link오도메트리 노드(휠·융합 — 7.6)
base_link → 센서robot_state_publisher(URDF 기반 — 5.5) 또는 정적 발행

map → odom이 "보정량"이라는 점이 자주 오해된다. 측위 노드는 map → base_link를 직접 발행하지 않는다 — 그러면 base_link의 부모가 둘이 되어 트리 제약을 위반한다(5.1.4). 대신 map → odom을 발행해 오도메트리의 누적 오차를 보정한다.

$$T_{\text{map} \leftarrow \text{odom}} = T_{\text{map} \leftarrow \text{base}}^{\text{추정}} \cdot \left(T_{\text{odom} \leftarrow \text{base}}\right)^{-1}$$

추가 규약(REP 103·REP 105):

항목규약
좌표 방향오른손 좌표계, x 앞·y 왼쪽·z 위
단위SI — 미터·라디안·초
회전라디안, 반시계가 양
광학 프레임카메라는 _optical_frame을 별도로 — z 앞·x 오른쪽·y 아래(비전 관례)

함정: 카메라의 camera_link와 camera_optical_frame은 축 방향이 다르다. 전자는 ROS 관례(x 앞), 후자는 비전 관례(z 앞). 이미지 처리 결과는 _optical_frame 기준이며, 이를 혼동하면 90도 회전된 결과를 얻는다. 카메라 드라이버가 두 프레임과 그 사이 변환을 함께 제공하는 것이 관례다(7.3).

5.1.8 조회 API

기본 조회:

// target ← source 변환. stamp 시점.
auto t = tf_buffer_->lookupTransform(target_frame, source_frame, stamp, timeout);

고급 조회(서로 다른 시각·기준 프레임):

// source_frame의 source_time 시점 위치를
// target_frame의 target_time 시점 기준으로
auto t = tf_buffer_->lookupTransform(
    target_frame, target_time,
    source_frame, source_time,
    fixed_frame,           // 두 시각을 잇는 고정 프레임 (보통 "map" 또는 "odom")
    timeout);

이것이 이동 로봇에서 유용하다. "1초 전에 본 물체가 지금 로봇 기준 어디인가"를 계산할 때 쓴다. fixed_frame은 그 시간 동안 움직이지 않았다고 가정할 프레임이다.

존재 확인:

if (tf_buffer_->canTransform(target, source, stamp, timeout, &error_msg)) {
  auto t = tf_buffer_->lookupTransform(target, source, stamp);
}

메시지 변환:

#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
#include "tf2_sensor_msgs/tf2_sensor_msgs.hpp"

geometry_msgs::msg::PointStamped p_in, p_out;
tf_buffer_->transform(p_in, p_out, "base_link", tf2::durationFromSec(0.1));

// 포인트클라우드도 (7.4)
sensor_msgs::msg::PointCloud2 cloud_out;
tf_buffer_->transform(cloud_in, cloud_out, "map");

tf2_* 변환 패키지를 포함해야 그 타입이 지원된다. 헤더를 빠뜨리면 컴파일 오류가 나는데, 원인이 명확하지 않아 헤맬 수 있다.

패키지지원 타입
tf2_geometry_msgsPoint·Pose·Vector3·Quaternion·Twist 등
tf2_sensor_msgsPointCloud2
tf2_eigenEigen 타입 상호 변환
tf2_kdlKDL 타입

Python:

import tf2_ros
from tf2_ros import TransformException
import tf2_geometry_msgs      # 임포트만 해도 변환이 등록된다

self.tf_buffer = tf2_ros.Buffer()
self.tf_listener = tf2_ros.TransformListener(self.tf_buffer, self)

try:
    t = self.tf_buffer.lookup_transform(
        'base_link', msg.header.frame_id, msg.header.stamp,
        timeout=rclpy.duration.Duration(seconds=0.1))
except TransformException as ex:
    self.get_logger().warn(f'변환 실패: {ex}', throttle_duration_sec=1.0)
    return

함정: Python에서 import tf2_geometry_msgs를 빠뜨리면 transform() 호출 시 타입을 모른다는 오류가 난다. 임포트 자체가 등록 부작용을 갖는 구조라 린터가 "미사용 임포트"로 경고할 수 있다.

5.1.9 성능 고려

tf2 조회에도 비용이 든다.

비용 요소설명
트리 탐색두 프레임 간 경로 길이에 비례
보간 계산샘플 검색 + 보간
락 경합버퍼는 스레드 안전하며 내부 락을 쓴다(2.9)
버퍼 크기캐시 시간 × 발행 주파수 × 프레임 수

고빈도 루프에서 매번 조회하면 부담이 된다.

완화설명
정적 변환 활용변하지 않는 것은 /tf_static으로 — 조회가 훨씬 비용이 낮다
조회 결과 캐싱같은 시각의 같은 변환을 반복 조회하지 않는다
프레임 체인 단축불필요한 중간 프레임 제거
조회 빈도 감소제어 주기마다가 아니라 필요할 때만
버퍼 캐시 시간 조정필요 이상으로 길게 두지 않는다

견해: 실시간 제어 루프(6.6) 안에서 tf 조회를 하는 것은 신중해야 한다. 락 경합과 예외 가능성이 있어 지터 원인이 된다(2.5.5). 제어에 필요한 변환은 비실시간 스레드에서 미리 조회해 원자적으로 넘기는 구조(2.9.4의 이중 버퍼)가 안전하다.

5.1.10 진단 도구

# 프레임 트리를 PDF로 생성 (가장 유용)
ros2 run tf2_tools view_frames
# → frames_<타임스탬프>.pdf 생성

# 두 프레임 간 변환을 실시간 출력
ros2 run tf2_ros tf2_echo base_link laser_link

# 발행 주기·지연 통계
ros2 run tf2_ros tf2_monitor
ros2 run tf2_ros tf2_monitor odom base_link

# 원시 토픽 확인
ros2 topic hz /tf
ros2 topic echo /tf_static --qos-durability transient_local --once

view_frames가 진단의 출발점이다. 트리 전체를 그림으로 보여주고, 각 변환의 발행 주기·최근 시각을 표시한다. 연결이 끊긴 부분·예상 밖의 부모 관계가 즉시 보인다.

tf2_monitor가 보여주는 것:

항목의미
발행 주기얼마나 자주 오는가
평균 지연(delay)스탬프와 수신 시각의 차이 — 시계 동기 문제 탐지(1.10.8)
최대 지연최악값
브로드캐스터 목록누가 발행하는가 — 중복 탐지(5.1.4)

견해: /tf_static을 echo할 때 QoS 옵션을 반드시 주라(0.7.5). 기본 QoS로는 아무것도 안 나오고, 그것을 "정적 변환이 발행되지 않았다"로 오해하기 쉽다.

5.1.13 tf2가 실제로 쓰는 QoS와 CLI 인자 — 실측

앞 절들이 "/tf는 보통, /tf_static은 래치"라고 말한 것의 실제 코드값이다. Jazzy 컨테이너에서 tf2_ros/qos.hpp를 직접 열어 확인했다(실행 확인).

5.1.13.1 QoS 네 종류

tf2는 QoS를 그때그때 만들지 않고 네 개의 이름 붙은 클래스로 고정해 둔다. 리스너와 브로드캐스터가 짝을 이루므로, 기본 설정끼리는 항상 맞는다.

클래스쓰는 곳depthdurability
DynamicBroadcasterQoSTransformBroadcaster → /tf100VOLATILE(기본)
DynamicListenerQoSTransformListener ← /tf100VOLATILE(기본)
StaticBroadcasterQoSStaticTransformBroadcaster → /tf_static100TRANSIENT_LOCAL
StaticListenerQoSTransformListener ← /tf_static100TRANSIENT_LOCAL

신뢰성은 넷 다 rclcpp::QoS의 기본값인 RELIABLE이다. 정적 쪽만 생성자에서 transient_local()을 호출한다 — 소스에서 두 정적 클래스에만 그 한 줄이 있고 동적 쪽에는 없다.

depth 100이 뜻하는 것. /tf의 큐가 100이라는 말은, 여러 브로드캐스터가 동시에 대량 발행해도 리스너가 웬만해선 놓치지 않는다는 뜻이다. 반대로 리스너가 오래 막히면 100개를 넘겨 버린다 — 콜백에서 부하가 큰 일을 하면 tf가 조용히 유실되는 경로가 여기다(5.1.9).

함정: 직접 /tf를 구독하는 코드를 작성할 때 depth를 10 같은 기본값으로 두면 tf2 내부 리스너보다 훨씬 먼저 유실이 시작된다. 로봇 하나에 브로드캐스터가 여럿이면(robot_state_publisher + 오도메트리 + 지역화) 100은 여유가 아니라 필요치다.

5.1.13.2 static_transform_publisher의 두 가지 인자 방식

CLI 인자 형식이 바뀌었고, 옛 형식도 아직 동작한다. 튜토리얼·블로그의 절대다수가 옛 형식이라 반드시 만난다.

# 새 방식 — 정식. --frame-id·--child-frame-id 는 필수
ros2 run tf2_ros static_transform_publisher \
  --x 0.1 --y 0 --z 0.2 --roll 0 --pitch 0 --yaw 0 \
  --frame-id base_link --child-frame-id laser_link

# 옛 방식 — 동작하지만 경고가 출력된다(실행 확인)
ros2 run tf2_ros static_transform_publisher 0.1 0 0.2 0 0 0 base_link laser_link
# [WARN] Old-style arguments are deprecated; see --help for new-style arguments

옛 방식의 인자 순서는 x y z yaw pitch roll parent child다(실행 확인 — 네 번째 값에 1.5708을 넣고 tf2_echo로 읽으면 RPY가 [0, 0, 1.571], 즉 yaw로 들어간다). 회전이 roll이 아니라 yaw부터라는 점이 함정이다. 새 옵션 방식은 --roll·--pitch·--yaw로 이름이 붙어 이 혼동이 없다 — 옮겨 쓸 이유가 이것이다.

함정: 옛 방식에는 쿼터니언 변형도 있었다(인자 9개면 x y z qx qy qz qw parent child). 인자 개수로 해석이 갈리므로, 값을 하나 빠뜨리면 오일러각이 쿼터니언으로 읽히며 조용히 틀린 자세가 나온다. 새 방식은 --qx 계열과 --roll 계열이 이름으로 갈려 이 사고가 없다.

5.1.11 함정 카탈로그

#함정증상대처
1TransformListener를 지역 변수로버퍼가 안 채워짐멤버로 보관
2lookupTransform 예외 미처리노드 크래시try/catch
3예외를 포착하고 무시조용한 실패 지속THROTTLE 로그 + 진단
4같은 자식 프레임을 두 노드가 발행변환이 흔들림tf2_monitor·융합 노드
5측위 노드가 map → base_link 발행트리 위반map → odom 발행
6프레임 이름에 앞 슬래시tf2 거부슬래시 없이(1.1.11)
7광학 프레임 혼동90도 회전된 결과_optical_frame 구분(7.3)
8움직이는 것을 정적으로 발행조용한 오차실제 가동성 확인
9/tf_static을 기본 QoS로 echo안 보임transient_local 옵션
10캐시 시간이 파이프라인보다 짧음조회 실패버퍼 시간 확대
11실시간 루프에서 tf 조회지터·예외사전 조회 + 이중 버퍼
12tf2_geometry_msgs 임포트 누락타입 미지원 오류헤더·임포트 추가
13map을 제어 기준으로 사용재측위 시 로봇이 튐odom 기준(5.1.7)
14정적 변환을 개별 호출로 여러 번일부만 유지될 수 있음벡터로 한 번에
15옛 static_transform_publisher 인자 순서4번째가 roll이 아니라 yaw옵션 방식(--roll·--yaw) 사용(5.1.13.2)

13번을 강조한다. map 프레임은 점프한다. 지역 제어·장애물 회피를 map 기준으로 구현하면 재측위 순간 목표가 급변해 로봇이 급격히 움직인다. REP 105가 두 프레임을 나눈 이유가 정확히 이것이다.

5.1.12 체크포인트

  • tf2가 푸는 문제 세 가지를 말할 수 있는가. 왜 변환을 전역 변수에 저장하면 안 되는가.
  • tf2의 다섯 구성 요소와 역할을 말할 수 있는가. "tf2는 토픽 두 개 위의 라이브러리"라는 사실의 실무적 함의는?
  • 트리 제약 세 가지와 각각의 위반 증상을 말할 수 있는가. 왜 그래프가 아니라 트리여야 하는가.
  • 오도메트리 소스가 여럿일 때의 표준적 구조는 무엇인가.
  • 버퍼의 보간은 무엇을 전제하며, 언제 오차가 커지는가. 외삽은 하는가.
  • /tf_static의 특수성 네 가지를 말할 수 있는가. "모든 시각에 유효"가 왜 유용한가.
  • REP 105에서 map과 odom을 나눈 이유를 두 요구사항으로 설명할 수 있는가. 지역 제어는 어느 기준인가.
  • 측위 노드가 map → base_link를 직접 발행하지 않는 이유는? 무엇을 발행하는가.
  • camera_link와 camera_optical_frame의 차이와, 혼동 시 증상은?
  • 서로 다른 시각의 변환을 조회하는 고급 API가 필요한 상황은? fixed_frame의 역할은?
  • tf 조회의 비용 요소 네 가지와 완화책 다섯 가지를 들 수 있는가. 실시간 루프에서 권장되는 구조는?
  • 진단의 출발점이 되는 명령과, tf2_monitor가 알려주는 것 네 가지는?
  • /tf와 /tf_static의 QoS는 각각 무엇이며 depth는? static_transform_publisher의 옛 인자 순서에서 네 번째 값은 무엇인가(5.1.13).

다음 장 5.2는 이 구조 위에서 실제로 겪는 문제들 — 외삽 오류·시간 불일치·정적 변환 함정을 집중적으로 다룬다. ROS 2 사용자가 tf에서 잃는 시간의 대부분이 그 장의 내용이다.