Devin.KR

프레임 트리와 변환 방향

150분 안팎

학습 목표

프레임 부모·자식 및 변환 방향을 그림으로 제출합니다.

개념

행렬이 맞아도 연결이 틀릴 수 있습니다

개별 회전과 이동을 올바르게 계산해도 센서 점의 frame_id를 무시하면 다른 기준의 점을 지도에 표시하게 됩니다. 프레임 트리는 어떤 원점과 축이 어떤 기준에 연결되는지 나타내는 구조입니다. 이번 레슨은 map, odom, base_link, sensor의 역할과 부모·자식 관계를 정의하고, 조회하려는 점의 변환 방향을 별도로 표시하는 문서를 작성합니다. 이 문서가 미션 구현의 입력 계약이 됩니다.

네 프레임의 책임을 나눕니다

base_link는 로봇 본체에 붙은 기준입니다. 이번 프로젝트에서는 로봇 중심을 원점으로 정하고 전방 x, 왼쪽 y를 사용합니다. sensor는 장착된 센서에 붙은 기준이며 이 실습에서는 전방 0.2m, 회전 0으로 고정합니다. 이 수치는 센서 제품의 공통 사양이 아니라 fixture의 가정입니다. 센서를 다른 곳에 옮기면 base_link에서 본 장착 변환부터 바꾸어야 합니다.

odom은 이동량 추정에 사용하는 기준입니다. 일반적인 이동 로봇 규약에서는 odom 기준 자세가 연속적으로 변하지만 장기적으로 오차가 누적될 수 있습니다. map은 지도와 연결한 기준이며 위치추정 보정으로 로봇의 표현 위치가 갑자기 달라질 수 있습니다. 그러므로 odom과 map을 이름만 다른 같은 프레임으로 취급하지 않습니다. 두 기준을 나누면 연속적인 운동 제어와 지도 기준 위치 보정의 책임을 구별할 수 있습니다.

이번 PC 모델은 map과 odom을 일치시키는 단위 변환을 사용하고, odom 기준 base_link를 (1,2)m, yaw +90도로 놓습니다. 이는 한 시점의 시험 자세입니다. 실제 로봇의 위치를 추정한 결과가 아니고, 모든 시각에서 같은 자세가 맞다는 주장도 아닙니다. 문서에는 fixture의 고정값과 실제 시스템에서 갱신할 관계를 구분해서 적습니다. 후속 시간 모듈에서 stamp_s와 자세 시각을 연결할 예정입니다.

트리 화살표와 점 적용 방향

구조 그림은 map → odom → base_link → sensor로 그립니다. 화살표는 부모에서 자식으로 연결된다는 뜻입니다. 각 연결에 저장하는 자세는 부모 기준에서 본 자식 원점과 회전입니다. 이 값을 행렬로 적용하면 자식 점이 부모 점으로 바뀝니다. 구조 화살표를 따라 map 점을 sensor 점으로 바꾸고 싶다면 저장 행렬을 그대로 쓰는 대신 역변환을 적용해야 합니다.

문서에서 parent→child라는 구조 표기와 parent_from_child라는 계산 이름을 나란히 적습니다. 예를 들어 base_link→sensor 연결의 base_from_sensor는 sensor의 (1,0)을 base_link의 (1.2,0)으로 바꿉니다. 센서 점을 지도에 표현하는 경로는 sensor → base_link → odom → map이며 구조 그림과 반대입니다. 두 그림에 모두 “변환 방향”이라고만 쓰면 의미가 섞이므로 범례를 붙입니다.

합성 경로를 검토합니다

map_from_sensor는 map_from_odom, odom_from_base_link, base_link_from_sensor 순서로 곱합니다. 오른쪽 행렬부터 입력 점에 적용되므로 가장 먼저 센서 장착 변환이 작용합니다. 중간 프레임 이름이 이어지는지 코드와 문서에서 함께 확인합니다. 결과 점은 map 기준이므로 출력 frame_id도 map으로 바꿉니다. 숫자만 변환하고 원래 frame_id를 남기면 뒤 단계에서 같은 변환을 두 번 적용할 수 있습니다.

