04 pygame编程入门之四:Surface模块简介

pygame编程入门之四:Surface模块简介

作者: Pete Shinners([email protected])

翻译: 杨晓宏([email protected])

Introduction

本教程将尝试介绍NumPy和pygame surfarray模块。对于初学者来说,使用surfarray的代码是相当令人生畏的。但实际上,只需要几个概念,你将会使用surfarray模块,就可以从直接的python代码中执行像素级操作。性能可以非常接近于c代码的级别。

通过修改像素值来获得更高级的效果是非常棘手的。仅仅掌握数字Python(SciPy的原始数组包是数字,NumPy的前身)需要大量的学习。在本教程中,用基本的知识,并使用大量的例子来演示。在完成本教程之后,应该对surfarray的工作原理有一个基本理解。

Numeric Python

如果您没有安装python NumPy包,现在需要这样做。可以从NumPy下载页面下载软件包,以确保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 "", line 1, in 
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时,还有一个切片的特性。切片数组还允许您指定切片的增量。带有增量的切片的语法是startindex:endindex: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肯定还有很多内容,这只是一个简单介绍。此外,我们还想继续玩那些有趣的东西,对吧?

Import Surfarray

为了使用surfarray模块,我们需要导入它。因为surfarray和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."

Surfarray Introduction

在surfarray中有两种主要的功能。一组用来创建一个数组的函数,它是一个表面像素数据的副本。其他函数创建了阵列像素数据的引用副本,因此对数组的更改会直接影响原始表面。还有其他一些功能允许您访问任何像素的alpha值作为数组,以及其他一些有用的功能。稍后讨论这些其他函数。
当处理这些表面阵列时,有两种表示像素值的方法。首先,它们可以被表示为映射整数。这种类型的数组是一个简单的2D数组,它带有一个表示表面的映射颜色值的整数。这种类型的数组有利于移动图像的某些部分。另一种类型的数组使用三个RGB值来表示每个像素的颜色。这种类型的数组使得执行改变每个像素颜色非常简单。这种类型的数组也有点棘手,因为它本质上是一个3D数字数组。尽管如此,一旦你的思维进入了正确的模式,它并不比使用普通的2D数组困难得多。
NumPy模块使用机器的自然数字类型来表示数据值,因此NumPy数组可以由8位、16位和32位的整数组成。(数组也可以使用其他类型,比如float和double,但是对于我们的图像处理,我们主要需要用整数类型)。由于整数大小的限制,您必须稍微注意一下,引用像素数据的数组类型可以正确地映射到其它适合类型的数据。从表面创建这些数组的函数是:

surfarray.pixels2d(surface)

创建一个引用原始表面数据的2D数组(整数像素值)。这将适用于所有表面格式,除了24位。

surfarray.array2d(surface)

创建一个从任何类型的表面复制的2D数组(整数像素值)。

surfarray.pixels3d(surface)

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

surfarray.array3d(surface)

创建一个从任何类型的表面复制的3D数组(RGB像素值)。
这里有一个小图表,可以更好地说明应该在哪些表面上使用哪些类型的函数。如您所见,arrayXD函数都适用于任何类型的表面。

  • 32-bit 24-bit 16-bit 8-bit(c-map)
  • pixel2d yes yes yes
  • array2d yes yes yes yes
  • pixel3d yes yes
  • array3d yes yes yes yes

Examples

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

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

第一个例子创建了一个黑色数组。当您需要创建一个特定大小的新数字数组时,最好使用零函数。这里我们创建一个所有0的2D数组并显示它。

striped
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数组,它是映射(原始)表面像素的副本。我们将在其余的样本中使用这个图像。

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

这里垂直地翻转图像。所要做的就是使用原始的图像数组并使用一个负增量来分割它。

scaledown
scaledown = rgbarray[::2,::2]
surfdemo_show(scaledown, 'scaledown')

根据最后一个例子,将图像缩小是非常合理的。我们只是用垂直和水平的增量来分割所有的像素。

scaleup
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
redimg = N.array(rgbarray)
redimg[:,:,1:] = 0
surfdemo_show(redimg, 'redimg')

现在我们使用3D数组来改变颜色。这里我们把绿色和蓝色的所有值设为零。留下了红色的通道。

soften
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阵列的一点上,算术运算的精度是由具有最大数据类型的阵列决定的。
因此,如果因子没有被声明为numpy.int32的数组,那么将使用numpy.int8来执行乘法,每个rgbarray元素是8位整数类型。这将导致值截断。为了避免截断,也必须声明柔化数组的整数大小比rgbarray更大。

xfade
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.5倍可以让你在两个图像之间选择一个线性的交叉渐变。
希望在这一点上,看到如何使用surfarray来执行特殊的效果和转换,而这些特效和转换只可能在像素级别上实现。至少,您可以使用surfarray来做大量的表面处理。设置Surface.set_at() Surface.get_at()类型操作。但是不要认为已经完成了,还有很多东西要学。

Surface 锁定

与pygame的其他部分一样,surfarray会在访问像素数据时自动锁定任何表面。不过,还有一件事需要注意。在创建像素阵列时,原始表面将在该像素阵列的生命周期内被锁定。记住这一点很重要。一定要“del”像素数组,或者让它超出范围(例如,当函数返回时)。
还要注意,您真的不想在硬件表面(HWSURFACE)上做很多(如果有的话)直接像素访问。这是因为实际表面数据存在于显卡上,而在pci/agp总线上传输像素并不快。

透明

surfarray模块有几种方法来访问表面的alpha/colorkey值。所有的alpha函数都不会受到表面的整体透明性的影响,只是像素的alpha值。下面是这些函数的列表。

surfarray.pixels_alpha(surface)

创建一个引用原始表面alpha数据的2D数组(整数像素值)。这只适用于带有8位alpha组件的32位图像。

surfarray.array_alpha(surface)

创建一个从任何类型的表面复制的2D数组(整数像素值)。如果surface没有alpha值,那么数组将是完全不透明的值(255)。

surfarray.array_colorkey(surface)

创建一个2D数组(整数像素值),它被设置为透明(0),无论哪个像素的颜色与Surface的颜色匹配。

Other Surfarray Functions

在surfarray中,只有很少的其他功能可用。您可以在surfarray参考页面上获得一个更好的列表,其中包含更多的文档。不过,有一个非常有用的功能。

surfarray.blit_array(surface, array)

这将把任何类型的2D或3D表面阵列转换到相同维度的表面。这个surfarray blit通常比为一个被引用的像素数组要快得多。尽管如此,它不应该像正常的表面那样快,因为这些都是经过优化的。

更多高级 NumPy

关于NumPy数组,你应该知道一些。当处理非常大的数组时,比如640x480的大数组,有一些额外的东西需要注意。主要是,在使用像+和*这样的操作符。这些操作符必须对数组的临时副本复制到另一个数组中。这可能会耗费很多时间。幸运的是,所有的NumPy操作符都有特殊的功能,可以执行“就地”操作。
例如,您想要替换屏幕:=screen+brightmap和更快的添加(屏幕、brightmap、屏幕)。无论如何,您需要阅读NumPy UFunc文档,了解更多关于这方面的内容。在处理数组时,它是很重要的。
在使用NumPy阵列时,需要注意的另一件事是阵列的数据类型。一些数组(特别是映射的像素类型)通常返回带有无符号8位值的数组。如果您不小心,这些数组很容易溢出。NumPy将使用您在C程序中相同的强制方法,因此将一个操作与8位数字和32位数字混合在一起将会得到32位数字的结果。你可以转换数组的数据类型,但是一定要知道你有什么类型的数组,如果NumPy在一个精度被破坏的情况下,它会引发一个异常。
最后,请注意,当在3D数组中分配值时,它们必须在0到255之间,否则就会得到一些未定义的截断。

毕业了

好了,快速入门的数字Python和surfarray。希望现在你明白了什么是可能的,即使你从来没有使用它们,你也不必害怕看到代码所做的事情。查看vgrade示例,以获得更多的数字数组操作。也有一些“火焰”演示使用surfarray来创建一个实时的火效果。
最重要的是,自己尝试一些事情。一开始慢慢来,我已经看到了一些很棒的东西,比如径向渐变那样的。祝你好运。

你可能感兴趣的:(04 pygame编程入门之四:Surface模块简介)