Skip to main content
Summary

로봇이 처음으로 방을 그리는 순간

지난 장에서 우리는 오도메트리를 배웠다. 바퀴가 얼마나 돌았는지 보고 로봇이 얼마나 움직였는지 추정하는 방식이었다. 오도메트리는 빠르고 유용하지만, 시간이 지날수록 조금씩 거짓말을 한다. 바퀴가 미끄러지고, 바닥이 울퉁불퉁하고, 회전할 때 작은 오차가 쌓인다. 처음에는 몇 센티미터였던 차이가 긴 복도를 한 바퀴 돌고 나면 벽 하나가 비스듬히 밀려 보일 수 있다.

LiDAR SLAM은 이 문제를 현실적으로 해결하는 가장 좋은 출발점이다. 특히 실내 이동 로봇에서는 2D LiDAR 하나와 바퀴 오도메트리만으로도 꽤 쓸 만한 지도를 만들 수 있다. 로봇은 바퀴로 "아마 이쯤 왔을 거야"라고 예측하고, LiDAR로 "그런데 벽 모양을 보니 조금 오른쪽이야"라고 고친다. 이 과정을 반복하면 로봇은 낯선 공간을 돌아다니며 자기 위치와 지도를 동시에 만들어 낸다.

이 장의 목표는 단순하다. 2D LiDAR의 /scan 데이터를 이해하고, slam_toolbox를 실행해 /map을 만들고, RViz에서 지도와 로봇 자세를 확인한 뒤, 완성된 점유 격자 지도를 저장하는 것이다. 오늘부터 로봇은 더 이상 어두운 방에서 더듬거리는 장난감이 아니다. 벽과 문, 책상과 복도를 자기만의 평면도로 바꾸는 작은 탐험가가 된다.

LiDAR SLAM으로 실내 지도 만들기

2D LiDAR는 회전하는 줄자다

2D LiDAR를 처음 보면 작고 납작한 원통처럼 보인다. 하지만 로봇 입장에서 LiDAR는 빠르게 회전하는 줄자에 가깝다. 한 바퀴 돌면서 여러 방향으로 빛을 쏘고, 빛이 물체에 맞고 돌아오는 시간을 이용해 거리를 잰다. 정면에는 벽이 2.1m 앞에 있고, 왼쪽에는 책장이 0.8m 앞에 있으며, 오른쪽 앞에는 문틀이 1.5m 앞에 있다는 식이다.

ROS2에서 2D LiDAR 데이터는 보통 sensor_msgs/msg/LaserScan 메시지로 표현된다. 이 메시지는 평면 레이저 거리 센서의 한 번의 스캔을 담기 위해 정의되어 있으며, 시작 각도, 끝 각도, 각도 간격, 최소·최대 측정 거리, 거리 배열 등을 포함한다.1

핵심 필드는 다음과 같다.

  • angle_min: 스캔이 시작되는 각도다. 단위는 라디안이다.

  • angle_max: 스캔이 끝나는 각도다.

  • angle_increment: 인접한 두 측정 방향 사이의 각도 간격이다.

  • range_min, range_max: 유효한 거리 측정값의 최소·최대 범위다.

  • ranges: 각 방향에서 측정된 거리값 배열이다.

  • header.frame_id: 이 스캔이 어느 좌표계 기준으로 측정되었는지 나타낸다. 보통 laser, base_scan, scan_link 같은 이름을 쓴다.

예를 들어 어떤 LiDAR 메시지가 다음과 같다고 해 보자.

angle_min: -3.14159
angle_max:  3.14159
angle_increment: 0.01745
range_min: 0.12
range_max: 12.0
ranges: [ ... ]

0.01745라디안은 거의 1도다. 즉 이 LiDAR는 한 바퀴를 약 1도 간격으로 나누어 거리를 측정한다. ranges[0]은 angle_min 방향의 거리이고, ranges[1]은 그보다 angle_increment만큼 회전한 방향의 거리다. 일반화하면 i번째 거리값의 방향은 다음처럼 계산할 수 있다.

angle_i = angle_min + i * angle_increment
거리_i = ranges[i]

이 배열을 극좌표처럼 생각하면 쉽다. 각도와 거리의 묶음이 계속 나열되어 있는 것이다. SLAM 알고리즘은 이 점들을 로봇 주변의 점 구름처럼 바라본다. 벽은 점들이 길게 늘어선 선분처럼 보이고, 책상 다리는 짧고 둥근 덩어리처럼 보이며, 열린 문은 점들이 갑자기 끊기는 틈처럼 보인다.

