5.23. パースペクティブ補正

警告

任意の3×3のtransform行列はOpenMV Cam N6でのみサポートされており、他のすべてのボードではこのキーワードは黙って無視されます。それ以外の場所で動作させる必要があるアプリケーションは、既製のrotation_corr()メソッド(そのcorners=形式を使う)を使うか、補正済み画像をボード外であらかじめ計算しておく必要があります。

既製のrotation_corr()メソッドは、特定の種類のパースペクティブのゆがみを小さなパラメータの集合の背後にまとめており、サポートされるすべてのボードで動作します。一部のアプリケーションは、その形式に当てはまらないゆがみを必要とします。ある四辺形から別の四辺形への任意の射影リマップ、すでにオフラインで求めてある既知の取り付けに対する校正済みの補正、上流のアルゴリズムから既製で渡されるゆがみ行列などです。それらには、draw_image()copy()crop()scale()とともに)がtransformキーワードを受け取り、ゆがみを直接記述する手作りの3×3行列を渡せます。

5.23.1. アフィン変換と射影変換

幾何ゆがみは同次座標で表現されます。ピクセル位置(x, y)1を付け加え、3×3行列を掛けたものです。

アフィン形式が出発点です。その最下行は\((0, 0, 1)\)に固定されています。

\[\begin{split}\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & c \\ d & e & f \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\end{split}\]

書き下すと、各出力座標は入力座標の一次結合に定数を加えたものになります。

\[x' = a x + b y + c, \qquad y' = d x + e y + f\]

これはスケーリング、回転、せん断、平行移動を任意に組み合わせたものをカバーし、それらすべての下で平行線は平行のままです。

射影(パースペクティブ)形式は最下行を自由にします。

\[\begin{split}\begin{bmatrix} x'' \\ y'' \\ w' \end{bmatrix} = \begin{bmatrix} a & b & c \\ d & e & f \\ g & h & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}, \qquad (x', y') = \left( \frac{x''}{w'}, \; \frac{y''}{w'} \right)\end{split}\]

書き下すと次のとおりです。

\[x' = \frac{a x + b y + c}{g x + h y + 1}, \qquad y' = \frac{d x + e y + f}{g x + h y + 1}\]

\(w' = g x + h y + 1\)による除算こそが、この変換を単なるアフィンではなく射影にするものです。\(g\)\(h\)がどちらもゼロのとき、\(w'\)は1のままで除算は何もしません。再びアフィン形式になります。いずれかがゼロでないとき、\(w'\)は入力位置に応じて変化し、異なる位置のピクセルが異なる量だけ短縮されるため、もはや平行線は平行のままではなくなります。これはまさに、平面を斜めの角度から見たときのキーストーン効果です。射影変換は、直線を直線に移す最も一般的な幾何ゆがみであり、スケーリング、反転、転置、回転、そして4コーナー回転補正はいずれもその特殊な場合です。

名前付き変換はアフィン形式から直接導かれます。恒等変換は単位行列であり、次のとおりです。

\[\begin{split}\underbrace{\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{translate by } (t_x, \; t_y)} \qquad \underbrace{\begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{scale by } (s_x, \; s_y)} \qquad \underbrace{\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{rotate by } \theta}\end{split}\]

ほとんどの手作り変換では、アプリケーションはこれらのいずれかを基礎として始め、追加の演算ごとにさらに行列を掛け合わせ、最終的に合成ゆがみを記述する単一の3×3行列に至ります。行列は右から左へ適用されます。\(M = T R S\)はまずスケールを実行し、次に回転、最後に平行移動を実行します。誰もがいずれ必要とする合成は、画像の中心まわりの回転です。むき出しの回転行列は画像を左上隅のピクセル原点まわりに回転させるため、中心を合わせた版では中心\((c_x, c_y)\)を原点へ移動し、回転させ、元に戻します。

\[\begin{split}M = \underbrace{\begin{bmatrix} 1 & 0 & c_x \\ 0 & 1 & c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{move centre back}} \underbrace{\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{rotate}} \underbrace{\begin{bmatrix} 1 & 0 & -c_x \\ 0 & 1 & -c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{move centre to origin}}\end{split}\]

5.23.2. transformキーワード

行列はtransformキーワードを通じて、3×3のulab.numpy.ndarrayとして与えられます。使うべきメソッドはdraw_image()で、これはソースを行列を通してゆがませながら宛先に描画します。結果はアプリケーションが制御するバッファに収まり、ゆがみは呼び出し上の他のすべて(スケーリング、アルファブレンディング、マスキング)と合成されます。

import ulab.numpy as np

M = np.array([[1.2,  0.0, -20.0],
              [0.0,  1.2, -15.0],
              [0.0,  0.0,   1.0]])

canvas.draw_image(img, transform=M)

この例ではimgcanvasに各方向へ1.2倍に拡大し、それぞれ左へ20ピクセル、上へ15ピクセルずらして描画します。上で説明した行列の要素から直接構築したアフィンゆがみです。copy()crop()scale()上の同じキーワードは、画像そのものにゆがみを適用します。