5.23. 透視校正

警告

任意 3x3 transform 矩陣僅在 OpenMV Cam N6 上受支援——在其他所有開發板上此關鍵字都會被無聲忽略。需要在其他平台上執行的應用程式必須改用現成的 rotation_corr() 方法(搭配其 corners= 形式),或在裝置外預先計算校正後的影像。

現成的 rotation_corr() 方法將一類特定的透視扭曲封裝在一小組參數之後,並可在每一款受支援的開發板上執行。有些應用程式需要不符合該形式的扭曲:從一個四邊形到另一個四邊形的任意投影重映射、針對已知安裝且已離線算出的校準校正,或是由某個上游演算法直接提供的現成扭曲矩陣。對於這些需求,draw_image()——連同 copy()crop()scale()——皆接受一個 transform 關鍵字,可直接接收手動建構、用以描述扭曲的 3x3 矩陣。

5.23.1. 仿射與投影變換

幾何扭曲以齊次座標(homogeneous coordinates)表示:將像素位置 (x, y) 附加一個 1 後,再乘以一個 3x3 矩陣。

仿射(affine)形式是起點。其底列固定為 \((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'\) 維持為一,除法不起作用——便回到仿射形式。當其中任一者非零時,\(w'\) 會隨輸入位置變化,使不同位置的像素受到不同程度的透視縮短,因而不再維持平行線平行——這正是從斜角觀看平面所產生的梯形效應。投影變換是能將直線映射為直線的最一般幾何扭曲;縮放、翻轉、轉置、旋轉以及四角點旋轉校正全都是它的特例。

那些具名變換可直接從仿射形式推導而出。恆等變換即單位矩陣,而:

\[\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}\]

對於大多數手動建構的變換,應用程式會以其中一個作為基底,再為每個額外運算乘入更多矩陣,最終得到一個描述複合扭曲的單一 3x3 矩陣。矩陣由右向左套用:\(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 關鍵字傳入,須提供為一個 3x3 的 ulab.numpy.ndarray。應採用的方法是 draw_image(),它會在將來源繪製到目的地時透過矩陣對其進行扭曲——結果會落在應用程式所控制的緩衝區中,且該扭曲會與呼叫上的其他一切一同合成:縮放、alpha 混合、遮罩。

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)

此範例將 img 扭曲繪製到 canvas 上,在各方向縮放 1.2 倍,並分別向左與向上移動 20 與 15 個像素——這是一個直接從上述矩陣項目建構而成的仿射扭曲。在 copy()crop()scale() 上使用相同的關鍵字,則會將該扭曲套用至影像本身。