ドイツ人と日本人のあいだ

ということで、仕事の話なんですが、挟まれてます(笑)どのくらい挟まれているかというと、タマゴサンドの卵の部分がパンの外に全部出てくるくらい挟まれてます。。。

GW明けからずっとドイツに来てまして、ひたすら大味飯(失礼!)を食べてました。なんだか今年はドイツ年になりそうで、、、ロボットをやる時間がなかなか取れなさそうです。。。うーん。どうしたもんかしら。

四年ぶりのハイデルベルク
f:id:rkoichi2001:20180527175731j:plain

フランクフルトのコインランドリー!見つかるまで、パンツ二連続で履いてたことは内緒ですが。
f:id:rkoichi2001:20180527175906j:plain

今からスイスです!
f:id:rkoichi2001:20180527175951j:plain

関数の最適化 ~勾配法~

バンドル調整に向けて,数学の復習をしておきます.参考文献はいつものごとく金谷先生の本です.

最適化とは

ある制約条件のもとで,ある関数の値を最大または最小にするような変数の値を求めること.

最大値・最小値を求めるためのトリック

ニュートン法,勾配法,レーベンバーグ・マーカート法等,非線形最適化をするアルゴリズムはたくさんありますが,基本となっている考え方は,,,

関数 f(x) の極値では,一次導関数 f'(x) = 0 となる.

です.結局アルゴリズムが違っても,f'(x) = 0 となるような x を探しているという点では同じです.なので,陰関数 f(x) = 0 を解くことができるツールであれば,最適化にも使えると思ってよいと思います.最適化の場合,解くべき陰関数が関数 f(x) ではなく,一次導関数 f'(x) となります.

一変数の場合の勾配法

まず導入として,一変数関数の場合の勾配法を考えます.基本となる考え方はシンプルで,上に凸の関数の場合,勾配がプラス(マイナス)の間はずっと x を増加(減少)させます.上に凸の関数であれば,この作業を続けていればどこかで必ず勾配がマイナス(プラス)になります.この間のどこかに勾配が0である場所があると考えられるので,これを求める最適解として採用します.
f:id:rkoichi2001:20180503161215j:plain

一変数の場合の勾配法アルゴリズム

最大値を求める場合の勾配法アルゴリズムは下記のとおりです.傾きの符号が変化するまで同一方向に進み,符号変化を検知した時点で進む距離(ステップ幅)を減らして反対方向に進む.という作業を繰り返し,最後には f'(x) = 0 を満たす変数を求めます.
f:id:rkoichi2001:20180503164237j:plain

多変数の場合の勾配法

少し複雑になりますが,多変数の場合も考え方は同じです.多変数の場合も曲面に対して接平面を考え,この接平面の傾きが一番大きい方向を基準に進んでいきます.で,この方向に進んでいく中で一番大きい値を探索します.最大値を見つけたら,その点に関して接平面を考え再び傾きが一番大きい方向を基準に進んでいき....と繰り返せば最大値が求まります.
f:id:rkoichi2001:20180503172209j:plain

多変数の場合の勾配法アルゴリズム

f:id:rkoichi2001:20180503172250j:plain

一変数の勾配法の実験 with Python

ということで,勾配法を実装してみました.まずは一変数の場合の勾配法です.最大値を求めたい関数として,下記の関数を考えます.

f(x) = sin(x) + cos(x/2 -1)
一変数の勾配法による実験結果

下記実験結果ですが,緑色の点が反復の途中仮定,赤色の点が最終結果になります.最大値の回りで反復が終わっていることがわかります.
f:id:rkoichi2001:20180504001838p:plain

多変数の勾配法の実験 with Python

ついで,多変数の勾配法です.変数の数が多いと可視化が難しいので,二変数の関数に対して最大値を求めます.

f(x, y) = -(15*(x-6)**2 + 2*(x-6)*(y-4) + 3*(y-4)**2 + 4*(x-6) + 3*(y-4) - 15)
多変数の勾配法による実験結果

下記実験結果ですが,楕円のてっぺんが最大値になります.各点で等高線に垂直な方向に進み,最終的には最大値にたどり着いている様子がわかります.緑色の点が途中経過で,黄色の点が最終解です.
f:id:rkoichi2001:20180504060114p:plain

Pythonコード

下記,まだちょっとおかしいです....収束しないときが多々あり...また時間ができたら見直します.

gradient_method.py

# -*- coding: utf-8 -*-

import numpy as np

def derivativeNd(func, xVec, d):
  dVec = np.zeros(xVec.shape)
  
  for i in range(xVec.shape[0]):
    xVecP = np.copy(xVec)
    xVecN = np.copy(xVec)
    
    xVecP[i] = xVec[i] + d/2
    xVecN[i] = xVec[i] - d/2

    dVec[i] = (func(xVecP) - func(xVecN)) / d

  return dVec

def differentiate(func, xVec, dirVec, d):

  dVec = np.zeros(xVec.shape)  
  dx = dirVec / np.linalg.norm(dirVec) * d
  
  for i in range(xVec.shape[0]):
    xVecP = np.copy(xVec)
    xVecN = np.copy(xVec)
    
    xVecP[i] = xVec[i] + dx[i] / 2
    xVecN[i] = xVec[i] - dx[i] / 2
  
    dVec[i] = (func(xVecP) - func(xVecN)) / d  

  return dVec
  
def hill_climbNd(func, xVecInit, stepInit, ipu):
  
  xVec = np.copy(xVecInit)
  xVecOld = np.copy(xVecInit)
  step = stepInit
  
  history = np.zeros((0, xVec.shape[0]))
  history = np.append(history, np.array([xVec]), axis=0)
  
  while True:
    xVecOld = np.copy(xVec)
    dirVec = derivativeNd(func, xVec, 0.000001)
    xVec, tmp = hill_climb_to_dir(func, xVec, dirVec, step, ipu)
    history = np.append(history, np.array([xVec]), axis=0)
    
    if np.linalg.norm(xVec - xVecOld) <= ipu:
      break
  
  return xVec, history

