2018년 1월 31일 수요일

CNN과 depth추정을 사용한 CNN SLAM

CNN과 depth추정을 사용한 CNN SLAM
이 기사는 slowsingle님의 CNN-SLAM 연재 기사
CNNとdepth推定を用いたオドメトリの算出 前編(CNN SLAM #1)
CNNとdepth推定を用いたオドメトリの算出 後編(CNN SLAM #2)
CNNによるdepth推定 (CNN SLAM #3)
에 대해 번역과 게재를 허락받은 것입니다.
스스로 번역을 하며 도저히 모르는 부분은 구글 번역기의 도움을 받았습니다.

CNN과 depth추정을 사용한 odometry 계산(CNN SLAM #1)

CNN SLAM이라는 SLAM에 딥러닝을 조합한 방법이 등장하여, 제 자신도 굉장히 흥미를 갖고 있었기 때문에 구현을 통해 이해하고자 이 기사를 쓰게 되었습니다.
deep learning을 사용하여 end-to-end로 해버리자! 라기보다는 어디까지나 시스템의 일부로서 deep learning을 넣는 느낌입니다.

처리 개요

CNNSLAM.png
RGB영상만을 입력으로 하여、depth추정과 segmentation의 두 부분을 CNN을 사용하여 추정합니다. depth추정이 가능하다면 그 다음은 RGB-D카메라를 사용한 SLAM과 같은 방식으로 진행됩니다. 어떤 방법을 사용하더라도, 카메라의 이동량(회전행렬과 병진벡터)즉 odometry를 구하는 것이 필수가 되어 있습니다.
단, depth추정의 처리에 시간이 걸리는 것과, 타겟과 일치한다는 보장이 없다(불확실성이 존재)는 문제가 있습니다.
처리시간 관련된 문제(전자)는, key-frame Initialization이라는 방법으로 해결을 도모합니다. 흘러가는 RGB의 영상 모두에 CNN을 적용하는 것이 아닌, 키 프레임마다 CNN을 적용시키자는 방식입니다. 솎아내는 느낌이네요.
추정오차가 남는 문제(후자)는 Frame-wise Depth Refinement으로 해결을 도모합니다. 이 부분은 조금 난해하므로 뒤에서 다루겠습니다.
갑자기 모든 것을 구현하려 하면 쉽지 않기 때문에, 이번엔 CNN SLAM에 있는 카메라의 odometry의 계산을 RGB영상과 depth영상을 사용하여 시험하고자 합니다. 따라서, depth 추정도 이번에는 하지 않습니다. 나중에 추가하려 합니다.
()여름방학이 끝날 즈음에는 CNN SLAM을 마스터 할 수 있도록 하자는 계획입니다)

odometry의 계산

r(u,T)=Iki(u)It(π(KTtkiVki(u))) r({\bf u}, {\bf T}) = I_{k_i}({\bf u}) - I_t(\pi({\bf K}{\bf T}_{t}^{k_i} V_{ki}({\bf u})))
t시점의 영상이 주어졌을 때, tt시점에 가장 가까운 키프레임 취득시각을 kik_i로부터의 odometry(이동량)TT를 계산합니다.uu는 영상의 각 픽셀의 좌표값을 나타내고, 각 픽셀의 휘도값을 기준으로, 변화전과 변화후에 어떻게 영상이 변화했는가를 픽셀마다 구합니다. 또한, KK는 카메라의 내부 파라미터로, VV함수는 영상좌표계(2차원)에서 카메라 좌표계(3차원)의 변환을 수행합니다.π\pi함수는 그 역변환입니다.
rr함수는 한 픽셀의 처리이므로, 각 픽셀마다의 rr함수의 총합을 구하여 그 오차의 최소화를 도모합니다.
ρ\rho함수와 σ\sigma함수가 붙어 있네요. 이 비용함수의 설계에 대해서는 아직 잘 모릅니다. 아마 ρ\rho함수와 σ\sigma함수 등에 의해 정밀도가 오르는 것이겠죠(내용 업데이트 필요)
E[Ttki]=uΩρ(r(u,Ttki)σ(r(u,Ttki))) E[{\bf T}_{t}^{k_i}] = \sum_{u \in \Omega}{\rho(\frac{r({\bf u}, {\bf T}_{t}^{k_i})}{\sigma(r({\bf u}, {\bf T}_{t}^{k_i}))})}
이번엔 간단하게 r(u,Ttki)r({\bf u}, {\bf T}_{t}^{ki})TT에 대한 최소화를 고려해봅시다. 즉, 제곱의 합(r2\sum{r^2})의 최소화를 생각해봅시다. 최소화의 방법으로는 GD가 유명하지만, 논문에서는 가우스 뉴턴 법을 사용하고 있는 것 같아, 그것을 이용하겠습니다.
위 wikipedia 기사의 내용에 따르면 rr은 지금까지 설명했던 방식대로 입니다. 그렇기 때문에, 다음으로 야코비안만 구할 수 있다면 최소화가 가능해집니다.
야코비안이라는 것은 아래와 같은 행렬입니다.
J=(r1x1r1xnrmx1rmxn) J = \left( \begin{array}{c} \frac{\partial {\bf r}_1}{\partial {\bf x}_1} & \ldots & \frac{\partial {\bf r}_1}{\partial {\bf x}_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial {\bf r}_m}{\partial {\bf x}_1} & \ldots & \frac{\partial {\bf r}_m}{\partial {\bf x}_n} \end{array} \right)
각 픽셀의 rr함수에 의한 오차를 n개의 xx 로 편미분한 것이 늘어져 있습니다. 파라미터 xx는 최소화 해야만 하는 각도 (α,β,γ)(\alpha, \beta, \gamma)와 병진(x,y,z)(x, y, z)의 6개가 있기 때문에, n=6n=6이 됩니다. 오차를 구할 때의 I는 영상의 휘도값을 취득하기 때문에, 이 부분은 수학적으로 미분이 불가능합니다. 따라서 수치미분을 취합니다. 아래와 같은 식으로 근사합니다. hh는 미소값으로 전제합니다. 1e-04이나 1e-07 같은 값입니다.
rx=r(x+h)r(xh)2h \frac{\partial r}{\partial x} = \frac{r(x + h) - r(x- h)}{2h}
이것으로 야코비안도 구했으므로, 다음은 가우스 뉴턴 법에 쓰여 있는 것처럼 반복계산을 수행해가는 것뿐입니다.

Depth영상의 보정

앞서 말씀드린 것처럼, depth"추정"을 하고 있으므로, 당연하게도 불확실성이 남아 있습니다. 논문에서는 이 불확실성을 수식으로 정의하고 있습니다. 영상은 시간에 따라 주어지는 것이고, 강한 상관관계가 있으므로 이것을 이용하여 추정 정밀도의 향상을 도모하려 하는 것입니다.
(이 문단은 자신있게 번역하지 못했으니 주의하시기 바랍니다)
구체적으로는 현재, 과거의 key-frame 기반의 depth 추정값과 불확실성 맵들이 있을 때, 불확실성을 weight로 하여 불확실성이 작은 쪽의 추정결과에 weight를 주고, 현재의 depth추정에 보정을 걸어가는 방법을 취합니다.
현재의 key-frame의 인덱스를 kik_i、바로 직전의 key-frame의 인덱스를 kjk_j로 하면, kik_i시점의 불확실성의 맵UkiU_{k_i}은 아래와 같이 구해집니다.
Uki(u)=Dki(u)Dkj(π(KTkjkiVki(u))) U_{k_i}({\bf u}) = D_{k_i}({\bf u}) - D_{k_j}(\pi({\bf K}{\bf T}_{k_j}^{k_i} V_{ki}({\bf u})))
depth영상이 제대로 구해지고 있고, 이동량 TT도 제대로 계산된다고 가정하면, 불확실성은 0이겠죠.
또한, kjk_j시점의 불확실성 맵도 아래와 같이 업데이트 됩니다.
U~kj(v)=Dkj(v)Dki(u)Ukj(v)+σp2 \tilde{U}_{k_j}({\bf v}) = \frac{D_{k_j}({\bf v})}{D_{k_i}({\bf u})}U_{k_j}({\bf v}) + \sigma_p^2
v=π(KTkjkiVki(u)) {\bf v} = \pi({\bf K}{\bf T}_{k_j}^{k_i} V_{ki}({\bf u}))
σp2\sigma_p^2는 노이즈를 첨부하는 것을 의미합니다.
불확실성의 맵은 값이 클수록 불확실하다는 것을 의미하므로, 값이 작으면 작을수록, 그 추정결과를 보다 강하게 신뢰하게 됩니다.
이것들을 사용한 업데이트 식은 논문 중의 (8), (9)에 쓰여 있는대로입니다.
논문중의 3.4절은 스테레오 매칭을 사용하는 것으로, 각 시점 t에 대한 depth영상을 추정하여, 이것을 사용한 시점 t의 key-frame의 depth영상을 보정하고 있습니다. (key-frame취득시의 시점은 kik_i). 스테레오 매칭은 카메라 2대를 사용하여 양안시차를 구하고, 배경의 깊이를 추정하는 것이지만, 이번에는 시계열을 따라 흘러나오는 이미지 2장을 기초로 하여 깊이를 추정하고 있습니다.

CNN과 depth추정을 사용한 odometry의 계산 후편(CNN SLAM #2)

odometry의 계산 복습

r(u,T)=Iki(u)It(π(KTtkiVki(u))) r({\bf u}, {\bf T}) = I_{k_i}({\bf u}) - I_t(\pi({\bf K}{\bf T}_{t}^{k_i} V_{ki}({\bf u})))
위 식의 rr의 최소화를 목표로 하고 있습니다. 이 시점의 TT(회전성분 + 병진성분)을 구합니다. 이것을 회전행렬RR와 병진 벡터tt로 나누어, 아래와 같이 나타냅니다.
r(u,T)=Iki(u)It(π(K(RtkiVki(u)+ttki))) r({\bf u}, {\bf T}) = I_{k_i}({\bf u}) - I_t(\pi({\bf K}({\bf R}_{t}^{k_i} V_{ki}({\bf u}) + {\bf t}_{t}^{k_i})))
회전행렬 파라미터는 3가지로, np.array([alpha, beta, gamma])을 인수로 회전행렬을 반환하는 함수를 미리 만들어둡니다. numpy모듈을 사용합니다.
def make_R(rads):
    if len(rads) != 3:
        print("len(rads) != 3")
    alpha, beta, gamma = rads
    R_alpha = np.array([[np.cos(alpha), 0.0, np.sin(alpha)],
                        [0.0, 1.0, 0.0],
                        [-np.sin(alpha), 0.0, np.cos(alpha)]])
    R_beta = np.array([[1.0, 0.0, 0.0],
                       [0.0, np.cos(beta), -np.sin(beta)],
                       [0.0, np.sin(beta), np.cos(beta)]])
    R_gamma = np.array([[np.cos(gamma), -np.sin(gamma), 0.0],
                        [np.sin(gamma), np.cos(gamma), 0.0],
                        [0.0, 0.0, 1.0]])
    R = np.dot(R_gamma, np.dot(R_beta, R_alpha))
    return R
KK는 카메라의 내부 파라미터입니다. 렌즈의 초점거리(fxf_x, fyf_y)와 영상의 중심좌표(cxc_x, cyc_y)를 사용하여 아래와 같이 나타냅니다.
K=(fx0cx0fycy001) K = \left( \begin{array}{c} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{array} \right)
fx = 525.0  # focal length x
fy = 525.0  # focal length y
cx = 319.5  # optical center x
cy = 239.5  # optical center y

K = np.array([[fx, 0, cx],
              [0, fy, cy],
              [0, 0, 1]], dtype=np.float64)
캘리브레이션을 해도 좋지만, 이번에는 데이터셋을 다운로드 하여 실험을 했기 때문에 그 정보를 이용하였습니다.
VV는 아래와 같은 식으로 나타납니다.
Vki(u)=K1u˙Dki(u) V_{k_i}({\bf u}) = K^{-1}\dot{u}D_{k_i}({\bf u})
u˙\dot{u}가 구체적으로 어떤 값인가는 모르지만, depth 영상DD를 사용하여 영상의 각 픽셀에 대응하는 3차원 좌표계, 즉 실세계의 어디에 있는가를 계산한 것 같습니다.
II는 rgb영상을 나타내고 있습니다. 영상은 (이번엔 그레이스케일 영상을 취급하고 있으므로)2차원 배열이 되어, 값을 참고할 때는 정수 타입이 될 필요가 있지만, 좌표변환을 할 때는 좌표계의 점이 소수가 되는 경우는 충분히 있을 수 있으므로, 오차를 뱉지 않도록 신경써놓았습니다. 예를 들어 인덱스 5.3의 값을 얻고 싶은 경우는 인덱스 5와 인덱스 6의 값을 적절히 믹스하여 반환하면 됩니다. 여기서는 선형보간을 사용하였습니다.
def get_pixel(img, x, y):
    rx = x - np.floor(x)
    ry = y - np.floor(y)
    left_up_x = int(np.floor(x))
    left_up_y = int(np.floor(y))
    val = (1.0 - rx) * (1.0 - ry) * img[left_up_y, left_up_x] + \
          rx * (1.0 - ry) * img[left_up_y, left_up_x + 1] + \
          (1.0 - rx) * ry * img[left_up_y + 1, left_up_x] + \
          rx * ry * img[left_up_y + 1, left_up_x + 1]
    return val
rr을 구하는 함수는 아래와 같이 됩니다.
def translate(rads, t, im_xs, im_ys, dep_vals):
    if not (len(im_xs) == len(im_ys) == len(dep_vals)):
        print("len(im_xs) == len(im_ys) == len(dep_vals) is False!!")
        raise ValueError

    n_data = len(im_xs)
    U = np.vstack((im_xs, im_ys, [1.0] * n_data))

    R = make_R(rads)
    invK = np.linalg.inv(K)

    invK_u = np.dot(invK, U)
    R_invK_u = np.dot(R, invK_u)
    s_R_invK_u_t = dep_vals * R_invK_u + t
    K_s_R_invK_u_t = np.dot(K, s_R_invK_u_t)
    translated_u = K_s_R_invK_u_t / K_s_R_invK_u_t[2, :]

    return translated_u


def r(rads, t, im_xs, im_ys, dep_vals, I1, I2):
    if not (len(im_xs) == len(im_ys) == len(dep_vals)):
        print("len(im_xs) == len(im_ys) == len(dep_vals) is False!!")
        raise ValueError

    n_data = len(im_xs)
    transed_u = translate(rads=rads, t=t, im_xs=im_xs, im_ys=im_ys, dep_vals=dep_vals)

    diff_arr = np.empty((n_data, 1))
    for i in range(n_data):
        im_x1, im_y1 = im_xs[i], im_ys[i]
        im_x2, im_y2 = transed_u[0, i], transed_u[1, i]
        val1 = get_pixel(img=I1, x=im_x1, y=im_y1)  # I[im_y1, im_x1]
        val2 = get_pixel(img=I2, x=im_x2, y=im_y2)  # I[im_y2, im_x2]
        diff_arr[i, 0] = val1 - val2

    return diff_arr
영상I1에서 영상I2로 변환을 할 때의 오차를 계산하는 함수입니다. rads는 회전성분의 파라미터, t는 병진성분의 파라미터입니다. 이번에는 최적화하고자 하는 파라미터에 적당히 초기값을 주어, 조금씩 값을 보정해가는 방식으로 최적의 값에 가까워지게 합니다. GD와 같은 방식입니다. 단, 이번엔 가우스뉴턴법으로 하기 때문에 야코비안 JJ가 필요합니다.
J=(r1x1r1xnrmx1rmxn) J = \left( \begin{array}{c} \frac{\partial {\bf r}_1}{\partial {\bf x}_1} & \ldots & \frac{\partial {\bf r}_1}{\partial {\bf x}_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial {\bf r}_m}{\partial {\bf x}_1} & \ldots & \frac{\partial {\bf r}_m}{\partial {\bf x}_n} \end{array} \right)
이번엔 x1x_1에서x6x_6까지의 파라미터를 각각 α,β,γ,x,y,z\alpha, \beta, \gamma, x, y, z에 대응합니다. rr의 값은 영상의 픽셀수에 대응합니다. 640x480의 영상을 사용하는 경우, 640x480=307200가 되지만, 이렇게 큰 행렬을 사용하면 계산이 느려지고 맙니다. 영상 안에서도 하얀 벽이라든가 모양이 없는 부분은 경사가 없기 때문에 그 부분의 야코비안의 성분은 0이 되기 쉽고 최적화에 도움이 되지 않기 때문에, 경사가 있는 부분에 한정하여 JJ를 구하는 쪽이 효율이 좋습니다.
그렇기 때문에, 경사가 있는 영상좌표의 리스트를 취득하여 그 점군(클러스터)만을 사용하여 회전성분과 병진성분을 구합니다. 이것이 util.py의 r함수에 im_xs, im_ys, dep_vals 인수가 있는 이유입니다. 논문에서도 경사가 큰 부분에 한정하여 계산하고 있는 것 같습니다. 모든 픽셀을 사용하여 계산하면 컴퓨터의 팬이 너무 열심히 돌기 때문에 불안합니다.
r함수를 사용한 야코비안의 형태를 만듭니다.
rad_eps = np.pi / 900.0
t_eps = 1e-04

def grad_r(rads, t, im_xs, im_ys, dep_vals, I_transed, index=-1):
    if not (len(im_xs) == len(im_ys) == len(dep_vals)):
        print("len(im_xs) == len(im_ys) == len(dep_vals) is False!!")
        raise ValueError

    I = I_transed

    if index < 0 or 5 < index:
        print("index is out of range or not defined")
        raise ValueError

    n_data = len(im_xs)

    if index < 3:
        rads_p, rads_m = rads.copy(), rads.copy()
        rads_p[index] += rad_eps
        rads_m[index] -= rad_eps
        u_p = translate(rads=rads_p, t=t, im_xs=im_xs, im_ys=im_ys, dep_vals=dep_vals)
        u_m = translate(rads=rads_m, t=t, im_xs=im_xs, im_ys=im_ys, dep_vals=dep_vals)
    else:
        t_p, t_m = t.copy(), t.copy()
        t_p[index - 3, 0] += t_eps
        t_m[index - 3, 0] -= t_eps
        u_p = translate(rads=rads, t=t_p, im_xs=im_xs, im_ys=im_ys, dep_vals=dep_vals)
        u_m = translate(rads=rads, t=t_m, im_xs=im_xs, im_ys=im_ys, dep_vals=dep_vals)

    grad = np.empty(n_data, )
    for i in range(n_data):
        im_x_p, im_y_p = u_p[0, i], u_p[1, i]
        im_x_m, im_y_m = u_m[0, i], u_m[1, i]
        val_p = get_pixel(img=I, x=im_x_p, y=im_y_p)  # I[im_y_p, im_x_p]
        val_m = get_pixel(img=I, x=im_x_m, y=im_y_m)  # I[im_y_m, im_x_m]
        grad[i] = -(val_p - val_m)

    if index < 3:
        grad /= (2.0 * rad_eps)
    else:
        grad /= (2.0 * t_eps)

    return grad
코드는 적당하다고 말하고 있지만, 공개용으로 적합한 설계는 아닌 것 같은 느낌입니다. 변수I_transed는 r에서 말하는 I2에 해당합니다.변환하려고 하는 영상이라는 뜻입니다. 변수 index는 x1x_1부터x6x_6까지 어떤 변수에 대해 미분을 수행할지를 설정합니다. 각 파라미터마다 총 6회의 함수호출을 하면 야코비안이 구해집니다.
from util import *

J_T = None  # ヤコビアンの転置行列
for ind in range(6):
    grad = grad_r(rads=rads, t=t, im_xs=xs, im_ys=ys, dep_vals=dep_vals, I_transed=img2, index=ind)
    if J_T is None:
        J_T = np.copy(grad)
    else:
        J_T = np.vstack((J_T, grad))
실제로는 야코비안의 전치행렬(transpose)이 얻어집니다. 파라미터의 업데이트는 numpy로 하기 때문입니다. 감탄하지 않을 수 없습니다.
JJ = np.dot(J_T, J_T.T)
invJJ = np.linalg.inv(JJ)
invJJ_J = np.dot(invJJ, J_T)
invJJ_J_r = np.dot(invJJ_J, diff_arr)

rads -= invJJ_J_r[:3, 0]
t -= invJJ_J_r[3:, :]
위 설명에서 언급하지 않았지만, 영상 내의 경사가 큰 부분을 찾기 위해서는 라플라시안 필터를 곱하는 것으로 얻을 수 있습니다. 여기에는 cv2 모듈을 사용했습니다. 코드를 보는 것이 빠르므로 전부 올렸습니다.
from PIL import Image
import numpy as np
import cv2
from util import *

WIDTH = 640
HEIGHT = 480

data_dir = "XXX/living_room_traj2_frei_png/"

# 1 -> 2 의 변환을 구함
image_file_1 = data_dir + "rgb/10.png"
depth_file_1 = data_dir + "depth/10.png"
image_file_2 = data_dir + "rgb/30.png"

# get image
img1 = Image.open(image_file_1)
img1 = img1.resize([WIDTH, HEIGHT], Image.ANTIALIAS)
raw_img1 = cv2.cvtColor(np.array(img1), cv2.COLOR_BGR2GRAY)
img1 = raw_img1.astype('float64') / 255.0

dep1 = Image.open(depth_file_1)
dep1 = dep1.resize([WIDTH, HEIGHT], Image.ANTIALIAS)
dep1 = np.array(dep1, 'float64') / 5000.0

img2 = Image.open(image_file_2)
img2 = img2.resize([WIDTH, HEIGHT], Image.ANTIALIAS)
raw_img2 = cv2.cvtColor(np.array(img2), cv2.COLOR_BGR2GRAY)
img2 = raw_img2.astype('float64') / 255.0

# 변환하는 영상 위의 좌표 리스트와 depth 값의 리스트를 취득
lap1 = np.abs(cv2.Laplacian(raw_img1, cv2.CV_32F, ksize=5))
th = sorted(lap1.flatten())[::-1][2000]

xs, ys, dep_vals = list(), list(), list()
bias = 30
for y in range(bias, HEIGHT - bias):
    for x in range(bias, WIDTH - bias):
        if lap1[y, x] > th:
            xs.append(x)
            ys.append(y)
            dep_vals.append(dep1[y, x])

xs = np.array(xs, dtype=np.float64)
ys = np.array(ys, dtype=np.float64)
dep_vals = np.array(dep_vals, dtype=np.float64)

# 가우스 뉴턴 법에 따라 변환행렬을 구함
# 초기값의 설정
rads = np.array([0.0, 0.0, 0.0]).reshape(3, )
t = np.array([0.0, 0.0, 0.0]).reshape(3, 1)

# 일단, 10회 반복을 시킴
for _ in range(10):
    diff_arr = r(rads=rads, t=t, im_xs=xs, im_ys=ys, dep_vals=dep_vals, I1=img1, I2=img2)

    J_T = None  # 아코비안의 회전행렬
    for ind in range(6):
        grad = grad_r(rads=rads, t=t, im_xs=xs, im_ys=ys, dep_vals=dep_vals, I_transed=img2, index=ind)
        if J_T is None:
            J_T = np.copy(grad)
        else:
            J_T = np.vstack((J_T, grad))

    JJ = np.dot(J_T, J_T.T)
    invJJ = np.linalg.inv(JJ)
    invJJ_J = np.dot(invJJ, J_T)
    invJJ_J_r = np.dot(invJJ_J, diff_arr)

    rads -= invJJ_J_r[:3, 0]
    t -= invJJ_J_r[3:, :]

    print(rads.reshape(-1))
    print(t.reshape(-1))
    print("-----")

# img1의 각 픽셀이 회전행렬과 병진벡터에 따라 어떤 식으로 이동하는가하면, 대상(이동할 곳? 번역 어려움)에서 얻은 img2를 사용하여 img1을 추정한다.
out = transform_image(rads=rads, t=t, dep_img=dep1, I=img2)
out = (255.0 * out).astype(np.uint8)

cv2.imshow('output', cv2.hconcat((raw_img1, raw_img2, out)))
cv2.waitKey(0)
def transform_image(rads, t, dep_img, I):
    im_xs, im_ys = np.meshgrid(np.arange(WIDTH), np.arange(HEIGHT))
    im_xs = im_xs.reshape(-1)
    im_ys = im_ys.reshape(-1)
    dep_vals = dep_img.reshape(-1)
    transed_u = translate(rads=rads, t=t, im_xs=im_xs, im_ys=im_ys, dep_vals=dep_vals)

    out = np.zeros((HEIGHT, WIDTH))
    for i in range(len(im_xs)):
        im_x1, im_y1 = im_xs[i], im_ys[i]
        im_x2, im_y2 = transed_u[0, i], transed_u[1, i]
        try:
            out[im_y1, im_x1] = get_pixel(img=I, x=im_x2, y=im_y2)
        except:
            out[im_y1, im_x1] = 0.0

    return out

테스트

・원본 이미지(img1)
10.png
・변환할 대상(img2)
30.png
・추정했던 파라미터를 이용하여 원본 이미지의 영상을 복원(그레이 스케일)
output.png
소파의 경사가 보정되어 있어, 완벽이라고는 할 수 없지만, 나름대로는 회전성분과 병진성분의 계산이 되어 있는 것 같습니다. 이번엔ICL-NUIM RGB-D dataset을 이용해보았습니다. 다른 데이터 셋에는 시험해보지 않았기 때문에 시도해봐도 괜찮은지 확인해봐야 합니다.

CNN에 의한 depth추정 (CNN SLAM #3)

논문에는 영역분할 태스크도 CNN으로 풀고 있지만, 여기서는 depth영상의 추정에 한정해서 설명하고자 합니다. 어느쪽이라도 네트워크의 기본적인 구성은 대부분 동일하므로 CNN SLAM을 이해하기 위해서라면 신경쓰지 않아도 좋을 것 같습니다.

정답은 github에

https://github.com/iro-cp/FCRN-DepthPrediction
이것을 clone하면 depth추정을 할 수 있습니다!
$ git clone https://github.com/iro-cp/FCRN-DepthPrediction.git
$ cd FCRN-DepthPrediction/tensorflow
$ python predict.py <학습이 되어 있는 모델파일> <depth추정하고 싶은 RGB영상>
tensorflow이 필요하므로 설치합니다. 저는 Python3.5.3으로, tensorflow는 1.2.1를 사용했습니다.
학습이 되어 있는 모델파일은 위의 github 페이지의 README에 링크가 되어 있습니다. Tensorflow Model이라고 쓰여 있는 곳을 클릭하면 모델을 얻을 수 있으므로, 이쪽에서 학습시킬 필요가 없습니다.
depth추정 시키고 싶은 영상은 이쪽에서 준비합니다. 예를 들어, 아래와 같은 영상을 주어 추정시키려 하면…
이러한 출력을 얻을 수 있습니다.
실제 값을 알 수 없기 때문에 평가를 할 수 없지만, 형태로써는 나쁘지 않아 보입니다.

학습을 시키고 싶을때는?

학습도 시키고 싶을 때는 Network.py내의 Network클래스의 인수 trainable에 True를 주어, 정답 영상과의 차이를 계산하여, back propagation하는 코드를 새로 추가할 필요가 있습니다.
저 자신으로서는 fcrn.py에 있는 것 같은 방식을 처음봤기 때문에, 사용하는 방식을 잘 몰랐습니다. 따라서 제 자신에게 쉬운 방법으로 바꾼 것이 아래 입니다.
#network.py
'''
setup함수는 Network클래스 내의 멤버함수
'''

    def setup(self, trainable):
        xs = self.inputs['data']

        conv1 = self.conv(xs, 7, 7, 64, 2, 2, relu=False, name='conv1')
        bn_conv1 = self.batch_normalization(conv1, relu=True, name='bn_conv1')
        pool1 = self.max_pool(bn_conv1, 3, 3, 2, 2, name='pool1')
        res2a_branch1 = self.conv(pool1, 1, 1, 256, 1, 1, biased=False, relu=False, name='res2a_branch1')
        bn2a_branch1 = self.batch_normalization(res2a_branch1, name='bn2a_branch1')

        res2a_branch2a = self.conv(pool1, 1, 1, 64, 1, 1, biased=False, relu=False, name='res2a_branch2a')
        bn2a_branch2a = self.batch_normalization(res2a_branch2a, relu=True, name='bn2a_branch2a')
        res2a_branch2b = self.conv(bn2a_branch2a, 3, 3, 64, 1, 1, biased=False, relu=False, name='res2a_branch2b')
        bn2a_branch2b = self.batch_normalization(res2a_branch2b, relu=True, name='bn2a_branch2b')
        res2a_branch2c = self.conv(bn2a_branch2b, 1, 1, 256, 1, 1, biased=False, relu=False, name='res2a_branch2c')
        bn2a_branch2c = self.batch_normalization(res2a_branch2c, name='bn2a_branch2c')

        res2a = self.add((bn2a_branch1, bn2a_branch2c), name='res2a')
        res2a_relu = self.relu(res2a, name='res2a_relu')
        res2b_branch2a = self.conv(res2a_relu, 1, 1, 64, 1, 1, biased=False, relu=False, name='res2b_branch2a')
        bn2b_branch2a = self.batch_normalization(res2b_branch2a, relu=True, name='bn2b_branch2a')
        res2b_branch2b = self.conv(bn2b_branch2a, 3, 3, 64, 1, 1, biased=False, relu=False, name='res2b_branch2b')
        bn2b_branch2b = self.batch_normalization(res2b_branch2b, relu=True, name='bn2b_branch2b')
        res2b_branch2c = self.conv(bn2b_branch2b, 1, 1, 256, 1, 1, biased=False, relu=False, name='res2b_branch2c')
        bn2b_branch2c = self.batch_normalization(res2b_branch2c, name='bn2b_branch2c')

        res2b = self.add((res2a_relu, bn2b_branch2c), name='res2b')
        res2b_relu = self.relu(res2b, name='res2b_relu')
        res2c_branch2a = self.conv(res2b_relu, 1, 1, 64, 1, 1, biased=False, relu=False, name='res2c_branch2a')
        bn2c_branch2a = self.batch_normalization(res2c_branch2a, relu=True, name='bn2c_branch2a')
        res2c_branch2b = self.conv(bn2c_branch2a, 3, 3, 64, 1, 1, biased=False, relu=False, name='res2c_branch2b')
        bn2c_branch2b = self.batch_normalization(res2c_branch2b, relu=True, name='bn2c_branch2b')
        res2c_branch2c = self.conv(bn2c_branch2b, 1, 1, 256, 1, 1, biased=False, relu=False, name='res2c_branch2c')
        bn2c_branch2c = self.batch_normalization(res2c_branch2c, name='bn2c_branch2c')

        res2c = self.add((res2b_relu, bn2c_branch2c), name='res2c')
        res2c_relu = self.relu(res2c, name='res2c_relu')
        res3a_branch1 = self.conv(res2c_relu, 1, 1, 512, 2, 2, biased=False, relu=False, name='res3a_branch1')
        bn3a_branch1 = self.batch_normalization(res3a_branch1, name='bn3a_branch1')

        res3a_branch2a = self.conv(res2c_relu, 1, 1, 128, 2, 2, biased=False, relu=False, name='res3a_branch2a')
        bn3a_branch2a = self.batch_normalization(res3a_branch2a, relu=True, name='bn3a_branch2a')
        res3a_branch2b = self.conv(bn3a_branch2a, 3, 3, 128, 1, 1, biased=False, relu=False, name='res3a_branch2b')
        bn3a_branch2b = self.batch_normalization(res3a_branch2b, relu=True, name='bn3a_branch2b')
        res3a_branch2c = self.conv(bn3a_branch2b, 1, 1, 512, 1, 1, biased=False, relu=False, name='res3a_branch2c')
        bn3a_branch2c = self.batch_normalization(res3a_branch2c, name='bn3a_branch2c')

        res3a = self.add((bn3a_branch1, bn3a_branch2c), name='res3a')
        res3a_relu = self.relu(res3a, name='res3a_relu')
        res3b_branch2a = self.conv(res3a_relu, 1, 1, 128, 1, 1, biased=False, relu=False, name='res3b_branch2a')
        bn3b_branch2a = self.batch_normalization(res3b_branch2a, relu=True, name='bn3b_branch2a')
        res3b_branch2b = self.conv(bn3b_branch2a, 3, 3, 128, 1, 1, biased=False, relu=False, name='res3b_branch2b')
        bn3b_branch2b = self.batch_normalization(res3b_branch2b, relu=True, name='bn3b_branch2b')
        res3b_branch2c = self.conv(bn3b_branch2b, 1, 1, 512, 1, 1, biased=False, relu=False, name='res3b_branch2c')
        bn3b_branch2c = self.batch_normalization(res3b_branch2c, name='bn3b_branch2c')

        res3b = self.add((res3a_relu, bn3b_branch2c), name='res3b')
        res3b_relu = self.relu(res3b, name='res3b_relu')
        res3c_branch2a = self.conv(res3b_relu, 1, 1, 128, 1, 1, biased=False, relu=False, name='res3c_branch2a')
        bn3c_branch2a = self.batch_normalization(res3c_branch2a, relu=True, name='bn3c_branch2a')
        res3c_branch2b = self.conv(bn3c_branch2a, 3, 3, 128, 1, 1, biased=False, relu=False, name='res3c_branch2b')
        bn3c_branch2b = self.batch_normalization(res3c_branch2b, relu=True, name='bn3c_branch2b')
        res3c_branch2c = self.conv(bn3c_branch2b, 1, 1, 512, 1, 1, biased=False, relu=False, name='res3c_branch2c')
        bn3c_branch2c = self.batch_normalization(res3c_branch2c, name='bn3c_branch2c')

        res3c = self.add((res3b_relu, bn3c_branch2c), name='res3c')
        res3c_relu = self.relu(res3c, name='res3c_relu')
        res3d_branch2a = self.conv(res3c_relu, 1, 1, 128, 1, 1, biased=False, relu=False, name='res3d_branch2a')
        bn3d_branch2a = self.batch_normalization(res3d_branch2a, relu=True, name='bn3d_branch2a')
        res3d_branch2b = self.conv(bn3d_branch2a, 3, 3, 128, 1, 1, biased=False, relu=False, name='res3d_branch2b')
        bn3d_branch2b = self.batch_normalization(res3d_branch2b, relu=True, name='bn3d_branch2b')
        res3d_branch2c = self.conv(bn3d_branch2b, 1, 1, 512, 1, 1, biased=False, relu=False, name='res3d_branch2c')
        bn3d_branch2c = self.batch_normalization(res3d_branch2c, name='bn3d_branch2c')

        res3d = self.add((res3c_relu, bn3d_branch2c), name='res3d')
        res3d_relu = self.relu(res3d, name='res3d_relu')
        res4a_branch1 = self.conv(res3d_relu, 1, 1, 1024, 2, 2, biased=False, relu=False, name='res4a_branch1')
        bn4a_branch1 = self.batch_normalization(res4a_branch1, name='bn4a_branch1')

        res4a_branch2a = self.conv(res3d_relu, 1, 1, 256, 2, 2, biased=False, relu=False, name='res4a_branch2a')
        bn4a_branch2a = self.batch_normalization(res4a_branch2a, relu=True, name='bn4a_branch2a')
        res4a_branch2b = self.conv(bn4a_branch2a, 3, 3, 256, 1, 1, biased=False, relu=False, name='res4a_branch2b')
        bn4a_branch2b = self.batch_normalization(res4a_branch2b, relu=True, name='bn4a_branch2b')
        res4a_branch2c = self.conv(bn4a_branch2b, 1, 1, 1024, 1, 1, biased=False, relu=False, name='res4a_branch2c')
        bn4a_branch2c = self.batch_normalization(res4a_branch2c, name='bn4a_branch2c')

        res4a = self.add((bn4a_branch1, bn4a_branch2c), name='res4a')
        res4a_relu = self.relu(res4a, name='res4a_relu')
        res4b_branch2a = self.conv(res4a_relu, 1, 1, 256, 1, 1, biased=False, relu=False, name='res4b_branch2a')
        bn4b_branch2a = self.batch_normalization(res4b_branch2a, relu=True, name='bn4b_branch2a')
        res4b_branch2b = self.conv(bn4b_branch2a, 3, 3, 256, 1, 1, biased=False, relu=False, name='res4b_branch2b')
        bn4b_branch2b = self.batch_normalization(res4b_branch2b, relu=True, name='bn4b_branch2b')
        res4b_branch2c = self.conv(bn4b_branch2b, 1, 1, 1024, 1, 1, biased=False, relu=False, name='res4b_branch2c')
        bn4b_branch2c = self.batch_normalization(res4b_branch2c, name='bn4b_branch2c')

        res4b = self.add((res4a_relu, bn4b_branch2c), name='res4b')
        res4b_relu = self.relu(res4b, name='res4b_relu')
        res4c_branch2a = self.conv(res4b_relu, 1, 1, 256, 1, 1, biased=False, relu=False, name='res4c_branch2a')
        bn4c_branch2a = self.batch_normalization(res4c_branch2a, relu=True, name='bn4c_branch2a')
        res4c_branch2b = self.conv(bn4c_branch2a, 3, 3, 256, 1, 1, biased=False, relu=False, name='res4c_branch2b')
        bn4c_branch2b = self.batch_normalization(res4c_branch2b, relu=True, name='bn4c_branch2b')
        res4c_branch2c = self.conv(bn4c_branch2b, 1, 1, 1024, 1, 1, biased=False, relu=False, name='res4c_branch2c')
        bn4c_branch2c = self.batch_normalization(res4c_branch2c, name='bn4c_branch2c')

        res4c = self.add((res4b_relu, bn4c_branch2c), name='res4c')
        res4c_relu = self.relu(res4c, name='res4c_relu')
        res4d_branch2a = self.conv(res4c_relu, 1, 1, 256, 1, 1, biased=False, relu=False, name='res4d_branch2a')
        bn4d_branch2a = self.batch_normalization(res4d_branch2a, relu=True, name='bn4d_branch2a')
        res4d_branch2b = self.conv(bn4d_branch2a, 3, 3, 256, 1, 1, biased=False, relu=False, name='res4d_branch2b')
        bn4d_branch2b = self.batch_normalization(res4d_branch2b, relu=True, name='bn4d_branch2b')
        res4d_branch2c = self.conv(bn4d_branch2b, 1, 1, 1024, 1, 1, biased=False, relu=False, name='res4d_branch2c')
        bn4d_branch2c = self.batch_normalization(res4d_branch2c, name='bn4d_branch2c')

        res4d = self.add((res4c_relu, bn4d_branch2c), name='res4d')
        res4d_relu = self.relu(res4d, name='res4d_relu')
        res4e_branch2a = self.conv(res4d_relu, 1, 1, 256, 1, 1, biased=False, relu=False, name='res4e_branch2a')
        bn4e_branch2a = self.batch_normalization(res4e_branch2a, relu=True, name='bn4e_branch2a')
        res4e_branch2b = self.conv(bn4e_branch2a, 3, 3, 256, 1, 1, biased=False, relu=False, name='res4e_branch2b')
        bn4e_branch2b = self.batch_normalization(res4e_branch2b, relu=True, name='bn4e_branch2b')
        res4e_branch2c = self.conv(bn4e_branch2b, 1, 1, 1024, 1, 1, biased=False, relu=False, name='res4e_branch2c')
        bn4e_branch2c = self.batch_normalization(res4e_branch2c, name='bn4e_branch2c')

        res4e = self.add((res4d_relu, bn4e_branch2c), name='res4e')
        res4e_relu = self.relu(res4e, name='res4e_relu')
        res4f_branch2a = self.conv(res4e_relu, 1, 1, 256, 1, 1, biased=False, relu=False, name='res4f_branch2a')
        bn4f_branch2a = self.batch_normalization(res4f_branch2a, relu=True, name='bn4f_branch2a')
        res4f_branch2b = self.conv(bn4f_branch2a, 3, 3, 256, 1, 1, biased=False, relu=False, name='res4f_branch2b')
        bn4f_branch2b = self.batch_normalization(res4f_branch2b, relu=True, name='bn4f_branch2b')
        res4f_branch2c = self.conv(bn4f_branch2b, 1, 1, 1024, 1, 1, biased=False, relu=False, name='res4f_branch2c')
        bn4f_branch2c = self.batch_normalization(res4f_branch2c, name='bn4f_branch2c')

        res4f = self.add((res4e_relu, bn4f_branch2c), name='res4f')
        res4f_relu = self.relu(res4f, name='res4f_relu')
        res5a_branch1 = self.conv(res4f_relu, 1, 1, 2048, 2, 2, biased=False, relu=False, name='res5a_branch1')
        bn5a_branch1 = self.batch_normalization(res5a_branch1, name='bn5a_branch1')

        res5a_branch2a = self.conv(res4f_relu, 1, 1, 512, 2, 2, biased=False, relu=False, name='res5a_branch2a')
        bn5a_branch2a = self.batch_normalization(res5a_branch2a, relu=True, name='bn5a_branch2a')
        res5a_branch2b = self.conv(bn5a_branch2a, 3, 3, 512, 1, 1, biased=False, relu=False, name='res5a_branch2b')
        bn5a_branch2b = self.batch_normalization(res5a_branch2b, relu=True, name='bn5a_branch2b')
        res5a_branch2c = self.conv(bn5a_branch2b, 1, 1, 2048, 1, 1, biased=False, relu=False, name='res5a_branch2c')
        bn5a_branch2c = self.batch_normalization(res5a_branch2c, name='bn5a_branch2c')

        res5a = self.add((bn5a_branch1, bn5a_branch2c), name='res5a')
        res5a_relu = self.relu(res5a, name='res5a_relu')
        res5b_branch2a = self.conv(res5a_relu, 1, 1, 512, 1, 1, biased=False, relu=False, name='res5b_branch2a')
        bn5b_branch2a = self.batch_normalization(res5b_branch2a, relu=True, name='bn5b_branch2a')
        res5b_branch2b = self.conv(bn5b_branch2a, 3, 3, 512, 1, 1, biased=False, relu=False, name='res5b_branch2b')
        bn5b_branch2b = self.batch_normalization(res5b_branch2b, relu=True, name='bn5b_branch2b')
        res5b_branch2c = self.conv(bn5b_branch2b, 1, 1, 2048, 1, 1, biased=False, relu=False, name='res5b_branch2c')
        bn5b_branch2c = self.batch_normalization(res5b_branch2c, name='bn5b_branch2c')

        res5b = self.add((res5a_relu, bn5b_branch2c), name='res5b')
        res5b_relu = self.relu(res5b, name='res5b_relu')
        res5c_branch2a = self.conv(res5b_relu, 1, 1, 512, 1, 1, biased=False, relu=False, name='res5c_branch2a')
        bn5c_branch2a = self.batch_normalization(res5c_branch2a, relu=True, name='bn5c_branch2a')
        res5c_branch2b = self.conv(bn5c_branch2a, 3, 3, 512, 1, 1, biased=False, relu=False, name='res5c_branch2b')
        bn5c_branch2b = self.batch_normalization(res5c_branch2b, relu=True, name='bn5c_branch2b')
        res5c_branch2c = self.conv(bn5c_branch2b, 1, 1, 2048, 1, 1, biased=False, relu=False, name='res5c_branch2c')
        bn5c_branch2c = self.batch_normalization(res5c_branch2c, name='bn5c_branch2c')

        res5c = self.add((res5b_relu, bn5c_branch2c), name='res5c')
        res5c_relu = self.relu(res5c, name='res5c_relu')
        layer1 = self.conv(res5c_relu, 1, 1, 1024, 1, 1, biased=True, relu=False, name='layer1')
        layer1_BN = self.batch_normalization(layer1, relu=False, name='layer1_BN')
        layer2 = self.up_project(layer1_BN, [3, 3, 1024, 512], id='2x', stride=1, BN=True)
        layer3 = self.up_project(layer2, [3, 3, 512, 256], id='4x', stride=1, BN=True)
        layer4 = self.up_project(layer3, [3, 3, 256, 128], id='8x', stride=1, BN=True)
        layer5 = self.up_project(layer4, [3, 3, 128, 64], id='16x', stride=1, BN=True)
        layer5_drop = self.dropout(layer5, name='drop', keep_prob=1.)
        self.predict = self.conv(layer5_drop, 3, 3, 1, 1, 1, name='ConvPred')

        if trainable:
            ts = self.inputs['true']
            differ = tf.subtract(x=self.predict, y=ts)
            abs_differ = tf.abs(differ)
            self.loss = tf.reduce_mean(abs_differ, name='loss')
            self.train_step = tf.train.GradientDescentOptimizer(0.001).minimize(self.loss)
self.inputs는 딕셔너리 타입으로, trainable==True일 때는 입력영상을 넣는 placeholder와 정답영상을 넣는 placeholder가 있어야 하고, False일 때는 입력영상을 넣는 placeholder만 준비해두면 프로그램이 제대로 돌아가도록 하였습니다.
아직 조사중이지만, loss 함수에 따라 추정 정확도에 차이가 생기는 것 같습니다. 추가 학습을 시킬 때는 가장 정확도가 높은 loss 함수를 선택하고 싶네요.