TIKI-MINI로 시작하는 ROS2 실무
목차

2. 시뮬레이션

2.10SLAM 설정 읽기

이 장에서는 SLAM을 실행하지 않습니다. VM의 시뮬레이션에는 /scan도 오도메트리도 없으므로 slam_toolbox를 띄워도 입력이 비어 있습니다. 대신 실제 로봇에서 쓰는 설정 파일을 읽고, 값 하나가 무엇을 바꾸는지 이해하는 데 집중합니다. 지도가 쌓이는 과정은 다음 장에서 브라우저 에뮬레이터로 직접 해 보고, 실제 SLAM 실행은 4.7장에서 합니다. 설정을 미리 읽어 두면 그때 지도가 이상하게 나와도 어디를 볼지 압니다.

SLAM이란

SLAM(Simultaneous Localization and Mapping) 은 지도가 없는 곳에서 로봇이 지도를 그리면서 동시에 그 지도 안의 자기 위치를 추정하는 문제입니다. 두 일이 서로를 필요로 합니다. 위치를 알아야 스캔을 지도의 올바른 자리에 붙일 수 있고, 지도가 있어야 위치를 보정할 수 있습니다.

slam_toolbox는 아래 세 가지 개념으로 이 문제를 풉니다.

  • 스캔 매칭: 새 LiDAR 스캔을 직전 스캔들과 겹쳐 보고 가장 잘 맞는 이동량을 찾습니다. 바퀴 오도메트리가 준 값을 출발점으로 삼아 그 주변만 탐색합니다.
  • 포즈 그래프: 처리한 스캔마다 노드를 하나 만들고, 노드 사이의 상대 이동을 간선으로 이어 둡니다. 지도는 이 노드들의 스캔을 격자에 그린 결과입니다.
  • 루프 클로저: 한 바퀴 돌아 처음 보던 곳에 돌아오면 지금 스캔이 아주 오래전 스캔과 겹칩니다. 이 연결을 그래프에 추가하면 그동안 쌓인 오차가 한꺼번에 줄어들도록 솔버(이 설정에서는 Ceres)가 모든 노드 위치를 다시 계산합니다.
포즈 그래프와 루프 클로저루프를 닫기 전루프 클로저 뒤 재계산오도메트리 오차가쌓여 휘어진 그래프같은 곳을 다시 봄Ceres가 모든 노드위치를 다시 계산루프 클로저 간선스캔 노드노드 사이 상대 이동오래전 스캔과 겹친 연결(루프 클로저)
스캔 노드를 상대 이동 간선으로 이은 포즈 그래프입니다. 처음 보던 곳을 다시 보면 루프 클로저 간선이 더해지고, 솔버가 모든 노드 위치를 다시 계산해 휘어 있던 그래프가 맞춰집니다.

실행 방식에는 두 가지가 있습니다. mode: mapping은 새 지도를 만들거나 이어 그립니다. mode: localization은 저장해 둔 포즈 그래프를 불러와 그 안에서 위치만 추정하고 지도는 건드리지 않습니다(slam_toolbox README 기준). Nav2의 AMCL과 비슷한 역할이지만 저장 형식이 다릅니다.

SLAM이 필요로 하는 데이터 체인

slam_toolbox가 지도를 그리려면 세 가지가 맞아야 합니다. 1.9장에서 본 프레임 규칙이 그대로 쓰입니다.

필요한 것공급 노드출처
/scan (LaserScan, frame_id: laser)rplidar_nodetiki_bringup/launch/rplidar.launch.py
odom → base_footprint TFdiff_controllertiki_control 설정의 enable_odom_tf: true
base_footprint → … → laser TFrobot_state_publishertiki_description의 URDF

slam_toolbox가 내놓는 것은 /map(nav_msgs/OccupancyGrid)과 map → odom TF입니다. map → odom 변환 덕분에 TF 트리가 map → odom → base_footprint → base_link → … → laser로 한 줄로 이어집니다. 1.9장의 REP-105 구조입니다.

아래 모형에서 노드를 껐다 켜 보세요. 어느 노드가 빠졌을 때 어느 링크가 끊기는지, 그때 slam_toolbox가 어떻게 반응하는지 확인합니다.