def hill_climb_to_dir(func, xVecInit, dirVec, stepInit, ipu):

  x = xVecInit
  step = stepInit
  d = 0.00001
  normDirVec = dirVec / np.linalg.norm(dirVec)
  history = [x]
  MINSTEP = 1.0e-15  

  while True:
    # Step 2 : Setup Step Length
    dfVec = differentiate(func, x, normDirVec, min(d, abs(step)/1000))
    
    if np.linalg.norm(dfVec) <= ipu:
      break
    
    sign = 1 if np.dot(dfVec, normDirVec) > 0 else -1   
    xb = x
    xa = x + sign * abs(step) * normDirVec
    
    # Step 3 : Find Another Side of Mountain.
    if func(xb) < func(xa):
      while True:
        step = 2*step
        xb = xa
        xa = xb + sign * abs(step) * normDirVec
        
        if func(xb) >= func(xa):
          break
      x = xb
      step = max(step/2, MINSTEP)
        
    # Step 4 : Overrun and Reset.
    else:
      while True:
        step = max(step/2, MINSTEP)
        xa = xa - sign * abs(step) * normDirVec
        
        if func(xb) <= func(xa):
          break
          
      x = xa
      step = 2*step
    
    # Step 5 : Convergence Check
    dfVec = differentiate(func, x, normDirVec, min(d, abs(step)/1000))   
    history.append(x)
    
    if np.linalg.norm(dfVec) < ipu:
      break
  
  return x, np.array(history)

test_gradient_method.py

# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt 
import numpy as np
import math

from optimization.gradient_method import hill_climb_to_dir
from optimization.gradient_method import hill_climbNd

def test_func1d(xVec):
  return math.sin(xVec[0]) + np.cos(xVec[0]/2 - 1)

def test_func2d(xVec):
  x = xVec[0]
  y = xVec[1]
  return (-(15*(x-6)**2 + 2*(x-6)*(y-4) + 3*(y-4)**2 + 4*(x-6) + 3*(y-4) - 15))
  
def test_hill_climb1d():
  
  x = np.linspace(-5, 5, 101)
  y = np.zeros(x.shape[0])
  for i in range(x.shape[0]):
    y[i] = test_func1d(np.array([x[i]]))
    
  ans, hist = hill_climb_to_dir(test_func1d, np.array([-2.0]),\
                                np.array([1.0]), 0.1, 0.00001)
  
  xVecInit = np.array([-2.0], dtype='float64')
  stepWidth = 1.0
  termCondition = 0.01
  xVec, history = \
    hill_climbNd(test_func1d, xVecInit, stepWidth, termCondition)
  
  fig = plt.figure()
  ax = fig.add_subplot(1, 1, 1)
  ax.plot(x, y)  
  
  for i in range(hist.shape[0]):
    x = hist[i]
    y = test_func1d(x)
    if i == hist.shape[0] - 1:
      size = 50
      color = 'red'
    else:
      size = 10
      color = 'green'
      
    ax.scatter(x, y, s=size, c=color, edgecolors=color)
    
  plt.show()

  
def test_hill_climb2d():
  
  x1 = np.linspace(-10, 10, 101)
  x2 = np.linspace(-10, 10, 101)
  X1, X2 = np.meshgrid(x1, x2)
  X = np.c_[np.ravel(X1), np.ravel(X2)]
  
  Z = np.empty((0, 1))
  for i in range(X.shape[0]):
    z = test_func2d(X[i])
    Z = np.append(Z, [[z]], axis=0)
  Z = np.reshape(Z, (X1.shape[0], X2.shape[0]))

  xVecInit = np.array([-3.0, -3.0], dtype='float64')
  stepWidth = 1.0
  termCondition = 0.01
  xVec, history = \
    hill_climbNd(test_func2d, xVecInit, stepWidth, termCondition)
  
  fig = plt.figure()
  ax = fig.add_subplot(1, 1, 1)

  values = set()
  for i in range(history.shape[0]):
    xVec = history[i]
    values.add(test_func2d(xVec))
  
  cont = ax.contourf(X1, X2, Z)
  for i in range(cont.levels.shape[0]):
    values.add(cont.levels[i])
  
  levels = list(values)
  levels.sort()
  ax.contourf(X1, X2, Z, levels)

  for i in range(history.shape[0]):
    xVec = history[i]

    if i == history.shape[0] - 1:
      size = 50
      color = 'yellow'
    else:
      size = 10
      color = 'green'      
    ax.scatter(xVec[0], xVec[1], s=size, c=color, edgecolors=color)
    
  plt.show()
  
if __name__ == "__main__":
  print("Gradient Method Test")
  
  #test_hill_climb1d()  
  test_hill_climb2d()

参考文献

これなら分かる最適化数学―基礎原理から計算手法まで

これなら分かる最適化数学―基礎原理から計算手法まで

最適化計算3 基礎行列の推定 実験編

下記のエントリで基礎行列計算の理論編を勉強しました.ということで,いつものごとくPythonを使って実験です.
daily-tech.hatenablog.com

問題設定

下記のように二次曲面の形状を設置し,この物体を複数の視点に置いたカメラで撮影する状況を考えます.
f:id:rkoichi2001:20180430142030j:plain

対称物体の投影図

OpenCVのProjectPoints関数を用いて,上図の三視点に置かれたカメラで物体を撮影します.

f:id:rkoichi2001:20180430143400p:plainf:id:rkoichi2001:20180430143331p:plainf:id:rkoichi2001:20180430143414p:plain

OpenCVのProjectPoint関数の定義は下記のようになっており,物体の座標系で点を指定することができます.物体の座標系とカメラの座標系の関係を tvecs, rvecs に指定してあげれば,入力点群が自動的にカメラ座標系に変換され,そののちに画像に投影されます.

cv2.projectPoints(pntsInObj, rvec2, tvec2, cameraMatrix, distCoeffs)

一点注意点としては,この時の rvecとtvecですが,下記数式の t と R (を Rodrigues形式に変換したもの) を渡してあげる必要があります.つまり,物体座標系からカメラ座標系への変換式の回転要素 R と並進要素 T を渡してあげればOKです.
CAMXP = CAMX t + CAMX R OBJOBJP

基礎行列の計算

今回の実験では,図の CAM1 と CAM2 の位置関係(既知)を使って基礎行列真値を計算します.そのあとに2画像の投影点ペアを用いて基礎行列を計算し,真値にたいしての誤差が計算方法によってどの程度変動するかを調べます.まず,既知の位置関係を使って,エピポーラ拘束式が満たされていることを確認します.二つの視点を図示したものが下記になります.
f:id:rkoichi2001:20180430153250j:plain