단, 모든 거리값을 믿으면 안 된다. range_min보다 작거나 range_max보다 큰 값은 센서가 제대로 측정하지 못한 값일 수 있다. inf, nan이 섞여 나오는 경우도 있다. 좋은 SLAM은 센서가 완벽하다고 가정하지 않는다. 쓸 수 있는 값과 버려야 할 값을 구분하면서 지도에 반영한다.

LaserScan이 점유 격자 지도가 되는 과정

LiDAR 스캔은 그 자체로 지도는 아니다. 스캔은 "지금 이 순간 로봇 주변에 무엇이 보이는가"를 나타내는 센서 데이터다. 지도는 여러 순간의 스캔을 로봇의 위치 추정값과 함께 차곡차곡 쌓아 만든 결과다.

실내 이동 로봇에서 가장 자주 쓰는 지도 표현은 점유 격자 지도, 즉 Occupancy Grid Map이다. 공간을 작은 정사각형 셀로 나누고, 각 셀이 비어 있는지, 장애물이 있는지, 아직 모르는지 표시한다. RViz에서 보면 보통 흰색은 이동 가능한 빈 공간, 검은색은 벽이나 장애물, 회색은 아직 관측하지 못한 미지 영역으로 보인다.

LiDAR 한 줄기가 지도에 반영되는 과정을 상상해 보자. 로봇의 LiDAR가 오른쪽 앞 2m 지점에서 벽을 발견했다. 그러면 센서에서 벽까지 이어지는 선 위의 셀들은 대체로 비어 있다고 판단할 수 있다. 레이저가 그 공간을 통과했기 때문이다. 반대로 레이저가 멈춘 끝점 근처의 셀은 장애물이 있을 가능성이 높다. 레이저가 그 지점에서 반사되어 돌아왔기 때문이다.

즉 하나의 거리 측정값은 두 가지 정보를 동시에 준다. 레이저가 지나간 경로는 빈 공간이고, 레이저가 닿은 끝점은 점유 공간이다. 이 작업을 수백 개의 방향에 대해 반복하고, 로봇이 이동하면서 수천 번의 스캔을 누적하면 회색 안개 속에서 벽, 방, 복도, 문틀이 서서히 드러난다.

하지만 여기서 중요한 질문이 나온다. "각 스캔을 지도 위의 어디에 그려야 할까?" 로봇 위치를 모르면 스캔을 지도에 붙일 위치도 모른다. 바로 이 지점에서 오도메트리와 SLAM이 만난다.

오도메트리와 LiDAR, 두 목격자를 함께 믿기

SLAM은 하나의 센서만 맹신하지 않는다. 오도메트리와 LiDAR는 서로 다른 방식으로 로봇의 움직임을 증언한다.

오도메트리는 바퀴 회전량을 이용해 짧은 시간 동안의 이동을 잘 추정한다. 방금 10cm 전진했는지, 왼쪽으로 조금 회전했는지는 빠르게 알려 준다. 하지만 긴 시간 동안은 오차가 누적된다. 반대로 LiDAR는 주변 구조를 잘 본다. 벽 모양, 코너, 문틈 같은 특징을 이용해 "이 스캔은 이전에 본 이 위치와 비슷하다"고 판단할 수 있다. 다만 특징이 부족하거나 비슷한 구조가 반복되면 헷갈릴 수 있다.

LiDAR SLAM은 보통 다음 흐름으로 동작한다.

  • 오도메트리로 로봇의 다음 자세를 예측한다.

  • 새로 들어온 LaserScan을 예측 자세 근처에 배치해 본다.

  • 이전 스캔 또는 이미 만들어진 지도와 가장 잘 맞는 위치를 찾는다.

  • 찾은 위치를 바탕으로 로봇 자세와 지도를 갱신한다.

  • 예전에 방문한 장소를 다시 발견하면 전체 경로를 다시 최적화한다.

여기서 스캔을 서로 맞추는 과정을 스캔 매칭이라고 한다. 새 스캔과 직전 스캔을 맞추면 스캔-스캔 정합이고, 새 스캔과 누적된 지도를 맞추면 스캔-지도 정합이라고 부를 수 있다. 표현은 조금 다르지만 핵심은 같다. "지금 본 벽 모양이 기존의 어느 위치에 가장 자연스럽게 들어맞는가?"를 찾는 것이다.

