659.64 포인트 (geometry_msgs::Point) 변환

1. 개요

3차원 공간에서의 점(point)은 위치(position)를 나타내는 가장 기본적인 기하학적 원소이다. 로봇 시스템에서 특정 좌표 프레임에서 표현된 점을 다른 좌표 프레임으로 변환하는 것은 센서 데이터 처리, 목표 위치 설정, 장애물 탐지 등 다양한 응용에서 필수적인 연산이다. TF2 프레임워크에서는 geometry_msgs::msg::Pointgeometry_msgs::msg::PointStamped 메시지 타입에 대한 좌표 변환을 tf2_geometry_msgs 패키지를 통해 지원한다.

2. geometry_msgs::msg::Point 메시지 구조

geometry_msgs::msg::Point는 3차원 공간에서의 위치를 나타내는 메시지로, 다음과 같은 구조를 갖는다.

geometry_msgs/Point
  float64 x
  float64 y
  float64 z

세 개의 float64 필드는 각각 좌표 프레임의 x, y, z 축 방향의 좌표 성분을 미터(m) 단위로 나타낸다.

2.1 PointStamped 메시지 구조

시간 및 프레임 정보가 포함된 PointStamped 메시지는 다음과 같다.

geometry_msgs/PointStamped
  std_msgs/Header header
    builtin_interfaces/Time stamp
    string frame_id
  geometry_msgs/Point point
    float64 x
    float64 y
    float64 z

header.frame_id는 해당 점이 표현된 좌표 프레임을 식별하며, header.stamp는 해당 시점의 타임스탬프를 나타낸다. TF2의 변환 파이프라인에서 PointStamped는 자동 프레임 추적이 가능하므로 Point보다 선호된다.

3. 점 변환의 수학적 원리

3.1 동차 좌표 변환

프레임 A에서 표현된 점 {}^{A}\mathbf{p} = (p_x, p_y, p_z)^{\top}를 프레임 B에서 표현된 좌표 {}^{B}\mathbf{p}로 변환하려면, 프레임 B에서 프레임 A로의 변환 행렬 {}^{B}T_{A}를 적용해야 한다.

\begin{bmatrix} {}^{B}\mathbf{p} \\ 1 \end{bmatrix} = {}^{B}T_{A} \begin{bmatrix} {}^{A}\mathbf{p} \\ 1 \end{bmatrix}

이를 회전 행렬 R과 병진 벡터 \mathbf{t}로 분해하면 다음과 같다.

{}^{B}\mathbf{p} = R \cdot {}^{A}\mathbf{p} + \mathbf{t}

여기서 R \in SO(3)3 \times 3 회전 행렬이고, \mathbf{t} \in \mathbb{R}^{3}는 병진 벡터이다.

3.2 점과 벡터의 변환 차이

점(point)의 변환에는 회전과 병진이 모두 적용된다. 이는 점이 공간상의 절대적 위치를 나타내기 때문이다. 반면, 자유 벡터(free vector)의 변환에는 회전만 적용되고 병진은 무시된다. 이 차이는 동차 좌표(homogeneous coordinates) 표현에서 점의 마지막 성분이 1이고, 벡터의 마지막 성분이 0인 것으로 구분된다.

\text{Point: } \begin{bmatrix} \mathbf{p} \\ 1 \end{bmatrix}, \quad \text{Vector: } \begin{bmatrix} \mathbf{v} \\ 0 \end{bmatrix}

4. C++에서의 Point 변환

4.1 doTransform()을 이용한 변환

#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
#include <tf2_ros/buffer.h>
#include <tf2_ros/transform_listener.h>

// PointStamped 변환
geometry_msgs::msg::PointStamped point_in;
point_in.header.frame_id = "sensor_link";
point_in.header.stamp = node->now();
point_in.point.x = 1.0;
point_in.point.y = 2.0;
point_in.point.z = 0.5;

geometry_msgs::msg::PointStamped point_out;

try {
  auto transform = tf_buffer->lookupTransform(
    "base_link", "sensor_link", point_in.header.stamp);
  tf2::doTransform(point_in, point_out, transform);
  
  RCLCPP_INFO(node->get_logger(),
    "변환된 점: (%.2f, %.2f, %.2f) in %s",
    point_out.point.x, point_out.point.y, point_out.point.z,
    point_out.header.frame_id.c_str());
} catch (const tf2::TransformException & ex) {
  RCLCPP_ERROR(node->get_logger(), "%s", ex.what());
}

4.2 Buffer::transform()을 이용한 간편 변환

geometry_msgs::msg::PointStamped point_in;
point_in.header.frame_id = "sensor_link";
point_in.header.stamp = node->now();
point_in.point.x = 1.0;
point_in.point.y = 2.0;
point_in.point.z = 0.5;

try {
  auto point_out = tf_buffer->transform(point_in, "base_link");
} catch (const tf2::TransformException & ex) {
  RCLCPP_ERROR(node->get_logger(), "%s", ex.what());
}

4.3 tf2 내부 타입을 이용한 변환

