LiDAR self compensation 시도

yeonsoo·2023년 3월 22일
1
post-thumbnail

LiDAR를 SLAM에 사용할 때 imu 값을 이용한 motion compensation을 전처리단에서 수행하는 경우가 많다.
왜냐하면 주행 차량에서 취득하는 경우와 같이 빠른 속도로 움직이는 경우 라이다 앵글컷 시작점과 종료점이 꽤 큰 차이를 가질 수 있기 때문이다.
이를 보상하지 않는 것은 라이다가 한바퀴 스캔하는 동안 정지해 있다고 가정하는 것과 같음.
근데 imu가 없다면 어떻게 해야할까?

현재 프레임의 포즈를 보상 없이 추정한 후, 맵을 쌓을 때 이전 포즈와 현재포즈의 차이를 이용해 보상한 프레임을 사용해보기로 했다.

Pandar 128 채널 라이다를 사용해 실험했다.

프레임 하나를 띄워보았다. 민트 화살표가 차량 주행 방향이다.
이 데이터는 후방이 +y, 왼쪽이 +x, 위가 +z인 좌표계를 사용함을 확인했다.

판다는 앵글 스캔이 +y축을 기준으로 시계방향으로 이루어진다. 즉 +y축이 앵글컷 지점이다.
앵글컷 시작점에서부터 종료점까지 point의 angle을 이용해 포즈 변화량에 곱해줄 상수를 구할 것이다.

pcl::PointCloud<pcl::PointXYZI>::Ptr original_frame(new pcl::PointCloud<pcl::PointXYZI>());
for (size_t i = 0; i < scan.points.size(); i++)
{
	int ang = constrainAngle(RAD2DEG(atan2(scan.points[i].y, scan.points[i].x)));
	ang = (90 - ang) % 360;
	if (ang < 0)
		ang += 360;
            
	pcl::PointXYZI pt;
    pt_.x = scan.points[i].x;
    pt_.y = scan.points[i].y;
    pt_.z = scan.points[i].z;
    pt_.intensity = static_cast<float>(ang)/360*255;
    original_frame->points.push_back(pt_);
}
original_frame->width    = original_frame->size();
original_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/original_frame.pcd", *original_frame);


위 코드를 사용하여 확인해보니 +y축을 앵글컷으로 하여 상수가 잘 들어갔다. ang/360이 이전 프레임과 현재 프레임 사이의 포즈 변화량을 반영하는 상수가 된다.
(위 그림에서 파란색=0, 빨간색=255)

정면으로 3m 이동했을 때 상황을 구현해보자.
헤딩은 아이덴티티, (0,1,0)에서 (0,-2,0)로 움직였다.

Eigen::Matrix4f cur_pose, prev_pose;
prev_pose << 1, 0, 0, 0, 
             0, 1, 0, 1,
             0, 0, 1, 0,
             0, 0, 0, 1;

cur_pose << 1, 0, 0, 0, 
            0, 1, 0, -2,
            0, 0, 1, 0,
            0, 0, 0, 1;

Eigen::Vector3f del_pose_t; //translation 변화량
del_pose_t << cur_pose(0,3) - prev_pose(0,3),
              cur_pose(1,3) - prev_pose(1,3),
              cur_pose(2,3) - prev_pose(2,3);