slam_toolbox는 ROS2에서 널리 쓰이는 2D SLAM 패키지로, LaserScan과 오도메트리, TF를 이용해 지도를 만들고 map → odom 변환을 발행한다. 또한 pose graph 기반 최적화, 동기·비동기 매핑 모드, 지도 저장, 이어서 매핑하기 같은 기능을 제공한다.2

pose graph는 로봇의 이동 경로를 점과 선으로 표현한 그래프라고 생각하면 된다. 로봇이 이동하면서 중요한 순간마다 노드가 생기고, 노드 사이에는 "이만큼 움직였다" 또는 "이 두 위치는 같은 장소로 보인다"는 제약이 연결된다. 로봇이 출발점 근처로 돌아왔을 때 SLAM이 "아, 여기는 전에 본 곳이네"라고 알아차리면 루프 클로저가 발생한다. 이때 그래프 전체가 다시 조정되며, 그동안 조금씩 휘어진 복도와 어긋난 벽이 한 번에 펴질 수 있다.

이 장면은 RViz에서 꽤 극적이다. 지도가 천천히 그려지다가, 로봇이 출발점 근처로 돌아오는 순간 벽들이 살짝 이동하며 딱 맞물리는 경우가 있다. 처음 보면 "지도 망가진 건가?" 싶지만, 사실은 반대다. SLAM이 과거의 오차를 인정하고 전체 지도를 다시 맞추는 중이다.

SLAM을 시작하기 전에 확인할 세 가지 생체 신호

SLAM을 실행하기 전에 로봇의 생체 신호를 확인해야 한다. 이전 장들에서 계속 반복했던 /scan, /odom, /tf다. 이 세 가지가 건강하지 않으면 slam_toolbox를 아무리 실행해도 좋은 지도가 나오지 않는다.

첫째, /scan이 있어야 한다. Nav2 문서에서도 slam_toolbox와 AMCL이 레이저 스캔 센서 정보를 사용하며, 관례적으로 sensor_msgs/msg/LaserScan 메시지는 /scan 토픽으로 발행된다고 설명한다.3

ros2 topic list | grep scan
ros2 topic info /scan
ros2 topic hz /scan
ros2 topic echo /scan --once

ros2 topic info /scan에서 메시지 타입이 sensor_msgs/msg/LaserScan인지 확인한다. ros2 topic hz /scan으로 주기가 지나치게 느리지 않은지도 본다. 보통 5Hz, 10Hz, 15Hz 정도로 들어오는 경우가 많다. 센서와 드라이버에 따라 다르지만, 1초에 한 번 들어오는 수준이라면 실시간 매핑이 불안정할 수 있다.

둘째, /odom이 있어야 한다. 오도메트리는 로봇의 짧은 시간 이동 예측을 담당한다.

ros2 topic list | grep odom
ros2 topic info /odom
ros2 topic echo /odom --once

여기서 중요한 것은 단순히 토픽이 존재하는지가 아니다. 로봇을 실제로 움직였을 때 위치와 방향 값이 자연스럽게 변해야 한다. 전진했는데 x, y가 전혀 변하지 않거나, 제자리 회전했는데 방향이 이상하게 튄다면 SLAM 지도도 같이 흔들린다.

셋째, TF 트리가 연결되어야 한다. LiDAR 스캔은 LiDAR 좌표계에서 측정된다. 하지만 지도에는 map 좌표계 기준으로 그려져야 한다. 그 사이를 이어 주는 것이 TF다.

SLAM 중에는 보통 다음 연결이 필요하다.

map → odom → base_link → laser_frame

odom → base_link는 오도메트리 또는 로봇 제어 시스템이 발행한다. base_link → laser_frame은 URDF와 robot_state_publisher, 또는 정적 TF가 발행한다. map → odom은 SLAM이 발행한다. 지난 장에서 배운 구조가 그대로 이어지는 셈이다.

아직 SLAM을 실행하기 전이라면 map → odom은 없을 수 있다. 하지만 최소한 odom → base_link → laser_frame은 연결되어 있어야 한다.

ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link laser_frame

LiDAR 프레임 이름은 로봇마다 다르다. /scan 메시지의 header.frame_id를 먼저 확인하자.

ros2 topic echo /scan --once | grep frame_id

만약 frame_id가 base_scan이라면 다음처럼 확인한다.

ros2 run tf2_ros tf2_echo base_link base_scan

