2019년 11월 30일 토요일

어린이도 손쉽게 만드는 인공지능 딥러닝 도구 - 구글 Teachable machine

더이상 인공지능 딥러닝은 사용하기 어려운 기술이 아니다. 좀 더 정확히 말하면, 단순한 데이터 입력-학습-사용 정도의 딥러닝은 기술이 아닌 어린이도 할 수 있는 세상이 되어가고 있다.

구글에서 만든 teachable machine은 누구나 딥러닝을 쉽게 할 수 있도록 한다.
teachable machine site

사용 방법은 매우 간단하다.

1. 해당 사이트를 방문하고, 이미지 인식, 시그널 분류 등 준비된 템플릿을 선택한다.
2. 이제 데이터를 입력하고, 학습하고, 실행한 결과를 본다. 데이터는 웹캠으로 입력 가능하다.

본인은 건설 공학에서 고전적인 문제인 균열 탐지 부분을 간단히 데이터 만들어 넣어 보았다.
딥러닝 학습 중인 모습

학습 후 균열 검출

정확도가 상당히 잘 나온다. 보통, 40~50개 정도 이미지를 준비하면 분류 모델 만드는 데 큰 문제가 없었다. 클래스를 추가하는 것도 간단하다. 다음과 같이 smile 이미지를 준비해 학습했다.
Smile 이미지 학습
인식 결과

학습 후 인식하면 인식이 잘 되는 것을 알 수 있다.

이렇게 생성된 딥러닝 모델은 keras, tensorflow js 등의 모델로 다운로드 받아 다양한 기기에서 딥러닝을 사용할 수 있다. 애써 만든 학습용 데이터는 다운로드해 다시 재활용할 수 있다.
Export model

저장된 모델을 이용해 다양한 방식으로 배포할 수 있다.

이를 이용하면, 스마트폰 등 다양한 기기에서 딥러닝 실행 결과를 손쉽게 얻을 수 있다.
ㅎ 이제 학습용 데이터만 준비 잘 하면 되겠다.^^
이미지 딥러닝 분류 영상

조만간 이를 이용한 다양한 사례가 쏟아져 나올 듯 보인다. 템플릿이 겨우 3개이며, 복잡한 딥러닝을 수행할 수는 없다. 하지만, 구글은 템플릿을 계속 개발하고 있으며, 다양한 딥러닝 모델 학습 기능을 추가할 계획이다.

딥러닝은 이제 누가 손쉽게 모델을 빨리 만들 수 있고, 사용할 수 있도록 하는 지가 기술이 될 듯 하다.

2019년 11월 28일 목요일

LSTM 딥러닝 모델 기반 이상 데이터 탐지

이 글은 LSTM 기반 이상신호 탐지용 딥러닝 모델 개발 방법을 간략히 설명한다. 이상탐지는 장비 모니터링, 사기 탐지, 해킹 등 다양한 분야에서 응용될 수 있다. 이 글은 LSTM(Long Short Term Memory) 딥러닝 모델을 이용한 이상탐지 방법을 간략히 설명하고 구현하는 방법을 설명한다.

머리말
시계열에서 이상탐지는 예상되는 미래값이 실제 얻은 값과 편차를 가지게 될 때 이상으로 판단한다. 학습 단계는 다음과 같다.

1. 시계열 데이터의 과거값들을 이용해, 한단계 바로 뒤의 값들을 예측하도록 학습 데이터를 만든다.
2. 다변량 가우스 분포를 이용해 오차 벡터를 계산한다.
3. 만약, 예측된 값과 실제값의 오차가 다변량 가우스 분포의 끝 부분에 위치해 있다면, 일반적으로 예측될 수 있는 값의 범위를 넘어가므로, 이상이라 판단한다.

다음 그림은 이를 보여준다.

이상 패턴 탐지
이상 패턴을 탐지하는 기법은 통계 분야에서 오래전부터 발전해 온 것이다. 대표적인 방법은 다음과 같다. 
  • 정규분포 표준편차 기반 이상 탐지. 예) 표준편차의 3시그마 이상은 이상값이라 판단
  • 경계 기준 기반 이상 탐지. 예) B1 < value < B2
  • kNN, DBScan, RCF(Random Cut Forest) 클러스터링 기법. 예) 클러스터링 그룹 중 sample 수 N 개 이하면 이상값
  • 유클리드, 마할라노비스 거리 기법. 다변수량 상관관계에 따른 거리 계산 후 이상값 탐지 가능
정규분포 표준편차

여기서는 다변수 데이터셋에 대한 이상패턴 탐지 기법을 알아본다.

Mahalanobis 거리
분포내 예측과 오차에 대한 희소한 양을 표시하기 위해 마할라노비스 거리란 함수를 사용한다. 이 거리는 x와 y로 구성된 벡터의 상관성을 계산할 수 있다.. 수식은 다음과 같다. 상세 개념은 여기를 참고한다.

