Applying concurrency to image processing

We have talked a lot about the basics of image processing and some common image processing techniques. We also know why image processing is a heavy number-crunching task, and that concurrent and parallel programming can be applied to speed up independent processing tasks. In this section, we will be looking at a specific example on how to implement a concurrent image processing application that can handle a large number of input images.

First, head to the current folder for this chapter's code. Inside the input folder, there is a subfolder called large_input, which contains 400 images that we will be using for this example. These pictures are different regions in our original ship image, which have been cropped from it using the array-indexing and -slicing options that NumPy provides to slice OpenCV image objects. If you are curious as to how these images were generated, check out the Chapter08/generate_input.py file.

Our goal in this section is to implement a program that can concurrently process these images using thresholding. To do this, let's look at the example5.py file:

from multiprocessing import Pool
import cv2

import sys
from timeit import default_timer as timer


THRESH_METHOD = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
INPUT_PATH = 'input/large_input/'
OUTPUT_PATH = 'output/large_output/'

n = 20
names = ['ship_%i_%i.jpg' % (i, j) for i in range(n) for j in range(n)]


def process_threshold(im, output_name, thresh_method):
gray_im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
thresh_im = cv2.adaptiveThreshold(gray_im, 255, thresh_method,
cv2.THRESH_BINARY, 11, 2)

cv2.imwrite(OUTPUT_PATH + output_name, thresh_im)


if __name__ == '__main__':

for n_processes in range(1, 7):
start = timer()

with Pool(n_processes) as p:
p.starmap(process_threshold, [(
cv2.imread(INPUT_PATH + name),
name,
THRESH_METHOD
) for name in names])

print('Took %.4f seconds with %i process(es).
' % (timer() - start, n_processes))

print('Done.')

In this example, we are using the Pool class from the multiprocessing module to manage our processes. As a refresher, a Pool object supplies convenient options to map a sequence of inputs to separate processes using the Pool.map() method. We are using the Pool.starmap() method in our example, however, to pass multiple arguments to the target function.

At the beginning of our program, we make a number of house-keeping assignments: the thresholding method to perform adaptive thresholding when processing the images, paths for the input and output folders, and the names of the images to process. The process_threshold() function is what we use to actually process the images; which takes in an image object, the name for the processed version of the image, and which thresholding method to use. Again, this is why we need to use the Pool.starmap() method instead of the traditional Pool.map() method.

In the main program, to demonstrate the differences between sequential and multiprocessing image processing, we want to run our program with different numbers of processes, specifically from one single process to six different processes. In each iteration of the for loop, we initialize a Pool object and map the necessary arguments of each image to the process_threshold() function, while keeping track of how much time it takes to process and save all of the images.

After running the script, the processed images can be found in the output/large_output/ subfolder in our current chapter's folder. You should obtain an output similar to the following:

> python example5.py
Took 0.6590 seconds with 1 process(es).
Took 0.3190 seconds with 2 process(es).
Took 0.3227 seconds with 3 process(es).
Took 0.3360 seconds with 4 process(es).
Took 0.3338 seconds with 5 process(es).
Took 0.3319 seconds with 6 process(es).
Done.

We can see a big difference in execution time when we go from one single process to two separate processes. However, there is negligible or even negative speedup after going from two to higher numbers of processes. Generally, this is because of the heavy overhead, which is the product of implementing a large number of separate processes, in comparison to a relatively low number of inputs. Even though we are not implementing this comparison in the interest of simplicity, with an increased number of inputs we would see better improvements from a high number of working processes.

So far, we have seen that concurrent programming could provide a significant speedup for image processing applications. However, if we take a look at our preceding program, we can see that there are additional adjustments that we can make to improve the execution time even further. Specifically, in our preceding program, we are reading in images in a sequential way by using list comprehension in the following line:

with Pool(n_processes) as p:
p.starmap(process_threshold, [(
cv2.imread(INPUT_PATH + name),
name,
THRESH_METHOD
) for name in names])

Theoretically, if we were to make the process of reading in different image files concurrent, we could also gain additional speedup with our program. This is especially true in an image processing application that deals with large input files, where significant time is spent on waiting for input to be read. With that in mind, let's consider the following example, in which we will implement concurrent input/output processing. Navigate to the example6.py file:

from multiprocessing import Pool
import cv2

import sys
from functools import partial
from timeit import default_timer as timer


THRESH_METHOD = cv2.ADAPTIVE_THRESH_GAUSSIAN_C
INPUT_PATH = 'input/large_input/'
OUTPUT_PATH = 'output/large_output/'

n = 20
names = ['ship_%i_%i.jpg' % (i, j) for i in range(n) for j in range(n)]


def process_threshold(name, thresh_method):
im = cv2.imread(INPUT_PATH + name)
gray_im = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
thresh_im = cv2.adaptiveThreshold(gray_im, 255, thresh_method, cv2.THRESH_BINARY, 11, 2)

cv2.imwrite(OUTPUT_PATH + name, thresh_im)


if __name__ == '__main__':

for n_processes in range(1, 7):
start = timer()

with Pool(n_processes) as p:
p.map(partial(process_threshold, thresh_method=THRESH_METHOD), names)

print('Took %.4f seconds with %i process(es).' % (timer() - start, n_processes))

print('Done.')

The structure of this program is similar to that of the previous one. However, instead of preparing the necessary images to be processed and other relevant input information, we implement them inside the process_threshold() function, which now only takes the name of the input image and handles reading the image itself.

As a side note, we are using Python's built-in functools.partial() method in our main program to pass in a partial argument (hence the name), specifically thresh_method, to the process_threshold() function, as this argument is fixed across all images and processes. More information about this tool can be found at https://docs.python.org/3/library/functools.html.

After running the script, you should obtain an output similar to the following:

> python example6.py
Took 0.5300 seconds with 1 process(es).
Took 0.4133 seconds with 2 process(es).
Took 0.2154 seconds with 3 process(es).
Took 0.2147 seconds with 4 process(es).
Took 0.2213 seconds with 5 process(es).
Took 0.2329 seconds with 6 process(es).
Done.

Compared to our last output, this implementation of the application indeed gives us a significantly better execution time.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.198.94