15
\$\begingroup\$

Google know where we are 24/7. If my camera does not have built-in GPS, but I would like to geotag my photos, is it possible to use the location history to automatically tag photos with my GPS coordinates at the time they were taken? If so, how?

I am not asking how to use Google Maps to select locations with which to tag photos.

\$\endgroup\$
0

6 Answers 6

8
\$\begingroup\$

The procedure I use is:

  1. Go to your Google Timeline
  2. Select day
  3. Click on gear icon, export this day in KML
  4. Convert the file to GPX (I use GPSBabel)

    gpsbabel -w -r -t -i kml -f history.kml -o gpx -F history.gpx
    
  5. Get (if you do not have) Geotag software

  6. Install (if you do not have) JVM
  7. Start Geotag software
  8. Add your images
  9. Load track from file
  10. Adjust camera time
  11. Save new locations (I save to XMP)

But as Google do not write in timeline GPS location marks so often I will no rely on this info for image geotagging. In my test I get 180 point for 8 hours trip (with other mobile software I get around 25000 points). Of course someone can interpolate the location, but is not so precise. Also Google (in my case) do not store the elevation.

Edit: I just now find another tool which can use your location history and directly add location info to your photos. Web site, python tool itself

\$\endgroup\$
10
  • \$\begingroup\$ The command I see is: gpsbabel -w -r -t -i kml -f history-2018-07-15.kml -o gpx -F out.gpx (not sure about parameters, I use graphic interface). The point is you should set the time of images to be as it is from Google (point 10) \$\endgroup\$ Jul 17, 2018 at 8:10
  • \$\begingroup\$ I think it does not working because Google Timeline info has too many gaps. Software cannot find times that it thinks are close enough to match. \$\endgroup\$
    – xiota
    Jul 17, 2018 at 8:40
  • 1
    \$\begingroup\$ I have used the android app GPS Logger since it is free, working well, adjustable and easy on the battery. When Googles Location history is turned on on my phone when this app is logging, I can also directly download the history as you described but with very good resolution, making the workflow easier. \$\endgroup\$
    – ThePMO
    Sep 18, 2021 at 12:44
  • 1
    \$\begingroup\$ this does not work anymore. the resulting gpx file does not contain timestamps, as the kml file is what google calls a "semantic location history". this still contains timestamps, but obviously not where gpsbabel expects them. \$\endgroup\$
    – ths
    Dec 13, 2022 at 13:31
  • 1
    \$\begingroup\$ the other tool, from the chuckleplant link in the edit, still works though, with the google checkout json. \$\endgroup\$
    – ths
    Dec 13, 2022 at 15:05
5
\$\begingroup\$

Google Location History KML files may be downloaded from:

  • Google Timeline, as RomeoNinov describes. The problem with these files is the timestamps and trackpoints are spread too far apart for some software to interpolate. Also, only one day can be downloaded at a time.

  • Unfortunately, Google Takeout no longer provides Location History as KML files, but now JSON only. I don't currently know of a JSON to KML converter.

Geotags may be added to images with exiftool, which can read KML files directly. It is also able to interpolate timestamps and locations without restriction. In the following command, replace history.kml with the location of your KML file, and replace -00:00 with your time zone.

exiftool -if 'not ($GPSLatitude or $GPSLongitude)' -geotag history.kml '-geotime<${createdate}-00:00' -r -ext jpg  .

Windows users may need to use double quotes. Different file extensions may be specified with additional -ext flags.

\$\endgroup\$
3
  • \$\begingroup\$ I guess your example "-00:00" is for negative timezones like "-06:00". For positive timezones it's "+07:00" is that correct? \$\endgroup\$
    – Underlines
    Apr 25, 2021 at 9:51
  • \$\begingroup\$ Hi, I cannot find a way to get the Takeout KML file you are talking about. When I select my Location History, the only format they offer me is JSON. Am I missing something ? \$\endgroup\$
    – Dabrule
    Aug 30, 2022 at 23:04
  • 1
    \$\begingroup\$ @Dabrule Looks like Google Takeout no longer exports to KML. Have to use Google Timeline. It exports only one day at a time though. \$\endgroup\$
    – xiota
    Aug 31, 2022 at 5:36
1
\$\begingroup\$

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}!")

\$\endgroup\$
0
\$\begingroup\$

If you don’t like commandline tools and prefer windows, you can give Enny GEOTagger a chance. Enny GEOTagger in Windows Store

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I do like command-line tools, and I don't prefer Windows. However, there may be others who would give your software a chance. \$\endgroup\$
    – xiota
    May 14, 2019 at 8:44
  • \$\begingroup\$ This does only support JPG/JPEG, no DNG/RAW support. \$\endgroup\$
    – Underlines
    Apr 25, 2021 at 9:50
0
\$\begingroup\$

If you don't like commandline tools, you can also give a try with Darktable. (a free and open-source photography application software and raw developer) https://www.darktable.org/usermanual/en/geotagging.html

\$\endgroup\$
0
\$\begingroup\$

I use ridewithgps but I suspect most other activity tracking software can export a .GPX file. I then import that to Lightroom, which is part of my usual workflow anyway, and use its autotag feature.

\$\endgroup\$
2
  • \$\begingroup\$ Does that app use Google Location History? \$\endgroup\$
    – xiota
    Sep 1, 2022 at 5:16
  • \$\begingroup\$ No, you have to start it recording. I don't know if you can get .GPX from Location History. \$\endgroup\$ Sep 1, 2022 at 13:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.