2019년 11월 17일 일요일

2차원 이미지에서 3차원 모델 자동 생성하는 SFM기반 OpenMVG

이 글은 드론, 로버 등에서 촬영한 2차원 이미지에서 3차원 모델인 point cloud를 생성하는 SFM기반 OpenMVG 을 간략히 다루어 본다. 이 기술은 이미 Pix4D, ContextCapture등 상업용 프로그램에서 널리 사용되고 있다. 이 프로그램들은 촬영된 2차원 이미지들에서 3차원 포인트 클라우드(점군)을 자동 생성한다.
SFM기반 3차원 모델 생성(OpenMVG)

SFM은 Structure From Motion의 약자로, 2차원으로 촬영한 이미지의 모션정보를 이용해, 촬영된 이미지의 카메라 위치와 방향을 역추적한 후, 이미지들과 카메라들의 관계를 구조화하는 알고리즘이다.

SFM을 이용해, 각 촬영 이미지의 고유한 특징점(feature point)를 얻고, 각 촬영 장면마다 특징점들과의 관계를 서로 매칭하고 계산해 카메라의 위치를 얻을 수 있다.

SFM을 사용하는 무료 프로그램 중 하나는 VisualSFM이다. 앞서 언급한 기능을 지원한다. 이 글에서는 직접 API(Application Program Interface)수준에서 프로그램을 만들고, 알고리즘을 리비전할 수 있는 OpenMVG를 사용해 본다. 참고로, 이와 유사한 Mapillary에서 개발된 OpenSFM도 있으나, 복잡한 디펜던시를 고려해 이 글에서는 언급하지 않는다. OpenSFM을 사용하려면 Docker 방식으로 이용하길 권장한다.
OpenSFM

OpenMVG 빌드 및 테스트를 위해 다음 링크를 방문해 관련 명령을 그대로 따라한다. 참고로, 개발 환경은 우분투 18.04에서 진행하였다.
github에서 소스코드를 다운로드 받고, 패키지를 설치한 후, 빌드한다.
git clone --recursive https://github.com/openMVG/openMVG.git
sudo apt-get install libpng-dev libjpeg-dev libtiff-dev libxxf86vm1 libxxf86vm-dev libxi-dev libxrandr-dev
sudo apt-get install graphviz
mkdir openMVG_Build && cd openMVG_Build
..
cmake -DCMAKE_BUILD_TYPE=RELEASE -DOpenMVG_BUILD_TESTS=ON ../openMVG/src/
cmake --build . --target install
make test

단위 테스트를 실행한 후, 다음과 같은 화면을 확인하면 성공한 것이다.
ctest --output-on-failure -j


각 단위 테스트는 SFM, SIFT (Scale Invariant Feature Transform) 등 예제를 포함한 API 호출 및 프로그램 실행 성공 결과를 보여준다.

참고로, OpenMVG는 다음 명령을 이용해 도커에서 실행할 수도 있다.
git clone --recursive https://github.com/openMVG/openMVG.git
docker build . -t openmvg
docker run -it openmvg /bin/sh

MVG 테스트해보기
테스트를 위해 다음 폴더에서 데모를 실행해 본다.
openMVG_Build/software/SfM/tutorial_demo.py

이 결과 다음과 같이 유럽식 건물을 180도 수평으로 이동하며 사진촬영한 이미지 11개를 다운로드 받아, SFM을 통해 카메라 위치를 생성하고, 특징점의 3차원 위치를 계산해 포인트 클라우드를 생성한다. 
입력 사진
SFM 계산 과정

생성된 결과는 다음과 같이 특정 폴더에 PLY파일로 저장된다. 기타, 계산된 특징점 및 카메라 정보가 함께 저장된다. 
저장된 포인트 클라우드 및 카메라 등 메타 정보
SFM 계산 결과

CloudCompare 및 MeshLab으로 생성된 PLY파일을 확인하면 다음과 같다.
MeshLab / CloudCompare

이제 다음과 같이 실행해 본다.
python SfM_SequentialPipeline.py ./ImageDataset_SceauxCastle/images ./ImageDataset_SceauxCastle/Castle_Incremental_Reconstruction

그럼, 다음과 같은 계산 결과를 확인할 수 있다. 

생성된 SFM 포인트 클라우드 결과물은 다음과 같다. Sparse Point Cloud로 생성된 결과를 확인할 수 있다.