del_pose_t /= 360.;
for (size_t i = 0; i < scan.points.size(); i++)
{
    Eigen::Matrix<float, 4, 1> ori_pt, cur_pt, cor_pt, next_pt;
    int ang = constrainAngle(RAD2DEG(atan2(scan.points[i].y, scan.points[i].x)));
    ang = (90 - ang) % 360;
    if (ang < 0)
        ang += 360;

    ori_pt << scan.points[i].x, scan.points[i].y, scan.points[i].z, 1;
    Eigen::Matrix4f part_pose;
    part_pose = prev_pose;
    part_pose(0, 3) += del_pose_t(0, 3) * ang;
    part_pose(1, 3) += del_pose_t(1, 3) * ang;
    part_pose(2, 3) += del_pose_t(2, 3) * ang;

    Eigen::Matrix4f prev_pose_inv;
    prev_pose_inv.block(0,0,3,3) = prev_pose.block(0,0,3,3).transpose();
    prev_pose_inv.col(3).topRows(3) = -prev_pose_inv.block(0,0,3,3)*prev_pose.col(3).topRows(3);
    cor_pt =  prev_pose_inv * part_pose * ori_pt;
    cor_pt =  part_pose * ori_pt;

    pt_compen.x = cor_pt(0,3);
    pt_compen.y = cor_pt(1,3);
    pt_compen.z = cor_pt(2,3);
    pt_compen.intensity = static_cast<float>(ang)/360*255;
    compen_frame->points.push_back(pt_compen);  
}
original_frame->width    = original_frame->size();
original_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/original_frame.pcd", *original_frame);
compen_frame->width    = compen_frame->size();
compen_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/compen_frame.pcd", *compen_frame);



짠.. 파란 점들은 prev_pose가 적용되었고 빨간 점들은 cur_pose가 적용되었다. 그 사이는 등속으로 보간되었다.
scan: 라이다 프레임
part_pose: 월드 프레임 (월드 프레임 상에서 각이 ang_i일때의 시간 t_i에서의 prev_pose->cur_pose 보간 위치)
compen_frame: 백 투 라이다 프레임

x, y를 한번에 움직여보자.

Eigen::Matrix4f cur_pose, prev_pose;
prev_pose << 1, 0, 0, 2, 
            0, 1, 0, 4,
            0, 0, 1, 0,
            0, 0, 0, 1;

cur_pose << 1, 0, 0, 7, 
            0, 1, 0, -2,
            0, 0, 1, 0,
            0, 0, 0, 1;

이번에는 prev_pose에서의 스캔 점군과 cur_pose에서의 스캔 점군, 그 사이 보상이 적용된 스캔 점군을 모두 월드 프레임 상에서 띄워보았다.

흰색: prev_pose
초록색: cur_pose


앵글컷 시작점인 파란점은 prev_pose와 잘 물려있고 빨간점은 cur_pose와 잘 물려있다.

rotation을 적용해보자.
yaw로 60도를 돌려본다.

좌단: prev_pose
우단: yaw로 60도 돌린 cur_pose
rotation은 quaternion 보간을 사용한다.
참고로 회전변환 보간의 경우 다양한 방법이 존재한다. 회전행렬에서 회전축과 각을 추출해 각만 1/n때려도 되고.. 쿼터니언 보간시에도 선형보간이나 2차보간 등 다 가능하지만 가장 많이 쓰이는 것은 slerp이다. Eigen 라이브러리에서도 지원한다.
아무튼 나는 항상 쿼터니언을 무조건적으로 선호하기때문에 쿼터니언 slerp 보간을 하도록 한다.
회전행렬 보간에 대한 이론을 더 알아보고 싶은 사람은 아래 링크의 ppt를 추천함.
https://mycourses.aalto.fi/pluginfile.php/1120229/mod_resource/content/1/06_quaternions.pdf

 Eigen::Matrix4f cur_pose, prev_pose;
 prev_pose << 1, 0, 0, 10, 
              0, 1, 0, 10,
              0, 0, 1, 0,
              0, 0, 0, 1;

cur_pose << 1, 0, 0, 10, 
            0, 1, 0, 10,
            0, 0, 1, 0,
            0, 0, 0, 1;

cur_pose.block(0, 0, 3, 3) = map_builder_.eul2RotMatF(0, 0, -60*3.14159/180);

Eigen::Matrix3f prev_R = prev_pose.block(0, 0, 3, 3);
Eigen::Matrix3f cur_R = cur_pose.block(0, 0, 3, 3);
Eigen::Quaternionf v0, v1, vt;
v0 = prev_R;
v1 = cur_R;

