2022년 2월 7일 월요일

사진측량 및 라이다 기반 3차원 포인트 클라우드 처리 라이브러리 소개

이 글은 최근 사용 사례가 크게 많아지고 있는 무인자율로봇의 비전, 사진측량이나 라이다 스캔을 통해 얻은 3차원 포인트 클라우드 데이터를 처리하는 방법을 간단히 설명한다.

3D 점군 응용 분야 예(Geospatial World, 2018.11, Surveying and Mapping - From Site to Structure, Realtime 3D Drone Localization with LiDAR)

이 분야는 3차원 계산 기하학, 수치해석 등 다양한 기술이 사용된다. 이와 관련된 수많은 라이브러리가 있으나, 이 중에 가장 많이 사용되고 있는 PCL(Point Cloud Library)을 중심으로 내용을 살펴본다. PCL은 3차원 포인트 클라우드 데이터를 다루고, 처리하는 데 필요한 일반적인 자료구조, 알고리즘을 제공한다. 당연히, 특수한 데이터 유형이나 알고리즘은 PCL을 확장해 별도 개발한다. 이를 위해서 관련 데이터 및 클래스 구조, 모듈 아키텍처 분석 내용, 개발방법을 공유한다.

이 글은 3차원 계산 기하학, 수치해석, 선형대수, 컴퓨터 그래픽스 등 내용은 자세히 설명하지 않는다. 이 라이브러리를 다루기 위해서는 별도로 이와 관련된 내용을 공부할 필요가 있다. 관련 용어 및 내용 등은 아래 링크를 참고한다.
이 글에서 개발환경은 아래의 리눅스 우분투 환경을 기본으로 한다. 
윈도우와 같은 다른 운영체제에서도 개발 절차는 비슷하며, 관련 메뉴얼을 제공하므로 참고한다.

PCL 소개
PCL은 대규모 오픈소스 프로젝트로, 2차원 및 3차원 이미지와 점군 처리를 지원하는 라이브러리이다. 이 라이브러리의 프레임웍은 필터링, 특징 예측, 표면 구성, 정합, 모델 피팅 및 세그먼테이션(segmentation)을 위한 최신 기술을 포함한다. 이러한 알고리즘은 다양한 곳에 사용될 수 있는 데, 예를 들면, 노이즈 데이터에서 아웃라이어(outliers) 필터링, 3차원 점군 스티치(stitch), 세그먼테이션, 키포인트 추출, 특징 기술자(descriptors) 계산, 표면 재구성 등에 사용된다. 

PCL은 BSD 라이센스로 개발되었고, 상업 및 연구적 목적에서 무료이다. 크로스 플랫폼 개발을 지원하여, 리눅스, 맥, 윈도우, 안드로이드 등에 배포될 수 있다. PCL은 잘 모듈화되어 있어 배포에 용이하다. 
PCL 모듈 구조

PCL은 유명한 오픈소스 프로젝트를 다수 지원하고 있다. 예를 들어, ROS(Robot Operating System), PDAL 등은 이 라이브러리를 직접 사용한다.

PCL은 X, Y, Z, R, G, B 등으로 구성된 3차원 점을 처리하는 데 특화되어 있다. 2011년 부터 개발된 PCL은 현재 1.12 버전까지 릴리즈되어있다.

다양한 3차원 점군 데이터 응용 기술

3차원 점군 처리에 가장 많이 활용되는 필터링을 확인해보자. 3차원 점군에서 필터 처리는 매우 중요하다. 필터는 노이즈 제거, LoD(Level of Detail) 처리, 법선 및 곡률 계산 등을 포함하며, 점군에서 어떤 처리를 통해 얻은 결과물의 품질을 직접적으로 좌우한다. 
Point Index 처리

스캔 데이터는 일반적으로 많은 노이즈를 가진다. 이런 데이터는 3차원 특징점을 계산하기 어렵게 한다. 이러한 몇몇 아웃라이어는 필터링 기법으로 삭제가 가능하다. 점군에 속한 각 점들은 이웃점을 가지며, 이와 관련된 평균 거리 등을 계산할 수 있다. 이에 대한 분포는 평균, 분산과 함께 가우시안 분포를 가진다. 이런 특징을 이용해 통계적으로 필터링을 할 수 있다.