실제 코딩해 보기
간단한 예제를 코딩해 보도록 한다. 아래와 같은 패키지가 우선 설치되어 있어야 한다.
  • ReNom 2.6.2
  • matplotlib 2.2.2
  • pandas 0.23.1
  • numpy 1.14.5
  • scikit-learn 0.19.1
  • scipy 1.1.0
제대로 설치되어 있다면, 다음 코드를 실행해 본다.
%matplotlib inline
import matplotlib.pyplot as plt
import pandas as pd 
import numpy as np 
from sklearn.model_selection import train_test_split
from copy import deepcopy
from sklearn.preprocessing import StandardScaler

import renom as rm
from renom.optimizer import Adam
from renom.cuda import set_cuda_active
set_cuda_active(False)

데이터셋은 다은 링크에서 ECG dataset, qtdb/sel102 ECG dataset 을 사용한다.
http://www.cs.ucr.edu/~eamonn/discords/

ECG data를 정규화하기 위해, 시간을 0 - 5000 부분만 플롯팅해 그 구조를 확인해 본다.
df = pd.read_csv('data/qtdbsel102.txt', header=None, delimiter='\t')
ecg = df.iloc[:,2].values
ecg = ecg.reshape(len(ecg), -1)
print('length of ECG data : ', len(ecg))

# standardize
scaler = StandardScaler()
std_ecg = scaler.fit_transform(ecg)

plt.style.use('ggplot')
plt.figure(figsize=(15,5))
plt.xlabel('time')
plt.ylabel('ECG\'s value')
plt.plot(np.arange(5000), std_ecg[:5000], color='b')
plt.ylim(-3, 3)
x = np.arange(4200,4400)
y1 = [-3]*len(x)
y2 = [3]*len(x)
plt.fill_between(x, y1, y2, facecolor='g', alpha=.3)
plt.show()

이 데이터는 주기적이다. 대략 4250 시간 근처에 주기가 붕괴되어 변화가 있다.

1단계 - LSTM 학습
1단계의 목적은 LSTM으로 정상 데이터를 학습하는 것이다. 그러므로, 5000이후 데이터를 학습용 데이터로 사용한다.
normal_cycle = std_ecg[5000:]

plt.figure(figsize=(10,5))
plt.title("training data")
plt.xlabel('time')
plt.ylabel('ECG\'s value')
plt.plot(np.arange(5000,8000), normal_cycle[:3000], color='b')# stop plot at 8000 times for friendly visual
plt.show()


이제, 다음 코드로 d 길이만큼 서브시퀀스를 생성하고, l 차원의 라벨을 생성한다.
 create data of the "look_back" length from time-series, "ts"
# and the next "pred_length" values as labels
def create_subseq(ts, look_back, pred_length):
    sub_seq, next_values = [], []
    for i in range(len(ts)-look_back-pred_length):  
        sub_seq.append(ts[i:i+look_back])
        next_values.append(ts[i+look_back:i+look_back+pred_length].T[0])
    return sub_seq, next_values

이 예제에서는 d = 10, l = 3이다. 아래 코드를 실행해, sub_seq와 예측해야할 라벨이 있는 next_values 시퀀스를 만든 후, 훈련 및 검증 데이터로 분리한다.
look_back = 10
pred_length = 3

sub_seq, next_values = create_subseq(normal_cycle, look_back, pred_length)


X_train, X_test, y_train, y_test = train_test_split(
    sub_seq, next_values, test_size=0.2)

X_train = np.array(X_train)
X_test = np.array(X_test)
y_train = np.array(y_train)
y_test = np.array(y_test)


train_size = X_train.shape[0]
test_size = X_test.shape[0]
print('train size:{}, test size:{}'.format(train_size, test_size))

LSTM 모델을 정의한다.
# model definition
model = rm.Sequential([
    rm.Lstm(35),
    rm.Relu(),
    rm.Lstm(35),
    rm.Relu(),
    rm.Dense(pred_length)
    ])

LSTM에서 35개 벡터를 출력하고, RELU 함수를 거친 후, 다시 LSTM 35개 벡터 출력, RELU 함수 처리, 3개 벡터값을 예측하기 위해 Dense 레이어를 추가했다.

배치 크기와 최대 세대를 설정하고, 최적화 함수는 ADAM으로 한다.
# params
batch_size = 100
max_epoch = 2000
period = 10 # early stopping checking period
optimizer = Adam()