RViz에서 TF 디스플레이를 켜는 것도 좋은 습관이다. 선으로 연결된 TF 트리를 보면 로봇의 뼈대가 제대로 이어졌는지 한눈에 알 수 있다. SLAM 문제의 상당수는 알고리즘이 아니라 TF 이름 하나, 프레임 방향 하나에서 시작된다.

slam_toolbox 설치와 online async 실행

이제 SLAM을 실행해 보자. 배포판 이름은 환경에 따라 humble, jazzy처럼 다를 수 있으므로 $ROS_DISTRO를 사용한다.

sudo apt update
sudo apt install ros-$ROS_DISTRO-slam-toolbox
sudo apt install ros-$ROS_DISTRO-nav2-map-server
sudo apt install ros-$ROS_DISTRO-teleop-twist-keyboard

시뮬레이션을 사용한다면 먼저 로봇과 월드를 실행한다. 예를 들어 TurtleBot3 시뮬레이션에서는 다음처럼 시작할 수 있다.

export TURTLEBOT3_MODEL=waffle
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

실제 로봇이라면 LiDAR 드라이버, 오도메트리, 로봇 모델, 제어 노드가 포함된 bringup launch를 실행한다. 로봇마다 명령은 다르지만 목표는 같다. /scan, /odom, /tf가 정상적으로 나와야 한다.

그다음 slam_toolbox를 online async 모드로 실행한다. Nav2의 SLAM 튜토리얼에서도 slam_toolbox의 online_async_launch.py를 사용해 /map과 map → odom을 제공하는 흐름을 안내한다.4

시뮬레이션에서는 /clock을 사용하므로 use_sim_time:=true를 함께 준다.

ros2 launch slam_toolbox online_async_launch.py use_sim_time:=true

실제 로봇에서는 보통 다음처럼 실행한다.

ros2 launch slam_toolbox online_async_launch.py

online async 모드는 들어오는 센서 데이터를 가능한 범위에서 비동기적으로 처리한다. 모든 스캔을 빠짐없이 처리하는 것보다 실시간성을 더 중시하는 방식이라고 이해하면 된다. 일반적인 실습과 실시간 로봇 매핑에서는 시작하기 좋은 선택이다. 더 높은 정밀도가 필요하고 처리 성능에 여유가 있다면 동기 모드를 검토할 수 있지만, 처음에는 online async로 충분하다.

로봇마다 프레임 이름이나 스캔 토픽 이름이 다르다면 파라미터 파일을 만들어 명시하는 편이 좋다.

slam_toolbox:
  ros__parameters:
    use_sim_time: true
    mode: mapping
    map_frame: map
    odom_frame: odom
    base_frame: base_link
    scan_topic: /scan
    resolution: 0.05

예를 들어 이 파일을 config/slam_toolbox.yaml로 저장했다면 launch에서 다음처럼 전달할 수 있다.

ros2 launch slam_toolbox online_async_launch.py \
  params_file:=/home/robot/ros2_ws/src/my_robot_bringup/config/slam_toolbox.yaml

여기서 resolution: 0.05는 지도 한 셀이 0.05m, 즉 5cm를 의미한다. 해상도가 작을수록 더 세밀한 지도가 만들어지지만 계산량과 파일 크기가 늘어난다. 일반적인 실내 주행에서는 0.05m가 무난한 출발점이다. 좁은 미로형 공간이나 작은 장애물이 중요한 환경에서는 0.03m 같은 더 세밀한 값이 필요할 수 있다. 반대로 넓은 창고에서는 0.05m나 0.1m가 더 현실적일 수 있다.

RViz에서 지도와 자세 확인하기

SLAM을 실행했다면 RViz를 열어 로봇이 그리는 지도를 보자.

rviz2

RViz에서 가장 먼저 할 일은 Fixed Frame을 map으로 설정하는 것이다. SLAM이 정상적으로 동작하면 map 프레임이 생기고, 그 아래에 odom, base_link, LiDAR 프레임이 이어진다.

그다음 다음 디스플레이를 추가한다.

  • Map: 토픽은 /map으로 설정한다.

  • LaserScan: 토픽은 /scan으로 설정한다.

  • TF: 좌표계 연결을 확인한다.

  • RobotModel: URDF 기반 로봇 모델을 확인한다.

  • Odometry 또는 Pose: 로봇 자세 추정을 확인한다.

명령줄에서도 출력을 확인할 수 있다.

ros2 topic list | grep map
ros2 topic info /map
ros2 topic echo /map --once