最後のスクリプトを実行すると,下記の結果が得られてエピポーラ拘束条件が満たされていることがわかります.

Max Element of m1Fm2 :  2.22044604925e-16
Max Element of m2Fm1 :  1.66533453694e-16

実験結果

ちょっと時間がないので,「最小二乗法 + SVDによるランク補正」と「Taubin法 + SVDによるランク補正」の二つの実行結果を載せます.基礎行列真値に対して,添付のスクリプトを実行すると下記の結果が得られました.一応,最小二乗法のよりも Taubin 法のほうが結果がかなりいいことがわかります.また時間ができたらほかのスキームも作って更新します.

Accuracy check for each numerical scheme.
FTrue VS FLSSVD       :  0.00273989625189
FTrue VS FTaubinSVD   :  0.000641120755582

Python スクリプト

ちょっとコードが長くなってしまったのですが,一応載せておきます.

test_fund_mat.py
# -*- coding: utf-8 -*-

import cv2
import numpy as np
from geometry_k.rotation import RMatFromEulerAngle  
from geometry_k.rotation import OuterProductOperator

from fund_mat import calc_fund_mat
from fund_mat import normalize_mat
from fund_mat import FundMatCalcMode
from fund_mat import calc_dist_between_2F

from data_manipulation.noise import add_gauss_noise

# Camera Intrinsic Parameter
f = 160
width = 640
height = 480
pp = (width / 2, height / 2)
cameraMatrix = np.matrix(np.array([[f, 0, pp[0]],
                           [0, f, pp[1]],
                           [0, 0, 1]], dtype = "double"))
distCoeffs = np.zeros((5, 1))

# Camera Extrinsic Parameter 0
xEul_C0_to_OBJ = 0
yEul_C0_to_OBJ = 0
zEul_C0_to_OBJ = 0
ROBJ_to_0 = RMatFromEulerAngle(xEul_C0_to_OBJ, yEul_C0_to_OBJ, zEul_C0_to_OBJ)
C0_to_OBJ_in_C0 = (0, 0, 10)


# Camera Extrinsic Parameter 1
xEul_C1_to_OBJ = 0
yEul_C1_to_OBJ = -45
zEul_C1_to_OBJ = 0
ROBJ_to_1 = RMatFromEulerAngle(xEul_C1_to_OBJ, yEul_C1_to_OBJ, zEul_C1_to_OBJ)
C1_to_OBJ_in_C1 = (0, 0, 10)


# Camera Extrinsic Parameter 2
xEul_C2_to_OBJ = 0
yEul_C2_to_OBJ = 45
zEul_C2_to_OBJ = 0
ROBJ_to_2 = RMatFromEulerAngle(xEul_C2_to_OBJ, yEul_C2_to_OBJ, zEul_C2_to_OBJ)
C2_to_OBJ_in_C2 = (0, 0, 10)


pntRow = 10
pntCol = 10
depth = 0.15


if __name__ == "__main__":
  print("Fundamental Mat Test")
    
  # Create Curve Surface in 3D World
  pntsInObj = np.zeros((0, 3))
  for i in range(pntCol + 1):
    for j in range(pntRow + 1):
      x = (j - pntCol / 2)
      y = (i - pntRow / 2)
      z = depth * x**2
      a = np.array([[x, y, z]])
      pntsInObj = np.append(pntsInObj, a, axis=0)
  
  # Calculate Translation, Rotation for Each Camera.
  rvec0, jac = cv2.Rodrigues(ROBJ_to_0)
  tvec0 = np.eye(3) * np.matrix(C0_to_OBJ_in_C0).T
  rvec1, jac = cv2.Rodrigues(ROBJ_to_1)
  tvec1 = np.eye(3) * np.matrix(C1_to_OBJ_in_C1).T
  rvec2, jac = cv2.Rodrigues(ROBJ_to_2)
  tvec2 = np.eye(3) * np.matrix(C2_to_OBJ_in_C2).T
  
  # Convert Points on 3D Curved Surface for Each Cam Image Points.
  imgPnts0, jac = cv2.projectPoints(pntsInObj, rvec0, tvec0, cameraMatrix, distCoeffs)
  imgPnts1, jac = cv2.projectPoints(pntsInObj, rvec1, tvec1, cameraMatrix, distCoeffs)
  imgPnts2, jac = cv2.projectPoints(pntsInObj, rvec2, tvec2, cameraMatrix, distCoeffs)
  
  # Draw Points
  img0 = np.full((height, width, 3), (255, 255, 255), np.uint8)
  img1 = np.full((height, width, 3), (255, 255, 255), np.uint8)
  img2 = np.full((height, width, 3), (255, 255, 255), np.uint8)
  
  for pnt in imgPnts0:
    cv2.circle(img0, (int(pnt[0][0]), int(pnt[0][1])), 3, (0, 0, 0), -1)
  for pnt in imgPnts1:
    cv2.circle(img1, (int(pnt[0][0]), int(pnt[0][1])), 3, (255, 0, 0), -1)
  for pnt in imgPnts2:
    cv2.circle(img2, (int(pnt[0][0]), int(pnt[0][1])), 3, (0, 0, 255), -1)
  
  # Calculate True Fundamental Matrix
  R1_to_2 = ROBJ_to_2 * ROBJ_to_1.T
  T1_in_C2 = np.matrix(C2_to_OBJ_in_C2).T - R1_to_2 * np.matrix(C1_to_OBJ_in_C1).T
  tMat_1_in_C2 = OuterProductOperator(T1_in_C2)
  Ainv = np.linalg.inv(cameraMatrix)
  Ftrue21 = normalize_mat(Ainv.T * tMat_1_in_C2 * R1_to_2 * Ainv)
  

  R2_to_1 = ROBJ_to_1 * ROBJ_to_2.T
  T2_in_C1 = np.matrix(C1_to_OBJ_in_C1).T - R2_to_1 * np.matrix(C2_to_OBJ_in_C2).T
  tMat_2_in_C1 = OuterProductOperator(T2_in_C1)
  Ainv = np.linalg.inv(cameraMatrix)
  Ftrue12 = normalize_mat(Ainv.T * tMat_2_in_C1 * R2_to_1 * Ainv)
  
  # Denoise Img Points
  noisedImgPnts1 = add_gauss_noise(imgPnts1, 0, 2)
  noisedImgPnts2 = add_gauss_noise(imgPnts2, 0, 2)
  #noisedImgPnts1 = imgPnts1
  #noisedImgPnts2 = imgPnts2
  
  
  # Calculate Fundamental Matrix 
  Fls = calc_fund_mat(noisedImgPnts1, noisedImgPnts2, FundMatCalcMode.LS)
  FlsSVD = calc_fund_mat(noisedImgPnts1, noisedImgPnts2, FundMatCalcMode.LSSVD)
  Ftaubin = calc_fund_mat(noisedImgPnts1, noisedImgPnts2, FundMatCalcMode.Taubin)
  FtaubinSVD = calc_fund_mat(noisedImgPnts1, noisedImgPnts2, FundMatCalcMode.TaubinSVD)
  
  m1Fm2 = []
  m2Fm1 = []
  for i in range(imgPnts1.shape[0]):
    m1 = (np.matrix([imgPnts1[i][0][0], imgPnts1[i][0][1], 1.0])).T
    m2 = (np.matrix([imgPnts2[i][0][0], imgPnts2[i][0][1], 1.0])).T
    m1Fm2.append(abs((m1.T * Ftrue12 * m2)[0, 0]))
    m2Fm1.append(abs((m2.T * Ftrue21 * m1)[0, 0]))

  print()
  print("Confirm if true F mat really satisfies epipolar equation.")
  print("Max Element of m1Fm2 : ", max(m1Fm2))
  print("Max Element of m2Fm1 : ", max(m2Fm1))
  
  print()
  print("Accuracy check for each numerical scheme.")
  print("FTrue VS FLS          : ", calc_dist_between_2F(Ftrue12, Fls))
  print("FTrue VS FLSSVD       : ", calc_dist_between_2F(Ftrue12, FlsSVD))
  print("FTrue VS FTaubin      : ", calc_dist_between_2F(Ftrue12, Ftaubin))
  print("FTrue VS FTaubinSVD   : ", calc_dist_between_2F(Ftrue12, FtaubinSVD))
    
  # Display
  cv2.imshow("CAM0", cv2.resize(img0, None, fx = 0.5, fy = 0.5))
  cv2.imshow("CAM1", cv2.resize(img1, None, fx = 0.5, fy = 0.5))
  cv2.imshow("CAM2", cv2.resize(img2, None, fx = 0.5, fy = 0.5))
  cv2.waitKey(0)
