LIO-SAM 실습해보기 (3)
들어가면서.
지난 차수까지, IMU데이터가 어떻게 들어오고, LiDAR데이터가 어떻게 들어오는지 맥락만 파악했다.
정확히 셈을 해본건 아니지만, 추후에 파라미터들을 좀 손봐가면서 값이 바뀌면 전체 맵이 어떻게 바뀌는지에 대한 확인을 해보면 좋을 것 같다.
학습 순서.
다음과 같은 순서로 최대한 스피디하게 진행해볼까 한다.
- ROS2에 설치해보기
- IMU/PointCloud 다뤄보기
- 데이터셋을 이용하여 LIO-SAM 구현해보기 <-오늘 할 부분
- 라즈베리파이에 실제 센서 연결해보기.
딱히 알려주는 사람이 없어서, ChatGPT를 활용하여 실습을 진행해본다.
Day 3. 데이터셋을 이용하여 LIO-SAM 구현해보기
이번에 확인해볼 목표는 다음과 같다.
1) LIO-SAM 전체 파이프라인을 실제 ROS 토픽과 연결해서 이해 2) RViz에서 정상/비정상 동작을 구분 3) params.yaml에서 중요한 파라미터를 직접 만져보기 4) featureExtraction과 mapOptimizer이해하기.
파라미터 실습해보기
일단 한번 돌려보자. day2의 기억을 떠올려보자.
터미널 1 - LIO-SAM
source /opt/ros/humble/setup.bash
source ~/lio_ws/install/setup.bash
ros2 launch lio_sam run.launch.py
터미널 2 - 데이터셋 재생 (Walking dataset)
source /opt/ros/humble/setup.bash
cd ~/lio_ws/dataset
ros2 bag play walking_dataset_ros2 --clock
터미널의 오른쪽 창을 보면, 좀 성가신 부분이 보인다. Error가 계속 출력되고 있다.
[lio_sam_imuPreintegration-3] [ERROR] [1786430187.044127045] [lio_sam_imuPreintegration]: "velodyne" passed to lookupTransform argument target_frame does not exist.
분명히 LiDAR가 들어오는 데이터가 있어서 화면은 잘 만들어지는데…. 뭔가 에러가 있다. 이 현상을 정리하면 이렇다.
- /points_raw의 header.frame_id는 velodyne
- 그런데 TF tree에는 velodyne frame이 없었음
- LIO-SAM의 params.yaml에는 lidarFrame: “velodyne”로 설정돼 있었음
- 그래서 imuPreintegration이 base_link ↔ velodyne TF를 찾다가 실패
로 정리해볼 수 있다. 그럼 이제 이 오류를 수정하려면 뭘 건드려야 하나.
우선 다른 터미널 창에서 다음 명령어를 실행해보면 갑자기 오류가 사라지는걸 확인할 수 있다.
ros2 run tf2_ros static_transform_publisher 0 0 0 0 0 0 lidar_link velodyne
이 명령은 ROS2의 tf2_ros 패키지의 static_transform_publisher 파일을 실행한다는 뜻이다. 뒤의 숫자는 x y z yaw pitch roll 를 뜻하고, parent frame = lidar_link, child frame = velodyne 이 된다는걸 뜻한다.
이건 임시방편인거고. 제대로 수정하려면 어떻게 해야 하나.
지난번 Day 2에서 params.yaml을 velodyne에 맞춰 수정한다고 엄한 곳을 손댔다. 이게 문제였다.
# Frames
lidarFrame: "velodyne"
baselinkFrame: "base_link"
odometryFrame: "odom"
mapFrame: "map"
저 위의 lidar frame을 원래대로 바꾸자.
# Frames
lidarFrame: "lidar_link" #요개 원래형태다.
baselinkFrame: "base_link"
odometryFrame: "odom"
mapFrame: "map"
그러면 잘 동작하는걸 확인할 수 있다. 이렇게 파라미터에서 각 노드들간의 관계를 잘 설정해야 한다. 이제 다른 파라미터들도 한번 비교해보자.
성능지표들
이제 파라미터들을 수정하면서 확인하려 하는 지표들은 다음과 같다.
- 맵의 벽/구조가 얼마나 선명한가
- 벽이 겹쳐 보이는 ghosting이 있는가
- trajectory가 안정적인가
- CPU가 버벅이거나 처리 지연이 있는가
우선 CPU의 처리 지연이 있는지 여부를 확인하기 위해 point_raw가 얼마나 잘 업데이트 되는지 확인해보자. lidar가 10Hz로 들어오고 있으니, 이 숫자에서 많이 밀리면 처리 지연이 발생한다는 의미가 된다.
ros2 topic hz /points_raw
추가로 tops 나 htops를 띄워 메모리를 얼마나 사용하는지도 병행해서 모니터링해보자. CPU% 점유울을 의미하고, RES 는 현재 사용중인 메모리를 뜻한다. H 키를 누르면 쓰레드 기준이 아니라 프로세스 기준으로 필터링해줄것이다.
위 이미지에서 보면 mapOptimization에서 CPU점유율을 20% 가량, 메모리(RES)는 220메가 수준을 쓰고 있다는건데, 아마 맵 크기가 더 커질수록 점점 늘어나겠다 싶다. 비교할 성능 지표들을 확인했으니까, Baseline을 측정해보도록 하자.
다른 방법으로는 로그를 기록하는 방법이 있는데, python으로 스크립트를 만들어 동작하는 방법이 있다.
여기선 파이썬 스크립트를 하나 만들어서 확인해보자. 요새 GPT 일 잘한다.
Base Line
최종적으로, CPU점유율은 평균 16%남짓, 최대 48%, 램 사용량은 330MB정도 사용한다. (맵 크기에 비해 다른 노드들은 크게 차지 하지 않는다.), /point_raw는 9.8Hz로 잘 유지되는 것을 확인했다. 중간에 9.6Hz로 떨어지는 현상도 있지만.
CPU점유율을 로그로 기록해서 정리해보면 이렇다.
맵의 중간중간을 보면 벽이 두겹이 생겼다거나 하는 부분들이 보이긴 하지만, 적어도 주요 Trajectory 주변에서의 벽들은 그래도 어느정도의 일관성은 유지하는것 처럼 보인다.
Voxel leaf size
Voxel의 크기를 한번 바꿔보자. params.yaml에 아래와 같이 정의되어있다.
# 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
지금은 outdoor기준으로 되어있는데, 이걸 indoor기준으로 변경해보자.
# voxel filter paprams
odometrySurfLeafSize: 0.2 # default: 0.4 - outdoor, 0.2 - indoor
mappingCornerLeafSize: 0.1 # default: 0.2 - outdoor, 0.1 - indoor
mappingSurfLeafSize: 0.2 # default: 0.4 - outdoor, 0.2 - indoor
일단 맵이결과는 이렇다.
이중벽이 있던곳은 그대로이고…
/point_raw의 결과도 9.8 Hz로 잘 유지되는 편이다. 중간에 프레임이 떨어지는건 되려 줄었다.
다만 voxel의 사이즈를 줄여버리는 바람에 연산량이 많이 늘어난듯 하다.
CPU 점유율이 120%가 보인다. 이는 한개 코어를 확실히 넘어선다는걸 알 수 있다.평균은 36%의 점유율로 기록된다.
RAM 사용량도 늘었다. 기존 320MB 에서 515MB 로 늘었다. 다른 프로세스들까지 포함하면 530 MB는 사용한다고 봐야 한다.
이젠 실험의 방향을 악화시켜보자.
# voxel filter paprams
odometrySurfLeafSize: 0.8 # default: 0.4 - outdoor, 0.2 - indoor
mappingCornerLeafSize: 0.5 # default: 0.2 - outdoor, 0.1 - indoor
mappingSurfLeafSize: 0.8 # default: 0.4 - outdoor, 0.2 - indoor
일단 맵이 완전히 깨졌다.
이게 어디서부터 난감한거냐….
CPU 점유율도 그렇게 높은 편은 아니지만, 아마 Map이 깨졌을 시점부터, mapOptimizer의 연산부하가 확 줄었을것같다는 느낌은 받는다.
고로 일단 voxel사이즈가 적당해야 한다.
searchRadius
이에 반대되어 탐색반경을 변경해보자.
# Surrounding map
surroundingkeyframeAddingDistThreshold: 1.0 # meters, regulate keyframe adding threshold
surroundingkeyframeAddingAngleThreshold: 0.2 # radians, regulate keyframe adding threshold
surroundingKeyframeDensity: 2.0 # meters, downsample surrounding keyframe poses
surroundingKeyframeSearchRadius: 50.0 # meters, within n meters scan-to-map optimization //defuat가 50이다.
SearchRadius를 20으로 바꿔보자.
벽이 밀려 두겹으로 되는 현상이 조금 더 많이 보인다.
긴 벽이 틀어지는 현상도 조금씩 보인다.
cpu 사용량은 조금 늘었다. 최대 56%, 평균 20%정도 된다. 왜일까.. 메모리 사용량도 조금 늘었다. 약 340MB 정도로 추정된다.
SearchRadius 를 80로 늘려보자.
다른 현상보다, 긴 벽이 틀어지는게 많이 줄어 보인다.
CPU 연산량에 있어서 큰 차이는 보이지 않는다. 최대 48%, 평균 18%정도의 점유율을 보이며, 메모리 크기는 약 340MB 정도 소모된다.
/point_raw도 9.8Hz로 적절하게 잘 유지되고 있다.
의뢰로 반대로 간다. Search Radius 가 커져도 별 문제가 없겠는데?
왜 그런가 생각해보자.
radius를 키워도 실제 그 범위 안에 들어오는 keyframe 수가 크게 늘지 않을 수 있다.예를 들어 50 m에서 이미 주변 keyframe 대부분을 포함하고 있었다면 80 m로 늘려도 추가되는 정보가 별로 없다는 뜻이다.
둘째, LIO-SAM은 주변 keyframe을 그대로 다 쓰는 게 아니라 surroundingKeyframeDensity 같은 설정으로 keyframe 자체를 downsampling해서 local map을 구성한다.
그래서 검색 반경이 커져도 최종 scan-to-map 최적화에 들어가는 포인트 수는 생각보다 많이 안늘어날수도 있다.
셋째, 현재 환경이 좁거나 rosbag 이동 범위가 작으면 SearchRadius = 50 m 일 때 이미 사실상 전체 맵을 포함하고 있을 수도 있다.
실제로 map의 위아래 최대거리는 220m 가량된다. 결국 80미터로 설정하면, 거의 맵의 80~90%를 계산범위에 넣기 때문이다.
이때 건드려볼 만한 파라미터가 surroundingKeyframeSearchRadius이다.
surroundingKeyframeDensity: 2.0 # meters, downsample surrounding keyframe poses
이 값을 늘여서 10.0으로 설정해보자. 탐색반경은 80m로 그대로 둔다.
일단 map이미지상으로 보는 결과는 비슷하다
CPU 연산량에 있어서도 별차이는 없는데… 최대 52%, 평균 22%정도의 점유율을 보이며, 메모리 크기는 약 340MB 정도 소모된다.
/point_raw도 9.9Hz로 적절하게 잘 유지되고 있다.
그럼 추가실험으로, 아까 망했던 실험에서 만약 탐색반경을 튜닝하면 성공할 수 있을까..?
안된다. 산으로간다.
density를 2.0으로 바꾸면 뒤로가던건 해결되는데 깨지는건 여전하다.
오묘한 default의 세계로다.
이젠 마지막으로 Loopclosure를 꺼보자. map의 일관성을 유지시켜주는 가장 막강한 기능이다.
baseline으로 바꾸고 테스트해본다.
…? 별 차이없는데?
진짜 별 차이없는데..?
아래 이미지는 탐색반경을 80미터로 놓고 했다.
이 경우 차이가 없는 이유는 다음과 같이 정리해볼수 있다.
- rosbag 경로가 실제로 출발지나 과거 위치를 다시 방문하지 않음
- 재방문은 했지만 historyKeyframeSearchRadius 안으로 충분히 가까이 안 들어옴
- historyKeyframeSearchTimeDiff 조건을 못 넘김
- 이미 odometry drift가 매우 작아서 loop closure 전후가 거의 같아 보임
- loop candidate는 생겼지만 ICP 정합이 충분히 좋지 않아 factor가 추가되지 않음
일단 오늘은 여기까지 돌려본다.
다음번엔 조금더 내용을 추가해서, loop closure성능을 다시한번 확인해보자.
댓글남기기