Location, location, location

My regular camera is a Canon G10, a nice little compact that sits between an elementary point-and-shoot and a DSLR. My iPhone 4’s camera is fine if the lighting is good, and there’s the “the best camera is the one with you” factor, but its images really don’t compare to the G10’s. The one big advantage the iPhone has is that its photos have the GPS location baked into the EXIF metadata. I’ve often wished I could pull the GPS info from the phone and stick it into the G10. Last night I wrote a script that kinda sorta does that.

The script is called coordinate, and it works like this: While I’m taking a bunch of photos at a spot with the G10, I take a single photo with the iPhone. When I have all the images transferred to my computer I run

coordinate -g iphone.jpg IMG*  

and the GPS data is read from the iPhone image (iphone.jpg) and copied to all the files from the G10 (IMG*). Boom.

I know that iPhoto and Flickr have interactive maps for adding location info to photos, but they’re slow and clumsy (especially iPhoto’s) compared to pulling the data directly from another image file.

Update 10/11/11
Ryan Gray in the comments points out that you can copy and paste location information from image to image within iPhoto. A very useful feature (which I never ran across) if you use iPhoto.

The source of the location info doesn’t have to be an iPhone, of course—it doesn’t even have to be your own camera phone. Is someone with you? Ask her to take a photo and email it too you.

And if you happen to have the latitude and longitude, coordinate has options for that, too. For example:

coordinate -n 41:53:03.96 -w 87:37:39.96 IMG*

will put your photos under the Marshall Field’s Macy’s Marshall Field’s clock at State and Randolph.

Coordinate is written in Python and relies on the pyexiv2 library, which binds to the C++ exiv2 library. Installing pyexiv2 on a Mac is kind of a pain in the ass (as described here); there are prebuilt installers for Windows and some flavors of Linux. Here’s the code:

python:
  1:  #!/usr/bin/env python
  2:  
  3:  import pyexiv2
  4:  import getopt
  5:  import sys
  6:  from fractions import Fraction
  7:  from functools import partial
  8:  
  9:  usage = """Usage: coordinate [options] [files]
 10:  
 11:  Options:
 12:    -g filename       photo file with GPS coordinates
 13:    -n ddd:mm:ss.ss   north coordinate
 14:    -s ddd:mm:ss.ss   south coordinate
 15:    -e ddd:mm:ss.ss   east coordinate
 16:    -w ddd:mm:ss.ss   west coordinate
 17:    -h                show this help message
 18:  
 19:  Add location metadata to each of the listed files. The location
 20:  can come from either the photo associated with the -g option or
 21:  with a -n, -s, -e, -w pair given on the command line."""
 22:  
 23:  # Functions for manipulating coordinates.
 24:  def makecoord(coordstring):
 25:    """Make a coordinate list from a coordinate string.
 26:  
 27:    The string is of the form ddd:mm:ss.ss and the list comprises
 28:    three Fractions."""
 29:  
 30:    angle = coordstring.split(':', 2)
 31:    loc = [ Fraction(x).limit_denominator(1000) for x in angle ]
 32:    return loc
 33:  
 34:  def setcoord(metadata, direction, coordinate):
 35:    """Set the latitude or longitude coordinate.
 36:  
 37:    Latitude is set if direction is 'N' or 'S', longitude if 'E' or 'W'.
 38:    The coordinate is a list of the form [dd, mm, ss], where the degrees,
 39:    minutes, and seconds are Fractions."""
 40:  
 41:    tags = {'lat': ('Exif.GPSInfo.GPSLatitudeRef', 'Exif.GPSInfo.GPSLatitude'),
 42:            'lon': ('Exif.GPSInfo.GPSLongitudeRef', 'Exif.GPSInfo.GPSLongitude')}
 43:    if direction in ('N', 'S'):
 44:      coord = 'lat'
 45:    else:
 46:      coord = 'lon'
 47:    metadata[tags[coord][0]] = direction
 48:    metadata[tags[coord][1]] = coordinate
 49:  
 50:  
 51:  # Get the command line options.
 52:  try:
 53:    options, filenames = getopt.getopt(sys.argv[1:], 'g:n:s:e:w:h')
 54:  except getopt.GetoptError, err:
 55:    print str(err)
 56:    sys.exit(2)
 57:  
 58:  # Set the option values.
 59:  gpsphoto = north = south = east = west = False       # defaults
 60:  for o, a in options:
 61:    if o == '-g':
 62:      gpsphoto = a
 63:    elif o == '-n':
 64:      north = makecoord(a)
 65:    elif o == '-s':
 66:      south = makecoord(a)
 67:    elif o == '-e':
 68:      east = makecoord(a)
 69:    elif o == '-w':
 70:      west = makecoord(a)
 71:    else:
 72:      print usage
 73:      sys.exit()
 74:  
 75:  # Valid option combinations.
 76:  ne = (north and east) and not (south or west or gpsphoto)
 77:  nw = (north and west) and not (south or east or gpsphoto)
 78:  se = (south and east) and not (north or west or gpsphoto)
 79:  sw = (south and west) and not (north or east or gpsphoto)
 80:  gps = gpsphoto and not (north or south or east or west)
 81:  
 82:  if not (ne or nw or se or sw or gps):
 83:    print "invalid location"
 84:    sys.exit()
 85:  
 86:  
 87:  # Create the coordinate setter functions.
 88:  if ne:
 89:    setlat = partial(setcoord, direction='N', coordinate=north)
 90:    setlon = partial(setcoord, direction='E', coordinate=east)
 91:  elif nw:
 92:    setlat = partial(setcoord, direction='N', coordinate=north)
 93:    setlon = partial(setcoord, direction='W', coordinate=west)
 94:  elif se:
 95:    setlat = partial(setcoord, direction='S', coordinate=south)
 96:    setlon = partial(setcoord, direction='E', coordinate=east)
 97:  elif sw:
 98:    setlat = partial(setcoord, direction='S', coordinate=south)
 99:    setlon = partial(setcoord, direction='W', coordinate=west)
