R@ndom Curve

Rediscover your travel steps (using Photo EXIFs)
Andres C. Rodriguez 2016-04-07

Trip Map

Problem

So I have a bunch of photos taken with my iPhone, mainly shot in the mountains, and I would like to extract a GPS Path in the form of a GPX file. Once I have the GPX file I can load it into any mapping tool that supports it. For example here is an image of an intermediate result in the MAC OS X app GPS Tracks:

Extracting Latitude and Longitude

I wanted to keep the solution as lightweight as possible, so to do that I used a node.js script in conjunction with a utility (that I did not know existed) in Mac OS X: mdls (short for MetaData LS). The latitude and longitude are “hidden” inside the photo(s) in what is called the EXIF (EXchangeable Image Format) segment. It is basically a set of properties and values inside the JPEG or TIFF format.

So, since IMG_3190.JPG is the first image I want to analyze, let’s try the following command:

mdls IMG_3190.JPG

The result:

Extracting EXIF

Note the properties kMDItemLatitude and kMDItemLongitude.

So all that remains is go over the files, extract the properties and list them. I am lucky in that the files are in the same lexicographic order as the chronological order, so no need no muck with dates.

Extracting Script

The following script receives a directory as the first parameter (or uses the current directory as default) and prints out the list of coordinates one by one.

/**
 * Script to extract GPS coordinates from a set of photos. You can pass the name of the directory as
 * first parameter, otherwise it will assume
 */

var _ = require("./lodash.js");
var fs = require("fs");
var childProcess = require("child_process");

var dir = process.argv[2] || "/Users/andres/Dropbox/Photos/2015/2015-07 Dolomites";

// Vector to store all EXIF structures
var exifs = [];

// List of all files ending in 'JPG' (i.e. images)
var files = fs.readdirSync(dir).filter(function(file) {
    return file.endsWith(".JPG");
});

// Iterate over all files extracting EXIF
for (var i = 0; i < files.length; i++) {
    var file = dir+"/"+files[i];
    var data = childProcess.execSync("mdls '"+file+"'").toString().trim();
    var exif = {};
    // Regular expression extract key and value (v2 is with quotes, v3 is bare)
    data.replace(/(\w+) *= *("(.+)"|(.+))/g, function(m, k, v1, v2, v3) {
        exif[k] = (v2 || v3);
    });
    // Only add if the photo has latitude and longitude
    if (exif.kMDItemLatitude && exif.kMDItemLongitude) exifs.push(exif);
}

var gpx = `
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<gpx xmlns="http://www.topografix.com/GPX/1/1" version="1.1">
    <metadata>
        <name>Photo Trail</name>
    </metadata>
    <rte>
    <% _.forEach(exifs, function(exif) { %>
        <rtept lat="<%= exif.kMDItemLatitude %>" lon="<%= exif.kMDItemLongitude %>"><name><%= exif.kMDItemDisplayName %></name></rtept>
    <% }); %>
    </rte>
</gpx>
`;

// Making sure the XML is trimmed, otherwise it will not work because of the empty line
console.log(_.template(gpx.trim())({ exifs: exifs }));

I then open the track in Google Earth but I discover that a couple of pictures have the wrong GPS location, which completely screws the track.

Extracting EXIF

I actually did not know that the GPS on the phone could get the location so wrong. Especially when not inside a building. I fixed the track by removing those three pictures from it, and then I created a route on Strava that follows the hiking path more closely (because the photos are scattered around with big gaps in between them).

Google Earth Flyover

With that route created, I was able to create the following image in Google Earth (I show here the accelerated version of the movie that lasts a little less than two minutes).

Pretty cool. The video still had some quirks. I omitted adding altitude to the GPX track because when I did I was either flying 50 meters above earth or underground. So I left Google Earth map the altitude to whatever their 3D model of earth says it should be. I suspect that is behind the “jittery” track on screen.