Reputation: 13
I am working on an algorithm for drawing layered rings on a circle that represent different layers of the interior of a planet. My goal is that each layer will be painted pixel by pixel in a randomized distribution according to a proportion of the things that compose that layer, with each component having a different color.
I have managed to get a brute force solution for this, but I'm now trying to optimize it since it is taking a lot of time, here is the code for the first shell:
from PIL import Image, ImageDraw
import numpy as np
width = 1200
height = 800
proportion1 = (0.3,0.6,0.1) # 3 component proportions
colors = ["c1","c2","c3"] # 3 component tags
center_x = width/2
center_y = height/2
radius = 1737 # radius of the planet
core_radius = 300 # radius of the planet's core
core_pixels = round(core_radius*center_x/radius) # pixel radius of the core
first_shell = 600 # outer radius of the first ring
first_shell_pix= round(first_shell*center_x/radius) # pixel radius of first shell
im = Image.new(mode="RGB",size=(width,height),color=(256,256,256)) # Setting up image
# Drawing first shell
for x in range(0,width):
for y in range(0,height):
# Check if pixel falls within the ring coordinates
if (center_x-x)**2 + (center_y-y)**2 >core_pixels**2 and (center_x-x)**2 + (center_y-y)**2 <first_shell_pix**2:
col=np.random.choice(colors,size=1,p=proportion1) # Get random tag
if col =="c1":
color = (0,100,0)
elif col == "c2":
color = (154,205,50)
elif col == "c3":
color = (255,140,0)
im.putpixel((x,y),color) # Paint
The real pictures I want will have several layers and more pixels, and doing the double for loop through all the pixels for each shell would be incredibly inefficient. The only thing that I can think of is having a list with all the pixel coordinates and remove them as they get painted so that the next shell doesn't iterate through all of them, but there has to be a smarter way than that.
Any recommendations?
Upvotes: 1
Views: 145
Reputation: 207425
I tried this by creating a complete array (height x width) of the 3 random colours in one call, then generating the mask annulus by drawing a thick white circle with OpenCV and then copying across the random colours inside the white annulus.
I got 950ms with Tim's code and 20ms with this method, so a 47x speedup. The code is a bit sloppy but you should get the idea:
#!/usr/bin/env python3
from PIL import Image
import numpy as np
import cv2
width = 1200
height = 800
proportion1 = (0.3,0.6,0.1) # 3 component proportions
colors = ["c1","c2","c3"] # 3 component tags
cmap = {'c1':(0,100,0),'c2':(154,205,50),'c3':(255,140,0)}
center_x = width//2
center_y = height//2
radius = 1737 # radius of the planet
core_radius = 300 # radius of the planet's core
core_pixels = round(core_radius*center_x/radius) # pixel radius of the core
first_shell = 600 # outer radius of the first ring
first_shell_pix= round(first_shell*center_x/radius) # pixel radius of first shell
def Tim():
im = np.zeros((height,width,3), dtype=np.uint8)
# Drawing first shell
dcore = core_pixels**2
dshell1 = first_shell_pix**2
for x in range(-center_x,center_x):
for y in range(-center_y,center_y):
diam = x*x+y*y
# Check if pixel falls within the ring coordinates
if dcore < diam < dshell1:
color=np.random.choice(colors,size=1,p=proportion1)[0] # Get random tag
im[y+center_y,x+center_x] = cmap[color]
im1 = Image.fromarray(im)
#im1.save('result.png')
return im1
def withOpenCV():
# Don't want dictionary lookups, just palette indices
# Generate complete random image in one go
ncolours = 3
random = np.random.choice(ncolours, size=(height,width), p=proportion1)
# Describe properties of annulus
centre_coordinates = (width//2, height//2)
radius = 200
colour = 255
thickness = 100
# Draw an annulus to use as mask of where we want to copy random image
mask = np.zeros((height,width), np.uint8)
mask = cv2.circle(mask, centre_coordinates, radius, colour, thickness)
# DEBUG Image.fromarray(mask).show()
# Create output image and copy from "random" where mask is white
im = np.where(mask>0, random, 255).astype(np.uint8)
# print(f'Mask: shape={mask.shape}, dtype={mask.dtype}')
# print(f'im: shape={im.shape}, dtype={im.dtype}')
res = Image.fromarray(im)
res.putpalette([0,100,0,154,205,50,255,140,0] + [0]*253)
return res
withOpenCV().show()
If you are not familiar with palette/index images they are very efficient when you have 255 colours or fewer and are described here.
Upvotes: 2
Reputation: 54698
Creating a numpy array and then converting it to image is somewhat faster. Maybe doing it through a palette could speed it up even more.
from PIL import Image
import numpy as np
width = 1200
height = 800
proportion1 = (0.3,0.6,0.1) # 3 component proportions
colors = ["c1","c2","c3"] # 3 component tags
cmap = {'c1':(0,100,0),'c2':(154,205,50),'c3':(255,140,0)}
center_x = width//2
center_y = height//2
radius = 1737 # radius of the planet
core_radius = 300 # radius of the planet's core
core_pixels = round(core_radius*center_x/radius) # pixel radius of the core
first_shell = 600 # outer radius of the first ring
first_shell_pix= round(first_shell*center_x/radius) # pixel radius of first shell
im = np.zeros((height,width,3), dtype=np.uint8)
# Drawing first shell
dcore = core_pixels**2
dshell1 = first_shell_pix**2
for x in range(-center_x,center_x):
for y in range(-center_y,center_y):
diam = x*x+y*y
# Check if pixel falls within the ring coordinates
if dcore < diam < dshell1:
color=np.random.choice(colors,size=1,p=proportion1)[0] # Get random tag
im[y+center_y,x+center_x] = cmap[color]
im1 = Image.fromarray(im)
im1.show()
Upvotes: 1