밀집 포인트 클라우드 생성
밀집된 포인트 클라우드(dense point cloud reconstruction)를 생성하려면 OpenMVS를 사용한다. 관련해 다음 링크를 참고한다.
OpenMVS를 빌드하기 위해, 다음과 같이 소스코드를 다운로드 받고, 관련 패키지를 설치한 후, 빌드한다.
#Prepare and empty machine for building:
sudo apt-get update -qq && sudo apt-get install -qq
sudo apt-get -y install build-essential git mercurial cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev libxmu-dev libxi-dev
main_path=`pwd`

#Eigen (Required)
hg clone https://bitbucket.org/eigen/eigen#3.2
mkdir eigen_build && cd eigen_build
cmake . ../eigen
make && sudo make install
cd ..

#Boost (Required)
sudo apt-get -y install libboost-iostreams-dev libboost-program-options-dev libboost-system-dev libboost-serialization-dev

#OpenCV (Required)
sudo apt-get -y install libopencv-dev

#CGAL (Required)
sudo apt-get -y install libcgal-dev libcgal-qt5-dev

#VCGLib (Required)
git clone https://github.com/cdcseacave/VCG.git vcglib

#Ceres (Required)
sudo apt-get -y install libatlas-base-dev libsuitesparse-dev
git clone https://ceres-solver.googlesource.com/ceres-solver ceres-solver
mkdir ceres_build && cd ceres_build
cmake . ../ceres-solver/ -DMINIGLOG=ON -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF
make -j2 && sudo make install
cd ..

#GLFW3 (Optional)
sudo apt-get -y install freeglut3-dev libglew-dev libglfw3-dev

#OpenMVS. 다음 main_path는 현재 폴더가 설정되므로, 빌드 에러가 날 경우 적절히 수정해야 함.
git clone https://github.com/cdcseacave/openMVS.git openMVS
mkdir openMVS_build && cd openMVS_build
cmake . ../openMVS -DCMAKE_BUILD_TYPE=Release -DVCG_ROOT="$main_path/vcglib"

#If you want to use OpenMVS as shared library, add to the CMake command:
-DBUILD_SHARED_LIBS=ON

#Install OpenMVS library (optional):
make -j2 && sudo make install

이제 Sample 폴더를 다운로드 받는다.
git clone https://github.com/cdcseacave/openMVS_sample

이 링크를 참고하여 다음 명령을 실행해, 입력된 이미지가 처리된 밀집 포인트 클라우드를 생성한다.
python MvgMvs_Pipeline.py ./images ./output_mvgmvs

참고로, 그냥 MvgMvs_Pipeline.py 소스를 실행하면, 문법 에러가 발생한다. 문법 에러를 수정한 후 다음과 같이 경로등이 포함된 코드를 적절히 수정한다.

# Indicate the openMVG and openMVS binary directories
OPENMVG_BIN = "/usr/local/bin/"
OPENMVS_BIN = "/usr/local/bin/OpenMVS/"

# Indicate the openMVG camera sensor width directory
CAMERA_SENSOR_WIDTH_DIRECTORY = "/home/ktw/project/openMVG/src/openMVG/exif/sensor_width_database"


출력된 폴더에 ply은 meshlab이나 cloudcompare로 확인할 수 있다. 결과는 다음과 같다. 


Dense Point  Cloud 결과

SFM 처리 시 문제 해결
Focal length 이슈
카메라 focal 값은 카메라 종류별로 차이가 있다. sensor_width_database 파일에 해당 카메라가 등록되어 있지 않거나, 촬영된 이미지의 EXIF 메타 데이터가 없을 경우, SFM를 위한 정보를 제대로 계산하지 못한다. 이 경우, 다음과 같이 잘못된 포인트 클라우드가 생성된다.
누락된 포인트 클라우드

이런 경우, 입력할 이미지 촬영에 사용된 카메라의 포컬(focal) 길이를 MvgMvs_Pipeline.py 에 지정해야 한다(참고 - calculate focal length, uncalibrated image issue). 이런 경우, 다음과 같이 MvgMvs_Pipeline.py 소스안에 해당 부분에 -f옵션으로 focal length 값을 수정한다.

    [   "Intrinsics analysis",
         os.path.join(OPENMVG_BIN,"openMVG_main_SfMInit_ImageListing"),
         ["-i", "%input_dir%", "-f", "586.3", "-o", "%matches_dir%", "-d", "%camera_file_params%"] ],


