Title: Fill an Apollonian circle packing fractal with images in Python
This post is a relatively straightforward joining of these posts:
Loading Images
When you click the Pick Image button, the following code executes.
def pick_image(self):
'''Let the user pick the image.'''
filetypes = [
('Image files', '*.png *.bmp *.jpg *.gif'),
('All Files', '*,*')]
filename = askopenfilename(filetypes=filetypes)
if len(filename) > 0:
image = Image.open(filename)
image = image.convert('RGBA')
self.image = elliptical_transparent_image(image)
self.draw_button.config(state=tk.NORMAL)
This code displays a file selection dialog so you can pick an image file. If you pick a file and click Open, the code uses Image.open to load the file. It calls the image's convert method to convert it to the RGBA format so we know it has an alpha (opacity) component.
Next, the code passes the result to elliptical_transparent_image to make the image elliptical. (See Make an elliptical image from an image that already has transparency in Python.)
After it has loaded the image and made it elliptical, the code enables the program's Draw button so you can draw the Apollonian circle packing.
Drawing the Circle Packing
When you click Draw, the following code draws the Apollonian circle packing.
def draw(self, *args):
'''Draw a big circle with three smaller circles inside it.'''
if self.draw_button['state'] != tk.NORMAL:
self.pick_image()
return
self.canvas.delete(tk.ALL)
self.photo_images = []
depth = int(self.depth_var.get())
self.canvas.update()
wid = self.canvas.winfo_width()
hgt = self.canvas.winfo_height()
margin = 10
radius = min(wid, hgt) / 2 - margin
cx = wid / 2
cy = hgt / 2
outline = 'blue'
fill = 'light blue'
draw_outlines = self.outline_var.get()
# Get the circles.
circles = make_apollonian_circle_packing(depth, cx, cy, radius)
# Draw the circles except for the outer one.
for x, y, r, depth in circles[1:]:
rect = (x-r, y-r, x+r, y+r)
# Draw the circle's background.
self.canvas.create_oval(rect, fill=fill, outline='')
self.photo_images.append(
draw_image_at(self.canvas, self.image, x, y, r, cx, cy))
if draw_outlines:
self.canvas.create_oval(rect, outline=outline)
# Draw the outer circle.
x, y, r, depth = circles[0]
rect = (x-r, y-r, x+r, y+r)
self.canvas.create_oval(rect, outline=outline)
The code first deletes everything on the program's canvas and gets the canvas's width and height. It uses those values to calculate the packing's radius and center position.
Next, the code calls make_apollonian_circle_packing (see Draw an Apollonian circle packing fractal in Python) to get the circles that define the packing. It then loops through the circles, skipping the first one which encloses the whole packing.
The code fills each circle in light blue. It then calls draw_image_at (described next) to draw the image in the circle. If the Outline box is checked, the code draws the circle's outline.
After it has drawn all of the circles except the first one, the code draws that circle's outline.
Drawing Images
The following draw_image_at function draws an image inside a circle with center (x, y) and radius r. It scales the image to fit and rotates it so its top is closest to the packing's center at (cx, cy).
def draw_image_at(canvas, image, x, y, r, cx, cy):
'''
Rotate the image so its top points toward (cx, cy), resize
it to fit the rectangle, draw it, and return the PhotoImage.
'''
if r < 1:
return
# Rotate.
angle = 90 - math.degrees(math.atan2(cy - y, cx - x))
image = image.rotate(angle)
# Resize.
wid = hgt = int(2 * r)
image = image.resize((wid, hgt))
# Draw.
photo_image = ImageTk.PhotoImage(image)
canvas.create_image(x, y, image=photo_image, anchor=tk.CENTER)
return photo_image
PIL raises an error if you try to resize an image so it's smaller than 1 pixel tall or wide, so the function first checks that the radius r is at least 1 and returns if it is not.
Next, the code uses mat.atan2 to get the angle from the center of this circle (x, y) to the center of the packing (cx, cy). It subtracts that angle from 90° to get the angle by which it must rotate the image to orient it properly. (Feel free to change that calculation if you want to change the image orientations.) The function then calls image.rotate to rotate the image.
After it rotates the image, the function resizes it to fit the circle. Finally, it converts the image into an ImageTk.PhotoImage that tkinter can understand, displays the image on the canvas, and returns the PhotoImage so the calling code can save it.
Conclusion
This post shows how you can fill an Apollonian circle packing with images of your cat, kids, self, or whatever you like. I'm not sure how often you'll need to do that, but the project does provide some interesting mathematical and image processing challenges.
Download the example to experiment with it and to see additional details. For an extra challenge, try modifying the program to display three different images arranged in some reasonable way.
|