pcl::PointXYZI pt;
pcl::PointCloud<pcl::PointXYZI>::Ptr original_frame(new pcl::PointCloud<pcl::PointXYZI>());
pcl::PointCloud<pcl::PointXYZI>::Ptr compen_frame(new pcl::PointCloud<pcl::PointXYZI>());
pcl::PointCloud<pcl::PointXYZI>::Ptr next_frame(new pcl::PointCloud<pcl::PointXYZI>());

for (size_t i = 0; i < scan.points.size(); i++)
{
	Eigen::Matrix<float, 4, 1> ori_pt, cur_pt, cor_pt, next_pt;
    int ang = map_builder_.constrainAngle(RAD2DEG(atan2(scan.points[i].y, scan.points[i].x)));
    ang = (90 - ang) % 360;
    if (ang < 0)
    	ang += 360;

	ori_pt << scan.points[i].x, scan.points[i].y, scan.points[i].z, 1;
    cur_pt =  prev_pose * ori_pt;

	pcl::PointXYZI pt_, pt_compen, pt_next;
    pt_.x = cur_pt(0,3);
    pt_.y = cur_pt(1,3);
    pt_.z = cur_pt(2,3);
    pt_.intensity = static_cast<float>(ang)/360*255;
    original_frame->points.push_back(pt_);

	Eigen::Matrix4f part_pose;
    part_pose = prev_pose;
    vt = v0.slerp(static_cast<float>(ang)/360., v1);
    part_pose.block(0,0,3,3) = vt.toRotationMatrix();

	Eigen::Matrix4f prev_pose_inv;
    prev_pose_inv.block(0,0,3,3) = prev_pose.block(0,0,3,3).transpose();
    prev_pose_inv.col(3).topRows(3) = -prev_pose_inv.block(0,0,3,3)*prev_pose.col(3).topRows(3);
    cor_pt =  prev_pose_inv * part_pose * ori_pt;
    cor_pt =  part_pose * ori_pt;

	pt_compen.x = cor_pt(0,3);
    pt_compen.y = cor_pt(1,3);
    pt_compen.z = cor_pt(2,3);
    pt_compen.intensity = static_cast<float>(ang)/360*255;
    compen_frame->points.push_back(pt_compen);  

	next_pt =  cur_pose * ori_pt;
    pt_next.x = next_pt(0,3);
    pt_next.y = next_pt(1,3);
    pt_next.z = next_pt(2,3);
    pt_next.intensity = static_cast<float>(ang)/360*255;
    next_frame->points.push_back(pt_next);  
}
original_frame->width    = original_frame->size();
original_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/original_frame.pcd", *original_frame);
compen_frame->width    = compen_frame->size();
compen_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/compen_frame.pcd", *compen_frame);
next_frame->width    = next_frame->size();
next_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/next_frame.pcd", *next_frame);

평행이동과 회전을 같이 적용해보자.

Eigen::Matrix4f cur_pose, prev_pose;
prev_pose << 1, 0, 0, 10, 
            0, 1, 0, 10,
            0, 0, 1, 0,
            0, 0, 0, 1;

cur_pose << 1, 0, 0, 14, 
            0, 1, 0, 2,
            0, 0, 1, 0,
            0, 0, 0, 1;

prev_pose.block(0, 0, 3, 3) = eul2RotMatF(0, 0, -30*3.14159/180);
cur_pose.block(0, 0, 3, 3) = eul2RotMatF(0, 0, -60*3.14159/180);

Eigen::Vector3f del_pose_t;
del_pose_t << cur_pose(0,3) - prev_pose(0,3),
                cur_pose(1,3) - prev_pose(1,3),
                cur_pose(2,3) - prev_pose(2,3);
del_pose_t /= 360.;

Eigen::Matrix3f prev_R = prev_pose.block(0, 0, 3, 3);
Eigen::Matrix3f cur_R = cur_pose.block(0, 0, 3, 3);
Eigen::Quaternionf v0, v1, vt;
v0 = prev_R;
v1 = cur_R;

