LIO-SAM 실습해보기 (1)
들어가면서.
LiDAR SLAM에서 가장 기본되는 알고리즘인 LIO-SAM을 공부해보자.
LiDAR Inertial Odometry via Smoothing And Mapping을 줄여서 LIO-SAM이라 한다. LiDAR가 가장 널리 쓰이는 센서로 정착되고, IMU의 사용이 쉬워짐과 동시에, 이런 다른 타입의 센서들을 Factor Graph로 통합하여 Map을 그리는 방식이 가장 흔하기 때문이다. 방 하나정도 공간에 대한 SLAM은 이 이전의 EKF나 PF 기반의 알고리즘과 마이컴 수준으로도 가능했지만 공간이 점점 커짐에 따라 다루어야 하는 메모리도 커지게 되고, 이를 대응하기 위에 여러 센서들을 적용하려면 결국 그래프방식을 쓸수밖에 없는 논리로 흘러가게 된다. 이 후 Loop Closing을 위한 기법들이 더 추가되는 것외에 기본적으로 Lio-SAM은 가장 먼저 적용해볼 수 있는 SLAM 알고리즘이다.
이 글에선 이론적인것은 일단 배제하고 실습중심으로 학습해나가볼까 한다.
그래도 필요한 경우 중간중간 논문을 인용해가면서 진행해보겠다.
학습 순서.
다음과 같은 순서로 최대한 스피디하게 진행해볼까 한다.
- ROS2에 설치해보기
- IMU/PointCloud 다뤄보기
- 데이터셋을 이용하여 LIO-SAM 구현해보기
- 라즈베리파이에 실제 센서 연결해보기.
딱히 알려주는 사람이 없어서, ChatGPT를 활용하여 실습을 진행해본다.
Day 1. 개발환경의 구축과 내부 구조 이해
1일차 실습의 목표는 다음과 같이 정해본다.
- LIO-SAM은 어떤 ROS Topic을 입력으로 사용하는가
- imageProjection의 역할은?
- deskew는 무엇이고 필요한 이유는?
- mapOptimization 은 어떤 역할을 하는가?
- params.yaml은 무엇이고 어떤 역할을 하는가?
이걸 염두해두고 진행해보자.
실습환경 확인
실습 환경은 다음과 같이 설정하고 진행해본다.
- ubuntu 22.04
- ROS 2 Humble
- gcc 11+
- RAM은 가급적 16GB이상.(이거 제한을 둬야 하나?)
이걸 이제 지금 시스템에서 확인해보자. ROS2설치와 같은 것들은 다른 곳에서 참조하자.
lsb_release -a
printenv ROS_DISTRO
ros2 doctor
만약 오류가 난다면 확인해보자. 가장 흔한 오류는 ros2: command not found로, source명령어 문제일수도 있다. 확인해보자.
source /opt/ros/humble/setup.bash
Workspace
LIO-SAM코드를 저장하고 돌려볼 workspace를 확인해보자.
mkdir -p ~/lio_ws/src
cd ~/lio_ws
tree -L 2
Download
원래 LIO-SAM은 ROS1 환경에서 발표되었으나, 이후 ROS2로 컨버전된 브랜치도 있다. 따라서 일단 공식 코드를 따라가보자.
cd ~/lio_ws/src
git clone https://github.com/TixiaoShan/LIO-SAM.git
cd LIO-SAM
git checkout ros2
Dependancy
LIO-SAM을 실행하기 위한 필수패키지들을 확인해보자.
cd ~/lio_ws
rosdep install \
--from-paths src \
--ignore-src \
-r \
-y
이것 외에도 gt-sam도 설치가 확인된다. 아래도 한번 실행해보자. 혹시나 오류가 뜬다면 개별적으로 알아보자.
내 경우는 아래 명령어에서 gtsam에서 문제가 발생했는데, 혹시나 모르니까 이 부분은 별도로 한번 알아보자.
sudo apt install \
libgtsam-dev \
libeigen3-dev \
libpcl-dev
일단 빌드
colcon build \
--symlink-install \
--cmake-args -DCMAKE_BUILD_TYPE=Release
빌드후 확인해보자.
source install/setup.bash
ros2 pkg list | grep lio
Launch
ros2 launch lio_sam run.launch.py
지금은 센서 데이터가 없으므로, 아무것도 보이지 않는다.
rviz도 실행되는걸 볼 수 있다.
Topic의 분석
다른 터미널 창을 띄워서 ROS2의 토픽을 보자.
ros2 topic list
나온 토픽들을 훑어보자.
입력: /points /imu/data /odometry/gpsz # GPS 사용 시, 설정 확인 필요
핵심 출력: /lio_sam/mapping/odometry /lio_sam/mapping/path /lio_sam/mapping/cloud_registered /lio_sam/mapping/map_local /lio_sam/mapping/map_global /odometry/imu
우선 입력들부터 보자. 만약 센서데이터를 처리하는 보드들이 있다면 아래 형태의 토픽으로 쏴줘야 한다는 소리다.
/points
LiDAR나 다른 센서로부터 들어온 point cloud 원본데이터다. 메세지 타입 형태는 PointCloud2 다.
/imu/data
IMU로 부터 들어온 데이터다.
/odometry/gpsz
만약 GPS를 사용한다면 들어오는 값이다.
/lio_sam/deskew/cloud_deskewed
IMU를 사용해 scan motion distortion을 보정한 포인트클라우드다. 보정에 관련한 알고리즘은 LOAM을 따른다. 그 부분을 참고하자.
/lio_sam/feature/cloud_corner
Edge 또는 corner feature point cloud다.
feature 매칭에 사용하는 데이터들이다.
관련해서 상세한건 LOAM과 LIO-SAM논문을 참조하자.
/lio_sam/feature/cloud_surface
평면 또는 surface feature point cloud다. 위와 같다.
/lio_sam/feature/cloud_info
Feature extraction 결과와 odometry 초기 추정 등을 mapping 모듈로 전달하는 내부 메시지다.
/lio_sam/deskew/cloud_info
↓
featureExtraction
↓
/lio_sam/feature/cloud_info
↓
mapOptimization
/odometry/imu_incremental
이제 IMU값들을 보자. 이름은 incremental이지만, 실제론 증분을 적분해서 얻은 현재 Pose를 Odometry 형태로 Publish한다. 아직 Pose를 보정하지 않은 값이다.
/odometry/imu
LiDAR mapping odometry와 IMU incremental odometry를 결합한 고주기 odometry 출력
/lio_sam/mapping/odometry
+
/odometry/imu_incremental
↓
TransformFusion
↓
/odometry/imu
/lio_sam/imu/path
앞서 얻은 /odometry/imu 를 시각화한 것이다.
/lio_sam/mapping/odometry
최종 핵심 출력값이다. factor graph 최적화가 반영된 mapping pose다.
/lio_sam/mapping/cloud_registered
현재 scan을 최적화된 pose로 map frame에 변환한 등록 포인트클라우드
/lio_sam/mapping/cloud_registered_raw
Feature extraction 전후 또는 downsampling 차이를 확인하기 위한 raw registered cloud
/lio_sam/mapping/map_local
현재 pose 주변의 local submap
/lio_sam/mapping/map_global
누적된 global point cloud map
/lio_loop/loop_closure_detection
외부 loop detector가 loop 후보를 제공하는 입력
/lio_sam/mapping/icp_loop_closure_history_cloud
ICP loop closure에 사용된 과거 keyframe cloud를 보여주는 디버깅 토픽
/lio_sam/mapping/loop_closure_constraints
검출·적용된 loop closure constraint를 시각화하는 토픽
/points
│
│
├─────────────────────────────┐
│ │
▼ │
imageProjection │
▲ │
│ │
/imu/data │
│ │
├──────────────┐ │
│ │ │
▼ ▼ │
deskew IMU preintegration │
│ │ │
│ └──► /odometry/imu_incremental
│ │
▼ │
/lio_sam/deskew/cloud_deskewed │
/lio_sam/deskew/cloud_info │
│ │
▼ │
featureExtraction │
│ │
├──► /lio_sam/feature/cloud_corner
├──► /lio_sam/feature/cloud_surface
└──► /lio_sam/feature/cloud_info
│
▼
mapOptimization
│
├──► /lio_sam/mapping/odometry
├──► /lio_sam/mapping/odometry_incremental
├──► /lio_sam/mapping/path
├──► /lio_sam/mapping/cloud_registered
├──► /lio_sam/mapping/map_local
└──► /lio_sam/mapping/map_global
params.yaml 둘러보기
~/lio_ws/src/LIO-SAM/config 폴더안에 가보면, params.yaml파일이 있다.
내용을 훑어보자. 우선 눈에 띄는건 Topics다. 이건 앞서서 이야기했던대로 입력과 관련된 주요 토픽들이 yaml에 나열되어있다.
# Topics
pointCloudTopic: "/points" # Point cloud data
imuTopic: "/imu/data" # IMU data
odomTopic: "odometry/imu" # IMU pre-preintegration odometry, same frequency as IMU
gpsTopic: "odometry/gpsz" # GPS odometry topic from navsat, see module_navsat.launch file
또 눈여겨 볼만한건 LiDAR 관련사항들이다.
# Sensor Settings
sensor: ouster # lidar sensor type, either 'velodyne', 'ouster' or 'livox'
N_SCAN: 64 # number of lidar channels (i.e., Velodyne/Ouster: 16, 32, 64, 128, Livox Horizon: 6)
Horizon_SCAN: 512 # lidar horizontal resolution (Velodyne:1800, Ouster:512,1024,2048, Livox Horizon: 4000)
downsampleRate: 1 # default: 1. Downsample your data if too many
# points. i.e., 16 = 64 / 4, 16 = 16 / 1
lidarMinRange: 1.0 # default: 1.0, minimum lidar range to be used
lidarMaxRange: 1000.0 # default: 1000.0, maximum lidar range to be used
특히 sensor, N_SCAN, Horizon_SCAN 값은 센서의 스펙에 맞춰 설정값을 바꿔줘야 한다.
IMU-LiDAR 외부 파라미터도 주의해야 한다.
현재는 IMU와 LiDAR 가 같은 위치에 있다고 가정하지만, 이 두 센서간의 위치가 다르다면 이에 맞춰 수정해줘야 한다. 이게 안맞면 로봇이 앞으로 움직였는데 옆으로 이동하는 불상사가 생긴다거나 trajectory가 발산할수도 있다.
extrinsicTrans: [ 0.0, 0.0, 0.0 ]
extrinsicRot: [-1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, -1.0 ]
extrinsicRPY: [ 0.0, 1.0, 0.0,
-1.0, 0.0, 0.0,
0.0, 0.0, 1.0 ]
imuRPYWeight 는 IMU를 얼마나 신뢰하는지에 대한 값이다. 참고하자.
feature 추출의 임계값도 설정할 수 있다.
# LOAM feature threshold
edgeThreshold: 1.0
surfThreshold: 0.1
edgeFeatureMinValidNum: 10
surfFeatureMinValidNum: 100
뭔가 포인트들간의 매칭이 잘 안되거나, 복도같은 곳들이 많다면 조정해봐야 한다.
비슷하게 Voxel과 관련된 부분도 성능에 영향을 받는다. 값이 커질수록 포인트 숫자도 감소하고, CPU부담도 줄어든다. 대신 작은 구조물을 놓칠 가능성이 있다.
# voxel filter paprams
odometrySurfLeafSize: 0.4 # default: 0.4 - outdoor, 0.2 - indoor
mappingCornerLeafSize: 0.2 # default: 0.2 - outdoor, 0.1 - indoor
mappingSurfLeafSize: 0.4 # default: 0.4 - outdoor, 0.2 - indoor
Mapping의 주기와 CPU 사용량을 조절할 수도 있다. GPU사용은 안되나?
# CPU Params
numberOfCores: 4 # number of cores for mapping optimization
mappingProcessInterval: 0.15 # seconds, regulate mapping frequency
위의 초기값에서 0.15초이므로 대략 6.7Hz로 맵핑을 수행한다.
만약 시스템에 부담이 크다면 낮춰볼수도 있겠다.
키프레임을 얼마나 생성할건지도 눈여겨 봐야 한다.
surroundingkeyframeAddingDistThreshold: 1.0
surroundingkeyframeAddingAngleThreshold: 0.2
현재는:
- 1m 이상 이동
- 또는 약 0.2 rad, 약 11.5도 이상 회전
하면 새 keyframe을 추가한다.
당연하게도 값을 작게 하면 keyframe 증가,지도 세밀,메모리와 CPU 증가를 기대할 수 있고,
값을 크게 하면 계산량 감소, 빠른 회전이나 좁은 공간에서 정보 손실이 예상된다.
scan-to-map optimization에 사용할 파라미터들도 볼 수 있다.
surroundingKeyframeDensity: 2.0 # meters, downsample surrounding keyframe poses
surroundingKeyframeSearchRadius: 50.0 # meters, within n meters scan-to-map optimization
loop closure관련한 항목도 있다.
시스템 성능에 따라 적절히 조절할 필요가 있다.
# Loop closure
loopClosureEnableFlag: true
loopClosureFrequency: 1.0 # Hz, regulate loop closure constraint add frequency
surroundingKeyframeSize: 50 # submap size (when loop closure enabled)
historyKeyframeSearchRadius: 15.0 # meters, key frame that is within n meters from
# current pose will be considerd for loop closure
historyKeyframeSearchTimeDiff: 30.0 # seconds, key frame that is n seconds older will be
# considered for loop closure
historyKeyframeSearchNum: 25 # number of hostory key frames will be fused into a
# submap for loop closure
historyKeyframeFitnessScore: 0.3 # icp threshold, the smaller the better alignment
만약 로봇이 2차원 평면에서만 움직인다면, Z축을 굳이 연산할 필요가 없다.
# robot motion constraint (in case you are using a 2D robot)
z_tollerance: 1000.0 # meters
rotation_tollerance: 1000.0 # radians
코드 둘러보기
LIO-SAM 소스폴더 들어가보면 파일이 참 단촐하게 있다.
코드의 분석은 좀 더 나중에 해보자.
댓글남기기