カメラ行列
Camera Matrix (Projection Matrix)
カメラモデルの全体像
3D空間の点を2D画像に投影するプロセスは、複数の座標変換の連鎖で表される。
座標系の約束(本記事を通じて)
- ワールド座標系:$Y$ 上向きの右手系(絶対的な3D空間の基準)。
- カメラ座標系:OpenCV 慣習の右手系で、$X_c$ 右・$Y_c$ 下・$Z_c$ 前方(=光軸=視線方向・深度方向)。
$Y_c$ を下向きに取るのは、画像のピクセル座標 $(u, v)$ が左上原点・$v$ 下向きだから。こうすると内部行列 $K$ による $(x, y) \to (u, v)$ の対応が符号反転なしで素直につながる。カメラ前方の点は深度 $Z_c > 0$。
内部パラメータ行列 K
内部パラメータ(intrinsic parameters)はカメラ固有の特性を表す。
内部パラメータ行列
$$K = \begin{pmatrix} f_x & s & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{pmatrix}$$- $f_x, f_y$: 焦点距離(ピクセル単位)。$f_x = f/p_x$, $f_y = f/p_y$($p_x, p_y$はピクセルサイズ)
- $(c_x, c_y)$: 主点(光軸と像平面の交点)
- $s$: スキュー係数(画素の $u$ 軸と $v$ 軸の非直交性を表す。ほとんどのカメラでは $s=0$ とみなせ、稀に一部のスキャナ画像などで $s \neq 0$ になる)
例: 一般的なWebカメラ
解像度 640×480、センサーサイズ 3.6mm×2.7mm、焦点距離 f=4mm の場合:
- $p_x = 3.6/640 = 0.005625$ mm/pixel
- $f_x = 4/0.005625 \approx 711$ pixel
- 主点: $(c_x, c_y) \approx (320, 240)$(理想的には画像中心)
外部パラメータ [R|t]
外部パラメータ(extrinsic parameters)はワールド座標系に対するカメラの位置と姿勢を表す。
なぜ $Z_w$ と $Z_c$ は逆を向く? カメラの $Y_c$ を下向きに取ったことと表裏一体。右手系のまま $Y$ を上(ワールド)から下(カメラ)へ反転するには、$Z$ も一緒に反転させるしかない($Y$ だけ反転すると左手系になってしまう)。つまり $Y_c = -Y_w$ と $Z_c = -Z_w$ は、$X$ 軸まわりの 180° 回転として対で起こる。別の言い方をすれば、「$Z_c$ が前方(シーンの奥)を向く」と「$Y_c$ が下」は OpenCV 慣習のセット。OpenGL 慣習($Y$ 上・カメラは $-Z$ を見る)なら $+Z_c$ は視点側=$Z_w$ と同じ向きになる。
座標系の主な流儀(OpenCV 系・OpenGL 系・DirectX/Unity 系)
本記事の $X_c$ 右・$Y_c$ 下・$Z_c$ 前方は OpenCV 系の流儀。ほかにも主要な流儀があり、分野によって多数派・少数派が入れ替わる。手系(右手系/左手系)も流儀で異なる。
| 流儀 | $X$ | $Y$ | $+Z$ の向き | 手系 | 多数派の分野 | 代表例 |
|---|---|---|---|---|---|---|
| OpenCV 系 | 右 | 下 | 前方=光軸(奥。前方の点は $Z_c > 0$) | 右手系 | コンピュータビジョン / SLAM / SfM / ロボット視覚 | OpenCV, COLMAP, 多くの SLAM/SfM, ROS の camera optical frame |
| OpenGL 系 | 右 | 上 | 後方(視点側。光軸は $-Z$) | 右手系 | CG / レンダリング | OpenGL / WebGL, Three.js, 多くのレンダラ |
| DirectX / Unity 系 | 右 | 上 | 前方=光軸(奥) | 左手系 | 一部の CG / ゲームエンジン | Direct3D, Unity |
$X$ 右・$Y$ 上・$Z$ 奥(前方)は左手系で、DirectX / Unity 系がこれ。OpenCV 系($Y$ 下)から $Y$ だけを上に反転すると、1本反転なので右手系→左手系に変わってこの流儀になる(Unreal も左手系だが $X$ 前方・$Z$ 上と軸の割り当てが異なる)。一方、OpenCV 系と OpenGL 系は $Y,Z$ を同時に反転した関係($X$ 軸まわり 180°)で、ともに右手系。本記事はコンピュータビジョンの文脈なので OpenCV 系を採用している。
外部パラメータ行列
$$[R | \mathbf{t}] = \begin{pmatrix} r_{11} & r_{12} & r_{13} & t_x \\ r_{21} & r_{22} & r_{23} & t_y \\ r_{31} & r_{32} & r_{33} & t_z \end{pmatrix}$$ワールド座標の点 $\mathbf{P}_w = (X, Y, Z, 1)^T$ をカメラ座標に変換:
$$\mathbf{P}_c = R \mathbf{P}_w + \mathbf{t}$$注意: $\mathbf{t}$ はワールド座標系でのカメラ位置ではなく、カメラ座標系でのワールド原点の位置。カメラのワールド座標は $\mathbf{C} = -R^T \mathbf{t}$
同じ $[R|\mathbf{t}]$ でも、どちらの座標系を固定して眺めるかで見え方が変わる。図3はワールド基準(ワールドを固定し、その中でのカメラの姿勢を描く。カメラの姿勢・軌跡を扱う用途はこちら)だった。逆にカメラを原点に固定すると、ワールド座標系のほうが $[R|\mathbf{t}]$ で回転・並進して配置される。これは投影式 $\mathbf{P}_c = R\,\mathbf{P}_w + \mathbf{t}$(world→camera)そのもので、レンダリングのビュー行列に対応する。
カメラ行列 P
内部パラメータと外部パラメータを合わせたものがカメラ行列(射影行列)である。
カメラ行列(Projection Matrix)
$$P = K [R | \mathbf{t}]$$$P$ は $3 \times 4$ 行列で、11自由度(内部5 + 外部6)を持つ。
投影式
$$\lambda \begin{pmatrix} u \\ v \\ 1 \end{pmatrix} = P \begin{pmatrix} X \\ Y \\ Z \\ 1 \end{pmatrix}$$$\lambda = Z_c$(カメラ座標系での深度)はスケール係数
展開すると:
$$u = \dfrac{p_{11}X + p_{12}Y + p_{13}Z + p_{14}}{p_{31}X + p_{32}Y + p_{33}Z + p_{34}}$$ $$v = \dfrac{p_{21}X + p_{22}Y + p_{23}Z + p_{24}}{p_{31}X + p_{32}Y + p_{33}Z + p_{34}}$$よくある混乱ポイント(落とし穴)
同じ記号や名前が資料ごとに別物を指すことが多く、これがコンピュータビジョン学習を難しくする最大の要因の一つ。ネットの資料が矛盾して見えるのは、多くが自分の採用した規約を明記していないため。まず、名前そのものが分野で衝突する:
| 呼び名 | ある文脈での意味 | 別の文脈での意味 |
|---|---|---|
| カメラ行列(camera matrix) | OpenCV:内部行列 $K$(3×3)。関数引数も cameraMatrix | Hartley–Zisserman(MVG):射影行列 $P = K[R\,|\,\mathbf{t}]$(3×4) |
| 射影行列(projection matrix) | CV:$P = K[R\,|\,\mathbf{t}]$(3×4、3D→2D) | OpenGL:透視投影行列(4×4、視錐台→クリップ空間)で別物 |
| 外部パラメータ / ビュー行列 | world→camera の $[R\,|\,\mathbf{t}]$ | 「カメラ姿勢」= camera→world(逆変換)を指すことも |
名前が同じでも指す行列が違うので、まず「その資料での定義」を確認するのが安全。次に、式の書き方・規約の食い違い:
- 行ベクトル vs 列ベクトル:列ベクトル規約 $\mathbf{p}' = R\,\mathbf{p}$(本記事・数学・OpenCV)か、行ベクトル規約 $\mathbf{p}' = \mathbf{p}\,R$(DirectX 等)か。転置で式が丸ごと入れ替わる。さらに行列のメモリ配置が row-major(C / NumPy)か column-major(OpenGL / Eigen 既定)かも別問題。
- 変換の向き:world→camera($R,\ \mathbf{t}$)か camera→world($R^{\top},\ \mathbf{C} = -R^{\top}\mathbf{t}$)か。同じ「外部パラメータ」でも逆を指すことがある(図3・図4)。
- 回転の能動 / 受動:点を回す(active)か座標系を回す(passive)か。$R$ と $R^{\top}$ が入れ替わる。
- 座標系の手系・軸:右手 / 左手・$Y$ 上 / 下・$Z$ 前 / 後(上の比較表)。
- 画像座標:原点が左上(CV)か左下(OpenGL)か。ピクセル中心が整数座標か $+0.5$ か。
- 回転の表し方:オイラー角の回転順序、クォータニオンの $w$ 先頭 / 末尾・Hamilton / JPL 規約。
座標系のまとめ
コード例(Python)
import numpy as np
# 内部パラメータ
fx, fy = 800, 800
cx, cy = 320, 240
K = np.array([
[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]
])
# 外部パラメータ(例: Z軸周りに45度回転、(1,0,5)に移動)
theta = np.radians(45)
R = np.array([
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0],
[0, 0, 1]
])
t = np.array([[1], [0], [5]])
# 外部パラメータ行列 [R|t]
Rt = np.hstack([R, t])
# カメラ行列 P = K[R|t]
P = K @ Rt
print("Camera Matrix P:")
print(P)
# 3D点の投影
P_world = np.array([0, 0, 0, 1]) # ワールド原点
p_homogeneous = P @ P_world
p = p_homogeneous[:2] / p_homogeneous[2]
print(f"Projected point: ({p[0]:.1f}, {p[1]:.1f})")
# カメラ位置(ワールド座標)
camera_position = -R.T @ t.flatten()
print(f"Camera position: {camera_position}")
# OpenCVでの使用
import cv2
# cv2.projectPoints() を使用する場合
rvec, _ = cv2.Rodrigues(R) # 回転行列 → 回転ベクトル
tvec = t # tvec はカメラ位置ではなく、カメラ座標系でのワールド原点の位置(= t)
points_3d = np.array([[0, 0, 0]], dtype=np.float32)
points_2d, _ = cv2.projectPoints(points_3d, rvec, tvec, K, None)
まとめ
- カメラ行列 $P = K[R|\mathbf{t}]$ は3D→2D投影を表す $3 \times 4$ 行列
- 内部パラメータ $K$: 焦点距離、主点(カメラ固有)
- 外部パラメータ $[R|\mathbf{t}]$: カメラの位置・姿勢
- 投影は同次座標で線形に表現できる
- 内部パラメータ $K$ はカメラキャリブレーションで求める(詳細は後続の章)。まずは次章の練習問題で理解を確認しよう