add code
This commit is contained in:
parent
2b1f2dddf0
commit
0ab7177a45
|
@ -1,3 +1,5 @@
|
|||
.idea
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
@ -15,7 +17,6 @@ dist/
|
|||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
|
25
README.md
25
README.md
|
@ -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,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
|
|
@ -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
|
|
@ -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)
|
|
@ -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')
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
|||
gpxpy==1.6.2
|
||||
pillow==10.2.0
|
||||
tzlocal==5.2
|
||||
pytz==2024.1
|
||||
piexif==1.1.3
|
Loading…
Reference in New Issue