앞의 -f 옵션은 단순히 focal length가 아닌 픽셀 단위의 focal length이다. 그래서, 다음과 같은 수식으로 계산해서 입력해야 한다.
focalpix=max(wpix,hpix)∗focalmmccdwmm
  • focalpix the EXIF focal length (pixels),
  • focalmm the EXIF focal length (mm),
  • wpix,hpix the image of width and height (pixels),
  • ccdwmm the known sensor width size (mm)
SFM에서 focal length는 다음과 같이 intrinsic parameters를 계산할 때 사용한(참고 - SFM manual document).
Image intrinsic parameters(focal plane, parameters computed)

참고로, iPhone 8의 경우, focal 값은 다음과 같다.



이 경우, 계산된 focal length in pixel 값은 3351.6이다. 이 값을 앞의 -f 옵션값으로 하면 된다. 보통 고프로, 소니, 캐논 같은 유명 메이커 카메라는 sensor_width_database 에 등록되어 있어 -f 값을 안줘도 된다.

CUDA 메모리 할당 에러
과대한 포인트 클라우드 생성으로 다음과 같이 CUDA 메모리 할당 에러가 발생할 수 있다(MVG GPU issue, RefineMesh --use-cuda 0 option).

CUDA 메모리 할당 에러

이런 경우, 다음과 같이 use-cuda 옵션을 0으로 수정하면 된다.
            [   "Refine the mesh",
                os.path.join(OPENMVS_BIN,"RefineMesh"),
                ["scene_dense_mesh.mvs", "-w","%mvs_dir%", "--use-cuda", "0"]],

다양한 카메라 이미지 테스트 결과 분석
MvgMvs_Pipeline.py 파이썬 파일을 실행하면 특징들을 추출하고, 매칭 정보를 생성한 후, 밀집 포인트 클라우드를 생성하는 등의 프로세스를 진행한다.  

다른 이미지 데이터를 테스트해 보기 위해, DJI 펜텀4 드론을 이용해 회사 전경을 7장 촬영해 다음과 같이 SFM 처리를 해 보았다. 

python MvgMvs_Pipeline.py ./kict4 ./kict4_output

처리 시간은 수 분정도 밖에 걸리지 않았다. 결과는 다음과 같다.



7장 사진으로 생성된 포인트 클라우드 치고는 괜찬은 품질이다. 

이제 회사 건물 앞면을 다음과 같이 아이폰8로 촬영하여, SFM을 처리해 보았다.

결과는 다음과 같다. 촬영 시 전체 건물이 보이지 않는 상태에서 근접 촬영한 이유로 품질이 좋지 않은 듯 하다.

회사 건물 뒷면도 다음과 같이 촬영해 SFM처리해 보았다.

결과는 다음과 같다. 오른쪽 부분에 구름다리가 있는 데, 이 부분은 제대로 나오지 않았고, 사진이 많이 오버랩되지 않고 적은 부분은 포인트 클라우드가 처리되지 않은 것을 알 수 있다.

실내 3차원 모델도 SFM으로 테스트해보았다. 집안의 테이블과 책꼿이를 중심으로 150도 범위로 사진이 중첩되게 촬영해 보았다. 촬영 기종은 Cannon 6D(5472x3648 pixel)이다.

결과는 다음과 같다. 실내에서 한정된 각도로 촬영한 사진임에도 불구하고 제대로 SFM처리되었음을 알 수 있다.

이제 상용 프로그램인 Pix4D와 비교를 위해, 몇 년전 DJI 펜텀3로 촬영한 회사 뒷편 플랜트 시설물 이미지 데이터를 이용해 다음과 같이 SFM을 처리해 보았다. 이미지 수는 70개가 넘는다.
입력 이미지 데이터

SFM 처리 과정은 다음과 같다. 

본인의 경우, SFM처리에 대략 90분정도 시간이 걸렸다(i7-6700, GTX 960M, CUDA 사용). 밀집(dense) 포인트 클라우드가 생성되면 다음과 같이 출력폴더에 ply 파일이 생성된다. 

밀집 포인트 클라우드는 바로 이전 단계에서 생성된 다음 같은 희소(sparse) 포인트 클라우드를 이용한다. 
희소 포인트 클라우드

밀집 포인트 클라우드를 CloudCompare로 다음과 같이 확인해 보았다.

그림과 같이 출력된 결과 품질은 좋은 수준이다.

참고로, SFM는 각 프로세스를 개별적으로 실행할 수도 있다.
openMVG_main_openMVG2openMVS -i sfm_data.bin -o scene.mvs -d scene_undistorted_images
DensifyPointCloud scene.mvs
TextureMesh scene_dense_mesh_refine.mvs

