2. 시뮬레이션
2.10SLAM 설정 읽기
이 장에서는 SLAM을 실행하지 않습니다. VM의 시뮬레이션에는 /scan도 오도메트리도 없으므로 slam_toolbox를 띄워도 입력이 비어 있습니다. 대신 실제 로봇에서 쓰는 설정 파일을 읽고, 값 하나가 무엇을 바꾸는지 이해하는 데 집중합니다. 지도가 쌓이는 과정은 다음 장에서 브라우저 에뮬레이터로 직접 해 보고, 실제 SLAM 실행은 4.7장에서 합니다. 설정을 미리 읽어 두면 그때 지도가 이상하게 나와도 어디를 볼지 압니다.
SLAM이란
SLAM(Simultaneous Localization and Mapping) 은 지도가 없는 곳에서 로봇이 지도를 그리면서 동시에 그 지도 안의 자기 위치를 추정하는 문제입니다. 두 일이 서로를 필요로 합니다. 위치를 알아야 스캔을 지도의 올바른 자리에 붙일 수 있고, 지도가 있어야 위치를 보정할 수 있습니다.
slam_toolbox는 아래 세 가지 개념으로 이 문제를 풉니다.
- 스캔 매칭: 새 LiDAR 스캔을 직전 스캔들과 겹쳐 보고 가장 잘 맞는 이동량을 찾습니다. 바퀴 오도메트리가 준 값을 출발점으로 삼아 그 주변만 탐색합니다.
- 포즈 그래프: 처리한 스캔마다 노드를 하나 만들고, 노드 사이의 상대 이동을 간선으로 이어 둡니다. 지도는 이 노드들의 스캔을 격자에 그린 결과입니다.
- 루프 클로저: 한 바퀴 돌아 처음 보던 곳에 돌아오면 지금 스캔이 아주 오래전 스캔과 겹칩니다. 이 연결을 그래프에 추가하면 그동안 쌓인 오차가 한꺼번에 줄어들도록 솔버(이 설정에서는 Ceres)가 모든 노드 위치를 다시 계산합니다.
실행 방식에는 두 가지가 있습니다. mode: mapping은 새 지도를 만들거나 이어 그립니다. mode: localization은 저장해 둔 포즈 그래프를 불러와 그 안에서 위치만 추정하고 지도는 건드리지 않습니다(slam_toolbox README 기준). Nav2의 AMCL과 비슷한 역할이지만 저장 형식이 다릅니다.
SLAM이 필요로 하는 데이터 체인
slam_toolbox가 지도를 그리려면 세 가지가 맞아야 합니다. 1.9장에서 본 프레임 규칙이 그대로 쓰입니다.
| 필요한 것 | 공급 노드 | 출처 |
|---|---|---|
/scan (LaserScan, frame_id: laser) | rplidar_node | tiki_bringup/launch/rplidar.launch.py |
odom → base_footprint TF | diff_controller | tiki_control 설정의 enable_odom_tf: true |
base_footprint → … → laser TF | robot_state_publisher | tiki_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가 어떻게 반응하는지 확인합니다.
- map
- 연결됨map → odom · 발행: slam_toolboxodom
- 연결됨odom → base_footprint · 발행: diff_controllerbase_footprint
- 연결됨base_footprint → base_link · 발행: robot_state_publisherbase_link
- 연결됨base_link → laser · 발행: robot_state_publisherlaser
| 필요한 것 | 공급 노드 | 상태 |
|---|---|---|
| /scan (LaserScan) | rplidar_node | 있음 |
| odom → base_footprint | diff_controller | 있음 |
| base_footprint → laser | robot_state_publisher | 있음 |
| /map, map → odom | slam_toolbox | 발행됨 |
노드의 존재 여부만 따지는 교육용 모형입니다. 실제로는 타임스탬프가 맞아야 하고, 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_frame | odom | 오도메트리 프레임. diff_controller의 odom_frame과 같아야 함 |
map_frame | map | slam_toolbox가 map → odom으로 발행하는 프레임 |
base_frame | base_footprint | 로봇 기준 프레임. diff_controller의 base_frame과 같아야 함 |
scan_topic | /scan | 구독할 토픽. README는 절대 경로로 쓰라고 안내함 |
mode | mapping | localization으로 바꾸면 저장한 포즈 그래프를 불러와 위치만 추정 |
세 프레임 이름이 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.10 | 50 | 2,500 | 1/4 |
| 0.05 (TIKI) | 100 | 10,000 | 1배 |
| 0.025 | 200 | 40,000 | 4배 |
해상도를 절반으로 줄이면 칸 수는 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_distance | 0.5 m | 0.5 / 0.2 = 2.5초 |
minimum_travel_heading | 0.5 rad (약 28.6도) | 0.5 / 0.85 약 0.59초 |
minimum_time_interval | 0.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_distance | 3.0 m | 현재 위치에서 이 거리 안의 옛 스캔만 후보로 삼음 |
loop_match_minimum_chain_size | 10 | 연속된 스캔 노드가 최소 이만큼 쌓여야 루프를 찾기 시작함 |
loop_match_minimum_response_coarse / fine | 0.35 / 0.45 | 대략 매칭과 정밀 매칭이 합격하려면 넘어야 하는 점수 |
loop_search_space_dimension | 8.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→odomTF를 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도 회전마다 하나씩 생깁니다. 제자리에서 빠르게 돌면 오도메트리 오차가 커져 지도가 겹쳐 그려집니다.
- 한 바퀴를 닫아 돌아오는 경로를 한 번 포함하면 루프 클로저가 일하는 모습을 볼 수 있습니다.
한눈에 정리
- SLAM은 지도를 그리면서 그 안의 자기 위치를 추정하는 문제이고, 스캔 매칭, 포즈 그래프, 루프 클로저로 푼다.
- slam_toolbox에는
/scan,odom→base_footprintTF,base_footprint→laserTF가 필요하고/map과map→odom을 낸다. slam.launch.py는async_slam_toolbox_node에config/slam.yaml을 넘기는 짧은 파일이다.resolution: 0.05는 1 m당 20칸이며 5 m x 5 m 방은 100 x 100 = 10,000칸이다.minimum_travel_distance/heading: 0.5는 최대 속도에서 직진 2.5초, 회전 약 0.59초마다 노드가 하나 생기는 간격이다.- 루프 클로저 합격선(
loop_match_minimum_response_*)은 낮추면 오인식, 높이면 미검출 쪽으로 기운다. - 지도는
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 설정을 읽습니다. 지도를 받은 로봇이 어떤 서버들의 협력으로 목표까지 가는지, 코스트맵의 비용 곡선을 조작하면서 확인합니다.