659.32 tf2::Buffer를 이용한 변환 저장과 조회
1. 개요
tf2::Buffer(C++) 및 tf2_ros.Buffer(Python)는 TF2 라이브러리의 핵심 데이터 구조로서, 좌표 변환을 시간순으로 저장하고 조회하는 기능을 제공한다. TransformListener가 수신한 변환 메시지는 Buffer에 축적되며, 사용자는 Buffer의 조회 메서드를 통하여 임의의 두 프레임 간의 변환을 취득한다. 본 절에서는 tf2::Buffer의 아키텍처, 저장 메커니즘, 조회 방법, 그리고 각종 옵션을 체계적으로 기술한다.
2. Buffer의 아키텍처
2.1 클래스 계층 구조
tf2::Buffer는 다음의 클래스 계층을 따른다.
tf2::BufferCoreBase
└── tf2::BufferCore
└── tf2_ros::Buffer
tf2::BufferCore: 변환 데이터의 저장과 조회를 담당하는 핵심 계층이다. 시간 캐시(time cache), 프레임 그래프(frame graph), 보간(interpolation) 로직을 포함한다.tf2_ros::Buffer:BufferCore를 상속하며, ROS2 노드와의 연동 기능(시계 통합, 비동기 조회 등)을 추가한다.
2.2 내부 데이터 구조
Buffer는 내부적으로 프레임 ID를 키(key)로 하는 해시 맵(hash map) 구조를 유지한다. 각 프레임에 대하여 시간순으로 정렬된 변환 캐시가 관리된다.
Buffer 내부 구조:
┌─────────────────────────────────────────┐
│ Frame: "base_link" │
│ └── TimeCache: [(t1, T1), (t2, T2), ...]│
│ Frame: "lidar_link" │
│ └── TimeCache: [(t1, T1'), (t2, T2'),...]│
│ Frame: "camera_link" │
│ └── TimeCache: [(t1, T1''), ...] │
└─────────────────────────────────────────┘
각 TimeCache는 해당 프레임에서 부모 프레임으로의 변환을 타임스탬프 순으로 저장한다.
3. Buffer 생성
3.1 C++ (rclcpp)
#include <tf2_ros/buffer.h>
// 기본 캐시 기간 (10초)
auto tf_buffer = std::make_unique<tf2_ros::Buffer>(this->get_clock());
// 캐시 기간 지정
auto tf_buffer = std::make_unique<tf2_ros::Buffer>(
this->get_clock(),
tf2::Duration(std::chrono::seconds(30)));
3.2 Python (rclpy)
from tf2_ros.buffer import Buffer
from rclpy.duration import Duration
# 기본 캐시 기간
tf_buffer = Buffer()
# 캐시 기간 지정
tf_buffer = Buffer(cache_time=Duration(seconds=30))
4. 변환 저장
4.1 setTransform 메서드
setTransform() 메서드는 변환을 Buffer에 저장한다. 일반적으로 TransformListener가 내부적으로 호출하며, 사용자가 직접 호출하는 경우는 드물다.
// C++
bool setTransform(
const geometry_msgs::msg::TransformStamped & transform,
const std::string & authority,
bool is_static = false);
# Python
tf_buffer.set_transform(transform, authority)
tf_buffer.set_transform_static(transform, authority)
| 매개변수 | 설명 |
|---|---|
transform | 저장할 TransformStamped 메시지 |
authority | 변환의 출처 (발행 노드 이름 등) |
is_static | 정적 변환 여부 (C++) |
authority 매개변수는 디버깅 시 특정 변환이 어떤 노드에 의하여 설정되었는지 추적하는 데 사용된다.
4.2 정적 변환과 동적 변환의 저장 차이
| 구분 | 동적 변환 | 정적 변환 |
|---|---|---|
| 캐시 만료 | 캐시 기간 경과 시 삭제 | 만료되지 않음 |
| 시간 의존성 | 특정 시간에 유효 | 모든 시간에 유효 |
| 보간 지원 | 지원 | 해당 없음 (고정값) |
| 메모리 관리 | 자동 정리 | 수동 삭제 필요 없음 |
5. 변환 조회
5.1 lookupTransform
lookupTransform() 메서드는 두 프레임 간의 변환을 조회한다.
// C++
geometry_msgs::msg::TransformStamped lookupTransform(
const std::string & target_frame,
const std::string & source_frame,
const tf2::TimePoint & time) const;
# Python
transform = tf_buffer.lookup_transform(
target_frame, source_frame, time)
5.1.1 매개변수 의미
target_frame: 변환 결과가 표현될 좌표 프레임이다. 즉, “이 프레임 기준으로 표현하라“는 의미이다.source_frame: 변환의 원점이 되는 좌표 프레임이다. 즉, “이 프레임에서 출발한다“는 의미이다.time: 변환이 유효한 시각이다.
5.1.2 반환값
geometry_msgs::msg::TransformStamped 메시지가 반환된다. 이 메시지는 source_frame에서 target_frame으로의 변환을 나타낸다.
5.2 시간 지정 옵션
5.2.1 최신 가용 변환
// C++
auto t = tf_buffer->lookupTransform(
"map", "base_link", tf2::TimePointZero);
# Python
t = tf_buffer.lookup_transform(
'map', 'base_link', rclpy.time.Time())
tf2::TimePointZero(C++) 또는 rclpy.time.Time()(Python)을 사용하면, 가장 최근에 수신된 변환이 반환된다.
5.2.2 특정 시점의 변환
// C++
auto query_time = this->get_clock()->now();
auto t = tf_buffer->lookupTransform(
"map", "base_link", query_time);
# Python
query_time = self.get_clock().now()
t = tf_buffer.lookup_transform(
'map', 'base_link', query_time)
지정된 시각의 변환이 캐시에 정확히 존재하지 않는 경우, Buffer는 전후 시점의 변환을 보간(interpolation)하여 결과를 생성한다.
5.2.3 과거 시점의 변환
// C++ - 2초 전 시점의 변환
auto past_time = this->get_clock()->now() - rclcpp::Duration::from_seconds(2.0);
auto t = tf_buffer->lookupTransform(
"map", "base_link", past_time);
# Python - 2초 전 시점의 변환
from rclpy.duration import Duration
past_time = self.get_clock().now() - Duration(seconds=2)
t = tf_buffer.lookup_transform(
'map', 'base_link', past_time)
5.3 고급 lookupTransform: 이중 시간 조회
서로 다른 시점의 두 프레임 간 변환을 조회할 수 있다. 이를 “시간 여행(time travel)” 기능이라 한다.
// C++
auto t = tf_buffer->lookupTransform(
"target_frame", target_time,
"source_frame", source_time,
"fixed_frame");
# Python
t = tf_buffer.lookup_transform_full(
'target_frame', target_time,
'source_frame', source_time,
'fixed_frame')
이 기능은 이동하는 프레임의 과거 위치를 현재 좌표계에서 표현하고자 할 때 유용하다.
5.4 canTransform
canTransform() 메서드는 변환이 조회 가능한지 사전에 확인한다.
// C++
bool canTransform(
const std::string & target_frame,
const std::string & source_frame,
const tf2::TimePoint & time,
std::string * error_msg = nullptr) const;
# Python
can = tf_buffer.can_transform(
target_frame, source_frame, time)
canTransform()은 예외를 발생시키지 않으므로, 조건부 변환 조회에 유용하다.
// C++ 사용 예
if (tf_buffer->canTransform("map", "base_link", tf2::TimePointZero)) {
auto t = tf_buffer->lookupTransform(
"map", "base_link", tf2::TimePointZero);
// 변환 사용
}
5.5 waitForTransform (비동기)
tf2_ros::Buffer는 비동기적으로 변환이 가용해질 때까지 대기하는 메서드를 제공한다.
// C++ (Future 기반)
auto future = tf_buffer->waitForTransform(
"map", "base_link",
tf2::TimePointZero,
std::chrono::seconds(5),
[](const%20tf2_ros::TransformStampedFuture%20&%20future) {
// 콜백 처리
});
6. 변환 경로 탐색
Buffer는 변환 트리에서 두 프레임 간의 경로를 자동으로 탐색한다.
map
│
odom
│
base_link
/ \
lidar_link camera_link
lookupTransform("map", "camera_link", ...)을 호출하면, Buffer는 다음의 경로를 탐색한다.
camera_link → base_link → odom → map
각 구간의 변환을 합성(composition)하여 최종 결과를 반환한다.
T_{map \leftarrow camera} = T_{map \leftarrow odom} \cdot T_{odom \leftarrow base} \cdot T_{base \leftarrow camera}
7. 프레임 정보 조회
7.1 등록된 프레임 목록
// C++
std::string frames;
tf_buffer->_allFramesAsString();
# Python
frames = tf_buffer.all_frames_as_string()
print(frames)
7.2 프레임 존재 확인
// C++
bool exists = tf_buffer->_frameExists("base_link");
7.3 부모 프레임 조회
// C++
std::string parent;
bool success = tf_buffer->_getParent("base_link", tf2::TimePointZero, parent);
8. 예외 처리
lookupTransform() 호출 시 발생할 수 있는 예외들이다.
| 예외 유형 | 발생 조건 | 대응 방법 |
|---|---|---|
LookupException | 프레임이 존재하지 않음 | 프레임 이름 확인 |
ConnectivityException | 두 프레임 간 경로 없음 | 변환 트리 구조 확인 |
ExtrapolationException | 요청 시간이 캐시 범위 초과 | TimePointZero 사용 또는 캐시 기간 조정 |
InvalidArgumentException | 잘못된 인자 | 프레임 ID 유효성 확인 |
// C++ 예외 처리
try {
auto t = tf_buffer->lookupTransform(
"map", "base_link", tf2::TimePointZero);
} catch (const tf2::LookupException & ex) {
RCLCPP_WARN(this->get_logger(), "Frame not found: %s", ex.what());
} catch (const tf2::ConnectivityException & ex) {
RCLCPP_WARN(this->get_logger(), "No path: %s", ex.what());
} catch (const tf2::ExtrapolationException & ex) {
RCLCPP_WARN(this->get_logger(), "Extrapolation: %s", ex.what());
}
# Python 예외 처리
from tf2_ros import TransformException, LookupException
from tf2_ros import ConnectivityException, ExtrapolationException
try:
t = tf_buffer.lookup_transform(
'map', 'base_link', rclpy.time.Time())
except LookupException as ex:
self.get_logger().warn(f'Frame not found: {ex}')
except ConnectivityException as ex:
self.get_logger().warn(f'No path: {ex}')
except ExtrapolationException as ex:
self.get_logger().warn(f'Extrapolation: {ex}')
9. 성능 고려 사항
9.1 조회 복잡도
lookupTransform()의 시간 복잡도는 변환 트리에서 두 프레임 간의 경로 길이에 비례한다. 일반적인 로봇 시스템에서 경로 길이는 수 단계 이내이므로, 조회 비용은 실질적으로 O(1)에 가깝다.
9.2 캐시 크기와 메모리
각 프레임 쌍에 대한 캐시 메모리 사용량은 다음과 같이 추정할 수 있다.
M \approx N_{frames} \times f_{pub} \times T_{cache} \times S_{entry}
여기서:
- N_{frames}: 프레임 쌍의 수
- f_{pub}: 발행 주파수 (Hz)
- T_{cache}: 캐시 기간 (초)
- S_{entry}: 캐시 항목 당 크기 (약 100바이트)
9.3 조회 빈도 최적화
고주기 루프에서 lookupTransform()을 호출하는 경우, canTransform()을 먼저 호출하여 예외 발생을 방지하는 것이 효율적이다. 예외 처리(exception handling)의 오버헤드가 canTransform() 호출보다 크기 때문이다.
10. 참고 자료
- tf2 API 문서, https://docs.ros2.org/latest/api/tf2/
- tf2_ros API 문서, https://docs.ros2.org/latest/api/tf2_ros/
- ROS2 공식 문서, “Using tf2 in ROS2”, https://docs.ros.org/en/humble/Tutorials/Intermediate/Tf2/Tf2-Main.html
- ROS2 Humble Hawksbill