Search code examples
pythonnumpyopencvimage-processingmultiprocessing

Python: Generating an image using Multiprocessing freezes


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


Solution

  • 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)