pcl::PointCloud<pcl::PointXYZI> scan;
pcl::fromROSMsg(*point_cloud_msg, scan);

pcl::PointXYZI pt;
pcl::PointCloud<pcl::PointXYZI>::Ptr original_frame(new pcl::PointCloud<pcl::PointXYZI>());
pcl::PointCloud<pcl::PointXYZI>::Ptr compen_frame(new pcl::PointCloud<pcl::PointXYZI>());
pcl::PointCloud<pcl::PointXYZI>::Ptr next_frame(new pcl::PointCloud<pcl::PointXYZI>());

for (size_t i = 0; i < scan.points.size(); i++)
{
    Eigen::Matrix<float, 4, 1> ori_pt, cur_pt, cor_pt, next_pt;
    int ang = constrainAngle(RAD2DEG(atan2(scan.points[i].y, scan.points[i].x)));
    ang = (90 - ang) % 360;
    if (ang < 0)
        ang += 360;

    ori_pt << scan.points[i].x, scan.points[i].y, scan.points[i].z, 1;
    cur_pt =  prev_pose * ori_pt;

    pcl::PointXYZI pt_, pt_compen, pt_next;
    pt_.x = cur_pt(0,3);
    pt_.y = cur_pt(1,3);
    pt_.z = cur_pt(2,3);
    pt_.intensity = static_cast<float>(ang)/360*255;
    original_frame->points.push_back(pt_);

    Eigen::Matrix4f part_pose;
    part_pose = prev_pose;
    part_pose(0, 3) += del_pose_t(0, 3) * ang;
    part_pose(1, 3) += del_pose_t(1, 3) * ang;
    part_pose(2, 3) += del_pose_t(2, 3) * ang;
    vt = v0.slerp(static_cast<float>(ang)/360., v1);
    part_pose.block(0,0,3,3) = vt.toRotationMatrix();

    Eigen::Matrix4f prev_pose_inv;
    prev_pose_inv.block(0,0,3,3) = prev_pose.block(0,0,3,3).transpose();
    prev_pose_inv.col(3).topRows(3) = -prev_pose_inv.block(0,0,3,3)*prev_pose.col(3).topRows(3);
    cor_pt =  prev_pose_inv * part_pose * ori_pt;
    cor_pt =  part_pose * ori_pt;

    pt_compen.x = cor_pt(0,3);
    pt_compen.y = cor_pt(1,3);
    pt_compen.z = cor_pt(2,3);
    pt_compen.intensity = static_cast<float>(ang)/360*255;
    compen_frame->points.push_back(pt_compen);  


    next_pt =  cur_pose * ori_pt;
    pt_next.x = next_pt(0,3);
    pt_next.y = next_pt(1,3);
    pt_next.z = next_pt(2,3);
    pt_next.intensity = static_cast<float>(ang)/360*255;
    next_frame->points.push_back(pt_next);  
}
original_frame->width    = original_frame->size();
original_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/original_frame.pcd", *original_frame);
compen_frame->width    = compen_frame->size();
compen_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/compen_frame.pcd", *compen_frame);
next_frame->width    = next_frame->size();
next_frame->height   = 1;
pcl::io::savePCDFile<pcl::PointXYZI>("/home/yeonsoo/next_frame.pcd", *next_frame);


예쁘게 잘 나온다.

그럼 이걸 이용해 slam을 한 번 해보자.
raw pointcloud frame이 들어왔을 때,
(0) NDT matching으로 cur_pose를 추정한다.
(1) 이전 프레임의 cur_pose 였던 prev_pose와, (0)에서 얻은 cur_pose를 이용해 위와 같이 프레임을 보상한다.
(2) 보상한 프레임으로 GICP를 돌려 최종 cur_pose를 얻고, 보상한 프레임을 키프레임으로 저장해 맵에 축적한다.

비교를 위해 라이다 보상파트를 넣지 않고
매 프레임별로 NDT -> GICP를 했을 때의 결과를 먼저 확인한다.

