5.23. 透视校正

警告

任意 3×3 的 transform 矩阵仅在 OpenMV Cam N6 上受支持——在其他所有开发板上该关键字都会被静默忽略。需要在其他平台上运行的应用程序必须使用现成的 rotation_corr() 方法(及其 corners= 形式),或在板外预先计算校正后的图像。

现成的 rotation_corr() 方法将一类特定的透视扭曲封装在一小组参数之后,并可在每一款受支持的开发板上运行。有些应用程序需要不符合该形式的扭曲:从一个四边形到另一个四边形的任意射影重映射、针对某个已离线计算好的已知安装的标定校正、或由某个上游算法直接提供的现成扭曲矩阵。对于这些情况,draw_image()——以及 copy()crop()scale()——接受一个 transform 关键字,该关键字直接接收手工构建的、描述该扭曲的 3×3 矩阵。

5.23.1. 仿射变换与射影变换

几何扭曲用齐次坐标(homogeneous coordinates)表示:像素位置 (x, y) 在末尾追加一个 1,再乘以一个 3×3 矩阵。

仿射(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\]

它涵盖了缩放、旋转、错切和平移的任意组合——并且在所有这些变换下,平行线始终保持平行。

射影(projective,即透视)形式解放了底行:

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

对于大多数手工构建的变换,应用程序会以其中之一作为基础,再为每个附加操作乘入更多矩阵,最终得到一个描述复合扭曲的单一 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(),它在将源图像绘制到目标上的同时,通过该矩阵对源图像进行扭曲——结果落入应用程序控制的缓冲区中,并且该扭曲会与本次调用中的其他一切操作复合在一起:缩放、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() 上使用同一关键字,则会将该扭曲应用于图像本身。