He who controls the past controls the future. He who controls the present controls the past. – George Orwell, 1984

Here’s a script that runs on Python that can add GPS tags to your photos (jpg) given your Google location history. You have to download the location history from https://takeout.google.com/ and run:

python location-geotag.py --dir { your photos directory } --json { your location history }

Do backup your photos before doing this, you may lose them.

Somebody’s watching me

I use Google Photos for keeping the thousands if not hundreds of thousands of pictures I’ve taken over the years. Unlimited storage (I love unlimited stuff) for the price of image compression and probably Google using your photos for machine learning, ads and other orwellian ends.

One feature I like is geotagging. Google Photos will use your location history to deduce where the photo was taken. Then they’ll show you a nice map when visiting the image and you’ll go ‘Oh look I’ve been there!’

However, I noticed that when exporting the photos out of Google, you don’t get the approximate geotag as metadata in your JPEG. This bothers me because the image gallery I’m using also shows you a small map if the image has GPS data in it (check it out!).

After some research I found that it’s actually not possible to export the geotags from Google Photos. I did find that you can actually download your whole location history, all the location data Google has on you. It’s a lot of data depending on how long you’ve used a smartphone.

So I made a small script that adds the GPS information on any photo using your location history as input. It’s quite simple but works surprisingly well.

Script

The script is written in Python. My location history was around 200Mb, so I wanted an efficient way to search through the whole file.

The Google export looks like this:

{ "locations" : [ { "timestampMs" : "1532122858202" , "latitudeE7" : 48445182 , "longitudeE7" : 24287419 , "accuracy" : 13 , "altitude" : 117 , "verticalAccuracy" : 2 }, { "timestampMs" : "1532122465164" , "latitudeE7" : 439445411 , "longitudeE7" : 26287254 , "accuracy" : 14 , "altitude" : 117 , "verticalAccuracy" : 2 }, { "timestampMs" : "1532122068529" , "latitudeE7" : 419945411 , "longitudeE7" : 12287254 , "accuracy" : 14 , "altitude" : 117 , "verticalAccuracy" : 2 }] }

I loaded the JSON and created a custom Location class which just registered the GPS info and the timestamp. I converted the timestamps to seconds when loading them. I also defined some operators to be able to sort and search the locations list.

# # Note I construct directly from the JSON dictionary # class Location ( object ): def __init__ ( self , d = {}): self . timestamp = None self . latitude = None self . longitude = None self . altitude = 0 for key in d : if key == 'timestampMs' : self . timestamp = int ( d [ key ]) / 1000 elif key == 'latitudeE7' : self . latitude = d [ key ] elif key == 'longitudeE7' : self . longitude = d [ key ] elif key == 'altitude' : self . altitude = d [ key ] def __eq__ ( self , other ): return self . timestamp == other . timestamp def __lt__ ( self , other ): return self . timestamp < other . timestamp def __le__ ( self , other ): return self . timestamp <= other . timestamp def __gt__ ( self , other ): return self . timestamp > other . timestamp def __ge__ ( self , other ): return self . timestamp >= other . timestamp def __ne__ ( self , other ): return self . timestamp != other . timestamp

Using the bisect python module I could have search times, which is as good as I can hope for. I had to reverse the locations because Google exports them in descending timestamp order.

I got the timestamp from my images using PIL black magic image._getexif()[36867] . And then it was a matter of finding the location with the closest timestamp to my image. From SO:

def find_closest_in_time ( locations , a_location ): pos = bisect_left ( locations , a_location ) if pos == 0 : return locations [ 0 ] if pos == len ( locations ): return locations [ - 1 ] before = locations [ pos - 1 ] after = locations [ pos ] if after . timestamp - a_location . timestamp < a_location . timestamp - before . timestamp : return after else : return before

In order to add the GPS information I used the pexif module at first. But I found that the Exif data written by the module was sometimes broken. I switched to piexif, which does basically the same. The documentation however was a bit harder to bite, luckily I found this gist that showed how to embed GPS data via exif.

I also added a time threshold of a couple of hours, any location out of that threshold would be considered inaccurate.

# # piexif library usage to add GPS info to an image # approx_location = find_closest_in_time ( my_locations , curr_loc ) hours_away = abs ( approx_location . timestamp - time_jpeg_unix ) / 3600 if ( hours_away < hours_threshold ): # Google stores these as x-e7 lat_f = float ( approx_location . latitude ) / 10000000.0 lon_f = float ( approx_location . longitude ) / 10000000.0 exif_dict = piexif . load ( image_file ) exif_dict [ "GPS" ][ piexif . GPSIFD . GPSVersionID ] = ( 2 , 0 , 0 , 0 ) exif_dict [ "GPS" ][ piexif . GPSIFD . GPSAltitudeRef ] = 0 if approx_location . altitude > 0 else 1 exif_dict [ "GPS" ][ piexif . GPSIFD . GPSAltitude ] = change_to_rational ( abs ( approx_location . altitude )) exif_dict [ "GPS" ][ piexif . GPSIFD . GPSLatitudeRef ] = 'S' if lat_f < 0 else 'N' exif_dict [ "GPS" ][ piexif . GPSIFD . GPSLongitudeRef ] = 'W' if lon_f < 0 else 'E' lat_deg = to_deg ( lat_f , [ "S" , "N" ]) lng_deg = to_deg ( lon_f , [ "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 ])) exif_dict [ "GPS" ][ piexif . GPSIFD . GPSLatitude ] = exiv_lat exif_dict [ "GPS" ][ piexif . GPSIFD . GPSLongitude ] = exiv_lng exif_bytes = piexif . dump ( exif_dict ) image . save ( image_file , exif = exif_bytes ) else : print 'Time threshold surpassed'

The full script can be found here. Make sure to make a backup of your images before running the script!