优化80x60 RGB像素数组的生命游戏迭代
好的,我有一段Python代码需要优化一下。
- 这段代码是在一个小的(80x60像素)图像上进行“生命游戏”的迭代,并提取其中的RGB值。
- 现在我使用的是嵌套的for循环;我想把这些for循环换成更快的
map()
函数,但如果这样做,我就不知道怎么获取x和y的值,也无法在需要定义的函数外部使用局部变量。 - 使用
map()
会比现在的for循环快吗?我该怎么用它还能够获取x和y呢? - 我现在使用的是pygame的表面(Surfaces),我试过
surfarray/pixelarray
模块,但因为我需要改变和获取每一个像素,所以速度比Surface.get_at()/set_at()
慢很多。 - 另外,有点不太相关……你觉得如果Python不是遍历一个数字列表,而是像其他语言那样只增加一个数字,会不会更快?为什么Python不同时提供普通的for()和foreach()呢?
- 条件语句的数量可能也让事情变慢,对吧?最慢的部分是检查邻居(在这里构建列表n)……我用2D数组的切片访问替换了那部分,但它没有正常工作。
代码的简化版本:
xr = xrange(80)
yr = xrange(60)
# surface is an instance of pygame.Surface
get_at = surface.get_at()
set_at = surface.set_at()
for x in xr:
# ....
for y in yr:
# ...
pixelR = get_at((x,y))[0]
pixelG = get_at((x,y))[1]
pixelB = get_at((x,y))[2]
# ... more complex stuff here which changes R,G,B values independently of each other
set_at((x,y),(pixelR,pixelG,pixelB))
函数的完整版本:
# xr, yr = xrange(80), xrange(60)
def live(surface,xr,yr):
randint = random.randint
set_at = surface.set_at
get_at = surface.get_at
perfect = perfectNeighbours #
minN = minNeighbours # All global variables that're defined in a config file.
maxN = maxNeighbours #
pos = actual # actual = (80,60)
n = []
append = n.append
NEIGHBOURS = 0
for y in yr: # going height-first for aesthetic reasons.
decay = randint(1,maxDecay)
growth = randint(1,maxGrowth)
for x in xr:
r, g, b, a = get_at((x,y))
del n[:]
NEIGHBOURS = 0
if x>0 and y>0 and x<pos[0]-1 and y<pos[1]-1:
append(get_at((x-1,y-1))[1])
append(get_at((x+1,y-1))[1])
append(get_at((x,y-1))[1])
append(get_at((x-1,y))[1])
append(get_at((x+1,y))[1])
append(get_at((x-1,y+1))[1])
append(get_at((x+1,y+1))[1])
append(get_at((x,y+1))[1])
for a in n:
if a > 63:
NEIGHBOURS += 1
if NEIGHBOURS == 0 and (r,g,b) == (0,0,0): pass
else:
if NEIGHBOURS < minN or NEIGHBOURS > maxN:
g = 0
b = 0
elif NEIGHBOURS==perfect:
g += growth
if g > 255:
g = 255
b += growth
if b > growth: b = growth
else:
if g > 10: r = g-10
if g > 200: b = g-100
if r > growth: g = r
g -= decay
if g < 0:
g = 0
b = 0
r -= 1
if r < 0:
r = 0
set_at((x,y),(r,g,b))
3 个回答
map(do_stuff, ((x, y) for x in xrange(80) for y in xrange(60)))
这里的 do_stuff
应该是这样定义的:
def do_stuff(coords):
r, g, b, a = get_at(coords)
# ... whatever you need to do with those ...
set_at(coords, (r, g, b))
你也可以用列表推导式来替代生成器表达式,作为 map
的第二个参数(把 ((x, y) ...)
换成 [(x, y) ...]
),并且用 range
代替 xrange
。不过我觉得这样做对性能的影响不会太大。
编辑:要注意,gs 说得对,for
循环并不是你代码中最需要优化的部分……减少对 get_at
的多余调用更为重要。实际上,我不太确定把循环换成 map
是否真的能提高性能……不过,我觉得 map
的写法更容易理解(也许是因为我有函数式编程的背景……),所以还是分享给你吧。 ;-)
让你的代码变慢的原因可能不是循环,因为循环其实非常快。
真正让代码变慢的是函数调用的次数。例如,
pixelR = get_at((x,y))[0]
pixelG = get_at((x,y))[1]
pixelB = get_at((x,y))[2]
这个要慢很多(大概慢了3倍吧)
r, g, b, a = get_at((x,y))
每次调用 get_at
和 set_at
都会锁定表面,所以直接用可用的方法访问像素会更快。看起来最合理的方法是 Surface.get_buffer
。
在你的例子中,使用 map
是不行的,因为你需要索引。即使只有80和60个数字,使用 range()
可能比 xrange()
更快。
因为你要处理每一个像素,所以我觉得不使用Surface
会让你的速度提升最大。
我建议你先把你的80x60的图像转换成一个普通的32位像素位图文件。然后把像素数据读入一个Python的array
对象中。这样你就可以遍历这个array
对象,读取值、计算新值,并快速地把新值放回去。完成后,保存你的新位图图像,然后再转换成Surface
。
你也可以使用24位像素,但那样速度可能会慢一些。32位像素意味着每个像素用一个32位的整数表示,这样处理像素数组会简单很多。而24位像素则是每个像素用3个字节表示,索引起来就麻烦多了。
我相信这种方法能让你比试图避免使用for
循环获得更快的速度。如果你试了这个方法,请在这里分享一下效果如何。祝你好运。
编辑:我原以为array
只有一个索引。我不太明白你是怎么让两个索引一起工作的。我本来期待你会这样做:
def __i(x, y):
assert(0 <= x < 80)
assert(0 <= y < 60)
i = (y*80 + x) * 4
return i
def red(x, y):
return __a[__i(x, y)]
def green(x, y):
return __a[__i(x, y) + 1]
def blue(x, y):
return __a[__i(x, y) + 2]
def rgb(x, y):
i = __i(x, y)
return __a[i], __a[i + 1], __a[i + 2]
def set_rgb(x, y, r, g, b):
i = __i(x, y)
_a[i] = r
_a[i + 1] = g
_a[i + 2] = b
# example:
r, g, b = rgb(23, 33)
因为Python的array
只能存放一种类型,所以你需要把类型设置为“无符号字节”,然后像我展示的那样进行索引。
当然,__a
就是实际的array
变量。
如果这些都没帮助,可以试试把你的位图转换成一个列表,或者三个列表。你可以使用嵌套列表来实现二维地址。
希望这些对你有帮助。如果没有帮助,那可能是我没理解你在做什么;如果你能多解释一下,我会尽量改进我的回答。