실행 중인 노드 고르기
TF 트리
  1. map
  2. 연결됨map → odom · 발행: slam_toolbox
    odom
  3. 연결됨odom → base_footprint · 발행: diff_controller
    base_footprint
  4. 연결됨base_footprint → base_link · 발행: robot_state_publisher
    base_link
  5. 연결됨base_link → laser · 발행: robot_state_publisher
    laser
필요한 것공급 노드상태
/scan (LaserScan)rplidar_node있음
odom → base_footprintdiff_controller있음
base_footprint → laserrobot_state_publisher있음
/map, map → odomslam_toolbox발행됨
SLAM이 동작할 조건을 갖췄습니다. 스캔과 두 TF 링크가 모두 있고, slam_toolbox가 map → odom 변환과 /map 을 발행합니다.

노드의 존재 여부만 따지는 교육용 모형입니다. 실제로는 타임스탬프가 맞아야 하고, TF가 있어도 값이 어긋나면 지도가 망가집니다.

직접 해 보기

  • diff_controller만 끄면 끊어지는 링크는 어디인가요? 이때 rplidar_node는 정상인데도 SLAM이 안 되는 이유는 무엇인가요?
  • robot_state_publisher를 끄면 /scan이 나와도 SLAM이 안 되는 이유를 설명해 보세요.
  • slam_toolbox만 켜고 나머지를 끄면 map 프레임은 생기나요?

모형의 로그 문구(Failed to compute odom pose, Failed to compute laser pose, aborting initialization)는 slam_toolbox humble 브랜치 소스에서 읽은 것입니다. 모형은 /scan이 없으면 입력을 기다린다고만 설명합니다.

실행 파일 읽기: slam.launch.py

tiki_slam/launch/slam.launch.py는 짧습니다. 핵심은 노드 하나와 인자 하나입니다.

start_async_slam_toolbox_node = Node(
    parameters=[slam_params_file],
    package='slam_toolbox',
    executable='async_slam_toolbox_node',
    name='slam_toolbox',
    output='screen')
  • 인자 slam_params_file의 기본값은 tiki_slam 패키지의 config/slam.yaml입니다. 다른 파일을 쓰려면 slam_params_file:=/내/경로.yaml로 덮어씁니다.
  • 노드 이름이 slam_toolbox라서 slam.yaml의 최상위 키 slam_toolbox: 아래 ros__parameters가 이 노드에 적용됩니다. 1.5장, 1.7장에서 배운 구조입니다.
  • 실행 파일은 async_slam_toolbox_node입니다. 비동기 방식은 스캔이 밀려도 최신 스캔을 처리하려는 쪽이라 실시간 지도 작성에 쓰입니다. 모든 스캔을 순서대로 처리하는 동기(sync) 방식도 있습니다.

설정 파일 읽기: slam.yaml

75줄 전부를 같은 비중으로 읽을 필요는 없습니다. 지도 결과와 CPU 부하에 영향이 큰 순서로 묶어서 읽습니다.

1. 프레임과 입력 토픽

odom_frame: odom
map_frame: map
base_frame: base_footprint
scan_topic: /scan
mode: mapping #localization
파라미터TIKI 값의미
odom_frameodom오도메트리 프레임. diff_controller의 odom_frame과 같아야 함
map_framemapslam_toolbox가 map → odom으로 발행하는 프레임
base_framebase_footprint로봇 기준 프레임. diff_controller의 base_frame과 같아야 함
scan_topic/scan구독할 토픽. README는 절대 경로로 쓰라고 안내함
modemappinglocalization으로 바꾸면 저장한 포즈 그래프를 불러와 위치만 추정

세 프레임 이름이 tiki_controllers.yaml과 한 글자도 다르면 TF 조회가 실패합니다. 위 모형에서 본 "Failed to compute ..." 로그가 이 경우에도 나옵니다. mode: localization으로 바꿀 때는 불러올 지도 파일을 map_file_name으로 지정해야 합니다. slam.yaml에는 이 줄이 주석 처리되어 있어서 localization으로 바꾸기만 해서는 불러올 지도가 없습니다.

2. 지도 해상도와 갱신

resolution: 0.05
max_laser_range: 12.0
map_update_interval: 5.0

resolution은 격자 한 칸의 한 변 길이입니다. 단위는 m/칸이고 0.05는 5 cm입니다. 1 m가 20칸입니다. 5 m x 5 m 방을 예로 계산하면 다음과 같습니다.

