Help – Learn how to fix missing location data in jpg/jpeg images (EXIF) using your Location History

Sometimes people take photos without GPS enabled or just in that moment the location isn’t determined by the smartphone, so the photos areen’t tagged with a location. There are multiple possibilities to tag existing photos in order to show them on a map. You can do it manually using the exiftool as example, or you can use your Google Location History (Timeline).

If you want to fix a bunch of photos without to much work, the second options is much better, it will save you a lot of time. The requirement is that you had that feature enabled when you took the pictures.

Here are the steps to follow if you want to fix them

  1. Go to Google Takout (Download your data)
  2. Deselect all
  3. Search “Location History” and choose JSON as export format
  4. Click on “Next step”
  5. Click on “Create Archive”
  6. Your archive will be generated and a link will be send to your E-mail address
  7. Extract that archive and put your json file somewhere. As example “C:\Users\denny\Downloads\Location History.json
  8. Save that script below somewhere. As example “C:\Users\denny\Downloads\fixer.py
  9. Make sure you’ve installed python 3.x

Run the following commands:

python -m pip install pillow piexif
python "fixer.py" "C:\Users\denny\Google Drive\Google Photos" "C:\Users\denny\Downloads\Location History.json" "C:\Users\denny\Downloads"
  • python argument: path to your .py file
  • script argument 1: The path to the folder containing your untagged photos
  • script argument 2: The path to your location .json file
  • script argument 3: The path to a folder where the output will be generated

Sample output:

Loading locations...
Locations: 709122
Loading photos...
Photos: 4223
Filtering photos by missing location...
Photos without location: 42
Filtering photos by fixable locations...
Found location for photos C:\Users\denny\Google Drive\Google Photos\2019\20190501_163719.jpg. Hours away: 0.0016988888714048597. In threshold: True
[...]
Found location for photos C:\Users\denny\Google Drive\Google Photos\2019\20190524_183505.jpg. Hours away: 0.053024166689978704. In threshold: True
Photos to fix: 42
Fixing...
Done

Process finished with exit code 0

All your tagged photos will be saved as a copy into the output folder. A thrashold of 3 hours is configured to not accidentially tag a photos with a false location.

If you want to re-upload them to Google Photos, please follow these instructions

  1. Delete the original photo from Google Photos
  2. Upload the modified photo
  3. Add that photo to the original album if needed

Script

import datetime
import json
import time
from bisect import bisect_left
from fractions import Fraction
from glob import glob
from os.path import join, isfile, splitext, basename, dirname

import piexif
from PIL import Image


# Copyrights:
# https://levionsoftware.wordpress.com/fixing-missing-locations/
# https://gist.github.com/chuckleplant/84b48f5c2cb743013462b6cb5f598f01
# https://gist.github.com/c060604/8a51f8999be12fc2be498e9ca56adc72


class Location(object):
    def __init__(self, d=None):
        for key in d or {}:
            if key == 'timestampMs':
                self.timestamp = int(d[key]) / 1000
            elif key == 'latitudeE7':
                self.latitude = d[key]
            elif key == 'longitudeE7':
                self.longitude = 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


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


def to_deg(value, loc):
    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


class LocationFixer:
    INCLUDED_EXTENSIONS = ['.jpg', '.JPG', '.jpeg', '.JPEG']
    HOURS_THRESHOLD = 3

    def __init__(self, photos_folder_path, locations_file_path, out_folder_path):
        self.photos_folder_path = photos_folder_path
        self.locations_file_path = locations_file_path
        self.out_folder_path = out_folder_path

        self.locations = []
        self.photos = []
        self.photos_without_location = []
        self.photos_fixable = []

    def run(self):
        self.state_00_load_location_history_file()
        self.state_01_get_photos()
        self.state_02_get_photos_with_missing_location()
        self.state_03_collect_fixable_photos()
        self.state_04_fix_photos()

    def state_00_load_location_history_file(self):
        print('Loading locations...')
        with open(self.locations_file_path) as f:
            location_data = json.load(f)
            location_array = location_data['locations']

            for location in location_array:
                location = Location(location)
                self.locations.append(location)

        print(f' Locations: {len(self.locations)}')

    def state_01_get_photos(self):
        print('Loading photos...')
        for file in glob(join(self.photos_folder_path, '**'), recursive=True):
            if isfile(file):
                file_name = basename(file)
                file_containing_folder_path = dirname(file)
                file_name_without_extension, ext = splitext(file_name)
                if ext in self.INCLUDED_EXTENSIONS:
                    self.photos.append((file, file_containing_folder_path, file_name_without_extension, ext))

        print(f' Photos: {len(self.photos)}')

    def state_02_get_photos_with_missing_location(self):
        print('Filtering photos by missing location...')
        for file, file_containing_folder_path, file_name_without_extension, ext in self.photos:
            image = Image.open(file)
            exif = image._getexif()
            location = exif.get(34853)
            if not location:
                self.photos_without_location.append(
                    (file, file_containing_folder_path, file_name_without_extension, ext, exif))

        print(f' Photos without location: {len(self.photos_without_location)}')

    def state_03_collect_fixable_photos(self):
        print('Filtering photos by fixable locations...')
        for file, file_containing_folder_path, file_name_without_extension, ext, exif in self.photos_without_location:
            time_exif = exif[36867]
            time_jpeg_unix = time.mktime(datetime.datetime.strptime(time_exif, "%Y:%m:%d %H:%M:%S").timetuple())

            curr_loc = Location()
            curr_loc.timestamp = int(time_jpeg_unix)

            approx_location = find_closest_in_time(self.locations, curr_loc)
            lat_f = float(approx_location.latitude) / 10000000.0
            lon_f = float(approx_location.longitude) / 10000000.0

            hours_away = abs(approx_location.timestamp - time_jpeg_unix) / 3600
            in_threshold = hours_away < self.HOURS_THRESHOLD

            print(f'  Found location for photos {file}. Hours away: {hours_away}. In threshold: {in_threshold}')

            if in_threshold:
                self.photos_fixable.append(
                    (file, file_containing_folder_path, file_name_without_extension, ext, lat_f, lon_f))

        print(f' Photos to fix: {len(self.photos_fixable)}')

    def state_04_fix_photos(self):
        print(f' Fixing...')
        for file, file_containing_folder_path, file_name_without_extension, ext, lat_f, lon_f in self.photos_fixable:
            dest_file = join(self.out_folder_path, file_name_without_extension + ext)

            img = Image.open(file)
            exif_dict = piexif.load(img.info['exif'])

            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.GPSVersionID: (2, 0, 0, 0),
                piexif.GPSIFD.GPSLatitude: exiv_lat, piexif.GPSIFD.GPSLatitudeRef: lat_deg[3],
                piexif.GPSIFD.GPSLongitude: exiv_lng, piexif.GPSIFD.GPSLongitudeRef: lng_deg[3]
            }

            exif_bytes = piexif.dump(exif_dict)
            img.save(dest_file, exif=exif_bytes)

        print(f' Done')


if __name__ == '__main__':
    import sys

    LocationFixer(sys.argv[1], sys.argv[2], sys.argv[3]).run()

%d bloggers like this: