冲浪阵列游戏攻略

作者

皮特·辛纳斯

联系方式

pete@shinners.org

引言

本教程将尝试向用户介绍NumPy和pyGame surfarray模块。对于初学者来说,使用surfarray的代码可能非常吓人。但实际上,只有几个概念需要理解,您就可以开始运行了。使用surfarray模块,可以从直接的python代码执行像素级操作。性能可以变得相当接近用C编写代码的水平。

您可能只想跳到 “例子” 部分了解使用此模块可以实现的功能,然后从这里开始学习。

现在我不会试图愚弄你,让你认为一切都很容易。要通过修改像素值来获得更高级的效果是非常棘手的。仅仅掌握Numera Python(本网站最初的数组包就是Numeric,也就是NumPy的前身)就需要大量的学习。在本教程中,我将坚持基础知识,并使用大量的例子,试图播下智慧的种子。完成教程后,您应该有一个基本的控制如何冲浪阵列的工作方式。

数字 Python

如果您尚未安装python NumPy包,则需要立即安装。您可以从 NumPy Downloads Page 为了确保NumPy为您工作,您应该从交互式的python提示符中得到类似下面这样的内容。**

>>> from numpy import *                    #import numeric
>>> a = array((1,2,3,4,5))                 #create an array
>>> a                                      #display the array
array([1, 2, 3, 4, 5])
>>> a[2]                                   #index into the array
3
>>> a*2                                    #new array with twiced values
array([ 2,  4,  6,  8, 10])

如您所见,NumPy模块为我们提供了一种新的数据类型 阵列 。该对象包含一个固定大小的数组,并且其中的所有值都是相同类型的。这些数组也可以是多维的,这就是我们将它们用于图像的方式。还有比这更多的东西,但足以让我们开始。

如果您查看上面的最后一个命令,您将看到对NumPy数组的数学运算适用于数组中的所有值。这被称为“基于元素的操作”。这些数组也可以像普通列表一样进行切片。切片语法与标准Python对象上使用的语法相同。 (因此,如果需要,请学习:]) 。下面是一些使用数组的更多示例。**

>>> len(a)                                 #get array size
5
>>> a[2:]                                  #elements 2 and up
array([3, 4, 5])
>>> a[:-2]                                 #all except last 2
array([1, 2, 3])
>>> a[2:] + a[:-2]                         #add first and last
array([4, 6, 8])
>>> array((1,2,3)) + array((3,4))          #add arrays of wrong sizes
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: operands could not be broadcast together with shapes (3,) (2,)

因为我们尝试将两个大小不同的数组相加,所以我们在最后的推荐中得到了一个错误。为了使两个数组相互运算,包括比较和赋值,它们必须具有相同的维度。切分原始数据创建的新数组都引用相同的值,这一点非常重要。因此,更改切片中的值也会更改原始值。如何做到这一点很重要。**

>>> a                                      #show our starting array
array([1, 2, 3, 4, 5])
>>> aa = a[1:3]                            #slice middle 2 elements
>>> aa                                     #show the slice
array([2, 3])
>>> aa[1] = 13                             #chance value in slice
>>> a                                      #show change in original
array([ 1, 2, 13,  4,  5])
>>> aaa = array(a)                         #make copy of array
>>> aaa                                    #show copy
array([ 1, 2, 13,  4,  5])
>>> aaa[1:4] = 0                           #set middle values to 0
>>> aaa                                    #show copy
array([1, 0, 0, 0, 5])
>>> a                                      #show original again
array([ 1, 2, 13,  4,  5])

现在我们来看一下两维的小数组。别太担心,入门和拥有二维元组是一样的 (元组中的元组) 。让我们从二维数组开始。**

>>> row1 = (1,2,3)                         #create a tuple of vals
>>> row2 = (3,4,5)                         #another tuple
>>> (row1,row2)                            #show as a 2D tuple
((1, 2, 3), (3, 4, 5))
>>> b = array((row1, row2))                #create a 2D array
>>> b                                      #show the array
array([[1, 2, 3],
       [3, 4, 5]])
>>> array(((1,2),(3,4),(5,6)))             #show a new 2D array
array([[1, 2],
       [3, 4],
       [5, 6]])