100:  elif gps:
101:    basemd = pyexiv2.ImageMetadata(gpsphoto)
102:    basemd.read()
103:    latref = basemd['Exif.GPSInfo.GPSLatitudeRef']
104:    lat = basemd['Exif.GPSInfo.GPSLatitude']
105:    lonref = basemd['Exif.GPSInfo.GPSLongitudeRef']
106:    lon = basemd['Exif.GPSInfo.GPSLongitude']
107:    setlat = partial(setcoord, direction=latref.value, coordinate=lat.value)
108:    setlon = partial(setcoord, direction=lonref.value, coordinate=lon.value)
109:  else:
110:    print "coordinate setter failed"
111:    sys.exit()
112:  
113:  # Cycle through the files.
114:  for f in filenames:
115:    md = pyexiv2.ImageMetadata(f)
116:    md.read()
117:    setlat(md)
118:    setlon(md)
119:    md.write()

Most of coordinate’s 119 lines deal with the options that allow you to enter the coordinates on the command line. I don’t really expect to use it that way very often, but I thought it was important to have that capability.

The makecoord function on Lines 24-32 takes a string in the form ddd:mm:ss.ss and turns it into the form needed by pyexiv. It’s a weird format: a list of three items, one each for degrees, minutes, and seconds, each expressed as a Fraction.

The setcoord function on Lines 34-48 puts the given coordinate information in the appropriate EXIF fields in the metadata. The Exif.GPSInfo.GPSLatitudeRef field is a string that can be either 'N' or 'S'. Similarly, the Exif.GPSInfo.GPSLongitudeRef can be either 'E' or 'W'. The Exif.GPSInfo.GPSLatitude and Exif.GPSInfo.GPSLongitude fields are lists of Fractions, as returned by makecoord.

The command line options are read and interpreted in Lines 59-84. Apart from the help option, -h, there are five possible command line switches. The -g switch is used if you’re pulling the GPS info from an existing photo file. The -n, -s, -e, and -w options are used if you’re entering the coordinate data directly. Lines 76-80 make sure that the options you’ve entered make sense—you can’t, for example, give both a north and a south latitude.

Lines 88-111 are my favorite part of the script because they use a high-level, Lispy feature of Python’s functools library. The library’s partial feature allows you to use a previously defined function as a template for making a new function. The new function behaves like the template but has fewer arguments because some of the arguments are baked into its definition. Here, the setlat and setlon functions are created on the fly from the setcoord function and the given coordinate information. Both setlat and setlon have only one argument: the metadata for the photo file being updated.

The advantage of partial is that it separates the branching (Lines 118-111) from the loop through the files (Lines 114-119). Without it, the structure of this part of the script would be

for f in filenames:
  md = pyexiv2.ImageMetadata(f)
  md.read()
  if ne:
    setcoord north
    setcoord east
  elif nw:
    setcoord north
    setcoord west
  elif se:
    setcoord south
    setcoord east
  elif sw:
    set coord south
    set coord west
  elif gps:
    gpsmd = pyexiv2.ImageMetadata(gpsphoto)
    gpsmd.read()
    setcoord latitude
    setcoord longitude
  else:
    error handling
  md.write()

For situations in which the coordinates are specified on the command line, this isn’t much different from functools solution. But when the location is taken from a photo—which I expect to be the usual case—this structure opens and reads the photo with the location many times instead of just once. So the use of functools wasn’t just fun, it was a more efficient solution.

I love this script. It was both fun and surprisingly easy to write once I had the pyexiv2 library in place. And there’s something magical about adding a location to dozens of files at once with just a single short command.