직접 촬영한 이미지가 없어도 테스트 용으로 아래 링크에 촬영해 놓은 이미지 예제들을 제공한다. 테스트를 위해 다운로드 후 사용할 수 있다.
SFM 도커 이미지 사용
도커가 설치되어 있다면, 앞에서 작업한 소스코드 빌드 등 복잡한 내용을 생략할 수 있다. 다음과 같이 Mappilary의 OpenSFM를 Docker에서 실행해 사용할 수도 있다.
sudo docker pull freakthemighty/opensfm
sudo docker images
sudo docker run -i -t -d -p 8080:8000 -v ~/project/docker:/mnt/common freakthemighty/opensfm /bin/bash
sudo docker ps -a


참고로, OpenSFM은 Open Drone Map에서도 사용된다. 

주의사항
촬영한 이미지의 카메라 정보가 아래 파일에 포함되어 있지 않으면 에러가 발생할 수 있다. 이 경우, 관련 메타정보를 추가해 줘야 한다.
/openMVG/src/openMVG/exif/sensor_width_database

실행파일과 파이썬에서 실행할 때 디펜던시 문제로 버전 불일치 문제가 발생하는 경우가 있다. 특히, 아나콘다를 설치하고, 실행파일과 파이썬 실행 환경이 서로 다르게 링크될 때 문제가 발생할 수 있다.

Appendix - CGAL, BREAKPAD CMake error
Open CMakeLists.txt
OpenMVS_USE_BREAKPAD:BOOL=OFF
CGAL_DIR:PATH=/usr

레퍼런스
좀 더 상세한 내용은 다음을 참고 한다.

2019년 10월 23일 수요일

LiDAR 스캐너 대형 로버 마운트 프레임 제작

이 글은 LiDAR 스캐너 대형 로버 마운트 프레임 제작 과정을 다룬다. 프레임 제작 노력을 줄이기 위해 프로파일을 사용하기로 한다.

프로파일

로버에 Trimble LiDAR 장비를 마운트하고 사람이 진입하기 어려운 영역에 스캔을 해 본다. 좀 더 시간이 있다면, ROS를 이용해 로버를 제어하여, 주변 장애물을 피해가며 스캔을 할 수 있도록 할 계획이다.

Trimble 장비를 올리기 위해서는 로버 페이로드 성능이 좋아야 하며, 프레임도 매우 튼튼해야 한다. 아울러, 무게 중심도 낮아 쓰러지지 말아야 한다.

로버는 판매되는 키트나 제품을 최대한 활용해 개발한다.

추신 - 연말 과제 평가가 시작되어 1주일마다 평가와 자료제출이다. 연구자 담당 평균 과제 갯수가 4개 그 이상이다. 연구 일 안하는 사람들 동기부여한다고 PBS 각종 제도도입하더니 오히려 연구질은 계속 떨어지고 행정만 늘어난다. 일 벌이는 연구자만 손해인 구조. 행정 때문에 추석 이후에 연구 몰입할 수가 없다. 연구 행정 간소화 기대했다가 우리나라는 진짜 바뀌지 않는 다는 것을 알기까지 불과 1년도 걸리지 않았다.

레퍼런스



2019년 10월 15일 화요일

이동식 공간정보맵핑을 위한 스캔 백팩 2차 개선 및 전시

앞서 모바일 스캔을 위한 스캔 백팩을 만들어 SLAM을 수행했었다. 이 스캔 팩백에는 임베디드 보드, 배터리, 스캐너 연결 보드 등이 들어 있어 이동시 편했다.
테스트 결과 다음과 같은 문제점이 있었다.
  • 센서를 오래동안 들고 있을 만큼 가볍지 않음
  • 화면을 보면서 SLAM 하기 힘듬
  • 현재 공간 맵핑 진행 상태를 알기 힘듬
  • 기타, 백팩에 든 보드와 배터리의 고정 문제 등
이런 문제를 고려해, 다음과 같이 좀 더 안정적인 스캔 백팩을 간단히 제작해 보았다. 




스캔 백팩 개발 결과 다음과 같은 장점이 있었다. 
  • 센서를 손에 들고 다니는 것 보다 훨씬 편하다. 
  • 모니터를 다른 한손으로 보며 작업 과정을 관찰할 수 있었다. 
  • 1차 백팩에 비해 무게 중심을 잡는 밸트를 추가해 안전성있게 이동이 가능하다. 
  • 사용하는 배터리 성능이 좋아 몇 시간 이상 사용 가능하였다. 
  • 프레임이 안정적이고 수평을 맞추기 용이하여 SLAM 결과가 좋아진다. 