resolution한 변 칸 수전체 칸 수0.05 대비
0.10502,5001/4
0.05 (TIKI)10010,0001배
0.02520040,0004배

해상도를 절반으로 줄이면 칸 수는 4배가 됩니다. 얇은 벽과 좁은 문틈은 세밀하게 그려지지만 지도 파일과 이후 Nav2 코스트맵 계산 부담이 늘어납니다. 2.12장에서 보듯 코스트맵 resolution도 0.05라서, 지도와 코스트맵 해상도를 맞춰 두는 편이 자연스럽습니다.

max_laser_range: 12.0(m)은 지도를 그릴 때 사용하는 스캔 거리의 상한입니다. 이보다 먼 점은 그리지 않습니다. 5 m 방에서는 12 m까지 볼 일이 없으므로 이 값은 사실상 영향이 없고, 넓은 복도에서 먼 반사점이 지도를 지저분하게 만들 때 줄입니다.

map_update_interval: 5.0(초)은 /map을 다시 발행하는 주기입니다. 값이 클수록 갱신이 드물어집니다. 현재 값이 5초라서 RViz의 지도는 5초에 한 번씩 갱신됩니다. 스캔 처리 자체와는 별개라서, 이 값을 줄이면 지도가 더 자주 보일 뿐 정확도는 같고 CPU 부하만 늘어납니다.

3. 스캔을 얼마나 자주 처리하나

throttle_scans: 1
minimum_travel_distance: 0.5
minimum_travel_heading: 0.5
minimum_time_interval: 0.5

모든 스캔을 매칭하면 CPU 부담이 커집니다. 그래서 일정 거리나 각도를 움직였을 때만 새 노드를 만듭니다. README는 이 세 값을 스캔 처리 빈도를 조절하는 값이라고 설명합니다. 단위는 m, rad, 초입니다.

TIKI의 속도 한계(linear.x 0.2 m/s, angular.z 0.85 rad/s)로 환산하면 감이 잡힙니다.

설정값최대 속도로 움직일 때 걸리는 시간
minimum_travel_distance0.5 m0.5 / 0.2 = 2.5초
minimum_travel_heading0.5 rad (약 28.6도)0.5 / 0.85 약 0.59초
minimum_time_interval0.5초시간 간격 하한

직진 2.5초마다 노드가 하나씩 생깁니다. 방이 작으면 노드가 너무 듬성해서 스캔끼리 겹침이 줄 수 있습니다. 0.5 m는 TIKI 크기 로봇에게 넉넉한 값이므로, 작은 실내에서 지도가 끊겨 보이면 이 값부터 줄여 봅니다. throttle_scans: 1은 스캔을 건너뛰지 않고 모두 후보로 본다는 뜻입니다.

4. 루프 클로저

do_loop_closing: true
loop_search_maximum_distance: 3.0
loop_match_minimum_chain_size: 10
loop_match_maximum_variance_coarse: 3.0
loop_match_minimum_response_coarse: 0.35
loop_match_minimum_response_fine: 0.45
loop_search_space_dimension: 8.0
loop_search_space_resolution: 0.05
loop_search_space_smear_deviation: 0.03

do_loop_closing이 켜져 있어야 위의 loop_* 값이 의미를 갖습니다. README는 켜 두기를 권합니다. 나머지는 후보를 찾는 범위와 합격선입니다.

파라미터값역할
loop_search_maximum_distance3.0 m현재 위치에서 이 거리 안의 옛 스캔만 후보로 삼음
loop_match_minimum_chain_size10연속된 스캔 노드가 최소 이만큼 쌓여야 루프를 찾기 시작함
loop_match_minimum_response_coarse / fine0.35 / 0.45대략 매칭과 정밀 매칭이 합격하려면 넘어야 하는 점수
loop_search_space_dimension8.0 m루프 매칭 탐색 영역의 크기

합격선을 낮추면 루프를 더 쉽게 인정하지만, 비슷하게 생긴 복도를 같은 곳으로 착각해 지도가 엉키는 위험이 커집니다. 높이면 안전한 대신 한 바퀴를 돌아도 루프가 닫히지 않아 어긋남이 그대로 남습니다.

5. 상관 탐색과 스캔 매처