필터링된 점군은 각 점마다 특징(피처, Feature)을 계산할 수 있다. 피쳐 모듈은 3차원 특징 예측을 위한 자료 구조와 알고리즘을 포함한다. 공간을 스캔한 점군은 점 주변에 기하학적인 패턴을 가진다. 자료 공간을 검색할 때 사용하는 k-이웃검색(k-neighborhood)를 통해, 특정 주변 점군을 선택할 수 있을 것이다. 이 점군들은 특정 표면의 측정 결과일 것이며, 표면에 대한 곡률과 법선 정보가 내포된 것이다. 

질의점 p에서 획득한 이웃점 k개를 획득할 때는 대용량 점군을 그대로 사용할 수 없다. 계산 성능을 개선하기 위해 공간인덱싱 기법 중 하나인 옥트리(octree), kD-tree를 사용한다. 그러므로, 이웃점을 질의하는 연산은 그리 간단한 문제가 아니다. 보통 이웃점 검색에 사용되는 파라메터는 반경 r혹은 이웃점 갯수 k이다. 

획득된 이웃점들을 이용해, 법선 벡터를 구하기 위해서, 고유벡터(eigen vector)를 계산해야 한다. 이를 위해, 고유값분해(digen decomposition)을 수행한다. 법선벡터는 이렇게 계산된 고유벡터 중 가장 작은 고유치를 가진 벡터가 된다. 
법선 벡터

각 3차원 점에 대한 법선벡터 n을 획득하였다면, 각 점에 대한 곡률벡터는 이웃점의 법선벡터로 계산할 수 있다. 여기서 획득한 값들은 곡면이나 객체를 분류할 때 유용하게 사용될 수 있다.
3차원 점군 곡률(곡률 높은 값=녹색, 낮은 값=적색. 모서리=청색)

PCL 설치 및 기본 코드 분석
PCL 최신버전을 설치하고, 기본 코드 중 하나인 특징점을 계산하는 PCL코드를 살펴보자.
우선 다음 링크를 참고해, PCL 코드를 다운로드 한 후 빌드한다. 빌드 중 다양한 이유로 에러가 발생할 수 있다. 하나씩 해결해 나가며 빌드해 본다.
빌드를 성공했다면, PCL루트 폴더에서 다음 명령을 입력해 법선계산 프로그램 코드를 찾아본다.
find -name '*normal*.cpp'


해당 코드를 열어보면 다음과 같다. 

이 중에 compute 함수를 찾아본다. 함수의 핵심 루틴은 다음과 같다.
 
    NormalEstimation<PointXYZ, Normal> ne;
    ne.setInputCloud (xyz);
    ne.setSearchMethod (search::KdTree<PointXYZ>::Ptr (new search::KdTree<PointXYZ>));
    ne.setKSearch (k);
    ne.setRadiusSearch (radius);
    ne.compute (normals);

각 코드의 의미는 다음과 같다.
1. 법선벡터 계산 템플릿 클래스 생성
2. 3차원 점군 설정
3. 이웃점 탐색 공간인덱싱 기법 설정
4. 이웃점 탐색 개수 설정
5. 이웃점 탐색 반경 설정
6. 법선벡터 계산

빌드된 법선벡터 계산 프로그램을 실행해 보자. 

참고로, 사용된 점군은 라이다에서 얻은 점군을 SLAM처리하여 얻은 대학교 캠퍼스(UNF) 데이터이다. 
UNF 캠퍼스 건물 스캔 데이터(Google Earth)

pcl_viewer를 실행해 법선벡터 값을 아래와 같이 확인해 본다. 곡률이 작으면 적색, 많으면 녹색이다. 이 값은 앞서 언급한 파라메터에 따라 달라질 수 있다.
곡률 계산 결과(PCL)

다음 명령을 이용해 las 데이터로 변환한 후 CloudCompare에서 확인해 본다.
for f in *.pcd; do pcl_convert_pcd_ascii_binary $f ./ascii/$f 0; done
for f in *.pcd; do pdal translate ./$f $f.las; done

곡률 계산 결과(CloudCompare)

PCL 패키지 모듈 분석
PCL모듈을 좀 더 명확히 이해하기 위해, Make파일을 통해 구조를 분석해 본다.