现在有了这个二维数组 (即日起以“2D”为准) 我们可以为特定值编制索引,并在两个维度上进行切片。只需使用逗号分隔索引,我们就可以在多个维度中进行查找/切片。仅使用“”作为索引 (或未提供足够的指数) 为我们提供了该维度的所有值。让我们看看这是如何运作的。**

>>> b                                      #show our array from above
array([[1, 2, 3],
       [3, 4, 5]])
>>> b[0,1]                                 #index a single value
2
>>> b[1,:]                                 #slice second row
array([3, 4, 5])
>>> b[1]                                   #slice second row (same as above)
array([3, 4, 5])
>>> b[:,2]                                 #slice last column
array([3, 5])
>>> b[:,:2]                                #slice into a 2x2 array
array([[1, 2],
       [3, 4]])

好的,跟我来,这是最难的了。当使用NumPy时,还有一个要切片的功能。切片数组还允许您指定一个 切片增量 。带增量切片的语法为 start_index : end_index : increment 。::

>>> c = arange(10)                         #like range, but makes an array
>>> c                                      #show the array
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> c[1:6:2]                               #slice odd values from 1 to 6
array([1, 3, 5])
>>> c[4::4]                                #slice every 4th val starting at 4
array([4, 8])
>>> c[8:1:-1]                              #slice 1 to 8, reversed
array([8, 7, 6, 5, 4, 3, 2])

好了,就是这样。这里有足够的信息让您开始将NumPy与surfarray模块一起使用。当然,NumPy还有更多内容,但这只是一个介绍。另外,我们想要的是有趣的东西,对吧?

导入曲面阵列

为了使用SURFARY模块,我们需要导入它。由于SURFARY和NumPy都是pyGame的可选组件,因此在使用它们之前确保它们正确导入是很好的。在这些示例中,我将把NumPy导入一个名为 N 。这将让您知道我正在使用的函数来自NumPy包。 (而且比在每个函数之前键入NumPy短得多) ::

try:
    import numpy as N
    import pygame.surfarray as surfarray
except ImportError:
    raise ImportError, "NumPy and Surfarray are required."

冲浪阵列游戏攻略

SURFARY中有两种主要类型的函数。用于创建作为表面像素数据的副本的阵列的一组函数。其他函数创建数组像素数据的引用副本,因此对数组的更改将直接影响原始曲面。还有其他函数允许您以数组的形式访问任何每像素的Alpha值,以及一些其他有用的函数。我们稍后将讨论这些其他函数。

使用这些曲面数组时,有两种表示像素值的方法。首先,它们可以表示为映射的整数。这种类型的数组是一个简单的2D数组,只有一个整数表示曲面的贴图颜色值。这种类型的数组非常适合移动图像的各个部分。另一种类型的数组使用三个RGB值来表示每种像素颜色。这种类型的数组使更改每个像素的颜色的效果类型变得极其简单。这种类型的数组也比较难处理,因为它本质上是一个3D数值数组。尽管如此,一旦你进入正确的思维模式,这并不比使用普通的2D阵列困难得多。

NumPy模块使用机器的自然数类型来表示数据值,因此NumPy数组可以由8位、16位和32位的整数组成。 (数组还可以使用其他类型,如浮点数和双精度数,但对于我们的图像操作,我们主要需要担心整数类型) 。由于整数大小的限制,您必须格外小心,以确保引用像素数据的数组类型可以正确映射到适当类型的数据。从曲面创建这些数组的函数为:

surfarray.pixels2d(surface)

创建二维数组 (整像素值) 引用原始曲面数据的。这将适用于除24位之外的所有表面格式。

surfarray.array2d(surface)

创建二维数组 (整像素值) 从任何类型的曲面复制的。

surfarray.pixels3d(surface)

创建3D数组 (RGB像素值) 引用原始曲面数据的。这仅适用于具有RGB或BGR格式的24位和32位曲面。

surfarray.array3d(surface)

创建3D数组 (RGB像素值) 从任何类型的曲面复制的。

这里有一个小图表,它可能更好地说明应该在哪些曲面上使用哪些类型的函数。如您所见,这两个arrayXD函数都可以处理任何类型的曲面。

32位

24位

16位

8位(c-map)

像素2d

阵列2d

像素3d

阵列3d

示例

