commit eb243c5aa7aecbaf0c6a1da7a777af28b2d2b9db Author: doofmars Date: Sat Oct 4 11:17:21 2025 +0200 Initial commit Filter images with dry run and bounding box diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c466cc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +build/ +dist/ +filtered/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..14012ff --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Filter Images", + "type": "debugpy", + "request": "launch", + "program": "filter_gps_images.py", + "args": [ + "--input_folder", "./test", + "--output_folder", "./filtered", + "--gps_box", "0.0", "-180.0", "90.0", "180.0", + "--dry_run" + ], + "console": "integratedTerminal" + }, + { + "name": "Package single executable", + "type": "debugpy", + "request": "launch", + "module": "PyInstaller", + "args": [ + "--onefile", + "--name", "filter_gps_images", + "filter_gps_images.py" + ], + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ab1fe2 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Filter Images +============= + +This project provides tools to filter images based on gps coordinates criteria. + + diff --git a/filter_gps_images.py b/filter_gps_images.py new file mode 100644 index 0000000..801cd27 --- /dev/null +++ b/filter_gps_images.py @@ -0,0 +1,122 @@ +# Iterate over a folder with images +# print the image gps coordinates +# filter for images within a certain gps coordinates box +# Use pillow to read image metadata +import os +import argparse +import shutil +from PIL import Image + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Filter images within a GPS coordinates box") + parser.add_argument("--input_folder", type=str, required=True, help="Path to the input folder containing images") + parser.add_argument("--output_folder", type=str, required=True, help="Path to the output folder to save filtered images") + gps_box_help = "Bounding box defined by min and max latitude and longitude" \ + " Swiss Alps region: --gps_box 46.0 8.0 48.0 9.0" \ + " Zurich area: --gps_box 47.2 8.3 47.6 8.7" \ + " Bernese Alps: --gps_box 46.4 7.8 46.8 8.3" + parser.add_argument("--gps_box", type=float, nargs=4, required=True, metavar=('LAT_MIN', 'LON_MIN', 'LAT_MAX', 'LON_MAX'), help=gps_box_help) + parser.add_argument("--dry_run", action="store_true", help="Enable dry run mode") + # Add help text for gps_box + parser.add_argument("--verbose", action="store_true", help="Enable verbose output") + + return parser.parse_args() + +def get_gps_coordinates(image): + """Extract GPS coordinates from image EXIF data""" + exifdata = image.getexif() + + # Check if GPS info is available + if 34853 not in exifdata: + return None, None + + # Use get_ifd() to access the actual GPS data dictionary + try: + gps_info = exifdata.get_ifd(34853) + except (KeyError, TypeError): + return None, None + + # Check if gps_info is actually a dictionary with GPS data + if not isinstance(gps_info, dict) or len(gps_info) == 0: + return None, None + + def convert_to_degrees(value): + """Convert GPS coordinates to decimal degrees""" + try: + if isinstance(value, (list, tuple)) and len(value) >= 3: + d, m, s = value[0], value[1], value[2] + # Handle rational numbers (tuples like (40, 1) meaning 40/1) + if isinstance(d, tuple) and len(d) == 2: + d = d[0] / d[1] + if isinstance(m, tuple) and len(m) == 2: + m = m[0] / m[1] + if isinstance(s, tuple) and len(s) == 2: + s = s[0] / s[1] + return float(d) + (float(m) / 60.0) + (float(s) / 3600.0) + return None + except (ValueError, TypeError, IndexError, ZeroDivisionError): + return None + + lat = None + lon = None + + try: + # Get latitude + if 2 in gps_info and 1 in gps_info: # GPSLatitude and GPSLatitudeRef + lat = convert_to_degrees(gps_info[2]) + if lat is not None and gps_info[1] == 'S': + lat = -lat + + # Get longitude + if 4 in gps_info and 3 in gps_info: # GPSLongitude and GPSLongitudeRef + lon = convert_to_degrees(gps_info[4]) + if lon is not None and gps_info[3] == 'W': + lon = -lon + except (TypeError, KeyError): + # Handle cases where gps_info doesn't support 'in' operator or key access + return None, None + + return lat, lon + +def is_within_gps_box(image, gps_box): + # Get the image gps coordinates from the image metadata + latitude, longitude = get_gps_coordinates(image) + + if latitude is None or longitude is None: + print("No GPS data found in image") + return False + + print(f"Image GPS coordinates: {latitude}, {longitude}") + lat_min, lon_min, lat_max, lon_max = gps_box + return lat_min <= latitude <= lat_max and lon_min <= longitude <= lon_max + +def main(): + args = parse_arguments() + + if not os.path.exists(args.output_folder): + os.makedirs(args.output_folder) + + for filename in os.listdir(args.input_folder): + if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')): + image_path = os.path.join(args.input_folder, filename) + # Load the image metadata + image = Image.open(image_path) + + if image is None: + print(f"Failed to load image: {image_path}") + continue + + if is_within_gps_box(image, args.gps_box): + if args.dry_run: + print(f"Dry run: Image {filename} is within the GPS box") + lat, lon = get_gps_coordinates(image) + print(f"coordinates: {lat}, {lon}") + continue + output_path = os.path.join(args.output_folder, filename) + shutil.copy2(image_path, output_path) + print(f"Saved filtered image: {output_path}") + else: + print(f"Image {filename} is outside the bounding box") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/filter_gps_images.spec b/filter_gps_images.spec new file mode 100644 index 0000000..0a85292 --- /dev/null +++ b/filter_gps_images.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['filter_gps_images.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='filter_gps_images', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..05f77cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +argparse +Pillow +piexif +PyInstaller +shutil \ No newline at end of file diff --git a/test/PXL_20240413_161426952.jpg b/test/PXL_20240413_161426952.jpg new file mode 100755 index 0000000..0a853bc Binary files /dev/null and b/test/PXL_20240413_161426952.jpg differ