patternpythonMinor
Fractal rendering fun time
Viewed 0 times
fractalrenderingtimefun
Problem
I wrote up a script a while back to let me play around with fractals. The idea was to have direct access to the script that creates the fractal. None of that close, edit, then run hassle; just edit then run.
renderscript.py contains the GUI:
```
import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfile
class View(tk.Frame):
count = 0
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
tk.Button(self, text="open", command=self.open).pack(fill=tk.X)
tk.Button(self, text="save", command=self.save).pack(fill=tk.X)
tk.Button(self, text="run program", command=self.draw).pack(fill=tk.X)
self.txt = tk.Text(self, height=30)
scr = tk.Scrollbar(self)
scr.config(command=self.txt.yview)
self.txt.config(yscrollcommand=scr.set)
scr.pack(side="right", fill="y", expand=False)
self.txt.pack(side="left", fill="both", expand=True)
self.pack()
def draw(self, size=500):
exec(str(self.txt.get(1.0, tk.END)))
self.pixels = [[(0, 0, 0) for y in range(size)] for x in range(size)]
self.pixels = render(self.pixels)
window = tk.Toplevel(self)
window.resizable(0,0)
canvas = tk.Canvas(window, width=size, height=size, bg='white')
canvas.pack()
img = tk.PhotoImage(width=size, height=size)
canvas.create_image((size/2, size/2), image=img, state="normal")
for y in range(size):
for x in range(size):
img.put(self.rgbtohex(self.pixels[x][y]), (x,y))
window.mainloop()
def rgbtohex(self, rgb):
return ("#" + "{:02X}" 3).format(rgb)
def open(self):
self.txt.delete(1.0, tk.END)
self.txt.insert(tk.END, open(askopenfilename()).read())
def save(self):
f = asksaveasfile(mode='w', defaultextension=".py")
if f is None:
return
text2save = str(self.txt.get(1.0, tk.END))
f
renderscript.py contains the GUI:
```
import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfile
class View(tk.Frame):
count = 0
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
tk.Button(self, text="open", command=self.open).pack(fill=tk.X)
tk.Button(self, text="save", command=self.save).pack(fill=tk.X)
tk.Button(self, text="run program", command=self.draw).pack(fill=tk.X)
self.txt = tk.Text(self, height=30)
scr = tk.Scrollbar(self)
scr.config(command=self.txt.yview)
self.txt.config(yscrollcommand=scr.set)
scr.pack(side="right", fill="y", expand=False)
self.txt.pack(side="left", fill="both", expand=True)
self.pack()
def draw(self, size=500):
exec(str(self.txt.get(1.0, tk.END)))
self.pixels = [[(0, 0, 0) for y in range(size)] for x in range(size)]
self.pixels = render(self.pixels)
window = tk.Toplevel(self)
window.resizable(0,0)
canvas = tk.Canvas(window, width=size, height=size, bg='white')
canvas.pack()
img = tk.PhotoImage(width=size, height=size)
canvas.create_image((size/2, size/2), image=img, state="normal")
for y in range(size):
for x in range(size):
img.put(self.rgbtohex(self.pixels[x][y]), (x,y))
window.mainloop()
def rgbtohex(self, rgb):
return ("#" + "{:02X}" 3).format(rgb)
def open(self):
self.txt.delete(1.0, tk.END)
self.txt.insert(tk.END, open(askopenfilename()).read())
def save(self):
f = asksaveasfile(mode='w', defaultextension=".py")
if f is None:
return
text2save = str(self.txt.get(1.0, tk.END))
f
Solution
Some suggestions:
So here is my version of
- In
calcator, I would use a ternary expression:return 1. if n is None else n/100.
calcatorcan be made even more efficient by havingiterate_mandelbrotreturn100.if the loop finished. Then you just divide the result of that by 100. This will result in 1. if the loop exits, avoiding theiftest entirely.
- I would only do run
self.mandelbrotin your loop, and store the result of that function to a 2D numpy array. Then you can vectorize the rest of the calculation, since it is all just basic math. This should substantially increase performance. You can even move then/100outside the for loop to further improve performance.
- If the previous suggestion does not increase performance enough, you might be able use
multiprocessing.Pool.imapto further increase the performance of the loop.
- For an even more extreme vectorization, you can do all your calculations on all pixels at once.
- Rather than having a
griditorfunction, just useitertools.product.
- Follow pep8
- I would put the current contents of the
if __name__ == "__main__":block in amainfunction and just call that function inside theif __name__ == "__main__":block.
- In
renderI would allow the code to pass a string (which defaults tomandelbrot, and usegetattrto dynamically call method with that name.
- I would rename
iterate_mandelbrottoiterate_pixel.
- I would put in
mandelbrotan argument forzthat lets the user changez. Similarly, I would put an argument injuliathat lets the user change(0.3, 0.6)to something else.
rendershould accept*args, **kwargsthat are then passed directly to themandelbrotorjuliamethod.
- I would move the
juliaandmandelbrotlambdas into their own methods. Or better yet, I would refactor so you just pass thecandzargument.
- In
render, I would let the user set the scale with an argument. Thescaleargument would default toNone. If it isNone, it would be computed automatically as is done now.
- In
render, you only ever work with integers. So I would usen//100incalcatorto make sure it returns an integer. This allows you to avoid the later integer conversions.
So here is my version of
Fractalimport numpy as np
class Fractal:
def mandelbrot(self, pixels, scale, center=(2.2, 1.5), z=0.):
return self.calcolor(pixels, scale, center, zs=z)
def julia(self, pixels, scale, center=(1.5, 1.5), c=(0.3, 0.6)):
if not hasattr(c, 'imag'):
c = complex(*c)
return self.calcolor(pixels, scale, center, cs=c)
def calcolor(self, pixels, scale, center, cs=None, zs=None):
pixels = pixels.asarray(pixels)
xpixels = np.arange(pixels.shape[0])[None, :]
ypixels = np.arange(pixels.shape[1])[:, None]
val = (xpixels+ypixels*1j)*scale-complex(*center)
if cs is None and zs is None:
raise ValueError('Either cs or zs must be specified')
if cs is None:
cs = val
if zs is None:
zs = val
ns = np.full_like(val, 100, dtype='int16')
for n in range(256):
zs = zs**2 +cs
ns[ns>0 & np.abs(zs)>2.0] = n
return ns
def calc_pixels(self, cs, zs):
def render(self, pixels, scale=None, method='mandelbrot', *args, **kwargs):
if scale is None:
scale = 1.0/(len(pixels[0])/3)
try:
func = getattr(self, method)
except AttributeError:
raise ValueError('Unknown method %s' % method)
i = func(pixels, scale, *args, **kwargs)*256
r = i%16 * 16
g = i*8 * 32
b = i%4 * 63
return np.dstack([r, g, b])Code Snippets
import numpy as np
class Fractal:
def mandelbrot(self, pixels, scale, center=(2.2, 1.5), z=0.):
return self.calcolor(pixels, scale, center, zs=z)
def julia(self, pixels, scale, center=(1.5, 1.5), c=(0.3, 0.6)):
if not hasattr(c, 'imag'):
c = complex(*c)
return self.calcolor(pixels, scale, center, cs=c)
def calcolor(self, pixels, scale, center, cs=None, zs=None):
pixels = pixels.asarray(pixels)
xpixels = np.arange(pixels.shape[0])[None, :]
ypixels = np.arange(pixels.shape[1])[:, None]
val = (xpixels+ypixels*1j)*scale-complex(*center)
if cs is None and zs is None:
raise ValueError('Either cs or zs must be specified')
if cs is None:
cs = val
if zs is None:
zs = val
ns = np.full_like(val, 100, dtype='int16')
for n in range(256):
zs = zs**2 +cs
ns[ns>0 & np.abs(zs)>2.0] = n
return ns
def calc_pixels(self, cs, zs):
def render(self, pixels, scale=None, method='mandelbrot', *args, **kwargs):
if scale is None:
scale = 1.0/(len(pixels[0])/3)
try:
func = getattr(self, method)
except AttributeError:
raise ValueError('Unknown method %s' % method)
i = func(pixels, scale, *args, **kwargs)*256
r = i%16 * 16
g = i*8 * 32
b = i%4 * 63
return np.dstack([r, g, b])Context
StackExchange Code Review Q#94970, answer score: 7
Revisions (0)
No revisions yet.