LIO-SAM 실습해보기 (4)
들어가면서.
지난 차수에선 기본 데이터셋을 가지고 파라미터들을 바꿔가면서 간단하게 파라미터들이 어떤 영향을 미치고, 크리티컬한건 무엇인지 들여다봤었다.
이번엔 좀 다른 데이터셋들도 한번 돌려보면서, LOAM에서 이어져온 featureExtraction이 뭐가 다른지, 그리고 IMU의 영향이 얼마나 큰건지 한번 알아보도록 하자.
이 과정에선 원본코드를 조금씩 손대야 하는 경우도 발생하니, 지속적으로 백업하면서 나아가보자.
학습 순서.
다음과 같은 순서로 최대한 스피디하게 진행해볼까 한다.
- ROS2에 설치해보기
- IMU/PointCloud 다뤄보기
- 데이터셋을 이용하여 LIO-SAM 구현해보기
- (New!) featureExtraction.cpp 둘러보기, Feature, IMU ablations <- 오늘
- mapOptimziation 분석, 라즈베리파이에 실제 센서 연결해보기.
Day 4.
랩실 선배들에게 물어봤다. 뭘 더봐야 할까.
우선 LIO-SAM은 LOAM에 기초해서 IMU/GPS를 적용시킨 시스템이지만, 조금씩 다르다고 한다. d오늘은 그 차이를 좀 보면서, 몇가지 Ablatation들을 해보자.
featureExtraction.cpp
deskew된 LiDAR 포인트 클라우드에서 곡률 기반으로 corner(edge) 특징과 surface 특징을 추출해 mapOptimization 단계로 전달하는 것이 이 파일에 핵심이다.
class의 시작
imageProjection
└─ ring 순서로 정렬된 deskew 포인트클라우드
+ range
+ range-image column index
+ ring별 시작/끝 index
│
▼
laserCloudInfoHandler()
├─ calculateSmoothness() 곡률 계산
├─ markOccludedPoints() 불안정한 포인트 제외
├─ extractFeatures() corner/surface 추출
└─ publishFeatureCloud() 결과 퍼블리시
│
▼
mapOptimization
코드의 서두를 따라가다보면 이런 부분이 나온다.
class FeatureExtraction : public ParamServer
{
public:
.......
FeatureExtraction(const rclcpp::NodeOptions & options) :
ParamServer("lio_sam_featureExtraction", options)
{
subLaserCloudInfo = create_subscription<lio_sam::msg::CloudInfo>(
"lio_sam/deskew/cloud_info", qos,
std::bind(&FeatureExtraction::laserCloudInfoHandler, this, std::placeholders::_1));
pubLaserCloudInfo = create_publisher<lio_sam::msg::CloudInfo>(
"lio_sam/feature/cloud_info", qos);
pubCornerPoints = create_publisher<sensor_msgs::msg::PointCloud2>(
"lio_sam/feature/cloud_corner", 1);
pubSurfacePoints = create_publisher<sensor_msgs::msg::PointCloud2>(
"lio_sam/feature/cloud_surface", 1);
initializationValue();
}
..........
이부분이 핵심이겠지. 일단 두려워하지 말고 천천히 잘라 보자.
subLaserCloudInfo = create_subscription<lio_sam::msg::CloudInfo>(
"lio_sam/deskew/cloud_info", qos,
std::bind(&FeatureExtraction::laserCloudInfoHandler, this, std::placeholders::_1));
ROS 2에서 “lio_sam/deskew/cloud_info” 토픽을 구독하고, 메시지가 도착할 때 현재 FeatureExtraction 객체의 laserCloudInfoHandler()를 호출하도록 설정한다.
각 부분의 의미는 다음과 같다.
create_subscription<lio_sam::msg::CloudInfo>(...): lio_sam::msg::CloudInfo 타입 메시지를 받는 ROS 2 구독 객체를 생성"lio_sam/deskew/cloud_info": 구독할 토픽 이름qos: 메시지 전달 방식과 버퍼 크기 등을 정하는 QoS 설정subLaserCloudInfo = ...: 생성된 구독 객체를 멤버 변수에 저장 이 변수를 유지하지 않으면 구독 객체가 소멸해 콜백이 더 이상 실행되지 않을 수 있다std::bind(&FeatureExtraction::laserCloudInfoHandler, this, std::placeholders::_1));: 콜백 부분. laserCloudInfoHandler라는 멤버 함수를 호출한다. this가 난무하니 헷갈린다만, featureExtraction 객체에 대해 호출한다는 말이고, std::placeholders::_1는 ROS2가 콜백에 전닿라는 첫번째 인자의 자리다.
cloud_info 토픽에 메시지 발행
↓
ROS 2가 메시지를 수신
↓
등록된 콜백 호출
↓
this->laserCloudInfoHandler(수신한 메시지)
초기화, initializationValue()
N_SCAN * Horizon_SCAN만큼 작업 배열을 할당한다.
| 변수 | 의미 |
|---|---|
cloudCurvature[i] |
i번째 점의 곡률 점수 |
cloudNeighborPicked[i] |
특징 후보에서 제외되었는지 |
cloudLabel[i] |
1: corner, -1: flat, 0: 미분류 |
cloudSmoothness[i] |
정렬을 위한 (곡률, 원래 인덱스) |
downSizeFilter |
surface 점군 voxel downsampling |
현재 설정에서는 보통 다음 파라미터가 사용된다.
- edgeThreshold = 1.0
- surfThreshold = 0.1
- odometrySurfLeafSize = 0.4
void initializationValue()
{
cloudSmoothness.resize(N_SCAN*Horizon_SCAN);
downSizeFilter.setLeafSize(odometrySurfLeafSize, odometrySurfLeafSize, odometrySurfLeafSize);
extractedCloud.reset(new pcl::PointCloud<PointType>());
cornerCloud.reset(new pcl::PointCloud<PointType>());
surfaceCloud.reset(new pcl::PointCloud<PointType>());
cloudCurvature = new float[N_SCAN*Horizon_SCAN];
cloudNeighborPicked = new int[N_SCAN*Horizon_SCAN];
cloudLabel = new int[N_SCAN*Horizon_SCAN];
}
프레임 처리 진입점 laserCloudInfoHandler()
프레임 하나를 처리하는 오케스트레이터
void laserCloudInfoHandler(const lio_sam::msg::CloudInfo::SharedPtr msgIn)
{
cloudInfo = *msgIn; // new cloud info
cloudHeader = msgIn->header; // new cloud header
pcl::fromROSMsg(msgIn->cloud_deskewed, *extractedCloud); // new cloud for extraction
calculateSmoothness();
markOccludedPoints();
extractFeatures();
publishFeatureCloud();
}
실질적으로 feature를 뽑아내는 과정을 순서대로 보여준다.
곡률의 계산 calculateSmoothness()
void calculateSmoothness()
{
int cloudSize = extractedCloud->points.size();
for (int i = 5; i < cloudSize - 5; i++)
{
float diffRange = cloudInfo.point_range[i-5] + cloudInfo.point_range[i-4]
+ cloudInfo.point_range[i-3] + cloudInfo.point_range[i-2]
+ cloudInfo.point_range[i-1] - cloudInfo.point_range[i] * 10
+ cloudInfo.point_range[i+1] + cloudInfo.point_range[i+2]
+ cloudInfo.point_range[i+3] + cloudInfo.point_range[i+4]
+ cloudInfo.point_range[i+5];
cloudCurvature[i] = diffRange*diffRange;//diffX * diffX + diffY * diffY + diffZ * diffZ;
cloudNeighborPicked[i] = 0;
cloudLabel[i] = 0;
// cloudSmoothness for sorting
cloudSmoothness[i].value = cloudCurvature[i];
cloudSmoothness[i].ind = i;
}
}
여기서 LOAM과 좀 다른 부분이 보인다. LOAM에서는 X,Y,Z 값 차이를 이용해서 곡률을 구한다면,
LIO-SAM에선 한 스캔 라인 안에서 거리값을 가지고 curvature를 구한다. 중심값은 10배 가중치를 두어 계산하는데, 이런 형태의 필터를 뭐라하더라….. 찾아보니 필터 커널이라고 하는데, 좀 더 조사해봐야겠다.
여튼 일종의 윈도우함수로 range값을 연산해서, point_range^2 를 취한다. 중앙값의 부호가 (-)네… 아………ㅡ_ㅡ.. 중점과 편차(residual) 누적이구나…
근데…. 이렇게 계산하면, 적당히 휘어져 있어야 평면 아닌가…? 이래도 되나……?
즉, 기하학적인 Curvature를 찾는게 아니라, 국소적으로 기울기가 일정한 곳을 찾는게 핵심이 된다. smoothness 지표라고 이해하면 좋다. 그리고 이런 점들이 surface candidate가 된다고 한다. 나중에 나오면 다시 보자.
신뢰하기 어려운점의 제거 markOccludedPoints()
곡률이 크다고 해서 모두 실제 edge는 아니다. 가려짐 경계나 표면을 비스듬하게 때린 점에서도 큰 값이 나오므로, 이런 점을 미리 제외한다.
void markOccludedPoints()
{
int cloudSize = extractedCloud->points.size();
// mark occluded points and parallel beam points
for (int i = 5; i < cloudSize - 6; ++i)
{
// occluded points
float depth1 = cloudInfo.point_range[i];
float depth2 = cloudInfo.point_range[i+1];
int columnDiff = std::abs(int(cloudInfo.point_col_ind[i+1] - cloudInfo.point_col_ind[i]));
if (columnDiff < 10){
// 10 pixel diff in range image
if (depth1 - depth2 > 0.3){
cloudNeighborPicked[i - 5] = 1;
cloudNeighborPicked[i - 4] = 1;
cloudNeighborPicked[i - 3] = 1;
cloudNeighborPicked[i - 2] = 1;
cloudNeighborPicked[i - 1] = 1;
cloudNeighborPicked[i] = 1;
}else if (depth2 - depth1 > 0.3){
cloudNeighborPicked[i + 1] = 1;
cloudNeighborPicked[i + 2] = 1;
cloudNeighborPicked[i + 3] = 1;
cloudNeighborPicked[i + 4] = 1;
cloudNeighborPicked[i + 5] = 1;
cloudNeighborPicked[i + 6] = 1;
}
}
// parallel beam
float diff1 = std::abs(float(cloudInfo.point_range[i-1] - cloudInfo.point_range[i]));
float diff2 = std::abs(float(cloudInfo.point_range[i+1] - cloudInfo.point_range[i]));
if (diff1 > 0.02 * cloudInfo.point_range[i] && diff2 > 0.02 * cloudInfo.point_range[i])
cloudNeighborPicked[i] = 1;
}
}
우선, depth1과 depth2에 포인트의 거리값이 저장된다. 그 다음 만약 이 둘의 차이가 매우 크다면 (> (+/-) 0.3,) 그 점을 기준으로 5개의 점을 빼버린다. Occlusion, 즉 물체에 의한 가림현상으로 보지, 이것이 기하학적인 특징점이 아니라고 판단한다.
그 다음 depth1과 depth2를 다시 구한다.이번엔 전후 포인트간의 range차이를 구해서, 만약 차이가 급격히 커진다면 그 포인트도 제외한다. 이건 LiDAR의 레이저 빔이 벽과 수평에 가깝게 쏘아질때 거리값의 변동이 매우 크게 나오기 때문에 이런걸 삭제하는 원리다. (parallel beam)
특징점의 분류. extractFeatures()
이제 본격적으로 특징점들을 분류하는 단계로 넘어가보자.
pcl::PointCloud<PointType>::Ptr surfaceCloudScan(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr surfaceCloudScanDS(new pcl::PointCloud<PointType>());
PCL point cloud 객체를 동적 생성해서 스마트 포인터로 들고 있는 것이다.
surfaceCloudScan 는 scan ring에서 모은 surface 후보 point cloud이고,
surfaceCloudScanDS는 그걸 grid 단위로 DownSample한 결과를 담는 cloud가 된다.
코너 찾기
for (int i = 0; i < N_SCAN; i++)
for (int j = 0; j < 6; j++)
한 ring 전체에서 곡률이 큰 점만 고르면 특정 방향이나 구조물에 특징이 몰릴 수 있다. 그래서 ring을 수평 방향으로 6등분하고 각 구간에서 독립적으로 특징을 고른다. LOAM에서도 특징점 추출을 위해 방위각을 고르게 나누었었다.
std::sort(cloudSmoothness.begin()+sp, cloudSmoothness.begin()+ep, by_value());
곡률순으로 정렬한다. cloudSmoothness값을 바탕으로 오름차순이다.
for (int k = ep; k >= sp; k--)
정렬 결과를 뒤에서부터 순회하면서,
if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] > edgeThreshold)
위의 조건을 만족하면,
- cloudLabel[ind] = 1
- cornerCloud에 추가
cornerCloud->push_back(extractedCloud->points[ind]); - 구간당 최대 20개 선택 (largestPickedNum <=20 이 false가 되면 break된다. )
- 같은 edge 주변 점이 반복 선택되지 않도록 좌우 5개를 picked 처리
for (int l = 1; l <= 5; l++)
Flat 찾기
for (int k = ep; k >= sp; k--)
다시한번 정렬 결과를 뒤에서부터 순회하면서, cloudCurvature[ind] < surfThreshold 인 점들을 cloudLabel[ind] = -1 로 표시하고, 마찬가지로 주변 5개도 중복선택되지 않도록 처리한다.
코너는 1이고 평면은 -1이구나…
if (cloudLabel[k] <= 0) surfaceCloudScan->push_back(extractedCloud->points[k]);
코너로 선택된 점을 제외하고 다음 나머지를 모두 surface 후보에 넣는다.
- 낮은 곡률의 명시적 flat 점
- 임계값 사이에 있는 미분류 점
- occlusion 등으로 특징 선택이 금지된 점
LOAM의 “less-flat surface cloud” 구성 방식이다.
따라서 surfThreshold는 최종 surface 점군을 직접 필터링하는 임계값이라기보다, flat 대표점을 선택하고 그 주변의 중복 선택 상태를 결정하는 데 더 가깝다.
Downsampling
surfaceCloudScanDS->clear();
downSizeFilter.setInputCloud(surfaceCloudScan);
downSizeFilter.filter(*surfaceCloudScanDS);
*surfaceCloud += *surfaceCloudScanDS;
각 ring의 surface 후보들을 voxel단위로 줄인뒤 surface 포인트클라우드에 합친다. 코너는 다운샘플링을 하지 않는다.
결과 퍼블리시.
먼저 데이터를 비운다.
void freeCloudInfoMemory()
{
cloudInfo.start_ring_index.clear();
cloudInfo.end_ring_index.clear();
cloudInfo.point_col_ind.clear();
cloudInfo.point_range.clear();
}
그다음 보낼 데이터를 넣고, 발행한다.
void publishFeatureCloud()
{
// free cloud info memory
freeCloudInfoMemory();
// save newly extracted features
cloudInfo.cloud_corner = publishCloud(pubCornerPoints, cornerCloud, cloudHeader.stamp, lidarFrame);
cloudInfo.cloud_surface = publishCloud(pubSurfacePoints, surfaceCloud, cloudHeader.stamp, lidarFrame);
// publish to mapOptimization
pubLaserCloudInfo->publish(cloudInfo);
}
코드리뷰에서 나오는 지적
현재 보고 있는 코드들에서, GPT가 몇가지 문제점들을 지적해준다.
정렬 범위
이게 아니라,
std::sort(
cloudSmoothness.begin() + sp,
cloudSmoothness.begin() + ep,
by_value());
이거 아닌가 하는 지적을 한다.
std::sort(
cloudSmoothness.begin() + sp,
cloudSmoothness.begin() + ep + 1,
by_value());
sorting시, 마지막 데이터가 안들어가는것 아닌가 하는 걱정을 하는건데….. 뭐 큰 문제 없나..?
입력 크기에 대한 방어코드
extractedCloud.size() <= N_SCAN * Horizon_SCAN
point_range.size() >= extractedCloud.size()
point_col_ind.size() >= extractedCloud.size()
start/end_ring_index.size() >= N_SCAN
파라미터 불일치나 잘못 구성된 메시지가 들어오면 raw array와 vector에서 out-of-bounds가 발생할 수 있다.
따라서 콜백 초기에 크기를 확인하고, 오류로그 출력화 함께 프레임을 버리는 방어로직이 필요하다고 지적한다.
오호라….
raw array의 소유권
cloudCurvature = new float[...];
cloudNeighborPicked = new int[...];
cloudLabel = new int[...];
대응하는 delete[]가 없다. 노드 수명 동안 한 번만 할당하므로 지속적인 프레임별 leak은 아니지만 RAII 관점에서는 좋지 않다고 한다. 권장하는 스타일은 다음과 같다고 한다.
std::vector<float> cloudCurvature;
std::vector<int> cloudNeighborPicked;
std::vector<int> cloudLabel;
매 프레임마다 임시 point cloud를 동적할당한다.
pcl::PointCloud<PointType>::Ptr surfaceCloudScan(new ...);
pcl::PointCloud<PointType>::Ptr surfaceCloudScanDS(new ...);
고주파 LiDAR 파이프라인에서는 멤버로 보관해 재사용하거나 stack 객체로 두는 것이 allocation jitter를 줄이는 데 유리하다는데, 정확히 무슨 이야긴지는 잘 모르겠다.
하드코딩된 센서 의존 임계값
다음 값들이 코드에 직접 들어 있다.
- 가려짐 거리 차이: 0.3m
- 인접 column 허용값: 10
- parallel-beam 비율: 2%
- 특징 주변 억제 반경: ±5
- ring 분할 수: 6
- 구간당 corner 최대값: 20
나중에 직접 튜닝해야 할 가능성도 생긴다.
코드의 요약 정리
각 scan ring을 6구간으로 나누고, range의 국소 변화량이 큰 안정적인 점을 corner로 선택한 뒤, corner가 아닌 점들을 ring별 voxel downsampling하여 surface 특징으로 전달한다.
Feature에 대한 Ablation test
cpp 파일을 건들면 빌드를 반드시 해야 한다. (그래서 좀 귀찮은 감이 없지 않다.)
baseline 다시 잡기
- 6 rings
- 20 corners/rings
- suppression ±5
- occlusion filtering 사용
- leaf size 0.4 m
간만에 다시하는거 까먹지 않았나 확인해보자.
이제 동작을 일단 확인해보자.
터미널 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 2 --rate 1.0
이젠 정밀도 확인을 위해서 최종좌표값도 같이 기록해보자.
- X : 86.0359
- Y : 59.3883
- Z : -5.13266 …(마이너스라고?)
markOccludedPoints 이 없다면?
급걱한 거리값의 변동은 가림현상으로 본다는걸 배제해보자.
void laserCloudInfoHandler(const lio_sam::msg::CloudInfo::SharedPtr msgIn)
{
cloudInfo = *msgIn; // new cloud info
cloudHeader = msgIn->header; // new cloud header
pcl::fromROSMsg(msgIn->cloud_deskewed, *extractedCloud); // new cloud for extraction
calculateSmoothness();
//markOccludedPoints(); //ablation study 2.
extractFeatures();
publishFeatureCloud();
}
수정했으면, 다시 빌드하고, setup.bash도 착실하게 실행시키자.
colcon build --packages-select lio_sam --cmake-args -DCMAKE_BUILD_TYPE=Release
source install/setup.bash
실험결과는 이렇다.
맵만봐선 잘모르겠는데, 좌표를 보면 제법 티가 난다.
- X : (baseline) 86.0359 -> 103.557
- Y : (baseline) 59.3883 -> 20.5525
- Z : (baseline) -5.13266 -> -4.39451
markOcclusion 의 세부 ablation
markOcclusion 에서는 가림현상과 parallel beam 현상을 핸들링하는데, 이중 가림현상만 한번 확인해보자.
void markOccludedPoints()
{
int cloudSize = extractedCloud->points.size();
// mark occluded points and parallel beam points
for (int i = 5; i < cloudSize - 6; ++i)
{
// occluded points
// Ablation3 : No depth-discontinuity filtering
/*
float depth1 = cloudInfo.point_range[i];
float depth2 = cloudInfo.point_range[i+1];
int columnDiff = std::abs(int(cloudInfo.point_col_ind[i+1] - cloudInfo.point_col_ind[i]));
if (columnDiff < 10){
// 10 pixel diff in range image
if (depth1 - depth2 > 0.3){
cloudNeighborPicked[i - 5] = 1;
cloudNeighborPicked[i - 4] = 1;
cloudNeighborPicked[i - 3] = 1;
cloudNeighborPicked[i - 2] = 1;
cloudNeighborPicked[i - 1] = 1;
cloudNeighborPicked[i] = 1;
}else if (depth2 - depth1 > 0.3){
cloudNeighborPicked[i + 1] = 1;
cloudNeighborPicked[i + 2] = 1;
cloudNeighborPicked[i + 3] = 1;
cloudNeighborPicked[i + 4] = 1;
cloudNeighborPicked[i + 5] = 1;
cloudNeighborPicked[i + 6] = 1;
}
}
*/
// parallel beam
float diff1 = std::abs(float(cloudInfo.point_range[i-1] - cloudInfo.point_range[i]));
float diff2 = std::abs(float(cloudInfo.point_range[i+1] - cloudInfo.point_range[i]));
if (diff1 > 0.02 * cloudInfo.point_range[i] && diff2 > 0.02 * cloudInfo.point_range[i])
cloudNeighborPicked[i] = 1;
}
}
실험결과는 이렇다.
- X : (baseline) 86.0359 -> 103.557 –> 102.083
- Y : (baseline) 59.3883 -> 20.5525 –> 28.365
- Z : (baseline) -5.13266 -> -4.39451 –> -4.431
그럼 반대로 beam을 없애본다면?
- X : (baseline) 86.0359 -> 103.557 –> 102.083 –> 104.98
- Y : (baseline) 59.3883 -> 20.5525 –> 28.365 –> 20.337
- Z : (baseline) -5.13266 -> -4.39451 –> -4.431 –> -5.3819
결론은 둘다 있어야 효과가 잘 나온다. rosbag 재생속도를 올렸더니만, 실험 결과가 엉망진창이 되네. 조심해야겠다.
base line 다시 확인해보기.
- X : (baseline) 86.0359 -> (markOcclunsion) 103.557 –> (depth jump) 102.083 –> (parallel beam)104.98 –> (base line) 88.502
- Y : (baseline) 59.3883 -> (markOcclunsion) 20.5525 –> 28.365 –> 20.337 –> (base line) 54.5099
- Z : (baseline) -5.13266 -> (markOcclunsion) -4.39451 –> -4.431 –> -5.3819 –> (base line) -0.3643
No regional segmentation / ring 전체에서 한 번만 feature 선택
이번엔 region을 6개로 나누는 부분과, 추출 포인트를 손대보자.
{
cornerCloud->clear();
surfaceCloud->clear();
pcl::PointCloud<PointType>::Ptr surfaceCloudScan(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr surfaceCloudScanDS(new pcl::PointCloud<PointType>());
for (int i = 0; i < N_SCAN; i++)
{
surfaceCloudScan->clear();
//for (int j = 0; j < 6; j++)
for (int j = 0; j < 1; j++)
{
//int sp = (cloudInfo.start_ring_index[i] * (6 - j) + cloudInfo.end_ring_index[i] * j) / 6;
//int ep = (cloudInfo.start_ring_index[i] * (5 - j) + cloudInfo.end_ring_index[i] * (j + 1)) / 6 - 1;
int sp = cloudInfo.start_ring_index[i];
int ep = cloudInfo.end_ring_index[i];
if (sp >= ep)
continue;
std::sort(cloudSmoothness.begin()+sp, cloudSmoothness.begin()+ep, by_value());
int largestPickedNum = 0;
for (int k = ep; k >= sp; k--)
{
int ind = cloudSmoothness[k].ind;
if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] > edgeThreshold)
{
largestPickedNum++;
//if (largestPickedNum <= 20){
if (largestPickedNum <= 120){
- X : (baseline) 86.0359 -> (markOcclunsion) 105.061 –> (ring) 104.491
- Y : (baseline) 59.3883 -> (markOcclunsion) 21.6516 –> 21.129
- Z : (baseline) -5.13266 -> (markOcclunsion) -4.712 –> -5.740
largestPickedNum 을 20으로 해도, 결과는 비슷하다. Baseline 대비해서 안좋아 지는 정도는 다들 비슷비슷하다.
No corner neighbor suppression
여기 아래부분을 주석처리하면 된다.
int largestPickedNum = 0;
for (int k = ep; k >= sp; k--)
{
int ind = cloudSmoothness[k].ind;
if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] > edgeThreshold)
{
largestPickedNum++;
if (largestPickedNum <= 20){
cloudLabel[ind] = 1;
cornerCloud->push_back(extractedCloud->points[ind]);
} else {
break;
}
cloudNeighborPicked[ind] = 1;
// Ablation: No corner neighbor suppression
/*
for (int l = 1; l <= 5; l++)
{
int columnDiff = std::abs(int(cloudInfo.point_col_ind[ind + l] - cloudInfo.point_col_ind[ind + l - 1]));
if (columnDiff > 10)
break;
cloudNeighborPicked[ind + l] = 1;
}
for (int l = -1; l >= -5; l--)
{
int columnDiff = std::abs(int(cloudInfo.point_col_ind[ind + l] - cloudInfo.point_col_ind[ind + l + 1]));
if (columnDiff > 10)
break;
cloudNeighborPicked[ind + l] = 1;
}
*/
}
이것도 마찬가지로 오차가 벌어지는건 비슷하다.
뭘해도…. 딱 벌어지는게 요만큼이다.
surface 주변 5개 포인트 suppression도 지워본다. 그 바로 아래 코드가 있다.
int ind = cloudSmoothness[k].ind;
if (cloudNeighborPicked[ind] == 0 && cloudCurvature[ind] < surfThreshold)
{
cloudLabel[ind] = -1;
cloudNeighborPicked[ind] = 1;
// Ablation: No surface neighbor suppression
/*
for (int l = 1; l <= 5; l++) {
int columnDiff = std::abs(int(cloudInfo.point_col_ind[ind + l] - cloudInfo.point_col_ind[ind + l - 1]));
if (columnDiff > 10)
break;
cloudNeighborPicked[ind + l] = 1;
}
for (int l = -1; l >= -5; l--) {
int columnDiff = std::abs(int(cloudInfo.point_col_ind[ind + l] - cloudInfo.point_col_ind[ind + l + 1]));
if (columnDiff > 10)
break;
cloudNeighborPicked[ind + l] = 1;
}
*/
}
추가로 해봐도…. 딱 벌어지는 만큼은 정해져있다.
Optimization의 위력인가…?
물론 map에 등록되는 포인트들 말고, 이동 중의 포인트 숫자들을 보면 좀 다르겠거니 싶지만, 결과만 놓고 봤을땐, 뭐하나 빼더라도 비슷비슷하다.
마찬가지로, if (largestPickedNum <= 5) 처럼 코너 선정 개수를 건들여봐도 SLAM이 실패하진 않고, 벌어지는 오차도 다른 ablation과 비슷하다.
만약 차이가 생긴다면, 코너 포인트가 발생하는 숫자나, 맵 옵티마이저의 연산스피드가 달라진다거나 하는 부분들이 있을 것같다.
아무리 뭘 바꿔봐도.. 옵티마이저가 모든걸 다 처리해버리는 기분이 강하게 든다. 신기하네.
오늘은 여기까지.
댓글남기기