개선점은 다음과 같다. 
  • 센서로 인해 무게중심이 높아 아직 급하게 이동하면 백팩이 뒤집힐까 조심스럽다. 
  • 임베디드 보드 패키징에 문제가 있다. 전선 등이 정리되어 있지 않아 급한 이동 시 문제가 될 수 있다.
  • 처음 스캔 배낭을 맬때 높은 무게중심으로 인해 넘어질 수도 있다. 
  • 스캔 마운트가 폴대와 붙어 있어, 다른 장치 마운트 시 불편하다.
  • 배터리 등 소모성 장비 장착, 탈착이 불편하다.  
  • 기타, 등산 배낭을 사용해 스타일이 그리 좋지 않다. 

부록: 스캔 백팩 전시 기록
스캔 백팩 성능 및 사람들 피드백도 확인해 볼 요량으로 2019 메이커페어 서울에 전시해 보기로 함. 2019년 5월 샌프란시스코 메이커페어까지 아이들이 만든 작품만 쭉 전시했었기 때문에, 10월 메이커페어는 부스를 두개로 분리해 한쪽은 내 전공분야 관련 작업을 전시하기로 하였음. 컨셉은 다음 그림과 같이 해외 스캔 백팩과 비교해서 가성비 좋게 직접 만들 수 있음을 이야기하기로 함.

다음과 같이 부스 반을 짤라 한쪽편에 작업을 설치하고 전시함.  

작품 특징 상 어른들, 주로 테크니션들이 관심을 많이 주고, 질문도 많이 함. 직접 만드는 방법에 관심이 많아 설명하였고, 정확도, 거리, 사용할 때 얼마나 가치가 있는 지 등에 대해 집중 질문. 해외 외국인들도 많이 보고 질문했는 데, 주로 대학원생 컴공, 전공, 기계과, 미디어아트 교환 학생들이었음. 

교수, 교사 및 강사분들도 많았고, 메이커페어 특징 상 특이한 경력의 사람들도 많았음. 시간이 많지 않아 방문한 분들과 이런 저런 이야기를 못나눈 것이 아쉬움.



2019년 10월 10일 목요일

우분투 부팅시 스크립트 자동 실행

이 글은 우분투 부팅시 스크립트 자동 실행 방법을 간략히 남긴다.

다음과 같이 스크립트를 작성한다.
cd /etc/init.d/
sudo gedit auto_bat.sh

다음과 같이 파일 편집한다. 여기서 &는 병렬 프로세스 실행 옵션이다. &&를 붙이면 앞의 명령이 제대로 실행된 후 조건에 따라 후속 실행된다.
#!/bin/bash

PATH=/usr/local/bin
sleep 3
time &

sleep 3
date &

파일을 저장한 후 다음 명령으로 부팅시 실행될 스크립트를 등록한다. 프로세스 <우선순위>는 생략가능하며, 99 이하 값을 준다.
cd /etc/init.d/
sudo chmod +x auto_bat.sh
sudo update-rc.d auto_bat.sh defaults <우선순위>
reboot

서비스 중인 상황을 다음 명령으로 확인한다.
initctl list
service --status-all


만약 제거하고자 한다면 다음처럼 하면 된다.
cd /etc/init.d/
sudo update-rc.d -f <script file name> remove

자세한 내용은 다음을 참고 바란다.


2019년 10월 3일 목요일

3차원 공간정보 스캔 마운트용 중량 6WD 로버 개발 방법

이 글은 중량 센서를 마운트할 수 있는 6WD 로버 제작 과정을 정리한 글이다.

이전에 개발한 SLAM장비를 직접 들고 스캔할 목적으로 스캔 백팩을 만들었으나 무거워 가지고 다니며 스캔하기 힘들다. 이런 이유로, 연구용 야외 공간정보 맵핑 및 탐색이 가능한 간단한 로버를 만들 계획이다. 공간맵핑용 장비는 앞서 개발한 벨로다인 LiDAR 기반 SLAM 장치, 로봇 암 등을 활용한다. 이를 마운트하려면 최소한 1,794 g (VLP16 830 g + TX2 144 g + 배터리 310 g x 2 + 기타 200 g) 페이로드가 필요하다. 일반 장난감 수준의 장비로는 움직이지도 않기 때문에, 별도 로버를 제작하기로 하였다.