미션 message_point는 원본 메시지를 복사하고 source_frame_id에 이전 이름을 남깁니다. seq와 stamp_s는 측정의 순서와 시각이므로 좌표를 바꿨다고 수정하지 않습니다. range_m도 센서 원점에서 읽은 거리로 유지하며 point_m를 새로 추가합니다. 한 개 거리만으로 2차원 방향이 정해지는 것은 아니므로 이 실습은 센서 +x 방향 빔이라는 가정을 추가합니다. 실제 스캔에서는 각도 정보를 함께 사용해야 합니다.

한 자식과 한 부모, 순환 없는 경로

같은 child에 두 parent를 연결하면 원점과 축의 정의가 두 개 생깁니다. map→base_link를 직접 추가하면서 odom→base_link도 유지하는 방식은 이번 트리 계약에서 거절합니다. map에서 base_link로 가는 관계는 기존 연결을 합성해 구합니다. 연결마다 자세를 만드는 책임자도 하나로 정합니다. 위치추정과 이동량 추정이 같은 연결을 서로 다른 값으로 갱신하면 수치보다 책임 경계를 먼저 조정합니다.

순환은 조상을 따라가다 이미 방문한 이름을 다시 만나는 경우입니다. a의 부모가 b이고 b의 부모가 a이면 루트에 도달하지 못합니다. FrameTree 생성 시 전체 입력을 먼저 검사해 cycle 오류로 거절합니다. 조회할 때 무한 반복하다 멈추는 방식은 쓰지 않습니다. 아직 조회하지 않은 가지의 순환도 생성 단계에서 확인해야 잘못된 설정이 일부 정상 결과 뒤에 숨어 있지 않습니다.

순환이 없어도 map까지 연결되지 않은 가지가 있을 수 있습니다. other→sensor만 있고 other가 등록된 루트와 이어지지 않았다면 disconnected frame 오류입니다. 조회 이름 자체가 등록되지 않은 laser라면 unknown frame 오류입니다. 이 두 경우를 구분하면 이름 오타를 고쳐야 하는지 누락된 연결을 추가해야 하는지 판단할 수 있습니다. 실패를 단위 행렬로 대체하면 좌표가 원래부터 map이었다는 잘못된 가정을 만듭니다.

고정 관계와 변화하는 관계

센서가 본체에 단단히 고정되었다는 가정에서는 base_link와 sensor의 관계가 일정합니다. odom과 base_link는 움직이는 로봇의 자세이므로 실제 시스템에서 시각별로 달라집니다. 로봇이 잠시 멈췄다는 이유로 동적 관계를 영구 고정으로 바꾸지 않습니다. map과 odom도 위치추정 보정이 있으면 변할 수 있습니다. 프레임 이름보다 관계가 시간에 따라 달라지는지로 고정 여부를 정합니다.

ROS 2의 실제 tf2는 관계의 발행, 수신 버퍼와 시각별 조회를 다룹니다. 여기의 FrameTree는 동적 버퍼나 보간을 구현하지 않은 순수 Python 모델입니다. 따라서 이 실습을 통과했다는 사실로 실제 ROS 연결 수신이나 시간 정렬이 검증되었다고 말하지 않습니다. 설치와 API 사용 예시는 더 읽기의 tf2 장을 참고하며 지금 제출할 문서는 변환 기준과 책임 경계를 명확히 하는 데 집중합니다.

제출물과 오류를 읽는 기준

실습 제출물은 트리 그림, 연결 표, 점의 적용 경로, 실패 처리 표입니다. 연결 표에는 parent, child, 이동 m, yaw rad, 고정 여부, 책임자를 넣습니다. 점 사례는 sensor의 (1,0)에서 base_link (1.2,0), odom (1,3.2), map (1,3.2)을 차례로 적습니다. 원점 사례와 역방향 복원 사례도 추가하면 표시만 보고 합성 순서를 검토할 수 있습니다. 보기 좋은 그림보다 모든 수치의 의미가 읽히는 문서를 목표로 합니다.