fund_mat.py
# -*- coding: utf-8 -*-

import numpy as np
import scipy.linalg as splinalg
from enum import Enum

def normalize_mat(F):
  sumMat = 0
  for j in range(F.shape[0]):
    for i in range(F.shape[1]):
      sumMat = sumMat + F[j, i]**2
  
  return F / sumMat**0.5

def create_cov_mat(dataVec1, dataVec2, f0):
  x1 = dataVec1[0, 0]
  y1 = dataVec1[1, 0]
  x2 = dataVec2[0, 0]
  y2 = dataVec2[1, 0]
  
  cov = np.matrix(np.zeros((9, 9)))
  cov[0, 0] = x1**2 + x2**2
  cov[0, 1] = x2 * y2
  cov[0, 2] = f0 * x2
  cov[0, 3] = x1 * y1
  cov[0, 6] = f0 * x1

  cov[1, 0] = x2 * y2
  cov[1, 1] = x1**2 + y2**2
  cov[1, 2] = f0 * y2
  cov[1, 4] = x1 * y1
  cov[1, 7] = f0 * x1

  cov[2, 0] = f0 * x2
  cov[2, 1] = f0 * y2
  cov[2, 2] = f0**2

  cov[3, 0] = x1 * y1
  cov[3, 3] = y1**2 + x2**2
  cov[3, 4] = x2 * y2
  cov[3, 5] = f0 * x2
  cov[3, 6] = f0 * y1

  cov[4, 1] = x1 * y1
  cov[4, 3] = x2 * y2
  cov[4, 4] = y1**2 + y2**2
  cov[4, 5] = f0 * y2
  cov[4, 7] = f0 * y1

  cov[5, 3] = f0 * x2
  cov[5, 4] = f0 * y2
  cov[5, 5] = f0**2

  cov[6, 0] = f0 * x1
  cov[6, 3] = f0 * y1
  cov[6, 6] = f0**2

  cov[7, 1] = f0 * x1
  cov[7, 4] = f0 * y1
  cov[7, 7] = f0**2

  return cov

def create_zeta_vec(dataVec1, dataVec2, f0):
  x1 = dataVec1[0, 0]
  y1 = dataVec1[1, 0]
  x2 = dataVec2[0, 0]
  y2 = dataVec2[1, 0]

  zeta = np.matrix(np.zeros(9)).T
  zeta[0, 0] = x1*x2
  zeta[1, 0] = x1*y2
  zeta[2, 0] = f0*x1
  zeta[3, 0] = y1*x2
  zeta[4, 0] = y1*y2
  zeta[5, 0] = f0*y1
  zeta[6, 0] = f0*x2
  zeta[7, 0] = f0*y2
  zeta[8, 0] = f0**2

  return zeta

class FundMatCalcMode(Enum):
  LS = 0
  Taubin = 1
  LSSVD = 2
  TaubinSVD = 3
  
def calc_fund_mat(imgPnts1, imgPnts2, calcMode):

  # For numeeical stability.
  f1 = max(max(imgPnts1[:, 0, 0]), max(imgPnts1[:, 0, 1]))
  f2 = max(max(imgPnts2[:, 0, 0]), max(imgPnts2[:, 0, 1]))
  f0 = max(f1, f2)
  f0 = 1
  
  # Calculation via LS.
  if (calcMode == FundMatCalcMode.LS):
    F = calc_fmat_by_LS(imgPnts1[:, 0], imgPnts2[:, 0], f0)
  elif (calcMode == FundMatCalcMode.Taubin):
    F = calc_fmat_by_Taubin(imgPnts1[:, 0], imgPnts2[:, 0], f0)
  elif (calcMode == FundMatCalcMode.LSSVD):
    Finit = calc_fmat_by_LS(imgPnts1[:, 0], imgPnts2[:, 0], f0)
    F = calc_fmat_by_SVD(Finit)
  elif (calcMode == FundMatCalcMode.TaubinSVD):
    Finit = calc_fmat_by_Taubin(imgPnts1[:, 0], imgPnts2[:, 0], f0)
    F = calc_fmat_by_SVD(Finit)
  else:
    raise Exception("Un-Supported Calc Mode!")

  return F
    