이 글에서 소개하는 로버는 최소 5 kg 이상의 페이로드를 지원하며, 야외 울퉁불퉁한 환경에서 공간정보 스캔을 할 수 있는 고정밀 센서 장치를 마운트 할 수 있다.
3D geo scan rover (425 x 300 mm. 4 kg)

공간정보 맵핑용 SLAM 장비 개발은 다음 링크를 참고한다.

로버 임무
이 로버는 3차원 공간정보 스캔을 위한 탐색용 중량 로버(Exploration heavy weight rover for scanning 3D geo-information. 3D geo scan rover)이다. 프레임 상단에 벨로다인 VLP-16 및 RGBD 센서가 마운트될 수 있어야 하며, 통신 거리 100-200미터 범위에서 로버를 조정할 수 있어야 한다.

참고로, 로버를 개발할 때는 다음과 같은 사항을 고려해야 성공할 수 있다.
  • 로버의 페이로드(운반무게) 결정. 페이로드 고려한 모터, 모터드라이버 및 배터리 필요
  • 로버의 운영환경 결정. 만약, 습기가 있는 곳이라면 방수가 되는 패키징 필요. 야외라면 울퉁불퉁한 지면을 다닐 수 있는 큰 바퀴와 내구성있는 바디 프레임 필요
  • 로버의 목적에 따른 센서 결정. 수색용이면 카메라, 적외선, LiDAR센서 및 이를 처리할 수 있는 임베디드 컴퓨터 필요. 물체취득이 필요하면 그립퍼, 로봇암등이 필요함
  • 프레임 결정. 로버 목적에 따라 센서나 장비를 쉽게 마운트 할 수 있어야 함
  • 로봇 통신 방법 결정. 유선인지 무선인지 결정 필요. 무선이면 몇 미터 거리 범위까지 통신 가능한지에 따라 통신장치 개발해야 함. 예를 들어 200미터 정도 거리 통신이 필요하다면 2.4GHz 송수신기를 사용해야 함
로버 파워
스펙 상으로 15kg 정도 페이로드를 지원하는 HEAVY 6WD rover 개발을 고려해 모터와 모터드라이버는 다음과 같이 결정한다.
  • 모터: DC Gear Motor. DC 6-12V. 
  • 모터드라이버: BTS7960. 43A 고전력 모터 드라이버
  • 배터리: 11.1V LiPO 2200mAh
