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:
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.
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.