def create_M_mat(imgPnts1, imgPnts2, f0):
  
  M = np.matrix(np.zeros((9, 9)))
  for i in range(imgPnts1.shape[0]):
    zeta = create_zeta_vec(np.matrix(imgPnts1[i, :]).T, np.matrix(imgPnts2[i, :]).T, f0)
    M = M + zeta * zeta.T
  return M / imgPnts1.shape[0]

def create_N_mat(imgPnts1, imgPnts2, f0):

  N = np.matrix(np.zeros((9, 9)))
  for i in range(imgPnts1.shape[0]):
    cov = create_cov_mat(np.matrix(imgPnts1[i, :]).T, np.matrix(imgPnts2[i, :]).T, f0)
    N = N + cov
    
  return N / imgPnts1.shape[0]

def calc_fmat_by_LS(imgPnts1, imgPnts2, f0):
  M = create_M_mat(imgPnts1, imgPnts2, f0)
  lamdas, v = np.linalg.eigh(M)
  #print(v[:, np.argmin(lamdas)])
  return normalize_mat(np.reshape(v[:, np.argmin(lamdas)], (3, 3)))
  
def calc_fmat_by_Taubin(imgPnts1, imgPnts2, f0):
  M = create_M_mat(imgPnts1, imgPnts2, f0)
  N = create_N_mat(imgPnts1, imgPnts2, f0)
  lamdas, v = splinalg.eig(N, M)
  return normalize_mat(np.reshape(v[:, np.argmax(np.absolute(lamdas))], (3, 3)))
  
def calc_fmat_by_SVD(F):
  u, s, v = np.linalg.svd(F)
  ss = (s[0]**2 + s[1]**2)**0.5
  Fsvd = u * np.matrix([[s[0]/ss, 0, 0],
                        [0, s[1]/ss, 0],
                        [0, 0, 0]]) * v
  return normalize_mat(Fsvd)
  
def calc_dist_between_2F(F1, F2):
  
  dist1 = 0
  dist2 = 0
  for j in range(F1.shape[0]):
    for i in range(F1.shape[1]):
      dist1 = dist1 + (F1[j, i] - F2[j, i])**2
      dist2 = dist2 + (F1[j, i] + F2[j, i])**2
  dist1 = dist1**0.5
  dist2 = dist2**0.5
                       
  return dist1 if dist1 < dist2 else dist2
  
geometry_k.rotation.py
# Python System Package
import numpy as np
import math

def importCheck():
  print("Geometry Imported!")

def RMatFromFixedAngle(xDeg, yDeg, zDeg):

  xRad = xDeg / 180 * math.pi
  yRad = yDeg / 180 * math.pi
  zRad = zDeg / 180 * math.pi

  Rx = np.matrix([[1, 0, 0], \
                 [0, math.cos(xRad), -math.sin(xRad)], \
                 [0, math.sin(xRad),  math.cos(xRad)]])
                 
  Ry = np.matrix([[ math.cos(yRad), 0, math.sin(yRad)], \
                  [0, 1, 0], \
                  [-math.sin(yRad), 0, math.cos(yRad)]])
                  
  Rz = np.matrix([[ math.cos(zRad), -math.sin(zRad), 0], \
                  [ math.sin(zRad),  math.cos(zRad), 0], \
                  [0, 0, 1]])

  return Rz * Ry * Rx

def RMatFromEulerAngle(xDeg, yDeg, zDeg):

  xRad = xDeg / 180 * math.pi
  yRad = yDeg / 180 * math.pi
  zRad = zDeg / 180 * math.pi

  Rx = np.matrix([[1, 0, 0], \
                 [0, math.cos(xRad), -math.sin(xRad)], \
                 [0, math.sin(xRad),  math.cos(xRad)]])
                 
  Ry = np.matrix([[ math.cos(yRad), 0, math.sin(yRad)], \
                  [0, 1, 0], \
                  [-math.sin(yRad), 0, math.cos(yRad)]])
                  
  Rz = np.matrix([[ math.cos(zRad), -math.sin(zRad), 0], \
                  [ math.sin(zRad),  math.cos(zRad), 0], \
                  [0, 0, 1]])
  return Rx * Ry * Rz

def OuterProductOperator(t):
  
  T = np.matrix([[0, -t[2, 0], t[1, 0]],
                 [t[2, 0], 0, -t[0, 0]],
                 [-t[1, 0], t[0, 0], 0]])  
  return T
  
def TMatFromRt(R, t):
  T = np.zeros((4, 4))
  T[0:3, 0:3] = R.A
  T[0:3, 3:4] = t.A
  T[3, :] = np.array([0, 0, 0, 1])
  return np.matrix(T)

def ExtParamsFromRt(R, t):
  E = np.zeros((3, 4))
  E[0:3, 0:3] = R.A
  E[0:3, 3:4] = t.A
  return np.matrix(E)

def InnerParamMat(f, Cu, Cv, skewU, skewV):
  A = np.matrix([[f/skewU, 0, Cu], [0, f/skewV, Cv], [0, 0, 1]])
  return A
  
data_manipulation.noise.py
# -*- coding: utf-8 -*-

import numpy as np

def add_gauss_noise(data, avg, stdv, absmax=0):
  noise = np.random.normal(avg, stdv, data.shape)
  if absmax != 0:
    noise = np.clip(noise, -(absmax + avg), absmax + avg)
  dataWError = data + noise
  return dataWError
  

最適化計算3 基礎行列の推定 理論編

最適化計算のエントリ第三弾ということで,ようやく基礎行列の話題まで来ました.参考文献はいつもの通り金谷先生の本です.

基礎行列・基本行列とは