PCL은 매우 모듈화된 구조를 제공한다. 예를 들어, 가시화 모듈인 pcl_visualization을 이용해, 다양한 목적의 3차원 점군 데이터 뷰어를 개발할 수 있다. 다음 그림과 같이, 해당 모듈 폴더에서 소스파일을 갱신한 후 메이크해본다.
touch pcl_visualizer.cpp 
cd ..
cd build
make
Make 결과

이 결과, lib 폴더 아래에 libpcl_visualization.so 라이브러리가 빌드되고, pcl_outofcore_viewer, pcl_oni_viewer, pcl_image_grabber_saver, pcl_vlp_viewer, pcl_pcd_image_viewer, pcl_viewer, pcl_octree_viewer 도구 등이 이 라이브러리를 이용하는 것을 확인할 수 있다.

소스파일 의존관계를 좀 더 깊게 확인해 보기 위해, make 파일을 분석해 본다. PCL은 CMake를 사용한다. CMake 파일은 아래와 같이 구성된다(참고. graphviz). 행렬수치해석 Eigen 등 사용하는 패키지를 포함하고, 루트 폴더 내 모듈 폴더들을 획득해, add_subdirectory 명령으로 하위 CMakeFile을 추가하고 있음을 확인할 수 있다.
...
# Eigen (required)
find_package(Eigen 3.1 REQUIRED)
include_directories(SYSTEM ${EIGEN_INCLUDE_DIRS})

# Boost (required)
include("${PCL_SOURCE_DIR}/cmake/pcl_find_boost.cmake")
...
collect_subproject_directory_names("${PCL_SOURCE_DIR}" "CMakeLists.txt" PCL_MODULES_NAMES PCL_MODULES_DIRS doc)

foreach(subdir ${PCL_MODULES_DIRS})
  add_subdirectory("${PCL_SOURCE_DIR}/${subdir}")
endforeach()

하위 폴더는 모듈을 빌드하는 CMake 파일이 존재한다. Common 모듈의 경우, 아래와 같다.
set(SUBSYS_NAME common)
set(SUBSYS_DESC "Point cloud common library")
set(SUBSYS_DEPS)

set(build TRUE)
PCL_SUBSYS_OPTION(build "${SUBSYS_NAME}" "${SUBSYS_DESC}" ON)
PCL_SUBSYS_DEPEND(build "${SUBSYS_NAME}" DEPS ${SUBSYS_DEPS} EXT_DEPS eigen boost)

set(range_image_incs
  include/pcl/range_image/bearing_angle_image.h
  include/pcl/range_image/range_image.h
  include/pcl/range_image/range_image_planar.h
  include/pcl/range_image/range_image_spherical.h
)

set(range_image_incs_impl
  include/pcl/range_image/impl/range_image.hpp
  include/pcl/range_image/impl/range_image_planar.hpp
  include/pcl/range_image/impl/range_image_spherical.hpp
)

set(range_image_srcs
  src/bearing_angle_image.cpp
  src/range_image.cpp
  src/range_image_planar.cpp
)

