diff --git a/ApplyImageOrientation.py b/ApplyImageOrientation.py index 9d252c2..8417800 100644 --- a/ApplyImageOrientation.py +++ b/ApplyImageOrientation.py @@ -8,6 +8,8 @@ from PIL import Image as PilImage import os +import multiprocessing +import threading class ApplyImageOrientation: @@ -28,11 +30,51 @@ class ApplyImageOrientation: "tiff" ] - def run(self, jpeg=False): + def run(self, threads_count=None, jpeg=False): images_paths = self.get_input_images() - for image_path in images_paths: + lock = threading.RLock() + threads = [] + if threads_count is None: + threads_count = multiprocessing.cpu_count() + + print("Applying orientation using %s threads" % (threads_count,)) + for i in range(threads_count): + t = threading.Thread( + target=self._apply_orientation_thread, + kwargs={ + "lock": lock, + "images_paths": images_paths, + "jpeg": jpeg + } + ) + threads.append(t) + t.start() + + try: + for t in threads: + t.join() + except KeyboardInterrupt: + with RAIILock(lock): + print("Detected keyboard interrupt; Aborting!") + images_paths.clear() + + def _apply_orientation_thread(self, lock: threading.RLock, images_paths: list, jpeg: bool): + + while True: + + with RAIILock(lock) as raii_lock: + + if len(images_paths) == 0: + break + + image_path = images_paths.pop() + common_path = os.path.commonpath([self.__input_folder, image_path]) + print("Rotating: %s (%s remaining)" % (image_path[len(common_path) + 1:], len(images_paths))) + + raii_lock.release() + self.apply_orientation( image_path=image_path, jpeg=jpeg @@ -79,13 +121,17 @@ class ApplyImageOrientation: rotation_degrees = 0 if orientation == exif.Orientation.LEFT_BOTTOM: rotation_degrees = 90 + elif orientation == exif.Orientation.BOTTOM_RIGHT: + rotation_degrees = 180 + elif orientation == exif.Orientation.RIGHT_TOP: + rotation_degrees = 270 if rotation_degrees != 0: pil_image_rotated = pil_image.rotate(rotation_degrees, expand=True) else: pil_image_rotated = pil_image - print(image_basename, pil_image.size, "==>", orientation, "==>", rotation_degrees) + # print(image_basename, pil_image.size, "==>", orientation, "==>", rotation_degrees) image_rotated_file_path = self.make_image_output_path(image_path=image_path, jpeg=jpeg) self.ensure_folder(os.path.dirname(image_rotated_file_path)) @@ -108,3 +154,38 @@ class ApplyImageOrientation: image_output_path = path_name + ".png" return image_output_path + + +class RAIILock: + + def __init__(self, lock: threading.RLock): + + self.__lock = lock + + def __enter__(self): + + self.acquire() + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + + if self.__lock: + + self.release() + self.__lock = None + + def acquire(self): + + if self.__lock: + + self.__lock.acquire() + + def release(self): + + if self.__lock: + + try: + self.__lock.release() + except RuntimeError: + pass diff --git a/main.py b/main.py index 06fa7da..ebd7392 100755 --- a/main.py +++ b/main.py @@ -33,7 +33,22 @@ def main(): dest="output_jpeg", default=False, action="store_true", - help="Output to jpeg files, not png" + help="Output to jpeg files, not png. Should be faster but sacrifices the quality of png." + ) + + parser.add_argument( + "--threads", + dest="threads_count", + default=None, + type=int, + help="Specify the number of threads to use. Default: Same as the number of detected CPU cores." + ) + parser.add_argument( + "--single-thread", + dest="threads_count", + default=None, + action="store_const", const=1, + help="Use only one thread." ) args = parser.parse_args() @@ -45,6 +60,7 @@ def main(): output_folder=args.output_folder ) app.run( + threads_count=args.threads_count, jpeg=args.output_jpeg )