下記のエントリにも書いた通り,3次元上の点を異なる二視点から撮影した時,点の二つの投影画像座標が満たす数式があります.この拘束条件をエピポーラ拘束といいますが,エピポーラ拘束式に出てくる F を基礎行列,E を基本行列といいます.今回のテーマでは,二つの画像の対応点を使って,この F を求めます.
daily-tech.hatenablog.com

基礎行列の計算

エピポーラ拘束より,下記の数式が成立します.
f:id:rkoichi2001:20180429141317j:plain

今回のエントリでは,上記エピポーラ方程式の 3 x 3 行列である F を計算します.
f:id:rkoichi2001:20180429141718j:plain

DLT法が使えるように,線形方程式に変換

楕円の当てはめエントリでも実施しましたが,線形方程式を立式するところは結局同じアプローチになります.
f:id:rkoichi2001:20180429143438j:plain

式展開の最後で,エピポーラ方程式を単純な内積の形に表せました.楕円の当てはめの時と同じように,すべての点ペアの内積和が最も小さくなるように F 行列の 9個の要素を決定するという問題になります.ここで一点注意ですが, F 行列は定数倍の自由度があるのでノルムを 1 であると勝手に決めて正規化します.ここで,このノルムを下記のように定義します.
f:id:rkoichi2001:20180429144316j:plain

