Personally I wanted a little more control over what happens, so I wrote some python code to handle Google's location history, combine it with geotags from other pictures and than apply the list of geotags to my pictures where I can adjust allowed difference in timestamps and also approximate positions between valid timestamps.
The code is only rudimentary but might be useful as a starting point for others:
# I called this script "GeotagHandling.py" and imported functions in the script below.
import xml.etree.ElementTree as ET
import datetime
import pytz # python timezones
import json
import os.path
import os
import math
import exiftool
# a "geotag" object is a dictionary with this entries:
# latitude float
# longitude float
# altitude float
# dt datetime.datetime object
# When using functions that require an exiftool instance, use e.g.:
#
# with exiftool.ExifTool() as et:
# geotag = extract_geotag(file, et)
#
# to create a single instance to be used in loops etc. and is
# closed when no longer needed.
# Load geotags stored in a json file in the aforementioned "geotag" object format
def load_geotags_from_json(json_file):
if os.path.isfile(json_file):
geotags = json.load(open(json_file, "r"), object_pairs_hook=load_geotags_from_json_hook)
geotags.sort(key=get_datetime)
return geotags
# function hook for parsing the "geotag" objects from JSON files
def load_geotags_from_json_hook(pairs, format="%Y-%m-%d %H:%M:%S%z"):
d = {}
for k, v in pairs:
if isinstance(v, str):
try:
d[k] = datetime.datetime.strptime(v, format)
except ValueError:
d[k] = v
else:
d[k] = v
return d
def store_geotags_to_json(json_file, geotags):
geotags.sort(key=get_datetime)
with open(json_file, "w") as outfile:
json.dump(geotags, outfile, indent=4, default=str)
# Read the Google location history *.kml file and yield the geotags between the start and end date
def read_kml_track(fname, start, end):
tree = ET.parse(fname)
root = tree.getroot()
ns = {
"kmlns": "http://www.opengis.net/kml/2.2",
"gx": "http://www.google.com/kml/ext/2.2",
}
# track_elements = root.find("kmlns:Placemark/gx:Track", namespaces=ns)
track_elements = root.find("kmlns:Document/kmlns:Placemark/gx:Track", namespaces=ns)
lst_when = track_elements.findall("kmlns:when", namespaces=ns)
lst_coord = track_elements.findall("gx:coord", namespaces=ns)
for when, coord in zip(lst_when, lst_coord):
dt = datetime.datetime.strptime(when.text, "%Y-%m-%dT%H:%M:%S%z")
if dt > start and dt < end:
geotag = {}
s = coord.text
longitude, latitude, altitude = s.replace(",", ".").split()
longitude, latitude, altitude = map(float, (longitude, latitude, altitude))
geotag['dt'] = dt
geotag['longitude'] = longitude
geotag['latitude'] = latitude
geotag['altitude'] = altitude
yield geotag
# Extract and return a geotag object from a media file if it has one, return None otherwise
# Needs an exiftool instance
def extract_geotag(file, et):
metadata = et.get_metadata(file)
if not ('EXIF:GPSLongitude' in metadata and 'EXIF:GPSLatitude' in metadata):
return
geotag = {}
if not 'EXIF:ModifyDate' in metadata:
dt = datetime.datetime.strptime(metadata['File:FileModifyDate'], "%Y:%m:%d %H:%M:%S%z")
else:
dt = datetime.datetime.strptime(metadata['EXIF:ModifyDate'], "%Y:%m:%d %H:%M:%S")
dt = dt.replace(tzinfo=pytz.UTC) #iceland uses UTC +0
geotag['dt'] = dt
geotag['longitude'] = metadata['EXIF:GPSLongitude']
geotag['latitude'] = metadata['EXIF:GPSLatitude']
geotag['altitude'] = metadata['EXIF:GPSAltitude']
if metadata['EXIF:GPSLatitudeRef'] == "S":
geotag['latitude'] *= -1;
if metadata['EXIF:GPSLongitudeRef'] == "W":
geotag['longitude'] *= -1;
return geotag
# Extract and return a list of geotag objects from all media files in the directory
def extract_geotags_from_files(directory):
geotags = []
dir_enc = os.fsencode(directory)
with exiftool.ExifTool() as et:
for file in os.listdir(dir_enc):
filename = os.fsdecode(file)
if filename.lower().endswith(".jpg") or filename.endswith(".mp4"):
geotag = extract_geotag(os.path.join(directory, filename), et)
if geotag is None:
continue
geotags.append(geotag)
else:
continue
return geotags
# Helper function to be used as a hook for sorting
def get_datetime(geotag):
return geotag.get('dt')
# Find the geotag from a list that is at or before the requested timestamp
def find_previous_geotag_index(dt, geotags):
for i,tag in enumerate(geotags):
if tag['dt'] >= dt:
return i-1
# Find a fitting geotag for a media file and write it to the files EXIF metadata.
# A geotag is deemed fitting if the difference between the files modify date is
# less than "timedelta" from the geotags timestamp. If there is a geotag before
# and after the files modify date, the two fitting geotags will be linear
# interpolated.
# Needs an exiftool instance
def find_and_set_geotag(file, geotags, et, timedelta, tzinfo = pytz.UTC):
metadata = et.get_metadata(file)
if 'EXIF:GPSLongitude' in metadata and 'EXIF:GPSLatitude' in metadata:
# print(f"{file} : data already present, skipping!")
return
dt = datetime.datetime.strptime(metadata['EXIF:ModifyDate'], "%Y:%m:%d %H:%M:%S")
dt = dt.replace(tzinfo=tzinfo)
i = find_previous_geotag_index(dt, geotags)
td1 = dt - geotags[i]['dt']
td2 = geotags[i+1]['dt'] - dt
if td1 <= timedelta and td2 <= timedelta:
geotag = {}
geotag['longitude'] = ((geotags[i]['longitude'] * td1.total_seconds()) + (geotags[i+1]['longitude'] * td2.total_seconds())) / (td1 + td2).total_seconds()
geotag['latitude'] = ((geotags[i]['latitude'] * td1.total_seconds()) + (geotags[i+1]['latitude'] * td2.total_seconds())) / (td1 + td2).total_seconds()
geotag['altitude'] = ((geotags[i]['altitude'] * td1.total_seconds()) + (geotags[i+1]['altitude'] * td2.total_seconds())) / (td1 + td2).total_seconds()
elif td1 <= timedelta and td1 < td2:
geotag = geotags[i]
elif td2 <= timedelta:
geotag = geotags[i+1]
else:
return
print(f"Timestamp of {file}: {dt}")
print(f"found valid geotag for {file}: {geotag}!")
if geotag['latitude'] < 0:
lat_ref = "S"
else:
lat_ref = "N"
if geotag['longitude'] < 0:
lon_ref = "W"
else:
lon_ref = "E"
# there is a problem in the exiftool where it seems to ignore the -j argument to return JSON data when
# the operation is successful
try:
et.execute_json(rf"-exif:GPSLongitude={abs(geotag['longitude'])}", rf"-exif:GPSLongitudeRef={lon_ref}", file)
except json.decoder.JSONDecodeError:
pass
try:
et.execute_json(rf"-exif:GPSLatitude={abs(geotag['latitude'])}", rf"-exif:GPSLatitudeRef={lat_ref}", file)
except json.decoder.JSONDecodeError:
pass
try:
et.execute_json(rf"-exif:GPSAltitude={geotag['altitude']}", file)
except json.decoder.JSONDecodeError:
pass
Based on this functions, only little code is needed to achieve what is needed:
# 1. read in kml & geotags from other photos
# 2. get sorted list of geotags
# 3. open next picture
# 4. get datetime from exif
# 5. find the nearest geotags before and after the timestamp
# 6. if both are close enough -> linear approximate both points
# 7. else if one is close enough -> take geotag
# 8. else goto 3
# 9. apply geotag to exif data
# 10. goto 3
import datetime
import os.path
import os
import exiftool
# this is the script above!
from GeotagHandling import read_kml_track, store_geotags_to_json, load_geotags_from_json, extract_geotags_from_files, find_and_set_geotag
timedelta = datetime.timedelta(0,300) # geotags within 5 minutes are valid
kml_file = 'Location History.kml'
json_file = 'location_history.json'
tagged_picture_dir = <Enter Directory>
target_picture_dir = <Enter Directory>
if not os.path.isfile(json_file):
start = datetime.datetime(2021,7,24,0,0,0,0,datetime.timezone(datetime.timedelta()))
end = datetime.datetime(2021,8,18,0,0,0,0,datetime.timezone(datetime.timedelta()))
print(f"Extracting geotags from Location History in {kml_file}:")
geotags = list(read_kml_track(kml_file, start, end))
print(f"Finished processing {kml_file}!")
print(f"Extracting geotags from files in {tagged_picture_dir}:")
geotags.extend(extract_geotags_from_files(tagged_picture_dir))
print(f"Finished processing {tagged_picture_dir}!")
store_geotags_to_json(json_file, geotags)
else:
try:
geotags = load_geotags_from_json(json_file)
if geotags is None or len(geotags) == 0:
raise ValueError('JSON File might be empty!')
print("Finished loading and sorting of geotags!")
except Exception as e:
print("Geotag JSON File can't be read!")
print(e)
exit()
directory = os.fsencode(target_picture_dir)
print(f"Tagging files in {target_picture_dir}:")
with exiftool.ExifTool() as et:
for file in os.listdir(directory):
filename = os.fsdecode(file)
if filename.lower().endswith(".jpg") or filename.lower().endswith(".jpeg") or filename.endswith(".mp4"):
find_and_set_geotag(os.path.join(target_picture_dir, filename), geotags, et, timedelta)
continue
else:
continue
print(f"Finished processing {target_picture_dir}!")