如何更快地渲染曼德布罗集?
我现在正在用PhotoImage和tkinter逐个像素地绘制曼德尔布罗特集合。我基本上是直接使用算法,没有做任何修改。有没有什么方法可以让计算更快呢?比如快速填充大面积的颜色,或者预先计算一些常量?
代码的一部分:
ITERATIONS = 50
WIDTH, HEIGHT = 600, 600
CENTER = (-.5, 0)
DIAMETER = 2.5
def mandel(c):
z = 0
for i in range(ITERATIONS):
z = z**2 + c
if abs(z) > 2:
return i
return ITERATIONS
root = Tk()
canvas = Canvas(root, width=WIDTH,height=HEIGHT)
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image((WIDTH/2, HEIGHT/2), image=img, state="normal")
real = CENTER[0] - 0.5 * DIAMETER
imag = CENTER[1] - 0.5 * DIAMETER
def color(i):
colors = ("#0000AA", "#88DDFF", "#FF8800", "#000000")
if i == ITERATIONS:
return colors[-1]
else:
choice = (i//2) % len(colors)
return colors[choice]
for x in range(WIDTH):
for y in range(HEIGHT):
i = mandel(complex(real, imag))
img.put(color(i), (x, HEIGHT-y))
imag += DIAMETER / HEIGHT
imag = CENTER[1] - 0.5 * DIAMETER
real += DIAMETER / WIDTH
mainloop()
6 个回答
大部分时间都花在了mandel()函数的内部循环里。用z*z
代替z**2
会有一点点效果。除此之外,我没看到还有什么能加速的地方。虽然我通常喜欢去掉其他循环中的常量,但这样做的效果不大。选择ITERATIONS
时,让ITERATIONS//2 % len(colors) == len(colors)-1
,比如46 //2 % 4 == 3
,可以让代码更简单。利用x轴的对称性可以把时间缩短一半。从0开始imag可以避免从+/- DIAMETER / 2进行300次减法时产生的误差,这样在图像中能得到一个干净的中心线。
from tkinter import *
ITERATIONS = 46
WIDTH, HEIGHT = 601, 601 # odd for centering and exploiting symmetry
DIAMETER = 2.5
start = (-.5 - DIAMETER / 2, 0) # Start y on centerline
d_over_h = DIAMETER / HEIGHT
d_over_w = DIAMETER / WIDTH
def mandel(c):
z = 0
for i in range(ITERATIONS):
z = z*z + c
if abs(z) > 2:
return i
return ITERATIONS
root = Tk()
canvas = Canvas(root, width=WIDTH,height=HEIGHT)
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image(((WIDTH+1)//2, (HEIGHT+1)//2), image=img, state="normal")
real, imag = start
colors = ("#0000AA", "#88DDFF", "#FF8800", "#000000")
ncolors = len(colors)
yrange = range(HEIGHT//2, -1, -1) # up from centerline
ymax = HEIGHT - 1
for x in range(WIDTH):
for y in yrange:
i = mandel(complex(real, imag))
color = colors[i//2 % ncolors]
img.put(color, (x, y))
img.put(color, (x, ymax - y))
imag += d_over_h
imag = start[1]
real += d_over_w
mainloop()
纯Python在处理数字计算时速度并不快。想要加快速度,最简单的方法就是使用PyPy这个工具。如果用PyPy还是不够快,那就可以试着用numpy来优化你的算法,让它们更高效。如果这样做还是不够快,那就可以考虑使用Cython,或者干脆用C语言重写一遍。
为了稍微提高一点速度(但这并不足以弥补编译语言和解释语言之间的差距),你可以提前计算一些值。
现在,你在每次内循环中都在计算 DIAMETER / HEIGHT
,而在每次外循环中也在计算 CENTER[1] - 0.5 * DIAMETER
和 DIAMETER / WIDTH
。你可以把这些计算提前做。
len(colors)
这个值也是不会变化的,可以用一个常量来替代。实际上,我可能会把那个函数写成
def color(i):
if i == ITERATIONS:
return "#000000"
else:
return ("#0000AA", "#88DDFF", "#FF8800", "#000000")[(i//2) % 4]
# are you sure you don't want ("#0000AA", "#88DDFF", "#FF8800")[(i//2) % 3] ?
另外,x**2
的计算速度比 x*x
慢(因为 x**y
这个操作符在 y==2
的简单情况下不会优化),所以你可以稍微加快这个计算。
这是我的代码,它可以在8到9秒内绘制一个640x480的曼德尔布集合。
每个像素最多进行256次计算,使用了一份颜色映射列表,只需要一次性把结果放到PhotoImage
中,并且不依赖对称性,所以它可以显示集合的任何缩放区域。
可惜的是,Tkinter不允许直接访问PhotoImage
的像素信息作为缓冲区,而且需要使用笨重的字符串。
from tkinter import Tk, Canvas, PhotoImage,NW,mainloop
from time import clock
def mandel(kx,ky):
""" calculates the pixel color of the point of mandelbrot plane
passed in the arguments """
global clr
maxIt = 256
c = complex(kx, ky)
z = complex(0.0, 0.0)
for i in range(maxIt):
z = z * z + c
if abs(z) >= 2.0:
return (255-clr[i],0,0)
return(0,0,0)
def prepare_mdb(xa,xb,ya,yb):
""" pre-calculates coordinates of the mandelbrot plane required for each
pixel in the screen"""
global x,y,xm,ym
xm.clear
ym.clear
xm=[xa + (xb - xa) * kx /x for kx in range(x)]
ym=[ya + (yb - ya) * ky /y for ky in range(y)]
x=640
y=480
#corners of the mandelbrot plan to display
xa = -2.0; xb = 1.0
ya = -1.5; yb = 1.5
#precalculated color table
clr=[ int(255*((i/255)**12)) for i in range(255,-1,-1)]
xm=[]
ym=[]
prepare_mdb(xa,xb,ya,yb)
#Tk
window = Tk()
canvas = Canvas(window, width = x, height = y, bg = "#000000")
t1=clock()
img = PhotoImage(width = x, height = y)
canvas.create_image((0, 0), image = img, state = "normal", anchor = NW)
pixels=" ".join(("{"+" ".join(('#%02x%02x%02x' % mandel(i,j) for i in xm))+"}" for j in ym))
img.put(pixels)
canvas.pack()
print(clock()-t1)
mainloop()
一次只设置一个像素可能是导致程序变慢的主要原因。与其每次为每个像素都调用一次put,不如先计算整行像素,或者整个像素矩阵,然后在循环结束时只调用一次put。
你可以在这里找到一个例子,其他地方也有类似的内容: https://web.archive.org/web/20170512214049/http://tkinter.unpythonic.net:80/wiki/PhotoImage#Fill_Many_Pixels_at_Once