共分散行列と代数的方法(最尤推定

楕円当てはめの時と同じように,式の構造を意識しながら誤差を考えてみます.楕円当てはめの時と同じように,線形化している関係上,入力ベクトル(ζ)が (x, y) の2変数ではなく9変数あるように見えますが,これは方程式を線形化する過程で意図的に作ったもです.なので,結局誤差が発生するのは x, y となります.あとの 7 項の誤差成分に関しては x, y の誤差を適切に仮定してあげれば計算から出てきます.
f:id:rkoichi2001:20180429164656j:plain

上式のΔ項を確率変数としてζの定義に入れて式展開し,誤差の次数に応じて数式を分割して書くと下記のようになります.
f:id:rkoichi2001:20180429165809j:plain

誤差の第二項は十分小さいとして無視すると,誤差分布は誤差の第一項だけで決まります.確率変数4つがそれぞれ独立で,期待値0,分散σの正規分布に従うとすると,共分散行列は下記のようになります.
f:id:rkoichi2001:20180429174312j:plain

共分散行列が求まったので,楕円の当てはめの時と同じように, Taubin法,重み反復法等を使って基礎行列を求めることができるようになります.楕円当てはめの時とアルゴリズムはまったく同じですが一応メモ程度に書いておきます.

最小二乗法

f:id:rkoichi2001:20180429175848j:plain

重み反復法

f:id:rkoichi2001:20180429175945j:plain

Taubin法

f:id:rkoichi2001:20180429180421j:plain

ランク拘束

ここまでで,基礎行列を求めることは形的に楕円当てはめの問題と同じになることがわかりました.が,上で説明した方法では,誤差のある点群からは正しく基礎行列が求まりません.この点が「楕円当てはめ」と「基礎行列計算」の違いになります.それは,基礎行列 F はランクが 3 ではなく,2 である必要があるためです.これをランク拘束と呼びます.上の方法では,誤差のある点群に対して基礎行列を計算すると,求まる解がランク3になってしまいます.ということで,このランク拘束を満たす必要があるのですが,このやり方で手法が大きく3つに大別されます.

1.事後補正法(ランク補正)

 ひとまずランク拘束を考慮せず θ を計算し,次にランク拘束を満たすように補正する方法.

まず,一番簡単な特異値分解によるランク補正法です.この方法では,一番小さい特異値を無理矢理0に変えます.
f:id:rkoichi2001:20180429231233j:plain

特異値によるランク補正では一番小さい特異値を無理矢理0に変えましたが,これをもっと最適に補正する方法が次の方法です.
f:id:rkoichi2001:20180429234035j:plain

2.内部接近法

 初めから基礎行列をランク2となるようにパラメータ化し,そのパラメータ空間内で J を最小にする基礎行列を求める方法です.
ちょっと書くのに疲れたので,あとでアップします....

3.外部接近法

 適当な初期値から出発し,サンプソン誤差 J を最小にするために反復を重ねつつ,かつランク拘束も満たすように収束させる方法です.
f:id:rkoichi2001:20180430000939j:plain

と,,,いうことで,なんだか内容が難しくなってきましたが,実験編に移ります!

参考文献

参考文献1

3次元コンピュータビジョン計算ハンドブック

3次元コンピュータビジョン計算ハンドブック

参考文献2
画像の三次元理解のための最適化計算[Ⅲ]

レイリー商を用いた最小値・最大値問題の解法

レイリー商と呼ばれる下記の数式を最小化することを考えてみます.
f:id:rkoichi2001:20180422170958j:plain

ここで,Aは実対称行列としているので,実数固有値が存在します.直交行列 Q によって A をスペクトル分解したとすると,上記レイリー商の定義式は下記のように変形できます.
f:id:rkoichi2001:20180422173051j:plain

変形後のレイリー商の分子に注目してほしいんですが,最小固有値を λ1,最大固有値を λnとすると分子の値はそれぞれ二つの固有値によって下限・上限が決まることがわかります.
f:id:rkoichi2001:20180422173819j:plain

分子項の下限・上限がはっきりしたので,これをもとにレイリー商を計算すると,
f:id:rkoichi2001:20180422175158j:plain

この問題の見方を少し変えると,条件付き最小値・最大値問題が簡単にとけます.例えば,下記のような問題を考えます.
f:id:rkoichi2001:20180422180233j:plain

上記の例のように,条件付き最小値・最大値問題が簡単にとけるようになります.この考え方を使って最適化問題を解いていきます.

最適化計算2 楕円の当てはめ 実験編

下記のエントリで楕円の当てはめの理論的な部分のエントリをしましたが,実際にパイソンのスクリプトを作って実験しました.
daily-tech.hatenablog.com

が,これが思ったより奥が深く...フィッティングはするようになったのですが,データに付加するノイズを増やしていくと安定しません....読んだ論文には KCR限界とかっていう理論的な限界があるみたいなのですが,おそらくきちんとは理解できないのかと.ただ,大分時間を使ってしまったので,もう基礎行列の計算に進みます.下記のスクリプトでは,一応 ”最小二乗法” による楕円当てはめと "重み付き繰り返し法" "FNS法" による楕円の当てはめを実装してます.見えている楕円弧が短い時に,当てはめをするのがすごく難しいです.

 
# -*- coding: utf-8 -*-

import matplotlib.pyplot as plt
import numpy as np
import math
import scipy as sp

def createMMat(data, weight):
  
  dataMod = np.matrix(np.zeros((data.shape[0], 6)))
  for i in range(data.shape[0]):
    dataMod[i, 0] = data[i, 0]**2
    dataMod[i, 1] = data[i, 0] * data[i, 1]
    dataMod[i, 2] = data[i, 1]**2
    dataMod[i, 3] = data[i, 0] * data[i, 2]
    dataMod[i, 4] = data[i, 1] * data[i, 2]
    dataMod[i, 5] = data[i, 2]**2
  
  M = np.zeros((6, 6))
  M = np.matrix(M)
  for i in range(dataMod.shape[0]):
    dM = dataMod[i, :].T * dataMod[i, :]
    M = M + weight[i, 0] * dM

  return M / dataMod.shape[0]

def createLMat(data, weight, theta, covs):
  
  dataMod = np.matrix(np.zeros((data.shape[0], 6)))
  for i in range(data.shape[0]):
    dataMod[i, 0] = data[i, 0]**2
    dataMod[i, 1] = data[i, 0] * data[i, 1]
    dataMod[i, 2] = data[i, 1]**2
    dataMod[i, 3] = data[i, 0] * data[i, 2]
    dataMod[i, 4] = data[i, 1] * data[i, 2]
    dataMod[i, 5] = data[i, 2]**2
  
  L = np.matrix(np.zeros((6, 6)))
  for i in range(dataMod.shape[0]):
    coeff = weight[i, 0]**2 * (dataMod[i, :] * theta)**2
    L = L + coeff[0, 0] * covs[i]
  
  return L / dataMod.shape[0]

def fittingEllipseWithLS(data, normTerm):
  
  weight = np.matrix(np.ones(data.shape[0])).T
  dataMod = np.concatenate((data / normTerm, np.ones((data.shape[0], 1))), axis=1)
  M = createMMat(dataMod, weight)  
  lamdas, v = np.linalg.eigh(M)
  #print(lamdas)
  #print(v)

  theta = v[:, np.argmin(np.absolute(lamdas))]
  #print(theta)
  
  lamdas, v = sp.linalg.eigh(M)
  #print(lamdas)
  #print(v)

  theta = np.matrix(v[:, np.argmin(np.absolute(lamdas))]).T
  #print(theta)
  theta[3, 0] = normTerm * theta[3, 0]
  theta[4, 0] = normTerm * theta[4, 0]
  theta[5, 0] = normTerm**2 * theta[5, 0]
  return theta

  
def fittingEllipseWithTaubin(data, normTerm):

  # Add normalized term.
  dataMod = np.concatenate((data / normTerm, np.ones((data.shape[0], 1))), axis=1)
    
  # Param Vector
  theta = np.matrix(np.zeros(6)).T
  
  # Covars
  covavg = np.matrix(np.zeros((6, 6)))
  for i in range(dataMod.shape[0]):
    data = dataMod[i, :]
    covavg = covavg + createCovMat(data)
  covavg = covavg / dataMod.shape[0]

  weight = np.ones(dataMod.shape[0])
  weight = np.matrix(weight).T
  M = createMMat(dataMod, weight);
  lamdas, v = sp.linalg.eig(covavg, M)
 
  theta = np.matrix(v)[:, np.argmax(np.absolute(lamdas))]
  
  theta[3, 0] = normTerm * theta[3, 0]
  theta[4, 0] = normTerm * theta[4, 0]
  theta[5, 0] = normTerm**2 * theta[5, 0]

  return theta  
  
           
def createCovMat(data):
  
  x = data[0, 0]
  y = data[0, 1]
  f0 = data[0, 2]
  xx = x**2
  yy = y**2
  xy = x*y
  f0x = f0*x
  f0y = f0*y
  f0f0 = f0**2
  
  cov = np.matrix([[xx,  xy,     0,   f0x,     0,    0], \
                   [xy,  xx+yy, xy,   f0y,   f0x,    0], \
                   [0,   xy,    yy,     0,   f0y,    0], \
                   [f0x, f0y,   0,    f0f0,    0,    0], \
                   [0,   f0x,   f0y,  0,     f0f0,   0], \
                   [0,   0,     0,    0,     0,      0]])
  
  return cov
           
def fittingEllipseWithItrReweight(data, normTerm):
  
  # Add normalized term.
  dataMod = np.concatenate((data / normTerm, np.ones((data.shape[0], 1))), axis=1)
  
  # Param Vector
  theta = np.matrix(np.zeros(6)).T
  thetaOrg = theta
  
  # Weight matrix.
  weight = np.ones(dataMod.shape[0])
  weight = np.matrix(weight).T

  # Covars
  covs = []
  for i in range(dataMod.shape[0]):
    data = dataMod[i, :]
    covs.append(createCovMat(data))

  loop = 0
  while True:
    
    # M Matrix
    M = createMMat(dataMod, weight)
    lamdas, v = np.linalg.eigh(M)
    thetaOrg = theta
    theta = v[:, np.argmin(np.absolute(lamdas))]
    
    term = np.linalg.norm(theta - thetaOrg)  
    if term < 0.0001 or loop > 20:
      break

    for i in range(dataMod.shape[0]):
      alp = theta.T * covs[i] * theta
      weight[i, 0] = 1 / (alp)
    
    loop = loop + 1
  
  theta[3, 0] = normTerm * theta[3, 0]
  theta[4, 0] = normTerm * theta[4, 0]
  theta[5, 0] = normTerm**2 * theta[5, 0]    
  return theta          

          
  
def fittingEllipseWithFNS(data, normTerm):
  
    # Add normalized term.
  dataMod = np.concatenate((data / normTerm, np.ones((data.shape[0], 1))), axis=1)
  
  # Param Vector
  theta = np.matrix(np.zeros(6)).T
  thetaNew = theta
  thetaOrg = theta
  
  # Weight matrix.
  weight = np.ones(dataMod.shape[0])
  weight = np.matrix(weight).T

  # Covars
  covs = []
  for i in range(dataMod.shape[0]):
    data = dataMod[i, :]
    covs.append(createCovMat(data))

  loop = 0
  while True:
    
    # M Matrix
    M = createMMat(dataMod, weight)
    L = createLMat(dataMod, weight, theta, covs)
    lamdas, v = np.linalg.eigh(M - L)
    thetaOrg = theta
    thetaNew = v[:, np.argmin(np.absolute(lamdas))]
    #theta = (thetaNew + thetaOrg) / 2
    theta = thetaNew
    #theta = theta / np.linalg.norm(theta)
    
    term = np.linalg.norm(theta - thetaOrg)  
    if term < 0.0001 or loop > 30:
      break

    for i in range(dataMod.shape[0]):
      alp = theta.T * covs[i] * theta
      weight[i, 0] = 1 / (alp)
    
    loop = loop + 1

  theta[3, 0] = normTerm * theta[3, 0]
  theta[4, 0] = normTerm * theta[4, 0]
  theta[5, 0] = normTerm**2 * theta[5, 0]  
  return theta
  
def plotData(dictDataPlot, dictDataScatter):
  
  fig, ax = plt.subplots(ncols = 1, figsize=(10, 10))
  plt.xlim(0, 10)
  plt.ylim(0, 10)
  
  for key in dictDataPlot:
    ax.plot(dictDataPlot[key][:, 0], dictDataPlot[key][:, 1], linewidth=1, label=key)
  
  for key in dictDataScatter:    
    ax.scatter(dictDataScatter[key][:, 0], dictDataScatter[key][:, 1], s = 2)
  
  ax.legend()

    
def addGaussError(data, avg, stdv, absmax):
  noise = np.random.normal(avg, stdv, data.shape)
  noise = np.clip(noise, -(absmax + avg), absmax + avg) 
  dataWError = data + noise
  return dataWError
 
if __name__ == "__main__":
  
  # Importing Library for Testing.
  import sys
  sys.path.append('../')
  from geometry_k.ellipse import generateVecFromEllipse
  from geometry_k.ellipse import getEllipseProperty
  
  print("Ellipse Fitting Sample")
  
  # Define ellipse property.
  axisX = 3
  axisY = 3
  centerX = 5
  centerY = 5
  rad = math.radians(45)
  
  # Prepare argumetns.
  axisOrg = np.matrix([[axisX], [axisY]])
  centOrg = np.matrix([[centerX], [centerY]])
  Rorg = np.matrix([[math.cos(rad), -math.sin(rad)], [math.sin(rad), math.cos(rad)]])
  
  # Generating Data
  dataOrg = generateVecFromEllipse(axisOrg, centOrg, Rorg)
  dataLimited = generateVecFromEllipse(axisOrg, centOrg, Rorg, [-2*math.pi/6 * 1, 2*math.pi/6], 200)

  # Add Noise
  dataNoised = addGaussError(dataLimited, 0, 0.03, 10.1)
  
  # Least Square Fitting
  pLs = fittingEllipseWithLS(dataNoised, max(centerX + axisX, centerY + axisY))
  print(pLs)
  
  # Generate Estimated Ellipse Property
  validLs, axisEstLs, centEstLs, TEstLs = \
  getEllipseProperty(pLs[0, 0], pLs[1, 0], pLs[2, 0], pLs[3, 0], pLs[4, 0], pLs[5, 0]) 
  
  # Iterative Reweight
  pItr = fittingEllipseWithItrReweight(dataNoised, max(centerX + axisX, centerY + axisY))
  #print(pItr)
  
  # Generate Estimated Ellipse Property
  validItr, axisEstItr, centEstItr, TEstItr = \
  getEllipseProperty(pItr[0, 0], pItr[1, 0], pItr[2, 0], pItr[3, 0], pItr[4, 0], pItr[5, 0]) 
    
  # Iterative Reweight
  pFns = fittingEllipseWithFNS(dataNoised, max(centerX + axisX, centerY + axisY))
  #print(pFns)
  
  # Generate Estimated Ellipse Property
  validFns, axisEstFns, centEstFns, TEstFns = \
  getEllipseProperty(pFns[0, 0], pFns[1, 0], pFns[2, 0], pFns[3, 0], pFns[4, 0], pFns[5, 0]) 
  
  # Taubin Method
  pTb = fittingEllipseWithTaubin(dataNoised, max(centerX + axisX, centerY + axisY))
  #pTb = fittingEllipseWithTaubin(dataNoised, 1)
  print(pTb)
  
  # Generate Estimated Ellipse Property
  validTb, axisEstTb, centEstTb, TEstTb = \
  getEllipseProperty(pTb[0, 0], pTb[1, 0], pTb[2, 0], pTb[3, 0], pTb[4, 0], pTb[5, 0])
  
  # Estimating Data
  dataEstLs = generateVecFromEllipse(axisEstLs, centEstLs, TEstLs)
  dataEstItr = generateVecFromEllipse(axisEstItr, centEstItr, TEstItr)
  dataEstFns = generateVecFromEllipse(axisEstFns, centEstFns, TEstFns)
  dataEstTb = generateVecFromEllipse(axisEstTb, centEstTb, TEstTb)
  
  dictData = {'Original' : dataOrg,\
              'Least SQ' : dataEstLs,\
              'Itr Weight' : dataEstItr,\
              'FNS' : dataEstFns, \
              'Taubin' : dataEstTb}
              
  dictDataScatter = {'Noised Sample' : dataNoised}
  
  plotData(dictData, dictDataScatter)
    


下記,当てはめの結果です.あと,上記のスクリプトに出てくる呼び出し関数は下記のエントリの関数を使ってます.ちなみに,グラフの原点 (0, 0) をまたぐような楕円をフィッティングさせようとすると桁落ちの問題が出てきて精度が出ません.これは理論的な問題というよりは計算上の問題です.なので,上記のスクリプトでは中心が (5, 5) の楕円で実験しています.
daily-tech.hatenablog.com

結果は,下記のようになります.一応,期待通り Taubin, FNS 法の結果が一番いいですね.
f:id:rkoichi2001:20180429162620p:plain