How to geocode photos

So if I have gpx tracks of a bicycle tour, e.g., and I have photos taken on that bicycle trip, well, it should be pretty easy to add coordinates to the exif header of the photos (jpgs) based on timestamps in both the gpx files and the jpgs.  I’ve done this once so far (on the first trip that I fairly faithfully recorded a gps track, November 2017 from Kırşehir to Antalya).

Sync timestamps

Make sure that the time is the same on the gps recorder (a phone in this case) and the camera.  If you forget to do this beforehand, well, you can fix it later.  If you change the time on one device in the middle of the trip, but not on the other, well, you’ll have problems.  To check the exact time on the phone, I needed to use a terminal emulator and the linux “date” command.  I then used shotwell to reset the times (off, in this case, by 6 min 16 sec).  However, that involves bringing the photos into shotwell first (before they’re geocoded, part of my pre-geocoding workflow).  A better solution would be to try exiv2:

exiv2 -a 6:16 ab *.jpg

I haven’t tried this yet.

Merge gpx files

The tool I’m using to put the coordinates into the exif headers accepts only one gpx file.  It’s easy enough to merge the gpx files with gpsbabel.  I use this bash code to make a command line to run gpsbabel with all the gpx files in the directory.

#This little bash script simply makes a command line that can run gpsbabel
#with all the gpx files in a directory, sorted in "sort" order (use sort
#flags for another order)
f="gpsbabel -t -i gpx"
for i in `ls *.gpx | sort`
  f+=" -f "$i
f+=" -o gpx -F merge.gpx"
echo $f

The output of that script may look something like this:

gpsbabel -t -i gpx -f 2017-11-10_10-40_Fri.gpx -f 2017-11-10_11gpx.gpx -f 2017-11-10_12-39_Fri.gpx -f 2017-11-11_08-37_Sat.gpx -o gpx -F merge.gpx

which would combine those four gpx files into one file called “merge.gpx”.

Add coordinates to jpgs

Now you’re ready.  However, you need and helper files ( and which I took from Seth Golub -v -l 30 -t 2 merge.gpx *.jpg

-l 30 means I want a gpx stamp within 30 seconds of the photo stamp.  What this means is I’m only geocoding photos that I took when the gpx tracking was active.  This means camp photos (usually taken when the phone (gps recorder) was turned off) won’t get geocoded.  This is my choice at this point. -t 2 is for the 2 hour time difference between GMT/Z time (gps time) and the time in Turkey (where this tour was).

I had to modify Seth’s a bit to use PIL instead of “image” whatever that is.  Here’s my version:

# Given one tracklog and one or more JPEG files, correlate the
# timestamps between tracklog and EXIF data and write the
# corresponding GPS data in the JPEG files.
# Seth Golub
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# I don't bother to provide a copy of the GNU General Public License
# along with this program, but you can get one from the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

#Bryan's notes: for whatever reason there seems to be a two-hour time difference between the camer and the gps

import sys
from time import strftime,gmtime,mktime
#Bryan added the following line
from time import strptime,tzset
from optparse import OptionParser
from PIL import Image
from tracklog import TrackLog
from geoexif import GeoExif
#from datetime import datetime

def geocode_jpeg(track, jpeg, tzoffset_secs, datum, limit):
  if type(jpeg) is str:
    jpegfile = jpeg
    #Bryan added .open to the following line
    #what can be down with image?  can I get the image time?
    image =
  elif isinstance(jpeg, Image):
    image = jpeg
    jpegfile = image.filename
    raise TypeError('jpeg argument to geocode_jpeg must be an Image or a string (the filename)')

  #point = track.find_nearest_in_time(image.image_time() - tzoffset_secs)
  #what kind of object is image.image_time() supposed to be?
  sTime = image._getexif()[36867]
  mTime = mktime(strptime(sTime, '%Y:%m:%d %H:%M:%S'))  
  point = track.find_nearest_in_time(mTime - tzoffset_secs)
  did_something = 0
  data = GeoExif()
  data.set('datum', datum)
  data.set('longitude', point.lon)
  data.set('timestamp', strftime('%H%M%S', gmtime(point.time)))
  if point.__dict__.has_key('elevation'):
    data.set('altitude', point.elevation)
  if limit < 0 or abs((mTime - tzoffset_secs) - point.time) <= limit:
    did_something = 1
  return (data, point, did_something, mTime)

def main():
  usage = "usage: %prog [opts] track.gpx file1.jpg [more jpegs]"
  optparser = OptionParser(usage)
  optparser.add_option('-e', '--exivbin', type='string', help='location of exiv2 binary', default='exiv2')
  optparser.add_option('-v', '--verbose', action='store_true', default=False)
  optparser.add_option('-l', '--limit', type='int', default=-1, help='Limit in seconds of absolute time difference between trackpoint and image. Images lacking a trackpoint within this limit will not be geocoded. The default is a negative number, which means no limit.')
  optparser.add_option('-d', '--datum', type='string', default='WGS-84')
  optparser.add_option('-t', '--tzoffset', type='float', default=0.0,
                       help='jpegs\' hours off UTC (e.g. -8 for PST)')
  (options, args) = optparser.parse_args()
  GeoExif.exivbin = options.exivbin

  tzoffset_secs = options.tzoffset * 60 * 60
  sys.stderr.write('Reading track log\n')
  track = TrackLog(args[0])
  sys.stderr.write('Processing photos\n')
  import time
  for jpegfile in args[1:]:
    #Bryan added .open to the following line
    #jpeg =
    #then I tried this
    jpeg = jpegfile
    (data, trackpoint, did_something, mTime) = geocode_jpeg(track, jpeg, tzoffset_secs, options.datum, options.limit)
    if options.verbose:
      print '%(file)s\t%(result)s\t%(lat)s,%(lon)s\t%(imgtime)s (TZ=%(tz)s)\t%(pointtime)s\t%(timediff)d' % {
        'file' : jpegfile,
        'imgtime' : time.asctime(time.localtime(mTime)),
        'pointtime' : trackpoint.time_str,
        'lat' : str(data.get('latitude')),
        'lon' : str(data.get('longitude')),
        'result' : (did_something and 'yes' or 'no'),
        'timediff' : abs((mTime - tzoffset_secs) - trackpoint.time),
        'tz' : str(options.tzoffset),

if __name__ == '__main__':

Leave a Reply

Your email address will not be published. Required fields are marked *