/map은 nav_msgs/msg/OccupancyGrid 타입이어야 한다. 이 메시지 안에는 지도 해상도, 원점, 가로·세로 크기, 셀 데이터가 들어 있다. 셀 데이터는 보통 -1, 0, 100에 가까운 값으로 해석한다. -1은 미지 영역, 0은 빈 공간, 100은 점유 영역이다. 실제 알고리즘 내부에서는 확률과 임계값을 사용하지만, RViz에서는 이 값들이 회색, 흰색, 검은색으로 보인다.

SLAM이 map → odom을 발행하는지도 확인한다.

ros2 run tf2_ros tf2_echo map odom

이 변환은 SLAM의 핵심 출력이다. 오도메트리가 만든 odom → base_link는 짧은 시간 동안 부드럽게 움직인다. SLAM은 LiDAR 매칭으로 누적 오차를 보정하면서 map → odom을 조금씩 조정한다. 결과적으로 map → odom → base_link를 따라가면 지도 좌표계에서의 로봇 위치를 얻을 수 있다.

만약 RViz에서 /scan은 보이는데 /map이 전혀 커지지 않는다면 다음을 의심하자. slam_toolbox가 /scan을 구독하지 못했거나, base_link와 LiDAR 프레임 사이 TF가 없거나, 오도메트리 프레임 이름이 파라미터와 맞지 않을 수 있다. 반대로 /map은 나오지만 로봇 모델이 엉뚱한 곳에 있다면 TF 트리나 Fixed Frame 설정을 먼저 확인한다.

teleop으로 천천히, 벽을 따라, 다시 출발점으로

이제 로봇을 움직여 지도를 채워 보자. 가장 간단한 방법은 키보드 teleop이다.

ros2 run teleop_twist_keyboard teleop_twist_keyboard

SLAM을 할 때는 레이싱 게임처럼 달리면 안 된다. 좋은 지도는 빠른 주행이 아니라 차분한 관찰에서 나온다. 로봇은 탐험가이면서 동시에 화가다. 너무 급하게 붓을 휘두르면 선이 삐뚤어진다.

처음에는 로봇을 벽이나 책장처럼 특징이 분명한 곳 근처에 놓고 시작하는 것이 좋다. 넓고 텅 빈 공간 한가운데에서 시작하면 LiDAR가 볼 만한 특징이 부족해 위치 추정이 흔들릴 수 있다. 벽을 따라 천천히 이동하면서 방의 윤곽을 먼저 잡고, 그다음 안쪽 공간을 채우는 방식이 안정적이다.

추천 주행 습관은 다음과 같다.

  • 직선 구간에서는 일정한 속도로 천천히 이동한다.

  • 회전할 때는 급격히 돌지 말고, 잠깐 멈추거나 낮은 각속도로 돈다.

  • 문 앞, 코너, 기둥처럼 특징이 많은 지점에서는 잠시 머물러 스캔이 충분히 쌓이게 한다.

  • 이미 지나간 경로와 새 경로가 적당히 겹치도록 움직인다.

  • 마지막에는 가능하면 시작 지점 근처로 되돌아와 폐루프 경로를 만든다.

폐루프는 SLAM의 하이라이트다. 로봇이 한 바퀴 돌아 출발점 근처에 오면, 알고리즘은 지금 보고 있는 벽과 처음에 보았던 벽이 같은 장소라는 사실을 알아차릴 수 있다. 그러면 그동안 쌓인 오차를 전체 경로에 나누어 보정한다. 길게 휘어졌던 복도가 곧게 펴지고, 살짝 어긋났던 방의 모서리가 맞물린다.

처음 실습할 때는 RViz를 계속 바라보며 움직이는 것이 좋다. 회색 미지 영역이 흰색과 검은색으로 바뀌는 모습, /scan 점들이 벽 위에 잘 얹히는 모습, 로봇이 회전할 때 지도가 흔들리는지 여부를 관찰한다. 지도 작성은 명령 몇 줄로 끝나는 작업이 아니라 로봇과 사람이 함께 공간을 읽는 과정이다.

지도가 이상해지는 대표적인 실내 장면

LiDAR SLAM은 실용적이지만 마법은 아니다. 실내 환경에는 LiDAR를 속이는 장면이 꽤 많다. 지도가 찢어지거나 벽이 두 겹으로 생기거나 복도가 바나나처럼 휘어진다면, 알고리즘을 탓하기 전에 환경과 주행 방식을 함께 보아야 한다.

