From 2d74aa9f57a5eae917f98cc90fc1ebf571048aaa Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Mon, 13 Sep 2021 17:06:51 +0000 Subject: [PATCH] Add resize contrib module --- contrib/resize/README.md | 16 +++ contrib/resize/requirements.txt | 2 + contrib/resize/resize.py | 169 ++++++++++++++++++++++++++++++++ opendm/gcp.py | 19 ++++ 4 files changed, 206 insertions(+) create mode 100644 contrib/resize/README.md create mode 100644 contrib/resize/requirements.txt create mode 100644 contrib/resize/resize.py diff --git a/contrib/resize/README.md b/contrib/resize/README.md new file mode 100644 index 00000000..1b277c49 --- /dev/null +++ b/contrib/resize/README.md @@ -0,0 +1,16 @@ +# Resize + +Resize a dataset (and optional GCP file). + +Resizes images, keeps Exif data. The EXIF width and height attributes will be updated accordingly also. ODM GCP files are scaled also. + +Usage: + +``` +pip install -r requirements.txt +python3 resize.py -i images/ -o resized/ 25% +python3 resize.py -i images/1.JPG -o resized.JPG 25% +python3 resize.py -i gcp_list.txt -o resized_gcp_list.txt +``` + +Originally forked from https://github.com/pierotofy/exifimageresize diff --git a/contrib/resize/requirements.txt b/contrib/resize/requirements.txt new file mode 100644 index 00000000..92e99bef --- /dev/null +++ b/contrib/resize/requirements.txt @@ -0,0 +1,2 @@ +Pillow==8.0.1 +piexif==1.1.2 diff --git a/contrib/resize/resize.py b/contrib/resize/resize.py new file mode 100644 index 00000000..10585fae --- /dev/null +++ b/contrib/resize/resize.py @@ -0,0 +1,169 @@ +import argparse +import os +import glob +import shutil +from PIL import Image +import piexif +import multiprocessing +from multiprocessing.pool import ThreadPool +import sys +sys.path.append("../../") +from opendm.gcp import GCPFile + +parser = argparse.ArgumentParser(description='Exif Image Resize') +parser.add_argument('--input', '-i', + metavar='', + required=True, + help='Path to input image/GCP or image folder') +parser.add_argument('--output', '-o', + metavar='', + required=True, + help='Path to output image/GCP or image folder') +parser.add_argument('--force', '-f', + action='store_true', + default=False, + help='Overwrite results') +parser.add_argument('amount', + metavar='', + type=str, + help='Pixel of largest side or percentage to resize images by') +args = parser.parse_args() + +def die(msg): + print(msg) + exit(1) + +class nonloc: + errors = 0 + +def resize_image(image_path, out_path, resize_to, out_path_is_file=False): + """ + :param image_path: path to the image + :param out_path: path to the output directory or file + :param resize_to: percentage ("perc%") or pixels + """ + try: + im = Image.open(image_path) + path, ext = os.path.splitext(image_path) + if out_path_is_file: + resized_image_path = out_path + else: + resized_image_path = os.path.join(out_path, os.path.basename(image_path)) + + width, height = im.size + max_side = max(width, height) + + if isinstance(resize_to, str) and resize_to.endswith("%"): + ratio = float(resize_to[:-1]) / 100.0 + else: + ratio = float(resize_to) / float(max_side) + + resized_width = int(width * ratio) + resized_height = int(height * ratio) + + im.thumbnail((resized_width, resized_height), Image.LANCZOS) + + driver = ext[1:].upper() + if driver == 'JPG': + driver = 'JPEG' + + if 'exif' in im.info: + exif_dict = piexif.load(im.info['exif']) + exif_dict['Exif'][piexif.ExifIFD.PixelXDimension] = resized_width + exif_dict['Exif'][piexif.ExifIFD.PixelYDimension] = resized_height + im.save(resized_image_path, driver, exif=piexif.dump(exif_dict), quality=100) + else: + im.save(resized_image_path, driver, quality=100) + + im.close() + + print("{} ({}x{}) --> {} ({}x{})".format(image_path, width, height, resized_image_path, resized_width, resized_height)) + except (IOError, ValueError) as e: + print("Error: Cannot resize {}: {}.".format(image_path, str(e))) + nonloc.errors += 1 + +def resize_gcp(gcp_path, out_path, resize_to, out_path_is_file=False): + """ + :param gcp_path: path to the GCP + :param out_path: path to the output directory or file + :param resize_to: percentage ("perc%") or pixels + """ + try: + if out_path_is_file: + resized_gcp_path = out_path + else: + resized_gcp_path = os.path.join(out_path, os.path.basename(gcp_path)) + + if resize_to.endswith("%"): + ratio = float(resize_to[:-1]) / 100.0 + else: + ratio = resize_to + + gcp = GCPFile(gcp_path) + if gcp.entries_count() > 0: + gcp.make_resized_copy(resized_gcp_path, ratio) + else: + raise ValueError("No GCP entries") + + print("{} --> {}".format(gcp_path, resized_gcp_path)) + except (IOError, ValueError) as e: + print("Error: Cannot resize {}: {}.".format(gcp_path, str(e))) + nonloc.errors += 1 + +if not args.amount.endswith("%"): + args.amount = float(args.amount) + if args.amount <= 0: + die("Invalid amount") +else: + try: + if float(args.amount[:-1]) <= 0: + die("Invalid amount") + except: + die("Invalid amount") + + +files = [] +gcps = [] + +if os.path.isdir(args.input): + for ext in ["JPG", "JPEG", "PNG", "TIFF", "TIF"]: + files += glob.glob("{}/*.{}".format(args.input, ext)) + files += glob.glob("{}/*.{}".format(args.input, ext.lower())) + gcps = glob.glob("{}/*.txt".format(args.input)) +elif os.path.exists(args.input): + _, ext = os.path.splitext(args.input) + if ext.lower() == ".txt": + gcps = [args.input] + else: + files = [args.input] +else: + die("{} does not exist".format(args.input)) + +create_dir = len(files) > 1 or args.output.endswith("/") or len(gcps) > 1 + +if create_dir and os.path.isdir(args.output): + if not args.force: + die("{} exists, pass --force to overwrite results") + else: + shutil.rmtree(args.output) +elif not create_dir and os.path.isfile(args.output): + if not args.force: + die("{} exists, pass --force to overwrite results") + else: + os.remove(args.output) + +if create_dir: + os.makedirs(args.output) + +pool = ThreadPool(processes=multiprocessing.cpu_count()) + +def resize(file): + _, ext = os.path.splitext(file) + if ext.lower() == ".txt": + return resize_gcp(file, args.output, args.amount, not create_dir) + else: + return resize_image(file, args.output, args.amount, not create_dir) +pool.map(resize, files + gcps) + +print("Process completed, {} errors.".format(nonloc.errors)) + diff --git a/opendm/gcp.py b/opendm/gcp.py index cd5ae998..a1c73aec 100644 --- a/opendm/gcp.py +++ b/opendm/gcp.py @@ -51,6 +51,25 @@ class GCPFile: def exists(self): return bool(self.gcp_path and os.path.exists(self.gcp_path)) + def make_resized_copy(self, gcp_file_output, ratio): + """ + Creates a new resized GCP file from an existing GCP file. If one already exists, it will be removed. + :param gcp_file_output output path of new GCP file + :param ratio scale GCP coordinates by this value + :return path to new GCP file + """ + output = [self.raw_srs] + + for entry in self.iter_entries(): + entry.px *= ratio + entry.py *= ratio + output.append(str(entry)) + + with open(gcp_file_output, 'w') as f: + f.write('\n'.join(output) + '\n') + + return gcp_file_output + def wgs84_utm_zone(self): """ Finds the UTM zone where the first point of the GCP falls into