有了这些信息,我们就可以开始尝试表面阵列了。下面是创建一个NumPy数组并在pyGame中显示它们的简短演示。这些不同的测试可以在 arraydemo.py 举个例子。有一个名为的简单函数 surfdemo_show 它会在屏幕上显示一个数组。

全黑
allblack = N.zeros((128, 128))
surfdemo_show(allblack, 'allblack')

我们的第一个示例创建了一个全黑数组。每当需要创建特定大小的新数值数组时,最好使用 zeros 功能。在这里,我们创建一个全零的二维数组并显示它。

条纹
striped = N.zeros((128, 128, 3))
striped[:] = (255, 0, 0)
striped[:,::3] = (0, 255, 255)
surfdemo_show(striped, 'striped')

这里我们处理的是一个3D数组。我们首先创建一个全红色的图像。然后,我们每隔三行切出一块,并将其分配给蓝色/绿色。正如您所看到的,我们可以将3D数组与2D数组几乎完全相同地处理,只是确保为它们分配3个值,而不是单个映射的整数。

Rgbarray
imgsurface = pygame.image.load('surfarray.png')
rgbarray = surfarray.array3d(imgsurface)
surfdemo_show(rgbarray, 'rgbarray')

在这里,我们使用图像模块加载图像,然后将其转换为整数RGB颜色元素的3D数组。曲面的RGB副本始终将颜色排列为 [R、C、0] 对于红色组件,一个 [R、C、1] 对于绿色组件,以及一个 [R、C、2] 蓝色的。这样就可以在不关心实际表面的像素是如何配置的情况下使用它,这与2D数组不同,后者是 mapped (原始)曲面像素。我们将在其余的示例中使用此图像。

翻转
flipped = rgbarray[:,::-1]
surfdemo_show(flipped, 'flipped')

在这里,我们垂直翻转图像。我们所需要做的就是获取原始图像数组,并使用负增量对其进行切片。

按比例缩减
scaledown = rgbarray[::2,::2]
surfdemo_show(scaledown, 'scaledown')

根据最后一个示例,缩小图像是非常合乎逻辑的。我们只需在垂直和水平方向上以2为增量切出所有像素。

按比例扩展
shape = rgbarray.shape
scaleup = N.zeros((shape[0]*2, shape[1]*2, shape[2]))
scaleup[::2,::2,:] = rgbarray
scaleup[1::2,::2,:] = rgbarray
scaleup[:,1::2] = scaleup[:,::2]
surfdemo_show(scaleup, 'scaleup')

放大图像需要做更多的工作,但与前面的缩小类似,我们都是通过切片来完成的。首先,我们创建一个大小是原始数组两倍的数组。首先,我们将原始数组复制到新数组的每隔一个像素中。然后,我们对每隔一个奇数列的像素重复一次。在这一点上,我们已经正确地缩放了图像,但每隔一行都是黑色的,所以我们只需要将每一行复制到它下面的那一行。然后我们就有了一张放大一倍的图像。

重定向
redimg = N.array(rgbarray)
redimg[:,:,1:] = 0
surfdemo_show(redimg, 'redimg')

现在我们使用3D数组来更改颜色。在这里,我们将所有绿色和蓝色的值设置为零。这就只剩下红色通道了。

软化
factor = N.array((8,), N.int32)
soften = N.array(rgbarray, N.int32)
soften[1:,:]  += rgbarray[:-1,:] * factor
soften[:-1,:] += rgbarray[1:,:] * factor
soften[:,1:]  += rgbarray[:,:-1] * factor
soften[:,:-1] += rgbarray[:,1:] * factor
soften //= 33
surfdemo_show(soften, 'soften')

在这里,我们执行3x3卷积滤波,它将柔化我们的图像。这里看起来有很多步骤,但我们正在做的是在每个方向上移动图像1个像素,并将它们全部相加(通过一些乘法来加权)。然后取所有值的平均值。它不是高斯的,但它很快。对于NumPy数组,算术运算的精度由数据类型最大的数组决定。因此,如果因子没有被声明为类型为umpy.int32的1元素数组,则将使用umpy.int8(每个rgbarray元素的8位整数类型)来执行乘法。这将导致值截断。软化数组还必须声明为具有大于rgbarray的整数大小,以避免截断。

渐变淡出
src = N.array(rgbarray)
dest = N.zeros(rgbarray.shape)
dest[:] = 20, 50, 100
diff = (dest - src) * 0.50
xfade = src + diff.astype(N.uint)
surfdemo_show(xfade, 'xfade')