좁은 복도는 가장 흔한 난관이다. 복도는 양쪽 벽이 길게 평행하게 이어진다. 앞뒤 방향으로는 특징이 부족해 스캔 매칭이 어느 정도 밀려도 비슷하게 보일 수 있다. 이때 오도메트리 오차가 크면 복도 길이가 늘어나거나 벽이 이중으로 그려진다. 좁은 복도에서는 속도를 낮추고, 코너나 문틀처럼 특징이 있는 지점에서 잠시 멈추는 것이 좋다.

유리벽과 거울도 문제다. LiDAR 빛은 표면 재질에 따라 반사, 투과, 난반사가 달라진다. 유리는 실제 벽 위치와 다른 거리값을 만들거나 아예 측정이 빠질 수 있다. 거울은 LiDAR에게 가짜 공간을 보여 주기도 한다. 사람 눈에는 분명한 벽이지만 LiDAR에게는 이상한 문처럼 보일 수 있다. 이런 공간에서는 유리면 아래쪽에 불투명한 표식을 붙이거나, 지도 작성 시 해당 영역을 조심스럽게 해석해야 한다.

비슷한 모양이 반복되는 공간도 어렵다. 같은 크기의 문이 일정 간격으로 반복되는 복도, 똑같은 선반이 줄지어 있는 창고, 기둥이 규칙적으로 배치된 로비에서는 로봇이 현재 위치를 다른 위치와 혼동할 수 있다. 이것을 장소 인식의 모호성이라고 볼 수 있다. 루프 클로저가 잘못 걸리면 지도 전체가 갑자기 비틀어질 수도 있다.

빠른 회전과 급가속은 직접적인 지도 왜곡 원인이다. LiDAR는 한 바퀴를 스캔하는 데 아주 짧지만 0이 아닌 시간이 걸린다. 로봇이 그 사이 빠르게 회전하면 한 스캔 안에서도 앞부분과 뒷부분이 서로 다른 자세에서 측정된 것처럼 된다. 오도메트리도 급가속과 미끄러짐에 약하다. SLAM 중에는 부드러운 조작이 정확한 지도보다 먼저다.

움직이는 사람과 의자도 조심해야 한다. SLAM은 기본적으로 정적인 환경을 지도화한다고 가정한다. 사람이 지나가면 그 순간에는 장애물로 보인다. 여러 번 반복해서 관측되면 지도에 얼룩처럼 남을 수도 있다. 지도 작성 중에는 가능하면 사람과 반려동물, 이동식 의자를 치우고 진행하자. TurtleBot4의 지도 생성 안내에서도 매핑할 공간에서 원치 않는 장애물을 치우고, 사람이나 동물이 움직이지 않는 상태가 이상적이라고 설명한다.5

문제 상황을 디버깅할 때는 다음 순서로 보는 것이 좋다.

ros2 topic hz /scan
ros2 topic echo /scan --once
ros2 topic echo /odom --once
ros2 run tf2_ros tf2_echo odom base_link
ros2 run tf2_ros tf2_echo base_link laser_frame
ros2 run tf2_ros tf2_echo map odom

센서가 안정적으로 들어오는지, 오도메트리가 튀지 않는지, TF가 끊기지 않는지, SLAM이 map → odom을 발행하는지 차례로 좁혀 간다. 좋은 디버깅은 추측을 줄이고 관찰을 늘리는 일이다.

완성된 지도 저장하기

지도가 만족스럽게 만들어졌다면 저장해야 한다. 저장하지 않은 지도는 RViz 창을 닫는 순간 사라지는 모래 그림과 같다. 이후 AMCL과 Nav2에서 재사용하려면 이미지 파일과 YAML 메타데이터 파일로 저장한다.

가장 흔한 방법은 nav2_map_server의 map_saver_cli를 사용하는 것이다. Nav2의 SLAM 튜토리얼에서도 SLAM으로 생성한 지도를 파일로 저장할 때 map_saver_cli를 사용한다.4

mkdir -p ~/maps
ros2 run nav2_map_server map_saver_cli -f ~/maps/office_1f

명령이 성공하면 보통 다음 두 파일이 생긴다.

~/maps/office_1f.pgm
~/maps/office_1f.yaml

office_1f.pgm은 지도의 이미지 표현이다. 검은색 벽, 흰색 빈 공간, 회색 미지 영역이 들어 있다. office_1f.yaml은 이 이미지가 실제 세계에서 어떤 크기와 위치를 갖는지 설명하는 메타데이터다.