duplicate parent: sensor는 같은 자식이 두 번 정의된 설정을 가리킵니다. cycle: a는 조상 순회가 a로 되돌아왔다는 뜻이고 unknown frame: laser는 조회한 이름의 누락입니다. disconnected frame: other는 루트까지 경로가 없다는 뜻입니다. 오류 이름만 외우지 말고 문제가 생긴 연결을 그림에서 표시합니다. 이전 모듈의 유효·오류 행 분류 결과와 새 좌표 변환 결과를 함께 제출해 기존 계약도 유지되었음을 확인합니다.

축과 단위는 ROS REP 103, map·odom·base_link의 역할은 ROS REP 105를 기준으로 검토합니다. sensor라는 이름과 장착 수치는 프로젝트에서 정한 계약입니다. 완료할 때는 구조의 부모 화살표와 점 계산의 적용 화살표를 각각 설명하고, 미등록 이름과 순환이 발생했을 때 정상 점을 만들지 않는 이유를 말할 수 있어야 합니다.

따라하기

구조와 적용 경로를 분리

화살표의 범례를 함께 출력하여 문서 초안을 만듭니다.

frames=["map","odom","base_link","sensor"]
print("parent -> child:", " -> ".join(frames))
print("point source -> target:", " -> ".join(reversed(frames)))
print("map_from_sensor = map_from_odom * odom_from_base_link * base_link_from_sensor")

실행 결과

parent -> child: map -> odom -> base_link -> sensor
point source -> target: sensor -> base_link -> odom -> map
map_from_sensor = map_from_odom * odom_from_base_link * base_link_from_sensor

순환을 설정 단계에서 검출

순환 설정을 방문 집합으로 거절하는 원리를 실행합니다.

parents={"a":"b","b":"a"}
node="a"
visited=set()
while node in parents:
    if node in visited:
        print("cycle:",node)
        break
    visited.add(node)
    node=parents[node]

실행 결과

cycle: a

기존 메시지를 점으로 연결

단일 빔 +x 가정에서 장착 이동과 자세를 적용하고 측정 메타데이터를 유지합니다.

import math
raw=dict(seq=3,stamp_s=.4,frame_id="sensor",range_m=1.0)
x_base=raw["range_m"]+.2
y_base=0.0
c,s=math.cos(math.pi/2),math.sin(math.pi/2)
point=(c*x_base-s*y_base+1,s*x_base+c*y_base+2)
result=dict(raw,frame_id="map",source_frame_id=raw["frame_id"],point_m=point)
print(f"point_m={result['point_m'][0]:.3f},{result['point_m'][1]:.3f}")
print("frames",raw["frame_id"],result["frame_id"])
print("metadata",result["seq"],result["stamp_s"],result["range_m"])

실행 결과

point_m=1.000,3.200
frames sensor map
metadata 3 0.4 1.0

확인 문제

실습

프레임 설계 문서 한 개를 제출합니다. ① 부모→자식 트리 map→odom→base_link→sensor와 범례 ② 각 연결의 parent·child·이동 m·yaw rad·고정 여부·책임자 표 ③ sensor (1,0)에서 base_link (1.2,0), odom (1,3.2), map (1,3.2)으로 가는 적용 경로와 곱셈 식 ④ map 점을 sensor로 복원하는 경로 ⑤ unknown frame·disconnected frame·cycle·duplicate parent의 잘못된 연결 예와 처리 기준을 포함합니다. map←odom은 단위 변환, odom←base_link는 (1,2,π/2), base_link←sensor는 (0.2,0,0)을 사용합니다. 실제 ROS 2 통신·시각별 조회와 PC 고정 fixture의 검증 범위를 구분합니다. 미션 ZIP README와 대조하고 bash check.sh 결과를 문서에 덧붙입니다.

더 읽기

면접 질문

  • 서로 다른 좌표계의 위치를 변환하는 과정을 설명해 주시면 됩니다.