set(srcs
  src/point_types.cpp
  src/pcl_base.cpp
  src/PCLPointCloud2.cpp
  src/io.cpp
...
set(LIB_NAME "pcl_${SUBSYS_NAME}")
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/include")
PCL_ADD_LIBRARY(${LIB_NAME} COMPONENT ${SUBSYS_NAME} SOURCES ${srcs} ${kissfft_srcs} ${incs} ${common_incs} ${impl_incs} ${tools_incs} ${kissfft_incs} ${common_incs_impl} ${range_image_incs} ${range_image_incs_impl})

PCL_MAKE_PKGCONFIG(${LIB_NAME} COMPONENT ${SUBSYS_NAME} DESC ${SUBSYS_DESC})
...

생성된 make 파일의 주요 부분은 다음과 같다. 적색 표시 부분은 각 폴더의 Make파일을 실행한 결과 생성되는 라이브러리 패키지이다.
...
# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/ktw/Projects/pcl-1.12

# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/ktw/Projects/pcl-1.12/build
...
# Special rule for the target list_install_components
list_install_components:
@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Available install components are: \"pcl_2d\" \"pcl_common\" \"pcl_features\" \"pcl_filters\" \"pcl_geometry\" \"pcl_io\" \"pcl_kdtree\" \"pcl_keypoints\" \"pcl_ml\" \"pcl_octree\" \"pcl_outofcore\" \"pcl_people\" \"pcl_recognition\" \"pcl_registration\" \"pcl_sample_consensus\" \"pcl_search\" \"pcl_segmentation\" \"pcl_stereo\" \"pcl_surface\" \"pcl_tools\" \"pcl_tracking\" \"pcl_visualization\" \"pclconfig\""
.PHONY : list_install_components
# The main clean target
clean/fast: clean
# Build rule for target.
uninstall: cmake_check_build_system
$(MAKE) -f CMakeFiles/Makefile2 uninstall
.PHONY : uninstall
...
# Build rule for target.
pcl_common: cmake_check_build_system
$(MAKE) -f CMakeFiles/Makefile2 pcl_common
.PHONY : pcl_common
...
# Build rule for target.
pcl_sample_consensus: cmake_check_build_system
$(MAKE) -f CMakeFiles/Makefile2 pcl_sample_consensus
.PHONY : pcl_sample_consensus

# fast build rule for target.
pcl_sample_consensus/fast:
$(MAKE) -f sample_consensus/CMakeFiles/pcl_sample_consensus.dir/build.make sample_consensus/CMakeFiles/pcl_sample_consensus.dir/build
.PHONY : pcl_sample_consensus/fast

PCL ROOT 폴더 구조를 보면, 각 모듈이 위치한 폴더들을 확인할 수 있다. 각 모듈은 include, src 소스파일 폴더로 구성된다. 

유지관리하기 편리하게, 모듈 패키지 별로 폴더를 구성했음을 확인할 수 있다.
패키지 다이어그램(UML)

모듈 패키지에서 제공하는 함수 역할을 다음과 같다.
  • cuda: CUDA 가속 계산 함수
  • features: 특징 계산
  • filters: 점군 필터링
  • geometry: 기하 형상 자료 구조 제공. polygon, mesh 등. 
  • gpu: GPU 가속 계산 함수
  • io: 센서 및 파일 데이터 입출력 함수
  • kdtree: kD-tree 공간 인덱싱 함수 
  • keypoints: 키포인트 계산 함수
  • ml: SVM(Support Vector Machine)등 기계학습 지원 함수
  • octree: Octree 공간 인덱싱 함수
  • ontoforce: 디스크 기반 공간 인덱싱 함수
  • recognition: 객체 인식 함수
  • registration: 3차원 정합 함수
  • sample_consensus: RANSAC(Random sample consensus) 처리 함수
  • search: 점군 검색 함수
  • segmentation: 점군 세그먼테이션 함수
  • simulation: 3차원 점군 시뮬레이션
  • stereo: 스테레오 이미지 생성 함수
  • surface: 표면 재구성 함수
  • test: 테스트 실행 함수
  • tools: PCL 도구
  • tracking: 3차원 점군 추적 함수
  • visualization: 3차원 점군 시각화 함수

이 중에 주요 패키지인 common, feature를 중심으로 코드를 역설계해본다. 핵심적인 코드는 common 패키지의 pcl_base, PCLHeader.h, point_cloud_h, feature의 feature.h이다. 
주요 코드

PCL 아키텍처는 처음부터 재활용성, 확장성과 유지보수성을 크게 염두해두고, 객체지향 소프트웨어 공학적으로 개발되었다. 이런 이유로, 새로운 모듈을 계속 확장하면서도, 복잡도를 줄일 수 있었다. 예를 들어, 3차원 점군은 다음과 같이 매우 다양한 유형을 가질 수 있다. 
  • 정수형 X, Y, Z
  • 실수형 X, Y, Z
  • RGBD
  • 반사강도
  • 사용자 데이터 등
이를 모두 표현하면서, 개발된 다양한 입출력 포맷, 알고리즘은 코드 수정없이 재활용할 수 있도록, 객체지향의 템프릿 클래스를 사용한다. 
  template <typename PointT>
  class PCL_EXPORTS PointCloud

핵심적인 클래스인 점군 자료구조, 입출력, 특징 계산과 관련된 아키텍처는 다음과 같다.

역설계된 PCL 주요 아키텍처(UML)

PCL은 이렇게 신뢰성, 확장성이 매우 뛰어나, ROS등 다양한 프레임웍에 기본으로 사용된다.
PCL 사용 회사

소프트웨어 개발 
PCL을 이용한 간단한 개발 방법을 살펴보자. 우선, 프로젝트 파일을 담을 폴더를 생성하고, PCD파일을 읽을 코드를 작성한다.
mkdir pcd_io
cd pcd_io

pcd_io.cpp 파일 코드는 다음과 같다. 
#include <stdio.h>
#include <iostream>
#include <pcl/PCLPointCloud2.h>
#include <pcl/io/pcd_io.h>
#include <pcl/features/normal_3d.h>

using namespace pcl;
using namespace pcl::io;
using namespace pcl::console;

int main(int argc, char** argv)
{
  // Load in the point cloud
  PointCloud<PointXYZ>::Ptr cloud_in (new PointCloud<PointXYZ> ());
  if (loadPCDFile (argv[1], *cloud_in) != 0)
  {
    print_error ("Could not load input file %s\n", argv[1]);
    return (-1);
  }
  std::cout << "Loaded " << cloud_in->width * cloud_in->height << " fields: " << std::endl;

  for (const auto& point: *cloud_in)
    std::cout << "    " << point.x << " "    << point.y << " "    << point.z << std::endl;  
return 0;
}

아래와 같이 CMakeFile.txt를 작성한다.
# set default env varibles
cmake_minimum_required(VERSION 3.16.1)

set(SUBSYS_NAME pcd_io)
set(SUBSYS_DESC "Useful PCD io command")
set(DEFAULT ON)
set(REASON "")
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
set(PCL_ROOT "/home/ktw/Projects/pcl-1.12")

message("BIN PATH = " ${CMAKE_BINARY_DIR})
message("PCL ROOT = " ${PCL_ROOT}/common/include)

# set project
project(pcd_io)

# set include and library
include_directories(PUBLIC ${PCL_ROOT}/common/include ${PCL_ROOT}/features/include ${PCL_ROOT}/filters/include ${PCL_ROOT}/geometry/include ${PCL_ROOT}/io/include ${PCL_ROOT}/kdtree/include ${PCL_ROOT}/keypoints/include ${PCL_ROOT}/octree/include ${PCL_ROOT}/recognition/include ${PCL_ROOT}/registration/include ${PCL_ROOT}/sample_consensus/include ${PCL_ROOT}/search/include ${PCL_ROOT}/segmentation/include ${PCL_ROOT}/visualization/include ${PCL_ROOT}/build/include /usr/include/eigen3)

link_directories(${PCL_ROOT}/build/lib/)
link_libraries(libpcl_common.so libpcl_features.so libpcl_io.so libpcl_kdtree.so libpcl_common.so libpcl_ml.so libpcl_keypoints.so libpcl_octree.so libpcl_outofcore.so libpcl_recognition.so libpcl_registration.so libpcl_sample_consensus.so libpcl_segmentation.so libpcl_search.so libpcl_surface.so libpcl_visualization.so)

# program
add_executable(pcd_io pcd_io.cpp)

다음과 같이 빌드한다.
mkdir build
cd build
cmake ..
make

그럼, 다음 같이 bin  폴더 아래 실행파일이 생성된다. 스캔된 PCD 파일을 테스트해 본다.

LDD명령을 사용해, 실행파일의 라이브러리 의존성을 체크해 본다. 제대로 링크되었음을 확인할 수 있다.
ldd ./pcd_density 

참고로, 아래와 같이 cmake --graphviz 옵션을 사용하면, 사용된 라이브러리 의존성을 그래픽으로 확인할 수 있다.
cmake .. --graphviz=dep.dot
dot -Tpng dep.dot -o dep.png

이런 방식으로 필요한 패키지를 사용해, 원하는 소프트웨어를 개발할 수 있다.

레퍼런스
좀 더 자세한 내용은 아래 링크를 참고한다.
PCL의 상세한 내용은 아래 문서를 살펴본다.
PCL 튜터리얼은 아래 문서를 살펴본다.
참고: pwndbg
pwndbg는 disassemble 기반 역설계 도구이다. 실행 파일을 어셈블리 기반으로 분석할 수 있다.

참고: QGIS 활용 SHP 파일 정보 확인
가끔 SHP 파일을 확인할 필요가 있다. 이 경우, SHP파일 렌더링을 위해, QGIS를 설치한다.
SHP파일을 QGIS로 드래그&드롭 한다.

댓글 없음:

댓글 쓰기