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)\):
展开后,每个输出坐标都是输入坐标的线性组合加上一个常数:
它涵盖了缩放、旋转、错切和平移的任意组合——并且在所有这些变换下,平行线始终保持平行。
射影(projective,即透视)形式解放了底行:
展开后:
正是对 \(w' = g x + h y + 1\) 的除法使该变换成为射影变换而非仅仅是仿射变换。当 \(g\) 和 \(h\) 都为零时,\(w'\) 保持为一,除法不起任何作用——又回到了仿射形式。当二者之一非零时,\(w'\) 随输入位置变化,不同位置的像素会被以不同程度透视缩短,这便不再保持平行线平行——这恰恰是从倾斜角度观看平面时的梯形效应。射影变换是把直线映射为直线的最一般的几何扭曲;缩放、翻转、转置、旋转以及四角旋转校正都是它的特例。
上述具名变换可直接从仿射形式中得出。恒等变换即单位矩阵,此外还有:
对于大多数手工构建的变换,应用程序会以其中之一作为基础,再为每个附加操作乘入更多矩阵,最终得到一个描述复合扭曲的单一 3×3 矩阵。矩阵从右向左作用:\(M = T R S\) 会先执行缩放,再执行旋转,最后执行平移。每个人最终都需要的复合变换是围绕图像中心的旋转——单纯的旋转矩阵会使图像围绕左上角的像素原点旋转,因此居中版本会先将中心 \((c_x, c_y)\) 移到原点、旋转、再移回去:
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() 上使用同一关键字,则会将该扭曲应用于图像本身。