Open3dでは標準で任意の視点からのlookatが出来ない。
Open3dでは標準で任意の視点からのlookatが出来ません。ViewControlにset_lookatという関数生えてるんですが、残念ながらGUIでマウスぐりぐりしたときにlookatする点を設定できるだけで、特定の座標から特定の座標をlookatする、みたいなことが出来ないのです。
解決策は自分でカメラのextrinsic matrixを計算して設定する他ありません。 extrinsic matrix、もとい外部行列が何か分からない場合は以下で。
ここで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に分かりやすく書いてあるので詳しくはそこらへんを読むといいと思います。
途中まではOpenGLと同じですが、カメラ座標系がOpenGLでは右手系Y up, z backwardであるのに対して、Open3Dではy down, z forwardの右手系です。
そのためOpenGLと同じような形で式を作った場合はscale_mat
で内積とって鏡映変換してあげる必要がある事に注意してください。
create_look_at_mat
関数で作ったLookAt行列をPinholeCameraParametersのextrinsic
に設定してあげればそれでOKです。
以下に使い方含めたサンプルを置いておきます。
とりあえず実装して動かしたい場合は以上となります。以降は行列の解釈的なお話をしたいと思います。
LookAt行列作る際の回転行列について。
上記のコードでやってるLookAt行列を作る処理は概ね、①カメラを原点とした座標系を作って②平行移動して③回転して④鏡映する、という流れです。
①②④はさほど難しいお話ではありません。が、回転行列についてはどうでしょうか。 コード中の回転行列は分かりやすく書くとこんな形です。 R(ight)がx軸、U(p)がy軸、D(irection)がz軸に対応します。
この行列の形はLearn OpenGLや床井研究室でも紹介されていますが、回転行列だよという紹介で終わっていたり、実際具体的にベクトルと内積とると上手くいってそうだよね、という説明でおわっていて、どうしてこういう行列に辿りついたかの理解には役に立ちません。
行列は回転行列です、とかいわれてハイそうですね、と納得できますか?私は出来なかったです。 何故なら見慣れている回転行列は以下のような形で、各軸にそれぞれ回転させていくものなので。
というわけで、行列をどう理解すると分かりが良いのかな~というのを考えたのですが、回転として捉えるより、基底の変換と捉えた方が分かりが良さそうです。
ベクトルを基底とした時の座標系上の点を標準基底(世界座標系)で表現してあげるための変換は非常に簡単で、以下のように計算してあげればよいです。
LookAt行列の場合やりたい事はこれの逆で、世界座標系上の座標がを基底とした座標系上の座標はどこですか?という事です。 なので逆行列をとってあげればいいわけですが、真面目に逆行列を計算する必要はありません。何故ならは正規直交基底だからです。外積で直交するようなベクトルを取り出して正規化をしてベクトルを作っているわけですから、正規直交基底であることは当然です。正規直交基底であるということは行列は直交行列です。そのためクソ真面目に逆行列を計算するまでもなく、転置をとれば逆行列になります。
ここで行列と行列を見比べると、行列は同次座標になっているだけなので、実質同じみたいなところがあります。 もしくは1次元追加したと考え、の4次元目にそれぞれ0を追加し、4つ目の基底としてを設定してあげれば、3次元の場合と同じ操作をする事で行列が得られることが容易に分かります(各基底を縦ベクトルとして横に並べて転置すればよいだけ!)。
以上の事から行列がどのように導き出されるかを示しました。
これで気持ち悪さを感じずcreate_look_at_mat
関数を使えるようになるはずです。
まとめ
Open3Dでlookatするための具体的方法を紹介しました。 そして中で使われている回転行列は回転として捉えるより基底の変換と捉えるほうが分かりやすくないですか?というお話でした。
後半、用語の使い方がだいぶ甘いと思われますので、そこらへんツッコミある方は優しくツッコミを入れてくれると幸いです。