From b1ed5c9e78f1cf7af352a69088345040c479845b Mon Sep 17 00:00:00 2001 From: Milkymap Date: Mon, 5 Sep 2022 16:59:05 +0200 Subject: [PATCH 1/2] new examples : a parallel pipeline for image processing(resize) --- examples/image-processing/log.py | 17 +++ examples/image-processing/main.py | 212 ++++++++++++++++++++++++++++ examples/image-processing/worker.py | 134 ++++++++++++++++++ 3 files changed, 363 insertions(+) create mode 100644 examples/image-processing/log.py create mode 100644 examples/image-processing/main.py create mode 100644 examples/image-processing/worker.py diff --git a/examples/image-processing/log.py b/examples/image-processing/log.py new file mode 100644 index 000000000..725365675 --- /dev/null +++ b/examples/image-processing/log.py @@ -0,0 +1,17 @@ +from sys import stdout + +from loguru import logger + +log_format = [ + '{time: YYYY-MM-DD hh:mm:ss}', + '{file:^15}', + '{function:^25}', + '{line:03d}', + '{level:^10}', + '{message:<50}', +] + +log_separator = ' | ' + +logger.remove() +logger.add(sink=stdout, level='TRACE', format=log_separator.join(log_format)) diff --git a/examples/image-processing/main.py b/examples/image-processing/main.py new file mode 100644 index 000000000..fa53df356 --- /dev/null +++ b/examples/image-processing/main.py @@ -0,0 +1,212 @@ +import multiprocessing as mp +import pickle +import time +from glob import glob +from os import path +from typing import List, Tuple + +import click +import cv2 +from loguru import logger +from worker import process_images + +import zmq + +""" + ZMQ client-server for parallel image processing(resize). + The server(router) create n workers(dealer): + # each worker can ask a job to the server + # a job is an image to resize and a path where the resized_image will be saved + # once, the image was resied, the worker can ask a new job + # server will keep sending job to workers until there is no images left or user hit ctl+c + NOTE: opencv, numpy, click, loguru must be installed +""" + +""" + This program has two mode : + # sequential + python main.py sequential-processing --path2initial_images /path/to/source/images --path2resized_images /path/to/target/images --image_extension '*.jpg' --size 512 512 + # parallel + python main.py parallel-processing --path2initial_images /path/to/source/images --path2resized_images /path/to/target/images --image_extension '*.jpg' --nb_workers 8 --size 512 512 + parallel mode can be 10x times faster than sequential mode +""" + + +@click.group(chain=False, invoke_without_command=True) +@click.pass_context +def cmd_group(clk: click.Context): + subcommand = clk.invoked_subcommand + if subcommand is not None: + logger.debug(f'{subcommand} was called') + else: + logger.debug('use --help to see availables subcommands') + + +@cmd_group.command() +@click.option( + '--path2initial_images', help='initial images location', type=click.Path(True) +) +@click.option( + '--path2resized_images', help='resized images location', type=click.Path(True) +) +@click.option( + '--image_extension', help='image file extension', default='*.jpg', type=str +) +@click.option( + '--size', help='new image size', type=click.Tuple([int, int]), default=(512, 512) +) +@click.pass_context +def sequential_processing( + clk: click.Context, + path2initial_images: click.Path(True), + path2resized_images: click.Path(True), + image_extension: str, + size: Tuple[int, int], +): + image_filepaths: List[str] = sorted( + glob(path.join(path2initial_images, image_extension)) + ) + nb_images = len(image_filepaths) + logger.debug(f'{nb_images:05d} were found at {path2initial_images}') + start = time.time() + for cursor, path2source_image in enumerate(image_filepaths): + bgr_image = cv2.imread(path2source_image) + resized_image = cv2.resize(bgr_image, dsize=size) + _, filename = path.split(path2source_image) + path2target_image = path.join(path2resized_images, filename) + cv2.imwrite(path2target_image, resized_image) + print(resized_image.shape, f'{cursor:04d} images') + + end = time.time() + duration = int(round(end - start)) + logger.success( + f'server has processed {cursor:04d}/{nb_images} images in {duration:03d}s' + ) + + +@cmd_group.command() +@click.option( + '--path2initial_images', help='initial images location', type=click.Path(True) +) +@click.option( + '--path2resized_images', help='resized images location', type=click.Path(True) +) +@click.option( + '--image_extension', help='image file extension', default='*.jpg', type=str +) +@click.option( + '--nb_workers', help='number of workers to process images', default=2, type=int +) +@click.option( + '--size', help='new image size', type=click.Tuple([int, int]), default=(512, 512) +) +@click.pass_context +def parallel_processing( + clk: click.Context, + path2initial_images: click.Path(True), + path2resized_images: click.Path(True), + image_extension: str, + nb_workers: int, + size: Tuple[int, int], +): + ZEROMQ_INIT = 0 + WORKER_INIT = 0 + try: + router_address = 'ipc://router.ipc' + publisher_address = 'ipc://publisher.ipc' + + ctx: zmq.Context = zmq.Context() + router_socket: zmq.Socket = ctx.socket(zmq.ROUTER) + publisher_socket: zmq.Socket = ctx.socket(zmq.PUB) + + router_socket.bind(router_address) + publisher_socket.bind(publisher_address) + ZEROMQ_INIT = 1 + + image_filepaths: List[str] = sorted( + glob(path.join(path2initial_images, image_extension)) + ) + nb_images = len(image_filepaths) + logger.debug(f'{nb_images:05d} were found at {path2initial_images}') + + if nb_images == 0: + raise Exception(f'{path2initial_images} is empty') + + workers_acc = [] + server_liveness = mp.Event() + worker_arrival = mp.Value('i', 0) + arrival_condition = mp.Condition() + workers_synchronizer = mp.Barrier(nb_workers) + for worker_id in range(nb_workers): + worker_ = mp.Process( + target=process_images, + kwargs={ + 'size': size, + 'worker_id': worker_id, + 'router_address': router_address, + 'publisher_address': publisher_address, + 'worker_arrival': worker_arrival, + 'server_liveness': server_liveness, + 'arrival_condition': arrival_condition, + 'workers_synchronizer': workers_synchronizer, + 'path2resized_images': path2resized_images, + }, + ) + + workers_acc.append(worker_) + workers_acc[-1].start() + + WORKER_INIT = 1 + arrival_condition.acquire() + server_liveness.set() # send signal to worker + arrival_condition.wait_for( + predicate=lambda: worker_arrival.value == nb_workers, timeout=10 + ) + + if worker_arrival.value != nb_workers: + logger.error('server wait to long for worker to be ready') + exit(1) + + logger.success('all workers are up and ready to process images') + cursor = 0 + keep_loop = True + start = time.time() + while keep_loop: + socket_id, _, msgtype, message = router_socket.recv_multipart() + if msgtype == b'req': + if cursor < nb_images: + path2source_image = image_filepaths[cursor] + router_socket.send_multipart( + [socket_id, b'', path2source_image.encode()] + ) + cursor = cursor + 1 + if msgtype == b'rsp': + content = pickle.loads(message) + if content['status'] == 1: + print(f"{content['worker_id']:03d}", f"{cursor:04d} items") + keep_loop = cursor < nb_images + # end loop over images + end = time.time() + duration = int(round(end - start)) + logger.success( + f'server has processed {cursor:04d}/{nb_images} images in {duration:03d}s' + ) + except Exception as e: + logger.error(e) + finally: + if WORKER_INIT: + logger.debug('server is waiting for worker to quit the loop') + publisher_socket.send_multipart([b'quit', b'']) + for worker_ in workers_acc: + worker_.join() + + if ZEROMQ_INIT == 1: + publisher_socket.close() + router_socket.close() + ctx.term() + + logger.success('server has released all zeromq ressources') + + +if __name__ == '__main__': + cmd_group() diff --git a/examples/image-processing/worker.py b/examples/image-processing/worker.py new file mode 100644 index 000000000..7afd74e09 --- /dev/null +++ b/examples/image-processing/worker.py @@ -0,0 +1,134 @@ +import multiprocessing as mp +from os import path +from typing import Tuple + +import cv2 +from log import logger + +import zmq + + +def process_images( + size: Tuple[int, int], + worker_id: int, + router_address: str, + publisher_address: str, + path2resized_images: str, + worker_arrival: mp.Value, + arrival_condition: mp.Condition, + server_liveness: mp.Event, + workers_synchronizer: mp.Barrier, +): + + ZEROMQ_INIT = 0 + try: + ctx: zmq.Context = zmq.Context() + dealer_socket: zmq.Socket = ctx.socket(zmq.DEALER) + subscriber_socket: zmq.Socket = ctx.socket(zmq.SUB) + + dealer_socket.connect(router_address) + subscriber_socket.connect(publisher_address) + subscriber_socket.set(zmq.SUBSCRIBE, b'') # subscribe to all topics + + ZEROMQ_INIT = 1 + + poller = zmq.Poller() + poller.register(dealer_socket, zmq.POLLIN) + poller.register(subscriber_socket, zmq.POLLIN) + + liveness_value = server_liveness.wait( + timeout=10 + ) # wait atleast 10s for server to be ready + if not liveness_value: + logger.error(f'worker {worker_id:03d} wait too long for server to be ready') + exit(1) + + arrival_condition.acquire() + with worker_arrival.get_lock(): + worker_arrival.value += 1 + logger.debug( + f'worker {worker_id:03d} has established connection with {router_address} and {publisher_address}' + ) + arrival_condition.notify_all() + arrival_condition.release() + + logger.debug(f'worker {worker_id:03d} is waiting at the barrier') + workers_synchronizer.wait(timeout=5) # wait at the barrier + + worker_status = 0 # 0 => free | 1 => busy + keep_loop = True + while keep_loop: + if not server_liveness.is_set(): + logger.warning(f'server is down...! worker {worker_id:03d} will stop') + break + + if worker_status == 0: + dealer_socket.send_multipart([b'', b'req', b'']) # ask a job + worker_status = 1 # worker is busy + + incoming_events = dict(poller.poll(100)) + dealer_poller_status = incoming_events.get(dealer_socket, None) + subscriber_poller_status = incoming_events.get(subscriber_socket, None) + if dealer_poller_status is not None: + if dealer_poller_status == zmq.POLLIN: + try: + _, encoded_path2image = dealer_socket.recv_multipart() + path2source_image = encoded_path2image.decode() + bgr_image = cv2.imread(path2source_image) + resized_image = cv2.resize(bgr_image, dsize=size) + _, filename = path.split(path2source_image) + path2target_image = path.join(path2resized_images, filename) + cv2.imwrite(path2target_image, resized_image) + dealer_socket.send_multipart([b'', b'rsp'], flags=zmq.SNDMORE) + dealer_socket.send_pyobj( + { + 'incoming_image': path2source_image, + 'worker_id': worker_id, + 'status': 1, + } + ) + except Exception as e: + logger.error(e) + logger.error( + f'worker {worker_id:03d} was not able to process : {path2source_image}' + ) + dealer_socket.send_multipart([b'', b'rsp'], flags=zmq.SNDMORE) + dealer_socket.send_pyobj( + { + 'incoming_image': path2source_image, + 'worker_id': worker_id, + 'status': 0, + } + ) + worker_status = 0 # worker is free => can ask a new job + + if subscriber_poller_status is not None: + if subscriber_poller_status == zmq.POLLIN: + topic, _ = subscriber_socket.recv_multipart() + if topic == b'quit': + logger.debug( + f'worker {worker_id:03d} got the quit signal from server' + ) + keep_loop = False + # end while loop ...! + + except KeyboardInterrupt: + pass + except Exception as e: + if workers_synchronizer.broken: + logger.warning( + f'worker {worker_id:03d} will stop. the barrier was broken by some workers' + ) + else: + logger.error(e) + finally: + if ZEROMQ_INIT == 1: + poller.unregister(subscriber_socket) + poller.unregister(dealer_socket) + + subscriber_socket.close() + dealer_socket.close() + + ctx.term() + + logger.success(f'worker {worker_id:03d} has released all zeromq ressources') From db5f2ee843812ca5e0d71e071fd0d12499c8f136 Mon Sep 17 00:00:00 2001 From: milkymap Date: Wed, 28 Sep 2022 10:26:50 +0200 Subject: [PATCH 2/2] replace opencv by pillow and update README.md --- examples/image-processing/README.md | 83 +++++++++++++++++++++ examples/image-processing/cpu_usage.jpg | Bin 0 -> 65823 bytes examples/image-processing/log.py | 21 ++---- examples/image-processing/main.py | 39 +++------- examples/image-processing/requirements.txt | 3 + examples/image-processing/worker.py | 10 +-- 6 files changed, 108 insertions(+), 48 deletions(-) create mode 100644 examples/image-processing/README.md create mode 100644 examples/image-processing/cpu_usage.jpg create mode 100644 examples/image-processing/requirements.txt diff --git a/examples/image-processing/README.md b/examples/image-processing/README.md new file mode 100644 index 000000000..85872a9d8 --- /dev/null +++ b/examples/image-processing/README.md @@ -0,0 +1,83 @@ +# ZMQ client-server for parallel image processing(resize). + +This program is an example of parallel programming with zeromq(for workers syncrhonization). it allows to apply a set of operations on images such as resize, to_gray, threshold, or flip. + +We have a client-server architecture (see https://zguide.zeromq.org/docs/chapter3/), the clients are workers who will process the images, each worker can request a task from the server. A worker cannot execute several tasks at the same time, which makes it possible to have a balanced system in terms of load. + +# cpu usage (parallel zeromq image processing) + + + +
+ +# benchmark + +- sequential-processing Flickr8k image dataset + +```bash + 2022-09-28 10:11:11,714 - image-logger - INFO: server has processed 8090/8091 images in 224s +``` + +- parallel-processing(zeromq ROUTER-DEALER) Flickr8k image dataset + +```bash + 2022-09-28 10:13:15,137 - image-logger - INFO: server has processed 8091/8091 images in 026s +``` + +# code organization + +- main.py + - this file is the entrypoint + - it exposes two modes(sequential-processing, parallel-processing) + - use **python main.py --help** to see availables subcommands +- worker.py + - this file contains the implementation of the zeromq worker + - each worker has two sockets(dealer and subscriber) + - the dealer socket will be used for async communcation between server and worker + - the subscriber socket allows the server to be able to broadcast messages such as (KILL SIGNAL) + +# initilization + +```bash + # create virtual env + python -m venv env + # activate virutal env + source env/bin/activate + # upgrade pip + pip install --upgrade pip + # install dependencies + pip install -r requirements.txt +``` + +# run the programm + +## helper + +```bash + # main help + python main.py --help + # sequential mode help + python main.py sequential-processing --help + # parallel mode help + python main.py parallel-processing --help +``` + +## This program has two modes : + +- sequential mode + ```bash + python main.py sequential-processing + --path2initial_images /path/to/source/images + --path2resized_images /path/to/target/images + --image_extension '*.jpg' + --size 512 512 + ``` +- parallel mode + ```bash + python main.py parallel-processing + --path2initial_images /path/to/source/images + --path2resized_images /path/to/target/images + --image_extension '*.jpg' + --nb_workers 8 + --size 512 512 + ``` diff --git a/examples/image-processing/cpu_usage.jpg b/examples/image-processing/cpu_usage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..49f74f1ee0db301b49b386df008cbca7b82d0360 GIT binary patch literal 65823 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 literal 0 HcmV?d00001 diff --git a/examples/image-processing/log.py b/examples/image-processing/log.py index 725365675..04ee82a20 100644 --- a/examples/image-processing/log.py +++ b/examples/image-processing/log.py @@ -1,17 +1,10 @@ -from sys import stdout +import logging -from loguru import logger +logging.basicConfig( + level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s: %(message)s' +) -log_format = [ - '{time: YYYY-MM-DD hh:mm:ss}', - '{file:^15}', - '{function:^25}', - '{line:03d}', - '{level:^10}', - '{message:<50}', -] +logger = logging.getLogger('image-logger') -log_separator = ' | ' - -logger.remove() -logger.add(sink=stdout, level='TRACE', format=log_separator.join(log_format)) +if __name__ == '__main__': + logger.debug('... log is up ...') diff --git a/examples/image-processing/main.py b/examples/image-processing/main.py index fa53df356..9d2dd1ce6 100644 --- a/examples/image-processing/main.py +++ b/examples/image-processing/main.py @@ -6,31 +6,12 @@ from typing import List, Tuple import click -import cv2 -from loguru import logger +from log import logger +from PIL import Image from worker import process_images import zmq -""" - ZMQ client-server for parallel image processing(resize). - The server(router) create n workers(dealer): - # each worker can ask a job to the server - # a job is an image to resize and a path where the resized_image will be saved - # once, the image was resied, the worker can ask a new job - # server will keep sending job to workers until there is no images left or user hit ctl+c - NOTE: opencv, numpy, click, loguru must be installed -""" - -""" - This program has two mode : - # sequential - python main.py sequential-processing --path2initial_images /path/to/source/images --path2resized_images /path/to/target/images --image_extension '*.jpg' --size 512 512 - # parallel - python main.py parallel-processing --path2initial_images /path/to/source/images --path2resized_images /path/to/target/images --image_extension '*.jpg' --nb_workers 8 --size 512 512 - parallel mode can be 10x times faster than sequential mode -""" - @click.group(chain=False, invoke_without_command=True) @click.pass_context @@ -70,16 +51,16 @@ def sequential_processing( logger.debug(f'{nb_images:05d} were found at {path2initial_images}') start = time.time() for cursor, path2source_image in enumerate(image_filepaths): - bgr_image = cv2.imread(path2source_image) - resized_image = cv2.resize(bgr_image, dsize=size) + image = Image.open(path2source_image) + resized_image = image.resize(size=size) _, filename = path.split(path2source_image) path2target_image = path.join(path2resized_images, filename) - cv2.imwrite(path2target_image, resized_image) - print(resized_image.shape, f'{cursor:04d} images') + resized_image.save(path2target_image) + print(resized_image.size, f'{cursor:04d} images') end = time.time() duration = int(round(end - start)) - logger.success( + logger.info( f'server has processed {cursor:04d}/{nb_images} images in {duration:03d}s' ) @@ -167,7 +148,7 @@ def parallel_processing( logger.error('server wait to long for worker to be ready') exit(1) - logger.success('all workers are up and ready to process images') + logger.info('all workers are up and ready to process images') cursor = 0 keep_loop = True start = time.time() @@ -188,7 +169,7 @@ def parallel_processing( # end loop over images end = time.time() duration = int(round(end - start)) - logger.success( + logger.info( f'server has processed {cursor:04d}/{nb_images} images in {duration:03d}s' ) except Exception as e: @@ -205,7 +186,7 @@ def parallel_processing( router_socket.close() ctx.term() - logger.success('server has released all zeromq ressources') + logger.info('server has released all zeromq ressources') if __name__ == '__main__': diff --git a/examples/image-processing/requirements.txt b/examples/image-processing/requirements.txt new file mode 100644 index 000000000..65042a5b3 --- /dev/null +++ b/examples/image-processing/requirements.txt @@ -0,0 +1,3 @@ +click +pillow +pyzmq diff --git a/examples/image-processing/worker.py b/examples/image-processing/worker.py index 7afd74e09..a301f0c35 100644 --- a/examples/image-processing/worker.py +++ b/examples/image-processing/worker.py @@ -2,8 +2,8 @@ from os import path from typing import Tuple -import cv2 from log import logger +from PIL import Image import zmq @@ -74,11 +74,11 @@ def process_images( try: _, encoded_path2image = dealer_socket.recv_multipart() path2source_image = encoded_path2image.decode() - bgr_image = cv2.imread(path2source_image) - resized_image = cv2.resize(bgr_image, dsize=size) + image = Image.open(path2source_image) + resized_image = image.resize(size=size) _, filename = path.split(path2source_image) path2target_image = path.join(path2resized_images, filename) - cv2.imwrite(path2target_image, resized_image) + resized_image.save(path2target_image) dealer_socket.send_multipart([b'', b'rsp'], flags=zmq.SNDMORE) dealer_socket.send_pyobj( { @@ -131,4 +131,4 @@ def process_images( ctx.term() - logger.success(f'worker {worker_id:03d} has released all zeromq ressources') + logger.info(f'worker {worker_id:03d} has released all zeromq ressources')