ねののお庭。

かりかりもふもふ。

Open3Dでlookatするためのextrinsic matrixの作り方、及びextrinsic matrix (CG的にはview matrix)の解釈。

Open3dでは標準で任意の視点からのlookatが出来ない。

Open3dでは標準で任意の視点からのlookatが出来ません。ViewControlset_lookatという関数生えてるんですが、残念ながらGUIでマウスぐりぐりしたときにlookatする点を設定できるだけで、特定の座標から特定の座標をlookatする、みたいなことが出来ないのです。

解決策は自分でカメラのextrinsic matrixを計算して設定する他ありません。 extrinsic matrix、もとい外部行列が何か分からない場合は以下で。

opencv.jp

ここでextrinsic matrixと呼んでいる行列ですが、外部行列, view matrix, などコンテキストによっていろいろ呼び方はありますが、ここではひっくるめてLookAt行列と呼称することにします。

で、どうするかというと以下みたいな関数でLookAt行列を作成します。

def create_translation_mat(x: float, y: float, z: float):
    trans_mat = np.eye(4)
    trans_mat[:, 3] = [x, y, z, 1]

    return trans_mat


def create_scale_mat(x: float, y: float, z: float):
    scale_mat = np.eye(4)

    scale_mat[0, 0] = x
    scale_mat[1, 1] = y
    scale_mat[2, 2] = z

    return scale_mat


def create_look_at_mat(eye, target, up=[0.0, 1.0, 0.0]):
    # Open3D world coordinate system: right-handed coordinate system (Y up, same as OpenGL)
    # Open3D camera coordinate system: right-handed coordinate system (Y down, Z forward, same as OpenCV) 
    # https://github.com/intel-isl/Open3D/issues/1347

    eye = np.array(eye, dtype=np.float32, copy=True)
    target = np.array(target, dtype=np.float32, copy=True)
    up = np.array(up, dtype=np.float32, copy=True)

    z = eye - target
    z = z / np.linalg.norm(z)

    x = np.cross(up, z)
    x = x / np.linalg.norm(x)

    y = np.cross(z, x)
    y = y / np.linalg.norm(y)

    rotate_mat = np.array([
        [x[0], x[1], x[2], 0.0],
        [y[0], y[1], y[2], 0.0],
        [z[0], z[1], z[2], 0.0],
        [0, 0, 0, 1]
    ])

    trans_mat = create_translation_mat(-eye[0], -eye[1], -eye[2])

    scale_mat = create_scale_mat(1, -1, -1)

    tmp = np.dot(rotate_mat, trans_mat)
    tmp = np.dot(scale_mat, tmp)

    return tmp

外積とってカメラのx,y,z軸(z軸不向きが見てる方向)を計算して、平行移動して回転して...というCG屋さん的には見慣れたお話。

ここら辺のお話はLearn OpenGLに分かりやすく書いてあるので詳しくはそこらへんを読むといいと思います。

learnopengl.com

途中まではOpenGLと同じですが、カメラ座標系がOpenGLでは右手系Y up, z backwardであるのに対して、Open3Dではy down, z forwardの右手系です。 そのためOpenGLと同じような形で式を作った場合はscale_matで内積とって鏡映変換してあげる必要がある事に注意してください。

create_look_at_mat関数で作ったLookAt行列をPinholeCameraParametersextrinsicに設定してあげればそれでOKです。

以下に使い方含めたサンプルを置いておきます。

github.com

とりあえず実装して動かしたい場合は以上となります。以降は行列の解釈的なお話をしたいと思います。

LookAt行列作る際の回転行列について。

上記のコードでやってるLookAt行列を作る処理は概ね、①カメラを原点とした座標系を作って②平行移動して③回転して④鏡映する、という流れです。

①②④はさほど難しいお話ではありません。が、回転行列についてはどうでしょうか。 コード中の回転行列は分かりやすく書くとこんな形です。 R(ight)がx軸、U(p)がy軸、D(irection)がz軸に対応します。

 
A = \begin{bmatrix}
R_x & R_y & R_z & 0 \\
U_x & U_y & U_z & 0 \\
D_x & D_y & D_z & 0 \\
0 & 0 & 0 & 1 \\
\end{bmatrix}

この行列の形はLearn OpenGL床井研究室でも紹介されていますが、回転行列だよという紹介で終わっていたり、実際具体的にベクトルと内積とると上手くいってそうだよね、という説明でおわっていて、どうしてこういう行列に辿りついたかの理解には役に立ちません。

行列 Aは回転行列です、とかいわれてハイそうですね、と納得できますか?私は出来なかったです。 何故なら見慣れている回転行列は以下のような形で、各軸にそれぞれ回転させていくものなので。

 
\begin{bmatrix}
1 & 0 & 0 \\
0 & \cos \theta & - \sin \theta \\
0 & \sin \theta & \cos \theta\\
\end{bmatrix}

というわけで、行列 Aをどう理解すると分かりが良いのかな~というのを考えたのですが、回転として捉えるより、基底の変換と捉えた方が分かりが良さそうです。

 R, U, Dベクトルを基底とした時の座標系上の点 (x', y', z')を標準基底(世界座標系)で表現してあげるための変換は非常に簡単で、以下のように計算してあげればよいです。

 
B= \begin{bmatrix}
R_x &  U_x & D_z \\
R_y & U_y & D_y \\
R_z & U_z & D_z\\
\end{bmatrix}

\begin{bmatrix}
x_w \\
y_w \\
z_w \\
\end{bmatrix}
 =

B
\begin{bmatrix}
x' \\
y' \\
z' \\
\end{bmatrix}

LookAt行列の場合やりたい事はこれの逆で、世界座標系上の座標 (x_w, y_w, z_w) R, U, Dを基底とした座標系上の座標はどこですか?という事です。 なので逆行列をとってあげればいいわけですが、真面目に逆行列を計算する必要はありません。何故なら R, U, Dは正規直交基底だからです。外積で直交するようなベクトルを取り出して正規化をして R, U, Dベクトルを作っているわけですから、正規直交基底であることは当然です。正規直交基底であるということは行列 Bは直交行列です。そのためクソ真面目に逆行列を計算するまでもなく、転置をとれば逆行列になります。

 
B^\mathsf{T} = \begin{bmatrix}
R_x & R_y & R_z  \\
U_x & U_y & U_z \\
D_x & D_y & D_z  \\
\end{bmatrix}
 
B^\mathsf{T} B = B  B^\mathsf{T} = I

ここで行列 B^\mathsf{T}と行列 Aを見比べると、行列 Aは同次座標になっているだけなので、実質同じみたいなところがあります。 もしくは1次元追加したと考え、 R, U, Dの4次元目にそれぞれ0を追加し、4つ目の基底として (0, 0, 0, 1)を設定してあげれば、3次元の場合と同じ操作をする事で行列 Aが得られることが容易に分かります(各基底を縦ベクトルとして横に並べて転置すればよいだけ!)。

以上の事から行列 Aがどのように導き出されるかを示しました。 これで気持ち悪さを感じずcreate_look_at_mat関数を使えるようになるはずです。

まとめ

Open3Dでlookatするための具体的方法を紹介しました。 そして中で使われている回転行列は回転として捉えるより基底の変換と捉えるほうが分かりやすくないですか?というお話でした。

後半、用語の使い方がだいぶ甘いと思われますので、そこらへんツッコミある方は優しくツッコミを入れてくれると幸いです。

github.com