This commit is contained in:
Cyberes 2024-03-15 16:12:30 -06:00
parent 2b1f2dddf0
commit 0ab7177a45
9 changed files with 226 additions and 2 deletions

3
.gitignore vendored
View File

@ -1,3 +1,5 @@
.idea
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
@ -15,7 +17,6 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/

View File

@ -1,3 +1,26 @@
# gpx-to-image-tags
_Geotag your photos from a GPX track._
Read a GPX track and correlate that to the time photos were taken.
This is a simple script used to add geotags (geographic metadata locating where the photo was taken) to images taken by
a non-GPS camera.
It reads the time a photo was taken and then finds the closest point in the GPX track.
## Install
1. `pip install -r requirements.txt`
## Use
```
python3 main.py <path to the folder containing your photos> <path to GPX file>
```
You might need the `--timezone` argument. Double check the EXIF times are correct via `identify -verbose`.
<br>
Optional Arguments:
- `--max-diff`: Maximum allowed difference in seconds between image timestamp and track point. Default: 300 (5 minutes).
- `--timezone`: Three letter timezone the photos are in (e.g. EST). Default: auto-detect your local timezone.

0
lib/__init__.py Normal file
View File

76
lib/geotags.py Normal file
View File

@ -0,0 +1,76 @@
import datetime
from fractions import Fraction
from typing import Union
import piexif
"""
Source: https://gist.github.com/c060604/8a51f8999be12fc2be498e9ca56adc72
"""
def to_deg(value, loc):
"""convert decimal coordinates into degrees, munutes and seconds tuple
Keyword arguments: value is float gps-value, loc is direction list ["S", "N"] or ["W", "E"]
return: tuple like (25, 13, 48.343 ,'N')
"""
if value < 0:
loc_value = loc[0]
elif value > 0:
loc_value = loc[1]
else:
loc_value = ""
abs_value = abs(value)
deg = int(abs_value)
t1 = (abs_value - deg) * 60
min = int(t1)
sec = round((t1 - min) * 60, 5)
return deg, min, sec, loc_value
def change_to_rational(number):
"""convert a number to rantional
Keyword arguments: number
return: tuple like (1, 2), (numerator, denominator)
"""
f = Fraction(str(number))
return f.numerator, f.denominator
def generate_geotags(lat: float, lon: float, alt: Union[int, float], timestamp_obj: datetime.datetime, original_exif: dict):
lat_deg = to_deg(lat, ["S", "N"])
lng_deg = to_deg(lon, ["W", "E"])
exiv_lat = (change_to_rational(lat_deg[0]), change_to_rational(lat_deg[1]), change_to_rational(lat_deg[2]))
exiv_lng = (change_to_rational(lng_deg[0]), change_to_rational(lng_deg[1]), change_to_rational(lng_deg[2]))
# Convert date and time to the format required by EXIF
datestamp = timestamp_obj.strftime("%Y:%m:%d")
timestamp = (change_to_rational(timestamp_obj.hour), change_to_rational(timestamp_obj.minute), change_to_rational(timestamp_obj.second))
if alt < 0:
altitude_ref = 1
altitude = abs(alt)
else:
altitude_ref = 0
altitude = alt
gps_ifd = {
piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0),
piexif.GPSIFD.GPSAltitudeRef: altitude_ref,
piexif.GPSIFD.GPSAltitude: change_to_rational(round(altitude)),
piexif.GPSIFD.GPSLatitudeRef: lat_deg[3],
piexif.GPSIFD.GPSLatitude: exiv_lat,
piexif.GPSIFD.GPSLongitudeRef: lng_deg[3],
piexif.GPSIFD.GPSLongitude: exiv_lng,
piexif.GPSIFD.GPSDateStamp: datestamp,
piexif.GPSIFD.GPSTimeStamp: timestamp,
}
gps_exif = {"GPS": gps_ifd}
# Update original exif data to include GPS tag.
original_exif.update(gps_exif)
return original_exif

32
lib/gpx.py Normal file
View File