이제, 다음과 같이 학습시킨다. 참고로, 아래 코드에서 np.random.permutation()은 학습을 잘 시켜주기 위해, 순열을 섞어주는 함수이다(링크). 큰 순서는 다음과 같다.
1. model.train()에서 학습한다.
2. 모든 데이터에 대해, 바로 학습된 모델에 학습데이터를 입력하여, 출력된 z와 실제 라벨값인 batch_y와 차이를 MSE(Mean Square Error)로 계산하여, train_loss 값을 누적한다.
3. 모든 데이터에 대해, 학습된 모델을 이용해 예측 에러가 얼마인지 계산한다.
앞의 과정을 테스트 손실이 특정 조건이 될때까지 반복해 학습시킨다.
# Train Loop
epoch = 0
loss_prev = np.inf

learning_curve, test_curve = [], []

while(epoch < max_epoch):
    epoch += 1

    perm = np.random.permutation(train_size)
    train_loss = 0

    for i in range(train_size // batch_size):
        batch_x = X_train[perm[i*batch_size:(i+1)*batch_size]]
        batch_y = y_train[perm[i*batch_size:(i+1)*batch_size]]

        # Forward propagation
        l = 0
        z = 0
        with model.train():
            for t in range(look_back):
                z = model(batch_x[:,t])
                l = rm.mse(z, batch_y)
            model.truncate()
        l.grad().update(optimizer)
        train_loss += l.as_ndarray()

    train_loss /= (train_size // batch_size)
    learning_curve.append(train_loss)

    # test
    l = 0
    z = 0
    for t in range(look_back):
        z = model(X_test[:,t])
        l = rm.mse(z, y_test)
    model.truncate()
    test_loss = l.as_ndarray()
    test_curve.append(test_loss)

    # check early stopping
    if epoch % period == 0:
        print('epoch:{} train loss:{} test loss:{}'.format(epoch, train_loss, test_loss))
        if test_loss > loss_prev*0.99:
            print('Stop learning')
            break
        else:
            loss_prev = deepcopy(test_loss)

plt.figure(figsize=(10,5))
plt.plot(learning_curve, color='b', label='learning curve')
plt.plot(test_curve, color='orange', label='test curve')
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(fontsize=20)
plt.show()

실행 결과는 다음과 같다.

2단계 - 가우스 분포 피팅
이상 탐지를 위해 가우스 분포 계산을 한다.
# computing errors
for t in range(look_back):
    pred = model(X_test[:,t])
model.truncate()
errors = y_test - pred

mean = sum(errors)/len(errors)

cov = 0
for e in errors:
    cov += np.dot((e-mean).reshape(len(e), 1), (e-mean).reshape(1, len(e)))
cov /= len(errors)

print('mean : ', mean)
print('cov : ', cov)

mean :  [-0.00471252  0.00561184  0.01125641]
cov :  [[0.00093565 0.00088413 0.00097755]
 [0.00088413 0.00208558 0.0025572 ]
 [0.00097755 0.0025572  0.00498106]]

3단계 - 이상탐지
이 예측 모델이 미지의 데이터에 대해서도 제대로 동작하는 지 검증한다. 우선, 서브 시퀀스와 라벨을 생성하고 에러 벡터를 계산한다.
# calculate Mahalanobis distance
def Mahala_distantce(x,mean,cov):
    d = np.dot(x-mean,np.linalg.inv(cov))
    d = np.dot(d, (x-mean).T)
    return d

# anomaly detection
sub_seq, next_values = create_subseq(std_ecg[:5000], look_back, pred_length)
sub_seq = np.array(sub_seq)
next_values = np.array(next_values)

for t in range(look_back):
    pred = model(sub_seq[:,t])
model.truncate()
errors = next_values - pred

다음으로 Mahalanobis 거리를 출력해 얼마나 이상한 값인지를 판단한다.
m_dist = [0]*look_back 
for e in errors:
    m_dist.append(Mahala_distantce(e,mean,cov))

fig, axes = plt.subplots(nrows=2, figsize=(15,10))

axes[0].plot(std_ecg[:5000],color='b',label='original data')
axes[0].set_xlabel('time')
axes[0].set_ylabel('ECG\'s value' )
axes[0].set_ylim(-3, 3)
x = np.arange(4200,4400)
y1 = [-3]*len(x)
y2 = [3]*len(x)
axes[0].fill_between(x, y1, y2, facecolor='g', alpha=.3)

axes[1].plot(m_dist, color='r',label='Mahalanobis Distance')
axes[1].set_xlabel('time')
axes[1].set_ylabel('Mahalanobis Distance')
axes[1].set_ylim(0, 1000)
y1 = [0]*len(x)
y2 = [1000]*len(x)
axes[1].fill_between(x, y1, y2, facecolor='g', alpha=.3)

plt.legend(fontsize=15)
plt.show()


앞의 그림을 보면 4250 근처에 Mahalanobis 거리가 가장 크다. 이를 통해, 주기적 데이터가 언제 붕괴가 일어나는 지를 확인할 수 있다.

이런 방식으로 어떤 데이터든지 Anomaly Detection이 가능하다.


레퍼런스

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 lengthuncalibrated 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 issueRefineMesh --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

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