왼쪽 그림에서 파란색으로 표시된 부분의 가로등을 보면, 기둥이 두 개로 생긴다.

그 다음은 보상을 해줬을 때.

음.. 기대와 다르게 별 효과가 없다. 오히려 더 더럽게 보인다.

아! 혹시 이게 문제인가?
현재는 라이다 스캔의 시작점이 prev_pose이고 끝점이 cur_pose라고 가정하고 있는데 사실은 prev_pose와 cur_pose의 중점을 스캔의 시작점으로 놓고, 끝점을 cur_pose를 prev_pose<->cur_pose 간의 속도를 이용해 1.5만큼 연장한 지점으로 잡는게 맞나?

그래서 아래와 같이 바꿔보았다.

Eigen::Matrix4f cur_pose, prev_pose, real_prev_pose;
cur_pose = pose_;
prev_pose = getEPose(previous_pose_);

Eigen::Vector3f del_pose_t;
del_pose_t << cur_pose(0,3) - prev_pose(0,3),
            cur_pose(1,3) - prev_pose(1,3),
            cur_pose(2,3) - prev_pose(2,3);



Eigen::Matrix3f prev_R = prev_pose.block(0, 0, 3, 3);
Eigen::Matrix3f cur_R = cur_pose.block(0, 0, 3, 3);
Eigen::Quaternionf v0, v1, vt;
v0 = prev_R;
v1 = cur_R;
vt = v0.slerp(0.5, v1);

real_prev_pose = prev_pose;
real_prev_pose.col(3).topRows(3) += del_pose_t/2;
real_prev_pose.block(0, 0, 3, 3) = vt.toRotationMatrix();

del_pose_t /= 360.;
Eigen::Matrix3f real_prev_R = real_prev_pose.block(0, 0, 3, 3);
v0 = real_prev_R;

Eigen::Matrix4f prev_pose_inv;
prev_pose_inv.block(0,0,3,3) = real_prev_pose.block(0,0,3,3).transpose();
prev_pose_inv.col(3).topRows(3) = -prev_pose_inv.block(0,0,3,3)*real_prev_pose.col(3).topRows(3);

pcl::PointXYZI pt;
for (size_t i = 0; i < input_cloud->points.size(); i++)
{
    Eigen::Matrix<float, 4, 1> ori_pt_f, compen_frame_w, compen_frame_f;
    ori_pt_f << input_cloud->points[i].x, input_cloud->points[i].y, input_cloud->points[i].z, 1;

    int ang = constrainAngle(RAD2DEG(atan2(input_cloud->points[i].y, input_cloud->points[i].x)));
    ang = (90 - ang) % 360;
    if (ang < 0)
        ang += 360;

    Eigen::Matrix4f part_pose; //on world frame
    part_pose = real_prev_pose;
    part_pose(0, 3) += del_pose_t(0, 3) * ang *2;
    part_pose(1, 3) += del_pose_t(1, 3) * ang *2;
    part_pose(2, 3) += del_pose_t(2, 3) * ang *2;

    vt = v0.slerp(static_cast<float>(ang)/360.*2, v1);
    part_pose.block(0,0,3,3) = vt.toRotationMatrix();

    compen_frame_w = part_pose * ori_pt_f; //on world frame
    compen_frame_f = prev_pose_inv * compen_frame_w; //back to prev frame

    pt.x = compen_frame_f(0);
    pt.y = compen_frame_f(1);
    pt.z = compen_frame_f(2);
    pt.intensity = input_cloud->points[i].intensity;

    output_cloud->points.push_back(pt);
}

여기 real_prev_pose가 prev_pose와 cur_pose의 중점이고, real_prev_pose와 prev_pose 사이의 포즈 변화량에 ang*2를 반영해줌으로써 구현하였다.
결과는 첫번째 시도와 비슷하게 안좋음..
그냥 imu 쓰자.

profile
to be enterprising

0개의 댓글