예를 들어 YAML은 다음과 비슷하다.

image: office_1f.pgm
mode: trinary
resolution: 0.05
origin: [-6.25, -4.10, 0.0]
negate: 0
occupied_thresh: 0.65
free_thresh: 0.25

resolution은 이미지 픽셀 하나가 실제 몇 미터인지 나타낸다. 0.05라면 한 픽셀이 5cm다. origin은 이미지 지도의 원점이 map 좌표계에서 어디에 놓이는지 나타낸다. occupied_thresh와 free_thresh는 픽셀 값을 점유, 자유 공간으로 해석하는 기준이다.

slam_toolbox 자체의 저장 서비스를 사용할 수도 있다. TurtleBot4 문서에서는 /slam_toolbox/save_map 서비스를 호출해 현재 지도를 저장하는 방법을 안내한다.5

ros2 service call /slam_toolbox/save_map slam_toolbox/srv/SaveMap "name:
  data: 'office_1f'"

두 방식 모두 지도 이미지와 YAML을 남기는 데 사용할 수 있다. 프로젝트에서는 한 가지 방식을 정해 일관되게 쓰는 것이 좋다. 나중에 launch 파일에서 지도 경로를 지정할 때 파일 이름과 위치가 자주 헷갈리기 때문이다.

지도 이미지뿐 아니라 pose graph를 저장하는 기능도 있다. pose graph는 단순한 흑백 이미지보다 SLAM 내부 정보를 더 많이 담는다. 나중에 같은 공간을 이어서 매핑하거나 지도를 정교하게 다듬고 싶다면 slam_toolbox의 serialization 기능을 검토할 수 있다. 다만 Nav2의 일반적인 AMCL 기반 위치 추정에는 보통 .pgm 또는 .png 같은 지도 이미지와 .yaml 메타데이터가 먼저 필요하다.

SLAM 지도가 "예쁘다"는 것과 "Nav2가 잘 쓸 수 있다"는 것은 조금 다르다. 사람 눈에는 그럴듯해 보여도, 로봇에게는 문이 막혀 있거나 벽이 두 겹이거나 복도 폭이 실제보다 좁게 표현되어 있을 수 있다. 이후 장에서 AMCL과 Nav2를 사용할 것이므로 지금부터 지도 품질 기준을 익혀 두자.

좋은 지도는 먼저 벽의 윤곽이 선명하다. 벽이 두껍게 번져 있거나 두 줄로 겹쳐 있으면 위치 추정과 경로 계획이 흔들린다. 특히 긴 복도에서 벽이 평행하게 두 벌 생겼다면 오도메트리 오차나 스캔 매칭 실패를 의심해야 한다.

둘째, 로봇이 실제로 지나갈 공간이 지도에서도 열려 있어야 한다. 문턱, 좁은 통로, 책상 사이 공간이 검은 픽셀로 막혀 있으면 Nav2는 그곳을 갈 수 없는 곳으로 판단한다. 반대로 실제로 막힌 곳이 흰색으로 뚫려 있으면 로봇이 벽을 향해 경로를 계획할 수 있다.

셋째, 운영 영역의 회색 미지 영역이 너무 많지 않아야 한다. 회색은 로봇이 관측하지 못한 공간이다. Nav2 설정에 따라 미지 영역을 지나갈 수 없다고 판단할 수도 있고, 위험하게 통과하려 할 수도 있다. 로봇이 실제로 다닐 구역은 가능한 한 충분히 스캔해 흰색과 검은색으로 확정해 두는 것이 좋다.

넷째, 일시적인 장애물이 지도에 남지 않아야 한다. 지도 작성 중 사람이 서 있던 자리, 열려 있던 박스, 임시로 놓인 의자가 검은 장애물로 남으면 나중에 로봇은 그 자리를 계속 피하려 한다. 반대로 평소에는 항상 있는 큰 가구가 지도 작성 때 빠져 있었다면 Nav2의 local costmap이 현장에서 계속 장애물 회피를 해야 한다.

다섯째, 루프가 잘 닫혀야 한다. 한 바퀴 돌아 출발점으로 왔을 때 벽과 벽이 자연스럽게 이어져야 한다. 출발점 부근에 벽이 어긋난 흔적이 남아 있다면 폐루프가 제대로 보정되지 않았을 수 있다. 이런 지도는 AMCL이 특정 지점에서 위치를 헷갈리기 쉽다.

