In the code below I tried generating a gray-scale image of the Mandelbrot set using multiprocessing.
To do that I set up a queue and ordered multiple processes to generate individual "strips" of the image, then put them in the queue. Finally the program was supposed to combine them all into the final image.
Unfortunately, after finishing all the processes the program seems to freeze and never return an image. Even though when I changed all the processes to also save their individual strips, there was no problem retrieving them.
Here's my code for reference, It's not the smallest but I tried keeping everything readable and commented:
import cv2
import numpy as np
import math
import random
import multiprocessing
import time
path = "C:/Users/tymon/Pictures/Mandelbrot"
# Simple (i.e., not complex LMAO) class allowing representation and manipulation of complex numbers
class complex:
def __init__(self, a, b=0):
self.a = a
self.b = b
def __add__(self, other):
return complex(self.a + other.a, self.b + other.b)
def __sub__(self, other):
return complex(self.a - other.a, self.b - other.b)
def __mul__(self, other):
return complex(
self.a * other.a - self.b * other.b,
self.a * other.b + self.b * other.a)
def __truediv__(self, other):
nominator = other.a * other.a + other.b * other.b
return complex(
(self.a * other.a + self.b * other.b)/nominator,
(self.b * other.a - self.a * other.b)/nominator)
def modulusSqrd(self):
return self.a*self.a + self.b*self.b
def modulus(self):
return math.sqrt(self.modulusSqrd()) # heavy duty calculation
def __str__(self):
sign = "+"
if self.b < 0:
sign = "-"
self.b *= -1
return F"{self.a} {sign} {self.b}i"
# Mandelbrot set is created by recursively applying the simple equation Z_(n+1) = Z_n^2 - c where c is the initial number and Z_0=0 until the magnitude of the number becomes > 2
def simple_equation(c, ite=2):
z = complex(0)
i = 0
while i <= ite and z.modulusSqrd() < 4:
z_next = z*z - c
if math.isinf(z.a) or math.isinf(z.b) or math.isnan(z.a) or math.isnan(z.b):
return i+1
else:
z = z_next
i+=1
return i
# Maps a number n in the range <min1, max1> to the range <min2, max2> such that the ratio of its distance from both ends of the interval remains the same
def map(n, min1, max1, min2, max2):
if math.isinf(n): # just in case
return max2
return (n - min1) / (max1 - min1) * (max2 - min2) + min2
# Function controlling one of the processes generating the Mandelbrot set image
'''
res[resolution] - both the length and width of the image in pixels
Larger res provide greater detail in the image
ite[iterations] - after how many iterations the program decides that applying the simple equation more times won't make the magnitude of the result > 2 (point belongs to Mandelbrot set)
Higher ite values give more detailed shape
total - total number of processes
id - process identifier (from 0 to total-1)
Through id and total, each process knows which part of the image to generate
queue - queue to which the program will submit fragments
'''
def mandelbrot_process(res, ite, total, id, queue, path=None):
heigth = res//total
prebake_div = 4/total
array = np.zeros((heigth, res), dtype=np.uint8)
try:
progress = 0
# for each point in the image, check after how many iterations of the simple equation (if any) it has moved more than 2 units away from (0,0) and color it accordingly
for y in range(heigth):
for x in range(res):
# convert pixel to complex number at the corresponding position
z = complex(
map(x, 0, res, -2, 2),
map(y,
0, heigth,
prebake_div * id - 2, prebake_div * (id+1) - 2)
)
# applying the simple equation is handled by this function (see def simle_equation(c, ite=2):)
score = simple_equation(z, ite)
# color the pixel accordingly
array[y][x] = map(score, 0, ite, 0, 255)
# update from processes
if 100*y/heigth > progress:
progress += 1
print(f"Finished {progress}% of process {id}! " + ["Awesome!", "Great!", "Amazing!", f"Good job process number {id}!"][random.randint(0,3)]) # motivating processes (100% required)
print(f"!\nFINISHED PROCESS {id} LET'S GOOOOO!\n!")
if path != None:
cv2.imwrite(path+f"/output_res{res}-ite{ite}-id{id}.jpg", array)
queue.put((id, array))
except Exception as e:
print(f"!\nWHAT JUST HAPPENED LIKE I DON'T EVEN KNOW??\nProcess {id} explain yourself!\n\nProcess {id}:")
print(e)
print("\n!")
# input but if you don't enter anything, it will be default
def input_int_with_def(default=0, prompt=""):
var = input(prompt + f"(default: {default})> ")
if var == "":
return default
else:
return int(var)
if __name__ == "__main__":
try:
# inputs
res = int(input("resolution (px)> "))
ite = input_int_with_def(12, "iterations")
pro = input_int_with_def(4, "processes")
# create a queue to collect results from individual processes
queue = multiprocessing.Queue()
# start all processes
processes = []
for id in range(pro):
p = multiprocessing.Process(target=mandelbrot_process, args=(res, ite, pro, id, queue, path))
processes.append(p)
p.start()
# wait until they finish
for p in processes:
p.join()
# create an empty array to store results from individual processes
results = [None] * pro
# retrieve results from the queue and place them in the appropriate positions in the results array
while not queue.empty():
id, result = queue.get()
results[id] = result
# concatenate results from individual processes into one image
final_image = np.vstack(results)
# save and display the image
please = cv2.imwrite(path+f"/output_res{res}-ite{ite}.jpg", final_image)
if please:
message = "Finished generating and saved correctly :)"
else:
message = "Finished generating !BUT SAVED INCORRECTLY! :("
print(message)
cv2.imshow("Mandelbrot_Set", final_image)
except Exception as e:
print("something happened in main")
print(e)
finally:
cv2.waitKey(1)
I have no idea what causes this behavior but I place trust in you kind strangers :)
I tried debugging the code by putting print statements around (not included in the code) and it seems like all processes finish their work but the next parts of the code (like waiting for them to finish) don't activate.
I also tried generating images with different resolutions and default settings for iterations and processes. The program functions as expected for resolution values 87 or below (?) while 88 or above cause the issue described above. No idea why :S
You should use multiprocessing.Pool
here (https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.map). With few changes in your code I was able to run it successfully. BTW complex
is already implemented in Python, no need to implement it on your own. Also avoid using built-ins names while naming functions/variables (i.e. map
).
Here is the code:
import math
import multiprocessing
from functools import partial
import cv2
import numpy as np
path = "./img"
def simple_equation(c, ite=2):
"""
Mandelbrot set is created by recursively applying the simple equation
Z_(n+1) = Z_n^2 - c where c is the initial number and Z_0=0 until the
magnitude of the number becomes > 2
"""
z = complex(0)
i = 0
while i <= ite and abs(z) < 2:
z = z * z - c
i += 1
return i
def map_range(n, min1, max1, min2, max2):
"""
Maps a number n in the range <min1, max1> to the range <min2, max2> such
that the ratio of its distance from both ends of the interval remains the
same
"""
if math.isinf(n): # just in case
return max2
return (n - min1) / (max1 - min1) * (max2 - min2) + min2
def mandelbrot_process(id, res, ite, total, path=None):
"""
Function controlling one of the processes generating the Mandelbrot set
image
"""
heigth = res // total
prebake_div = 4 / total
array = np.zeros((heigth, res), dtype=np.uint8)
try:
progress = 0
# for each point in the image, check after how many iterations of the
# simple equation (if any) it has moved more than 2 units away from
# (0,0) and color it accordingly
for y in range(heigth):
for x in range(res):
# convert pixel to complex number at the corresponding position
z = complex(
map_range(x, 0, res, -2, 2),
map_range(
y,
0,
heigth,
prebake_div * id - 2,
prebake_div * (id + 1) - 2,
),
)
# applying the simple equation is handled by this function (see
# def simle_equation(c, ite=2):)
score = simple_equation(z, ite)
# color the pixel accordingly
array[y][x] = map_range(score, 0, ite, 0, 255)
# update from processes
if 100 * y / heigth > progress:
progress += 1
print(f"!\nFINISHED PROCESS {id} LET'S GOOOOO!\n!")
if path is not None:
cv2.imwrite(path + f"/output_res{res}-ite{ite}-id{id}.jpg", array)
return (id, array)
except Exception as e:
print(
f"!\nWHAT JUST HAPPENED LIKE I DON'T EVEN KNOW??\nProcess {id} "
f"explain yourself!\n\nProcess {id}:"
)
print(e)
print("\n!")
def input_int_with_def(default=0, prompt=""):
"""
input but if you don't enter anything, it will be default
"""
var = input(prompt + f"(default: {default})> ")
if var == "":
return default
else:
return int(var)
if __name__ == "__main__":
try:
# inputs
res = int(input("resolution (px)> "))
ite = input_int_with_def(12, "iterations")
pro = input_int_with_def(4, "processes")
with multiprocessing.Pool(pro) as pool:
ret = pool.map(
partial(mandelbrot_process, res=res, ite=ite, total=pro, path=path),
range(pro),
)
# create an empty array to store results from individual processes
results = [None] * pro
# retrieve results from the queue and place them in the appropriate
# positions in the results array
for id, result in ret:
results[id] = result
# concatenate results from individual processes into one image
final_image = np.vstack(results)
# save and display the image
please = cv2.imwrite(path + f"/output_res{res}-ite{ite}.jpg", final_image)
if please:
message = "Finished generating and saved correctly :)"
else:
message = "Finished generating !BUT SAVED INCORRECTLY! :("
print(message)
cv2.imshow("Mandelbrot_Set", final_image)
except Exception as e:
print("something happened in main")
print(e)
finally:
cv2.waitKey(1)