この記事について
3Dモデルを生成・処理するライブラリとして、COLMAP・Blender・OpenCV等があるが、ツールごとにカメラ座標系の軸の定義方法が異なる。
例えば、COLMAPで計算した座標変換行列(世界座標系→カメラ座標系)をBlender上でもそのまま適用しようとすると、後段の処理に失敗することがある。
そこでこの記事では、以下の3点について説明する。
- カメラ座標系の基礎
- それぞれのツールにおけるカメラ座標系の定義
- 座標変換の方法
1. カメラ座標系の基礎
1.1 座標系とは?
3D空間上に (x,y,z) 軸をどのように設定するかのルール。3Dモデリングツールではこの座標系が統一されていないことから混乱を招きがち。
1.2 さまざまな座標系
- 世界座標系: 空間中のある位置を原点とする座標系
- カメラ座標系: カメラ位置を原点とする座標系
- 局所座標系: 各3Dモデル固有の座標系
- スクリーン座標系: 画面表示で使う座標系
1.3 座標変換(剛体変換)
- モデリング変換: 局所座標系→世界座標系
- ビューイング変換: 世界座標系→カメラ座標系
例えば、COLMAPでは世界座標系からカメラ座標系へのビューイング変換行列(回転行列+並進成分)が与えられる。ただし、COLMAPのカメラ座標系における軸の取り方は、後段の処理で使うツールのカメラ座標系の軸の取り方と同じとは限らないため、必要に応じて座標変換を施す必要がある。
2. ツールごとのカメラ座標系の定義
カメラ位置を原点とした時のそれぞれの軸の向きを表す。
例えば、代表的な3Dモデリングツールで使用されるカメラ座標系は以下の図・表の通りである。
ツール | x | y | z | 系 |
---|---|---|---|---|
COLMAP | right | down | forward | 右手系(rdf) |
OpenCV | right | down | forward | 右手系(rdf) |
pytorch3D | left | up | forward | 右手系(luf) |
OpenGL | right | up | backward | 右手系(rub) |
Blender | right | up | backward | 右手系(rub) |
Unity | right | up | forward | 左手系(ruf) |
参考までに、NeRFでも実装によってカメラ座標系における軸の定義方法が異なるのが厄介。
ツール | x | y | z | 系 | 備考 |
---|---|---|---|---|---|
NeRF1 | right | up | backward | 右手系(rub) | 公式実装 |
InstantNGP2 | right | up | backward | 右手系(rub) | |
Nerfstudio1 | right | up | backward | 右手系(rub) | |
Nerfies3 | right | down | forward | 右手系(rdf) | |
LLFF4 | down | right | backward | 右手系(drb) |
3. 座標変換の方法
ここでは、COLMAP等で出力されるビューイング変換(世界座標系→カメラ座標系)を後段で使用するツールのカメラ座標系に対応するように変換する方法を考える。
まず、
ビューイング変換(world-to-camera, w2c
)は、回転行列と並進ベクトルを用いて、次のように定義できる。
\begin{align}
\begin{bmatrix}
x_c \\
y_c \\
z_c
\end{bmatrix}
&=
\begin{bmatrix}
r_{x1} & r_{y1} & r_{z1} & t_{x}\\
r_{x2} & r_{y2} & r_{z2} & t_{y}\\
r_{x3} & r_{y3} & r_{z3} & t_{z}
\end{bmatrix}
\begin{bmatrix}
x_w \\
y_w \\
z_w \\
1
\end{bmatrix}\\
\mathbf{x}_c
&=
\mathbf{T}_{wc}
\begin{bmatrix}
\mathbf{x}_w \\
1
\end{bmatrix}\\
&= \mathbf{R}_{wc} \mathbf{x}_w + \mathbf{t}_{wc}
\end{align}
ビューイング変換で用いる上の行列 $\mathbf{T}_{wc}$ のことを一般的には外部パラメータとも呼ぶ。
また、カメラ座標系から世界座標系への逆変換(camera-to-world, c2w
)は次のように表せる。
\begin{align}
\mathbf{x}_w
&=
\mathbf{R}_{wc}^{-1} (\mathbf{x}_c - \mathbf{t}_{wc}) \\
&=
\mathbf{R}_{wc}^{T} \mathbf{x}_c - \mathbf{R}_{wc}^{T} \mathbf{t}_{wc} \\
&=
\mathbf{R}_{cw} \mathbf{x}_c + \mathbf{t}_{cw}
\end{align}
ただし、
\mathbf{R}_{cw} = \mathbf{R}_{wc}^{T}, \quad \mathbf{t}_{cw} = - \mathbf{R}_{wc}^{T} \mathbf{t}_{wc}
とした。
ここで、世界座標系におけるカメラの位置は$\mathbf{t}_{cw}$であり、カメラ座標系の軸の取り方によって変わることはない。
ただし、回転行列$\mathbf{R}_{cw}$は、カメラ座標系の軸の取り方によって意味が変わってしまうので、適切な変換が必要となる。
3.1 座標変換方法
ここでは、OpenCVの座標系 (rdf) で記述された c2w
変換行列から、OpenGLの座標系 (rub) における c2w
変換行列を求める場合 (rdf_to_rub) を考える。
上の図からわかる通り、2つの座標系では x 軸の取り方が共通で、(y, z) 軸の取り方がいずれも反転している。そのため、回転行列 $\mathbf{R}_{cw}^\mathrm{rdf}$ の (y, z) 軸成分(2, 3列目)に-1をかければよい。
\begin{align}
\mathbf{R}_{cw}^\mathrm{rdf}
&=
\begin{bmatrix}
r_{x1} & r_{y1} & r_{z1} & t_{x}\\
r_{x2} & r_{y2} & r_{z2} & t_{y}\\
r_{x3} & r_{y3} & r_{z3} & t_{z}
\end{bmatrix}\\
\mathbf{R}_{cw}^\mathrm{rub}
&=
\begin{bmatrix}
r_{x1} & -r_{y1} & -r_{z1} & t_{x}\\
r_{x2} & -r_{y2} & -r_{z2} & t_{y}\\
r_{x3} & -r_{y3} & -r_{z3} & t_{z}
\end{bmatrix}\\
\end{align}
上の例では、各軸の成分を反転させることで変換できるが、場合によっては軸を入れ替えることもある。例えば、LLFF (drb) からNeRF (rub) へカメラ座標系を変換する場合 (drb_to_rub)、$\mathbf{R}_{cw}^\mathrm{drb}$ のx軸を反転させたあと、x軸とy軸を入れ替える必要がある。
\begin{align}
\mathbf{R}_{cw}^\mathrm{drb}
&=
\begin{bmatrix}
r_{x1} & r_{y1} & r_{z1} & t_{x}\\
r_{x2} & r_{y2} & r_{z2} & t_{y}\\
r_{x3} & r_{y3} & r_{z3} & t_{z}
\end{bmatrix}\\
\mathbf{R}_{cw}^\mathrm{rub}
&=
\begin{bmatrix}
r_{y1} & - r_{x1} & r_{z1} & t_{x}\\
r_{y2} & - r_{x2} & r_{z2} & t_{y}\\
r_{y3} & - r_{x3} & r_{z3} & t_{z}
\end{bmatrix}\\
\end{align}
3.2 座標変換コード
[num_poses, 3, 4]
のビューイング逆変換から、使用されることの多いrubのビューイング逆変換を求めるコードを以下に示す。
def rdf_to_rub(c2w):
"""
Input: c2w (num_poses, 3, 4) as (right, down, forward)
Output: c2w (num_poses, 3, 4) as (right, up, backward)
"""
c2w_ = np.zeros_like(c2w)
c2w_ = np.concatenate([c2w[:, :, [0]], -c2w[:, :, [1]], -c2w[:, :, [2]], c2w[:, :, [3]]], axis=2)
return c2w_
def luf_to_rub(c2w):
c2w_ = np.zeros_like(c2w)
c2w_ = np.concatenate([-c2w[:, :, [0]], c2w[:, :, [1]], -c2w[:, :, [2]], c2w[:, :, [3]]], axis=2)
return c2w_
def drb_to_rub(c2w):
c2w_ = np.zeros_like(c2w)
c2w_ = np.concatenate([c2w[:, :, [1]], -c2w[:, :, [0]], c2w[:, :, [2]], c2w[:, :, [3]]], axis=2)
return c2w_
def drf_to_rub(c2w):
c2w_ = np.zeros_like(c2w)
c2w_ = np.concatenate([c2w[:, :, [1]], -c2w[:, :, [0]], -c2w[:, :, [2]], c2w[:, :, [3]]], axis=2)
return c2w_
def ufl_to_rub(c2w):
c2w_ = np.zeros_like(c2w)
c2w_ = np.concatenate([-c2w[:, :, [2]], c2w[:, :, [0]], -c2w[:, :, [1]], c2w[:, :, [3]]], axis=2)
return c2w_
4. Tips
- 座標系変換行列がc2wなのかw2cなのか明示されていないことがあり、ツールによって異なる
- 外部パラメータはw2cを指す
- 回転行列は直交行列のため、その逆行列は転置行列となる
参考記事
- 3Dライブラリのカメラ座標系の違い
- OpenCVのカメラとOpenGLでレンダリングする
- なぜ座標には右手系と左手系があるのか
- カメラモデルと透視投影(CVMLエキスパートガイド)
- Converting camera poses from OpenCV to OpenGL can be easy
- Camera poses in Python
- Coordinate Frames (Stereo Labs)
- What are the coordinates?
-
https://docs.nerf.studio/quickstart/data_conventions.html#camera-view-space: NeRF, Nerfstudioのカメラ座標系について記載 ↩ ↩2
-
https://github.com/NVlabs/instant-ngp/issues/61#issuecomment-1016849021: InstantNGPではblenderの座標系を採用 ↩
-
https://github.com/google/nerfies/issues/39#issuecomment-986345766: NerfiesではOpenCVの座標系を採用 ↩
-
https://github.com/Fyusion/LLFF?tab=readme-ov-file#using-your-own-poses-without-running-colmap: LLFFのカメラ座標系について記載 ↩