tf2::Vector3tf2::Transform을 직접 사용하여 보다 저수준의 변환을 수행할 수 있다.

#include <tf2/LinearMath/Transform.h>
#include <tf2/LinearMath/Vector3.h>

tf2::Transform T;
tf2::fromMsg(transform_msg.transform, T);

tf2::Vector3 point_in(1.0, 2.0, 0.5);
tf2::Vector3 point_out = T * point_in;
// 또는: tf2::Vector3 point_out = T(point_in);

이 방식은 geometry_msgs 메시지를 거치지 않으므로 메시지 직렬화/역직렬화 오버헤드가 없으며, 대량의 점을 일괄 변환할 때 유리하다.

5. Python에서의 Point 변환

5.1 do_transform_point()를 이용한 변환

from geometry_msgs.msg import PointStamped
from tf2_geometry_msgs import do_transform_point

# 입력 점 생성
point_in = PointStamped()
point_in.header.frame_id = 'sensor_link'
point_in.header.stamp = node.get_clock().now().to_msg()
point_in.point.x = 1.0
point_in.point.y = 2.0
point_in.point.z = 0.5

try:
    transform = tf_buffer.lookup_transform(
        'base_link', 'sensor_link',
        point_in.header.stamp)
    
    point_out = do_transform_point(point_in, transform)
    
    node.get_logger().info(
        f'변환된 점: ({point_out.point.x:.2f}, '
        f'{point_out.point.y:.2f}, {point_out.point.z:.2f}) '
        f'in {point_out.header.frame_id}')
except TransformException as ex:
    node.get_logger().error(f'{ex}')

5.2 Buffer.transform()을 이용한 간편 변환

try:
    point_out = tf_buffer.transform(point_in, 'base_link')
except TransformException as ex:
    node.get_logger().error(f'{ex}')

6. 다중 점의 배치 변환

6.1 효율적 배치 처리

다수의 점을 동일한 프레임으로 변환해야 하는 경우, lookupTransform()을 한 번만 호출하고 doTransform()을 반복 적용하는 것이 효율적이다.

// 변환을 한 번만 조회
auto transform = tf_buffer->lookupTransform(
  "map", "sensor_link", time);

// 다수의 점을 일괄 변환
for (const auto & pt_in : sensor_points) {
  geometry_msgs::msg::PointStamped pt_out;
  tf2::doTransform(pt_in, pt_out, transform);
  map_points.push_back(pt_out);
}

6.2 Eigen을 이용한 고성능 배치 처리

대규모 점군(point cloud)의 좌표 변환에는 Eigen 라이브러리를 활용한 행렬 연산이 효율적이다.

#include <Eigen/Geometry>

// TransformStamped → Eigen::Isometry3d
Eigen::Isometry3d T = Eigen::Isometry3d::Identity();
T.translate(Eigen::Vector3d(
  transform.transform.translation.x,
  transform.transform.translation.y,
  transform.transform.translation.z));
T.rotate(Eigen::Quaterniond(
  transform.transform.rotation.w,
  transform.transform.rotation.x,
  transform.transform.rotation.y,
  transform.transform.rotation.z));

// Eigen::MatrixXd를 이용한 배치 변환
// points: 3×N 행렬
Eigen::MatrixXd transformed = T.rotation() * points;
transformed.colwise() += T.translation();

이 방식은 SIMD(Single Instruction, Multiple Data) 최적화의 이점을 활용할 수 있으며, 개별 doTransform() 호출에 비해 수천 개 이상의 점을 처리할 때 현저한 성능 향상을 제공한다.

7. 변환 결과의 해석

7.1 헤더 갱신

doTransform() 또는 Buffer::transform()을 통해 변환된 PointStampedheader 필드는 다음과 같이 갱신된다.

  • header.frame_id: 대상 프레임(target frame)으로 변경
  • header.stamp: 변환의 타임스탬프로 변경

7.2 좌표계 확인

변환 결과의 좌표 성분은 대상 프레임의 축 방향에 따라 해석해야 한다. 예를 들어, REP 103에 따르면 로봇 기체 프레임(base_link)은 전방이 x, 좌측이 y, 상방이 z이며, 카메라 광학 프레임(camera_optical)은 우측이 x, 하방이 y, 전방이 z이다. 동일한 점이라도 표현되는 좌표 프레임에 따라 좌표 성분이 달라진다.

8. 일반적 오류

8.1 프레임 불일치 오류

입력 점의 header.frame_idlookupTransform()의 소스 프레임이 일치하지 않으면, 변환은 정상적으로 수행되지만 결과가 물리적으로 의미 없는 값이 된다. 이 오류는 컴파일 시나 런타임에 검출되지 않으므로 주의가 필요하다.

8.2 타임스탬프 불일치

입력 점의 타임스탬프와 변환 가용 시간 범위가 불일치하면 ExtrapolationException이 발생한다. 최신 가용 변환을 사용하려면 타임스탬프를 rclcpp::Time(0) (C++) 또는 Time() (Python)으로 설정한다.

9. 참고 문헌


버전: 2026-03-26