correlation_search_space_dimension: 0.5
correlation_search_space_resolution: 0.01
correlation_search_space_smear_deviation: 0.1
distance_variance_penalty: 0.5
angle_variance_penalty: 1.0
fine_search_angle_offset: 0.00349
coarse_search_angle_offset: 0.349
coarse_angle_resolution: 0.0349
minimum_angle_penalty: 0.9
minimum_distance_penalty: 0.5
use_response_expansion: true

일반 스캔 매칭(correlation_*)은 오도메트리가 준 위치 주변을 격자 모양으로 훑으며 가장 잘 겹치는 자리를 찾습니다. 0.5 m 크기의 영역을 0.01 m(1 cm) 간격으로 훑습니다. smear_deviation은 스캔 점을 약간 번지게 해서 작은 오차에 점수가 급락하지 않게 합니다.

각도 탐색은 두 단계입니다. coarse_search_angle_offset 0.349 rad(약 20도) 범위를 coarse_angle_resolution 0.0349 rad(약 2도) 간격으로 먼저 훑고, 후보 주변을 fine_search_angle_offset 0.00349 rad(약 0.2도)로 정밀하게 다듬습니다.

distance_variance_penalty, angle_variance_penalty, minimum_*_penalty는 후보가 오도메트리 추정에서 멀리 떨어져 있을 때 그 점수를 깎는 강도를 정합니다. 바퀴가 미끄러지는 바닥이라 오도메트리를 믿기 어렵다면 이 값들을 조정해 후보를 더 넓게 인정하는 방향으로 튜닝합니다. 실제로 어느 방향으로 얼마나 바꿔야 하는지는 로봇과 바닥마다 다르고, reference 파일에는 튜닝 근거가 없습니다. use_response_expansion: true는 매칭이 안 될 때 탐색 격자를 넓혀 다시 시도하게 합니다.

6. 솔버, TF 발행, 인터랙티브 모드

solver_plugin: solver_plugins::CeresSolver
transform_publish_period: 0.02
transform_timeout: 0.2
tf_buffer_duration: 30.0
enable_interactive_mode: true
  • solver_plugin: 포즈 그래프 전체를 최적화하는 계산기입니다. README의 선택지는 Ceres, SPA, g2o이고 TIKI는 Ceres입니다. 나머지 ceres_* 다섯 줄은 Ceres의 수치 계산 방식을 고르는 값이라 처음에는 건드리지 않습니다.
  • transform_publish_period: 0.02(초)는 map → odom TF를 50 Hz로 발행한다는 뜻입니다. 주석대로 0이면 이 TF를 발행하지 않으므로 Nav2가 map 프레임을 못 찾습니다.
  • transform_timeout: 0.2(초)는 TF를 기다리는 한도이고, tf_buffer_duration: 30.0(초)은 TF 기록을 보관하는 길이입니다.
  • enable_interactive_mode: true는 RViz에서 포즈 그래프 노드를 끌어 옮겨 수정하는 기능에 쓰입니다. 대신 스캔을 캐시하므로 메모리를 더 씁니다(README). 지도가 클 때는 끕니다.

stack_size_to_use: 40000000은 큰 지도를 직렬화할 때 필요한 스택 크기(약 40 MB)입니다. 이외의 값은 slam_toolbox 기본 설정 파일을 거의 그대로 가져온 것이어서, 문제가 생기기 전에는 바꿀 이유가 없습니다.

증상별로 어디를 볼까

증상먼저 볼 곳
지도가 아예 안 나옴TF 두 링크와 /scan (위의 모형), 프레임 이름 일치
직선 복도가 휘어짐바퀴 오도메트리 오차(wheel_radius, wheel_separation), 스캔 매처 값
한 바퀴를 돌아도 벽이 두 겹으로 남음do_loop_closing, loop_match_minimum_response_*
RViz 지도가 늦게 갱신됨map_update_interval(5초)
작은 방에서 지도가 듬성함minimum_travel_distance, minimum_travel_heading

실기기에서 실행하기 (4.7장 미리보기)

아래 명령은 VM이 아니라 TIKI-MINI용입니다. 지금 실행하지 마세요. 흐름만 눈에 익혀 둡니다.

# 터미널 1: 로봇 기본 구동 (rsp, 컨트롤러, LiDAR)
ros2 launch tiki_bringup tiki.launch.py