BTS7960은 43A 모터 구동까지 가능하므로, 큰 힘이 필요한 상황에서 사용할 수 있다(예. #1, #2). 다음과 같이 배터리 팩 연결 단자를 남땜하고 모터드라이버와 연결한다. 참고로, 여기서는 지금 가지고 있는 전선을 이용했지만, 안전성을 고려해 30A 전류를 허용할 수 있는 직경을 가진 전선을 사용하는 것이 좋다(부록:AWG 참고). 

프레임
프레임은 가공업체에 맡겨 제작하거나 모터만 부착된 로봇 로버 프레임(#)을 구매해 사용한다. 센서를 마운트하기 편하게 HOLE이 있어야 한다. 다만, 범용 프레임은 빠르게 개발할 수 있는 장점은 있으나, 특수목적 및 환경에서 사용할 때는 별도 개발이 필요하다.

여기서 사용할 프레임은 링크와 유사하다. 다만, 자체가 낮고, 가공이 쉬우며, 배터리나 모터 드라이버를 내부에 장착할 수 있는 프레임으로 결정하였다. 프레임 크기는 425 x 300 mm 이며, 바퀴를 제외하면 380 x 120 mm 이다.
센서
센서는 LiDAR와 RGBD를 지원한다. 이 결과로 고정밀 포인트 클라우드와 이미지를 획득할 수 있다. 사용 센서는 다음과 같다.
  • LiDAR: VLP-16. 임베디드는 고성능 NVIDIA TX2 보드 사용
  • RGBD: ZED. 임베디드는 NVIDIA NANO 사용
통신
무선통신을 지원한다. 여기에서는 아래의 2.4GHz 송수신기를 사용한다. 송수신기는 다양한 종류가 있으니, 본인 개발 목적에 맞는 것을 사용한다.
송신기와 수신기 바인딩은 다음 링크를 참고한다.
송수신기를 서로 바인딩 한 후에는 다음과 같이 편리하게 서보모터 등을 제어할 수 있다. 
FS-TH9X, R9B binding

회로
회로는 다음과 같이 연결한다. 회로는 FS-R9B 수신기 CH2, CH3에서 DRIVE, STEER 신호를 각각 받아, 모터드라이버에 모터 구동신호를 전달한다. 배터리와 BTS7960은 2개를 사용해 모터에 전류를 충분히 공급한다. 참고로, L298N과 같이 2A미만 모터드라이버를 사용하면, 프레임 자체 자중도 견디기 어려울 것이다.
D22 - FS-R9B.CH2
D24 - FS-R9B.CH3
5V - FS-R9B.VCC
GND - FS-R9B.GND
D2 - RIGHT MOTOR BTS7960 RPWM
D3 - RIGHT MOTOR BTS7960 LPWM
D4 - RIGHT MOTOR BTS7960 L_EN
D5 - RIGHT MOTOR BTS7960 R_EN
D6 - LEFT MOTOR BTS7960 RPWM
D7 - LEFT MOTOR BTS7960 LPWM
D8 - LEFT MOTOR BTS7960 L_EN
D9 - LEFT MOTOR BTS7960 R_EN
VCC - BTS7960 VCC
GND - BTS6960 GND
11.1V BAT GND - BTS7960.GND
11.1V BAT VCC - BTS7960.VCC
RIGHT MOTOR BTS7960.M1 - RIGHT MOTOR +
RIGHT MOTOR BTS7960.M2 - RIGHT MOTOR -
LEFT MOTOR BTS7960.M1 - LEFT MOTOR +
LEFT MOTOR BTS7960.M2 - LEFT MOTOR -




회로 연결 과정

코딩
코딩은 다음과 같다(github 링크). 소스코드는 필드테스트 후에 다음과 같은 몇 가지 수정이 있었다.
  • 로버 전후좌우 이동 이외에도 후진 방식 추가
  • 송신기 전원 껴져 있는 경우, 최대속도 주행 오류 있어, 신호 없을 경우 멈춤기능 추가
  • 특정 모터 드라이버 구동 코드 및 반복된 소스코드 함수화
  • 모터 드라이버 회로 선 연결 착오로 핀 번호 수정
참고로, 아래 소스코드 사용할 경우, 수신기에서 얻은 채널별 디지털 신호의 범위는 테스트하여 구해야 하며, 이를 map, constrain 함수를 이용해 캘리브레이션해야 한다.

// 6WD rover control code. FS-R9B. BTS7960 driver
// by Taewook, Kang. laputa99999@gmail.com. 2019.10.8
// MIT license
//
#define chA 22
#define chB 24

#define R_RPWM 2
#define R_LPWM 3
#define R_LEN  4
#define R_REN  5
#define L_LPWM 6
#define L_RPWM 7
#define L_LEN  8
#define L_REN  9

void setup() {
  pinMode(chA, INPUT);
  pinMode(chB, INPUT);

  for(int i = R_RPWM; i <= L_REN; i++)
    pinMode(i, OUTPUT);
  for(int i = R_RPWM; i <= L_REN; i++)
    digitalWrite(i, LOW);

  delay(1000);
  Serial.begin(9600);
}

void runMotor(int leftDrive, int leftReverse, int rightDrive, int rightReverse)
{
  digitalWrite(R_REN, HIGH);
  digitalWrite(R_LEN, HIGH);
  digitalWrite(L_REN, HIGH);
  digitalWrite(L_LEN, HIGH);
  delay(100);

  analogWrite(L_LPWM, leftDrive);        
  analogWrite(L_RPWM, leftReverse);        
  analogWrite(R_LPWM, rightDrive);
  analogWrite(R_RPWM, rightReverse);    
}

void loop() {
  int drive = pulseIn (chA, HIGH);     // drive, back
  int steering = pulseIn (chB, HIGH);  // left, right

  String data = "D = " + String(drive);
  Serial.print(data);
  data =  ", S = " + String(steering);
  Serial.print(data);

  int motorDrive = 0;
  int motorSteer = 0;

  motorDrive = map(drive, 1070, 1880, 0, 255);
  motorDrive = constrain(motorDrive, 0, 255);
  motorSteer = map(steering, 1040, 1880, 0, 255);
  motorSteer = constrain(motorSteer, 0, 255);
  
  data =  ", MD = " + String(motorDrive);
  Serial.print(data);
  data =  ", MS = " + String(motorSteer);
  Serial.println(data);
  
  bool forward = true;
  if(90 <= motorDrive && motorDrive <= 110)
  {
    motorDrive = 0;
  }
  else if(motorDrive > 110)
  {
    motorDrive = map(motorDrive, 110, 255, 0, 255);
    motorDrive = constrain(motorDrive, 0, 255);
  }
  else 
  {
    motorDrive = map(motorDrive, 0, 90, 255, 0);
    motorDrive = constrain(motorDrive, 0, 255);   
    forward = false; 
  }

  // |_rover__>>
  // 
  if(motorSteer == 0)   // if transmitter power off the motorSteer value is 0
  {
    runMotor(0, 0, 0, 0);
  }
  else if(100 <= motorSteer && motorSteer <= 140)    // go straight
  {
    runMotor(forward ? motorDrive : 0, forward ? 0 : motorDrive, 
             forward ? motorDrive : 0, forward ? 0 : motorDrive);
  }
  else if(motorSteer < 100) // turn left
  {
    runMotor(forward ? motorDrive : 0, forward ? 0 : motorDrive, 
             forward ? 0 : motorDrive, forward ? motorDrive : 0);
  }
  else  // turn right
  {
    runMotor(forward ? 0 : motorDrive, forward ? motorDrive : 0, 
             forward ? motorDrive : 0, forward ? 0 : motorDrive);
  }
}

로버와 배터리를 연결하고, 송신기를 켠 후, 시리얼모니터를 확인하면, 다음과 같은 제어 신호 데이터를 확인할 수 있다.

송신기로 제어하면, 로버가 정상적으로 제어되는 것을 확인할 수 있다.

필드 테스트 
로버 성능은 어떻게 되는 지, 어느 정도 중량을 견디는 지 등을 확인하고, 이슈가 있는 지 확인하기 위해 필드 테스트를 한다. 중량 테스트를 위해, 다음과 같이 로버 등의 하중을 측정한다.
로버 자중 = 4.0 kg
배터리 2개 + 에나멜 통 2개 중량 = 4.8 kg
큰 에나멜 통 1개 중량 - 2.8 kg
작은 에나멜 통 1개 = 1.1 kg
배터리 한개 501 g
로버 위 에나멜 통 2개 + 배터리 3개 = 5.3 kg
에나멜 통 1개 + 배터리 3개 = 2.7 kg 

다양한 조건과 환경을 고려해 다음과 같이, 로버 테스트를 해 보았다.
1차 동작제어 및 주행속도 테스트 (자중 4 kg)
2차 중량 테스트 (하중 3.8 kg)
3차 장애물 테스트 (다양한 크기의 장애물 + 하중 2.8 kg)
4차 장애물 테스트 (다양한 크기의 장애물 + 하중 2.8 kg)
5차 중량 테스트 (하중 2.7 kg)
6차 중량+자유곡선 이동 테스트 (하중 2.7 kg)
7차 사면 등판테스트 (자중 4.0 kg)

테스트 해 본 결과 2.6kg 정도는 충분히 로버 프레임이 지지하며, 5kg은 로버의 서스펜션이 바닥에 닿는 문제를 제외하고는 큰 문제 없이 구동되었다. 서스펜션을 조정하면 5kg 페이로드도 가능하다. 경사 등판은 표면이 거친 경우, 40도 각도도 주행 가능하며, 무게중심이 낮다면 그 이상 각도도 등판 가능하다. 주먹만한 크기의 자갈길이나 작은 바위 위에서도 주행이 가능해 보인다.

마무리
앞의 개발 결과로 RF 제어가 되는 중량 센서 장치가 마운트될 수 있는 로버를 만들었다. 앞으로 로버 위에 개발된 LiDAR 및 RGBD 스캔 장치를 마운트하여, 공간정보 맵핑에 사용하려 한다. 여기에 비전, ROS, 딥러닝 등을 붙이면 장애물을 피해가며 지정된 구간을 스캔할 수 있는 장비를 개발할 수 있다.

부록: AWG 전선 규격표
AWG는 미국에서 만든 전산 사용 지침으로, 직경에 따라 저항 및 허용전류를 확인해 적절한 전선을 사용할 수 있도록 만든 가이드라인이다. 허용전류가 높은 데, 적은 직경의 전선을 사용하면, Joule 열이 발생하고, 전선 피복이 녹아 벗겨져 합선이나 화재가 발생할 수 있다. AWG는 이를 방지한다. 

레퍼런스