@ -0,0 +1,32 @@
from datetime import timedelta
import gpxpy
from gpxpy.gpx import GPXTrackPoint
def get_closest_point(gpx, timestamp, max_diff) -> GPXTrackPoint | None:
closest_point = None
closest_diff = timedelta.max
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
diff = abs(point.time - timestamp)
if diff < closest_diff and diff.total_seconds() <= max_diff:
closest_diff = diff
closest_point = point
return closest_point
def get_gpx_data(file):
gpx_file = open(file, 'r')
gpx = gpxpy.parse(gpx_file)
data = []
for track in gpx.tracks:
for segment in track.segments:
for point in segment.points:
data.append((point.time, point.latitude, point.longitude))
return data

23
lib/image.py Normal file
View File

@ -0,0 +1,23 @@
from datetime import datetime
import pytz
from PIL.ExifTags import TAGS
def get_image_timestamp(img, timezone):
exif = img._getexif()
if not exif:
raise ValueError("No EXIF metadata found")
found_timestamp = None
for (idx, tag) in TAGS.items():
if tag == 'DateTime':
found_timestamp = exif.get(idx)
elif tag == 'DateTimeOriginal':
found_timestamp = exif.get(idx)
if found_timestamp:
dt = datetime.strptime(found_timestamp, '%Y:%m:%d %H:%M:%S')
tz = pytz.timezone(timezone)
return tz.localize(dt)

7
lib/time.py Normal file
View File

@ -0,0 +1,7 @@
from datetime import datetime
from tzlocal import get_localzone
def get_local_timezone():
return datetime.now(tz=get_localzone()).strftime('%Z')

57
main.py Normal file
View File

@ -0,0 +1,57 @@
import argparse
from pathlib import Path
import gpxpy
import piexif
import pytz
from PIL import Image
from lib.geotags import generate_geotags
from lib.gpx import get_closest_point
from lib.image import get_image_timestamp
from lib.time import get_local_timezone
def main(args):
with open(args.gpx, 'r') as gpxfile:
gpx = gpxpy.parse(gpxfile)
for file in args.photos.glob('*'):
if file.name.lower().endswith(('.jpg', '.jpeg')):
img = Image.open(file)
timestamp = get_image_timestamp(img, args.timezone)
del img
if not timestamp:
print(f'FAIL - "{file.name}" does not have a timestamp.')
continue
point = get_closest_point(gpx, timestamp, args.max_diff)
if not point:
point_forced = get_closest_point(gpx, timestamp, 99999999999999999999999999)
print(f'FAIL - Could not match image "{file.name}" @ {timestamp}. Closest point: {point_forced.latitude},{point_forced.longitude} @ {point_forced.time.astimezone(pytz.timezone(args.timezone))}')
continue
else:
original_exif = piexif.load(str(file))
geotags = generate_geotags(point.latitude, point.longitude, point.elevation, point.time, original_exif)
exif_bytes = piexif.dump(geotags)
piexif.insert(exif_bytes, str(file))
print('OKAY - ', file.name)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Add location tags to photos based on a GPX track.')
parser.add_argument('photos', help='Path to the directory containing the photos')
parser.add_argument('gpx', help='Path to the GPX file')
parser.add_argument('--max-diff', type=int, default=300, help='Maximum allowed difference in seconds between image timestamp and track point. Default: 300')
parser.add_argument('--timezone', default=get_local_timezone(), help='Three letter timezone the photos are in (e.g. EST). Default: auto-detect your local timezone')
args = parser.parse_args()
args.photos = Path(args.photos).expanduser().absolute().resolve()
if args.photos.is_file() or not args.photos.exists():
print('Invalid path to photos. Must be a directory.')
quit(1)
args.gpx = Path(args.gpx).expanduser().absolute().resolve()
if args.gpx.is_dir() or not args.gpx.exists():
print('Invalid path to GPX file.')
quit(1)
main(args)

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
gpxpy==1.6.2
pillow==10.2.0
tzlocal==5.2
pytz==2024.1
piexif==1.1.3