前向映射和反向映射应用于图像变换
本文介绍并解释了两种图像变形的算法:前向映射和反向映射。除了在理论层面上介绍这些算法外,还将应用于实际图像,以查看每种算法的结果和能力。
为了充分理解本文中的所有内容,有必要熟悉2D变换矩阵,这在先前的文章中已经介绍和解释了。
计算机视觉的2D矩阵变换
通过变换矩阵进行缩放、旋转和平移以进行计算机视觉
小猪AI.com
介绍
如前一篇文章所述,对图像应用变形的方法是迭代地对图像的每个像素进行处理,对每个像素单独应用变形。然而,有些用例无法直接应用变换,因为例如一些像素的新位置可能会超出图像域。另一个可能的问题是,新图像可能会有空像素(白色条纹),因为在变换后将所有原始图像的像素映射到新图像的所有像素是困难的。
为避免其中一些问题,将介绍本文中将呈现的两种算法:前向映射 和反向映射,它们应用不同的技术来正确地转换图像。
前向映射
前向映射过程包括简单的图像变换过程,这已在介绍和先前的文章中讨论过:它迭代处理图像的所有像素,并针对每个像素单独应用相应的变换。但是,必须考虑以下示例所示的那些新位置处于变换后的像素之外的情况。

要执行前向映射过程,首先定义一个函数,该函数接收像素的原始坐标作为参数。该函数将对原始像素坐标应用变换,并返回变换后像素的新坐标。以下代码示例显示了旋转变换的函数。
def apply_transformation(original_x: int, original_y: int) -> Tuple[int, int]: # 定义旋转矩阵 rotate_transformation = np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0], [np.sin(np.pi/4), np.cos(np.pi/4), 0], [0, 0, 1]]) # 在原始向量的坐标中设置齐次坐标后应用变换。 new_coordinates = rotate_transformation @ np.array([original_x, original_y, 1]).T # 将新坐标四舍五入到最近的像素 return int(np.rint(new_coordinates[0])), int(np.rint(new_coordinates[1]))
一旦有了这个函数,您只需要迭代处理图像的每个像素,应用变换并检查新像素坐标是否位于原始图像的域之内。如果新坐标在域内,则新图像上的新坐标像素将取原始图像中原始像素的值。如果像素超出了图像,则会省略该像素。
def forward_mapping(original_image: np.ndarray) -> np.ndarray: # 创建与原始形状相同的新图像 new_image = np.zeros_like(original_image) for original_y in range(original_image.shape[1]): for original_x in range(original_image.shape[0]): # 在原始像素的坐标上应用旋转 new_x, new_y = apply_transformation(original_x, original_y) # 检查新坐标是否落在图像域内 if 0 <= new_y < new_image.shape[1] and 0 <= new_x < new_image.shape[0]: new_image[new_x, new_y, :] = original_image[original_x, original_y, :] return new_image
应用旋转变换的结果如下图所示,左边是原始图像,右边是变换后的图像。需要注意的是,对于此图像,坐标原点位于左上角,因此图像沿着逆时针方向绕该点旋转。
![计算机视觉中的前向和反向映射 数据科学 第3张-四海吧 应用正向映射的结果。左侧图像提取自 MNIST 数据集 [1]。全图作者提供](https://miro.medium.com/v2/resize:fit:640/format:webp/1*O9vpwYNeMeOTv_FDQLNnhQ.png)
关于变换的结果,可以看到变换后的图像没有原始图像的全黑背景,而是有许多白条纹。如前所述,这是因为原始图像的像素不总是映射到新图像的所有像素。由于新坐标是四舍五入计算得到的,这导致许多中间像素永远不会接收到值。在这种情况下,由于新图像初始化为所有像素都为空白,因此在变换期间未被赋值的像素将保持为空白,生成变换后图像中的白条纹。
此外,还应注意到另一个明显的问题:重叠。当原始图像的两个像素变换为新图像的同一像素时,就会出现这个问题。对于本文中使用的代码,如果原始图像有两个像素映射到新图像的同一像素,则新像素将采用最后一个已变换的原始像素的值,覆盖已设置的第一个原始像素的值。
反向映射
反向映射算法的出现是为了消除由于变换而生成的图像中的白条纹,以及可能的重叠。正如已经提到的,这些条纹出现在不是所有变换后图像的像素都接收到值的情况下,因为在正向映射过程中计算新坐标时进行了四舍五入,而重叠则是由于原始图像的两个或多个像素映射到新图像的同一像素。
这个算法背后的逻辑很简单:不是将原始图像的每个像素变换为其在新图像中的新坐标(正向),而是将所有新图像的像素反向变换为原始图像的像素(反向)。这样,新图像中将不会有任何像素没有值,因为它们都将采用原始图像的单个像素的值,从而解决了两个问题。
幸运的是,使用变换矩阵对像素的坐标进行的变换可以通过使用逆变换矩阵按相同过程撤消。变换矩阵的这个属性,以及其证明,可以在下图中看到。

考虑到这一属性,算法包括迭代每个新图像的像素,并将逆变换应用于每个像素的坐标,以了解它们必须从原始图像的哪个像素中获取值。
def apply_inverse_transformation(new_x: int, new_y: int) -> Tuple[int, int]: # 定义逆旋转矩阵 rotate_transformation = np.array([[np.cos(np.pi/4), -np.sin(np.pi/4), 0], [np.sin(np.pi/4), np.cos(np.pi/4), 0], [0, 0, 1]]) inverse_rotate_transformation = np.linalg.inv(rotate_transformation) # 应用变换,将位置向量的齐次坐标设置为 1。 original_coordinates = inverse_rotate_transformation @ np.array([new_x, new_y, 1]).T # 将原始坐标四舍五入到最近的像素 return int(np.rint(original_coordinates[0])), int(np.rint(original_coordinates[1]))
请注意,apply_inverse_transformation() 函数的输入是新图像中的坐标,输出是原始图像中的坐标,而不是像正向映射一样输入原始坐标并返回新的坐标。
def backward_mapping(original_image: np.ndarray) -> np.ndarray: # 创建与原始图像形状相同的新图像 new_image = np.zeros_like(original_image) for new_y in range(new_image.shape[1]): for new_x in range(new_image.shape[0]): # 在新像素的坐标上应用反向旋转 original_x, original_y = apply_inverse_transformation(new_x, new_y) # 检查原始坐标是否在图像域内 if 0 <= original_y < original_image.shape[1] and 0 <= original_x < original_image.shape[0]: new_image[new_x, new_y, :] = original_image[original_x, original_y, :] return new_image
应用反向映射旋转变换的结果如下图所示,左侧是原始图像,右侧是变换后的图像。如先前所述,图像围绕坐标原点旋转,该点位于左上角。
![计算机视觉中的前向和反向映射 数据科学 第5张-四海吧 Results of applying Backward Mapping. Left image extracted from MNIST Dataset [1]. Full image by author](https://miro.medium.com/v2/resize:fit:640/format:webp/1*BDeIYvLDNDbRp0bBHf8zZA.png)
在图像中,您可以看到在应用正向映射时出现的所有白色条纹在应用反向映射时已经消失。实际上,您可以看到变换后图像的质量非常好(必须考虑到原始图像的质量不是很好),因此我们可以认为反向映射算法比正向映射更好,特别是在变换过程中出现白色条纹的情况下。
结论
正向映射是一种简单易用的算法,因为它将原始图像的每个像素直接变换到新图像中。然而,该算法存在重叠问题和许多像素没有值的问题,这会显著降低变换后图像的质量。反向映射算法的实现方式与正向映射一样直观,并且具有更好的结果,解决了这两个问题,因为它为新图像的所有像素提供了单个值。
关于算法的执行时间,两者具有相同的复杂度,因此通常使用反向映射算法是更好的选择,因为它具有更好的结果。在理想情况下,负责对每个像素施加变换的函数(在本文中称为apply_transformation()和apply_inverse_transformation())不会构建变换矩阵,而是将其作为参数接收。这将节省正向映射算法构建变换矩阵所需的执行时间,以及反向映射算法构建和反转矩阵所需的执行时间。
总之,与正向映射相比,反向映射算法实现了非常好的结果,两者的执行时间几乎相同。然而,应该注意的是,对于高分辨率图像,两种算法都需要很长时间才能完成变换,尽管它们仍然非常有用,可以为其他更强大的变换算法奠定基础。
参考文献
[1] http://yann.lecun.com/exdb/mnist/