마지막으로 지도의 해상도와 로봇 크기가 맞아야 한다. 너무 거친 지도에서는 좁은 통로가 막힌 것처럼 보일 수 있다. 너무 세밀한 지도는 계산량이 늘고 노이즈가 더 잘 드러날 수 있다. 로봇 반지름, LiDAR 위치, costmap inflation 설정까지 고려해 해상도를 선택해야 한다. 이 부분은 이후 Costmap 장에서 다시 깊게 다룬다.

작은 실내 지도 실습 시나리오

마지막으로 이 장의 내용을 하나의 실습 흐름으로 정리해 보자. 책상과 벽, 문이 있는 작은 방 하나를 매핑한다고 가정한다.

먼저 로봇을 벽 근처에 놓는다. 완전히 빈 중앙보다 벽과 코너가 보이는 위치가 좋다. bringup을 실행하고 /scan, /odom, /tf를 확인한다.

ros2 topic info /scan
ros2 topic hz /scan
ros2 topic echo /odom --once
ros2 run tf2_ros tf2_echo odom base_link

시뮬레이션이라면 use_sim_time:=true를 사용해 SLAM을 실행한다.

ros2 launch slam_toolbox online_async_launch.py use_sim_time:=true

실제 로봇이라면 시간 설정을 로봇 bringup 구조에 맞춘다.

ros2 launch slam_toolbox online_async_launch.py

RViz를 열고 Fixed Frame을 map으로 설정한다. Map, LaserScan, TF, RobotModel을 추가한다. /scan 점들이 로봇 주변 벽과 맞고, /map이 조금씩 자라기 시작하면 준비가 된 것이다.

이제 teleop으로 천천히 움직인다.

ros2 run teleop_twist_keyboard teleop_twist_keyboard

방 외곽을 따라 한 바퀴 돈다. 코너에서 잠시 멈추고, 문 앞에서 천천히 회전하고, 책상 주변을 너무 빠르게 돌지 않는다. 중간중간 RViz에서 벽이 두 겹으로 생기지 않는지 확인한다. 마지막에는 시작점 근처로 돌아와 루프 클로저가 일어나는지 관찰한다.

지도 품질이 만족스럽다면 저장한다.

mkdir -p ~/maps
ros2 run nav2_map_server map_saver_cli -f ~/maps/room_map

저장된 파일을 확인한다.

ls ~/maps

다음과 같은 파일이 있어야 한다.

room_map.pgm
room_map.yaml

이 두 파일은 다음 단계로 가는 입장권이다. 앞으로 AMCL은 이 지도를 기준으로 로봇의 현재 위치를 추정하고, Nav2는 이 지도 위에서 목표 지점까지의 경로를 계획한다. 오늘 만든 지도는 단순한 그림이 아니다. 로봇이 앞으로 움직일 세계의 바탕이다.

LiDAR SLAM은 자율주행의 첫 번째 현실감이다

ROS2를 배우다 보면 처음에는 노드, 토픽, 메시지, TF가 따로따로 흩어진 개념처럼 느껴진다. 그런데 LiDAR SLAM을 실행하는 순간 이 조각들이 한곳에 모인다. LiDAR 노드는 /scan을 발행하고, 오도메트리 노드는 /odom과 TF를 제공하며, slam_toolbox는 이 데이터를 받아 /map과 map → odom을 만들어 낸다. RViz는 그 모든 흐름을 눈앞에 보여 준다.

이것이 ROS2의 재미있는 지점이다. 명령어 하나가 신기한 것이 아니라, 여러 노드가 각자 작은 책임을 맡고 함께 로봇의 인식을 만들어 낸다는 점이 중요하다. LiDAR SLAM은 그 구조를 가장 선명하게 보여 주는 실습이다.

이제 로봇은 실내 공간의 지도를 만들 수 있다. 다음에는 카메라를 이용한 Visual SLAM을 살펴본다. LiDAR가 거리의 선을 읽는 센서라면, 카메라는 색과 무늬와 특징점을 읽는 센서다. 두 방식은 서로 다른 눈을 가진 로봇의 세계 인식 방법이다.

참고

1sensor_msgs/msg/LaserScan 메시지 정의

2slam_toolbox ROS2 패키지 문서

3Nav2 Mapping and Localization 문서

4Nav2 Navigating while Mapping (SLAM) 튜토리얼

5TurtleBot4 지도 생성 튜토리얼

책 목록

ROS2로 완성하는 자율주행 로봇: 노드·TF·SLAM·Nav2 실전 설계

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기