본문 바로가기

FOSCAR-(Autonomous Driving)/대학생 창작 모빌리티 경진대회

[LiDAR팀] 라이다 스터디 Lesson 2

반응형

Lidar 교육은 Udacity에서 올려주는 강의(ls1~4) 및 깃허브 코드로 스터디하는 내용을 정리할거다.

(출처:https://github.com/udacity/SFND_Lidar_Obstacle_Detection/https://www.youtube.com/watch?v=f4bx0tzpBBU)

Lesson 2: Point Cloud Segementation

이제, 우리는 주어진 상황에서의 장애물들을 찾아내고 싶다. 하지만, 주어진 도로 상황에서 어떤 물체들은 우리가 보고 싶은 장애물들이 아니다. 예를 들자면, 도로위의 자유공간들이 해당된다. 만약 도로가 평평하면 도로가 아닌 지점에서 도로의 지점들을 골라내는 과정은 매우 간단하다. 이를 위해 우리는 Planar Segementation(평면분할)이라는 방법을 사용할것이다.(RANSAC(random sample consensus) 알고리즘이 사용된다)

(지난 Lesson 1 마지막에서 언급한대로,

도로와 관찰하는 차량의 지붕에 찍히는 Point들을 없애줬다.)

첫 째로, 할것은 processPointClouds.cpp 객체를 생성하는 것이다. 이 객체는 라이다 객체를 처리하는데 사용되는 모든 방법들을 포함하고 있다. 이 process 객체에는 PCD 파일들을 로드하고 저장해주는 방법들도 갖고 있다.

방법: environment.cpp 파일의 simpleHighway 함수 안에 processPointClouds 객체를 생성한다. (힙 혹은 스택 위에 하면 된다.) 프로세서는 pcl::PointXYZ의 pointcloud 타입을 이용해야 된다.

그 다음으로는 PCL로 평면을 분할하는 것을 해볼거다. processPointsClouds.cpp에 segmentPlane 함수를 선언해줄 것이다. 이 함수의 윗 부분에 PointT 라는 템플릿 변수를 확인 할 수 있다. 이 변수를 이용해서 모든 형태의 point cloud를 대신해줄 수 있다.

segmentPlane 함수

이 함수는 pointcloud, maxiterations(최대반복),distance tolerance(거리공차)를 인수로 받아들인다. 분할은 반복작업을 거친다. 더 많은 반복 횟수는 더 좋은 결과를 추출하지만 더 오랜 시간이 걸린다는 단점이 있다. 분할 알고리즘은 평면을 점에 맞추고 거리공차(distance tolerance)를 사용해 어떤 점들이 해당 평면에 해당하는지 판단한다. 더 큰 공차는 평면에 더 많은 점을 포함시킨다.

위의 코드의 리턴 타입을 봐보자. SegmentPlane함수는 point cloud 포인터 타입을 포함한 std::pair을 반환할 것이다. 이 pair 객체는 장애물 point cloud와 도로 point cloud를 분할한 결과값을 저장하는데 사용할 것이다. 이런 방식은 추후에 장애물과 도로 두개의 point cloud를 둘다 시각화 할 수 있고, 그로 인해 결과를 분석할 수 있다는 장점이 있다.

segmentPlane 함수에서 가장 먼저 보이는 것은 타이머다. 이것은 함수를 실행하는데 걸리는 시간을 측정 할 수 있다는 장점이 있다. 만약, 분할하는 과정이 정말 오래 걸린다면, 이 함수는 실시간으로 실행시켜야하는 자율주행 자동차에 적용하는데는 무리가 있다.

이제, 위 함수를 pcl의 분할 객체를 사용해서 채워보자.

코드에 대한 설명을 하자면, 미리 말했듯이 pcl::SACSegementation에 point T로 적어준다. (포인트 프로세서가 pointT라는 템플릿을 정의하고 있기 때문). 그 다음에 나오는 pointIndices 는 추후 pointcloud를 조각들로 분리할때 사용된다.

이후 나오는 Ransac은 뒤에서 자세하게 알아보자.(maxIterations 랑 distanceThreshold는 Ransac에서 중요한 인자들이다.)

위쪽에서 inliers를 생성했고, 밑에 seg.segement(*inliers,*coefficients); 로 넘겨서 처리를 하는 과정을 진행한다.

결국, 위의 코드들을 실행하면 우리의 Cloud를 분할할 준비가 다 된 것이다.

이제 직접 point cloud를 분리해볼 것이다. 이전 실습에서 inliers값들을 구했다. 이제는 이런 inliers를 활용해서 평면과 장애물의 point cloud를 생성할거다.

processPointCloud에서 SeperateClouds함수를 불러오면 point clouds들을 분리할 수 있다. 이 함수를 SegmentPlane 안에 미리 계산한 inliers와 input cloud와 함께 이용하면 된다. SeperateClouds 함수 안에, 두개의 새로운 point cloud 포인터를 생성한다.(한개는 장애물용, 나머지는 도로용). Inlier 인덱스를 루프하고 해당하는 inlier 점을 평면 cloud의 포인트 벡터로 밀어넣어서 inliers를 평면 cloud에 추가할 수 있다.

장애물 cloud를 생성하기 위해 PCL를 사용하는 방법으로는 extract 객체를 사용하는 것이다. 평면 cloud를 input cloud로 부터 빼낸다. 이제, 우리는 std::pair를 새롭게 만든 장애물, 평면 cloud와 반환할 수 있다.

이제 environment.cpp파일에서 pointProcessor 함수를 input cloud에 부를 수 있고, 두 개로 분할된 cloud를 서로 다른 색으로 만들 수 있다.

위의 코드에서는, 0.2meter의 거리공차를 갖고 100번의 반복실행을 한다. 우리가 작업하는 환경은 매우 간단해 100번의 실행은 필요 이상이다. 반복횟수를 바꿔가면서 분할하는 데 걸리는 시간 측정을 할 수도 있다. 두 개의 cloud를 만들기 전에 Lesson1 에서 만든 input cloud 렌더링을 꺼야 된다. (cloud들이 겹치게되고 분할된 것들을 구별하기에 어렵다). 실습에서는 장애물 cloud는 빨간색으로 만들어지고, 도로 cloud는 초록색으로 만들어진다.

processPointClouds.cpp

environment.cpp

위에서 설명한 대로 procesPointClouds에 정의해주고, environment.cpp에서 정의 해준 함수들에 적당한 값들을 넣어서 실행 시켜보자.

실행시킨 ./environment 의 모습

이제 마지막으로 RANSAC에 대해 알아보도록 하자.

RANSAC 버전의 한 유형은 적합한 점의 최소 부분집합을 선택한다. 예를들어 선이면, 두개의 점일 것이고 평면이면 세개의 점일 것이다. 그 다음으로, 남아있는 모든 점들에 대한 거리 계산을 반복하며 inliers의 수가 계산된다. 모델에서의 적정 거리(distance tolerance)에 있는 점들은 inliers로 계산된다. 가장 많은 수의 inlier를 가진 반복상황이 가장 좋은 모델이다. 추후 quiz로 풀어볼 것이다.

왼쪽과 같인 적정 거리에 있는 점들은 inliers로 계산되고

초록색으로 그러져 있는 선에 있는 점들이 inliers 이다.

평면에 초록색 직선 이외의 다양한 직선을 그을 수 있지만,

초록색 직선이 가장 많은 inliers를 포함하기에, 가장 좋은 모델이라 판단 할 수 있다.

더 자세한 내용은 아래의 주소로 가서 확인하면 될 것 같다.

(출처:https://darkpgmr.tistory.com/61)

이제, 우리가 직접 RANSAC을 구현해보자.

ransac2d.cpp에 있는 Ransac함수를 채워 넣어보자. 함수는 point cloud, max iterations, distance tolerance값들을 인수로 가진다. point cloud는 실제로 pcl:PointXYZ 형식이지만, 우리는 평면에서의 ransac을 구현할거기 때문에 z값은 0으로 설정하면 된다.

2D point cloud data에서 직선의 일반적인 방정식은 Ax + By + C = 0 이다.

두개의 점, (x1,y1), (x2,y2)을 이은 직선의 방정식은 (y1-y2)x + (x2-x1)y + (x1*y2-x2*y1) = 0 이다.

직선의 방정식을 구한 다음, 모든 점들에 대해 직선과의 거리를 계산하며 그 점이 inliers인지 판단하는 과정을 반복한다. 가장 많은 inliers를 가진 직선이 가장 좋은 모델이 되는 것이다.

여기에 대한 RANSAC 알고리즘의 구현 방법에 따라 구현 해주면 아래와 같이 나온다.

 

여기서 왼쪽과 오른쪽을 비교해보면 당근 왼쪽이 더 좋은 모델이다.

여기서 차이가 나는 부분의 코드를 비교해보면

왼쪽: std::unordered_set<int> inliers = Ransac(cloud,10,1.0)

오른쪽: std::unordered_set<int> inliers = Ransac(cloud,1,1.0)

왼쪽은 iterations 값을 100번(극단적)으로 설정해줬고, 오른쪽은 iterations 값은 1번으로 설정해줬다.

결국 iterations의 값이 늘수록 더 정확하고 좋은 모델이 나오는 것을 알 수 있다.

이제 Lesson 2의 마지막 실습으로, 2d 에서 구현한 RANSAC을 3d 환경에서 구현해보자.

위에서의 내용들을 바탕으로 코드르 짜보자.

아까 2D에서 사용했던 코드에서의 내용을 3D 내용으로 바꿔줘야된다.

방법> pcl::PointCloud<pcl::pointXYZ>::Ptrcloud = CreateData3D(); 를 사용해주면 된다.

이후, 2D에서의 거리 계산을 했던 부분, 즉 Ransac 함수를 주석 처리해주고 // 3D에서의 거리 계산할 수 있는 RansacPlane 함수를 정의해주고, main함수 내에서 Ransac 대신 RansacPlane을 불러와준다.

방법: std::unordered_set<int> inliers = RansacPlane(cloud,10,1.0); 로 변경!

최종적으로 make를 해주고 ./quizRansac을 쳐주면 위와 같은 모양으로 출력된다. (제대로 된건지는 잘 모르겠다..)

이것으로 Lesson 2: Point Cloud Segmentation 블로깅을 마무리해보겠다! 

반응형