# 터미널 2: SLAM 시작
ros2 launch tiki_slam slam.launch.py

# 터미널 3: 키보드로 천천히 돌아다니며 지도 만들기
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# 지도가 충분히 그려지면 저장 (파일 2개가 생김)
ros2 run nav2_map_server map_saver_cli -f ~/maps/room1
  • map_saver_cli는 nav2_map_server 패키지에 있고, 옵션은 -f <이름>(파일 이름), -t <토픽>(기본 map), --occ, --free, --fmt, --mode입니다(navigation2 humble 소스의 도움말 문구 기준). -f를 주고 실행하면 이미지 파일(.pgm)과 지도 설명 파일(.yaml)이 만들어지는 것이 일반적이며, 2.12장의 map:= 인자가 이 .yaml을 가리킵니다.
  • slam_toolbox가 /map을 발행하는 중이어야 저장됩니다. 지도를 저장하기 전에 RViz2에서 map 프레임 기준으로 지도가 보이는지 확인합니다.
  • 천천히 움직이세요. 위 계산에서 봤듯 노드는 0.5 m 이동이나 약 28.6도 회전마다 하나씩 생깁니다. 제자리에서 빠르게 돌면 오도메트리 오차가 커져 지도가 겹쳐 그려집니다.
  • 한 바퀴를 닫아 돌아오는 경로를 한 번 포함하면 루프 클로저가 일하는 모습을 볼 수 있습니다.

한눈에 정리

  1. SLAM은 지도를 그리면서 그 안의 자기 위치를 추정하는 문제이고, 스캔 매칭, 포즈 그래프, 루프 클로저로 푼다.
  2. slam_toolbox에는 /scan, odom → base_footprint TF, base_footprint → laser TF가 필요하고 /map과 map → odom을 낸다.
  3. slam.launch.py는 async_slam_toolbox_node에 config/slam.yaml을 넘기는 짧은 파일이다.
  4. resolution: 0.05는 1 m당 20칸이며 5 m x 5 m 방은 100 x 100 = 10,000칸이다.
  5. minimum_travel_distance/heading: 0.5는 최대 속도에서 직진 2.5초, 회전 약 0.59초마다 노드가 하나 생기는 간격이다.
  6. 루프 클로저 합격선(loop_match_minimum_response_*)은 낮추면 오인식, 높이면 미검출 쪽으로 기운다.
  7. 지도는 ros2 run nav2_map_server map_saver_cli -f <이름>으로 저장한다. 이 실행은 4.7장에서 한다.

확인 문제

Q1. diff_controller를 실행하지 않은 채 slam_toolbox와 rplidar_node, robot_state_publisher만 켰습니다. 어떤 변환이 빠지고 SLAM은 어떻게 되나요?

odom → base_footprint 변환이 빠집니다. slam_toolbox는 스캔 시각의 오도메트리 자세를 구하지 못하므로 스캔을 처리하지 못하고 지도도 map → odom 변환도 나오지 않습니다. /scan이 정상이어도 마찬가지입니다.

Q2. 8 m x 6 m 방을 resolution 0.05로 그리면 지도 격자는 가로 x 세로 몇 칸이고, 0.1로 바꾸면 전체 칸 수가 몇 배가 되나요?

0.05이면 가로 160칸, 세로 120칸으로 19,200칸입니다. 0.1이면 80 x 60 = 4,800칸이라 전체 칸 수는 1/4배가 됩니다.

Q3. 로봇이 최대 속도 0.2 m/s로 직진할 때 minimum_travel_distance 0.5 m 조건만으로는 몇 초마다 새 스캔 노드가 생기나요? 방이 아주 좁아 지도가 듬성하다면 무엇을 조정해 볼까요?

0.5 / 0.2 = 2.5초마다입니다. 좁은 방에서는 minimum_travel_distance와 minimum_travel_heading을 줄여 스캔 노드를 더 촘촘히 만들어 봅니다. CPU 부하는 늘어납니다.

다음 장

다음 장에서는 같은 방식으로 tiki_navigation의 Nav2 설정을 읽습니다. 지도를 받은 로봇이 어떤 서버들의 협력으로 목표까지 가는지, 코스트맵의 비용 곡선을 조작하면서 확인합니다.