最后,我们在原始图像和纯蓝图像之间进行交叉褪色。这并不令人兴奋,但目标图像可以是任何图像,更改0.50倍增将允许您在两个图像之间的线性交叉淡入淡出中选择任何步长。

希望在这一点上,您可以开始看到如何使用surfarray来执行仅在像素级别上可能的特殊效果和变换。至少,您可以使用surfarray非常快速地执行许多Surface.set_at()Surface.get_at()类型的操作。但不要认为你已经完成了,还有很多东西需要学习。

表面锁定

与pyGame的其他功能一样,surfarrow会在访问像素数据时自动锁定所需的任何表面。不过,还有一件事需要注意。在创建 像素 阵列,则原始表面将在该像素阵列的生命周期内被锁定。记住这一点很重要。一定要 “德尔” 像素阵列或让它超出范围 (即函数返回的时间等)

也要意识到你真的不想做太多 (如有) 硬件表面上的直接像素访问 (HWSURFACE) 。这是因为实际的表面数据存储在图形卡上,并且通过PCI/AGP总线传输像素更改不是很快。

透明度

SURFARY模块有几种访问Surface的Alpha/Colorkey值的方法。所有Alpha函数都不受曲面整体透明度的影响,只受像素Alpha值的影响。以下是这些函数的列表。

surfarray.pixels_alpha(surface)

创建二维数组 (整像素值) 引用原始曲面Alpha数据的。这将仅适用于具有8位Alpha分量的32位图像。

surfarray.array_alpha(surface)

创建二维数组 (整像素值) 从任何类型的曲面复制的。如果表面没有Alpha值,则数组将是完全不透明的值 (255)

surfarray.array_colorkey(surface)

创建二维数组 (整像素值) 设置为透明的 (0) 该像素颜色与Surface Colorkey匹配的任何位置。

其他SurfARRAY函数

SURFARY中只有几个其他功能可用。您可以获得一个更好的列表,其中包含更多关于 surfarray reference page 。不过,有一个非常有用的功能。

surfarray.blit_array(surface, array)

这会将任何类型的2D或3D曲面阵列传输到相同尺寸的曲面上。这种曲面阵列BLIT通常比将数组指定给引用像素数组更快。尽管如此,它应该不会像正常的曲面漂移一样快,因为这些都是非常优化的。

更高级的NumPy

关于NumPy数组,有几件事您应该知道。在处理非常大的数组时,比如640x480大小的数组,有一些额外的事情需要注意。主要是,当使用像+和 在阵列上使它们易于使用,但在大型阵列上也非常昂贵。这些运算符必须制作数组的新临时副本,然后通常复制到另一个数组中。这可能会非常耗时。幸运的是,所有的NumPy运算符都带有可以执行该操作的特殊函数 “就位”*。例如,您可能希望替换 screen[:] = screen + brightmap 以更快的速度 add(screen, brightmap, screen) 。无论如何,您将想要阅读NumPy UFunc文档以获得更多关于这方面的信息。在处理数组时,这一点很重要。

使用NumPy数组时需要注意的另一件事是数组的数据类型。一些数组(尤其是映射的像素类型)通常返回具有无符号8位值的数组。如果您不小心,这些数组很容易溢出。NumPy将使用与您在C程序中找到的相同的强制,因此将一个操作与8位数字和32位数字混合将得到32位数字的结果。您可以转换数组的数据类型,但一定要知道您拥有的是什么类型的数组,如果NumPy遇到精度会被破坏的情况,它将引发异常。

最后,请注意,在将值赋给3D数组时,它们必须介于0和255之间,否则将得到一些未定义的截断。

毕业

好了,现在你就知道了。我对数值 Python 和冲浪阵列的快速入门。希望您现在看到了什么是可能的,即使您从未为自己使用过它们,当您看到可以使用它们的代码时,您也不必害怕。有关更多数值化的数组操作,请查看vgrad示例。还有一些 《火焰》 周围漂浮着的演示程序使用surfarray来创建实时射击效果。

最重要的是,自己尝试一些事情。一开始慢慢地做,我已经看到了SURFARY的一些很棒的东西,比如径向渐变等等。祝好运。




Edit on GitHub