sylvain durand

GPS data from photos with Python

Most recent cameras and mobile phones record photographs with geographical data (longitude, latitude, but also altitude). If this data is readable with most photo and file explorer software, it is also possible to access it with Python.

Reading metadata

The Python Imaging Library (PIL) provides easy access to EXIF data with the function _getexif().

It is easily installed, under Python3, with the fork Pillow. We use the following command (depending on your configuration, pip3 can be directly accessible with pip):

pip3 install pillow

However, this results in an indexed dictionary with numerical identifiers. To get the corresponding names, we use ExifTags and rename the keys of the dictionary:

from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS

def get_exif(filename):
    exif = Image.open(filename)._getexif()

    if exif is not None:
        for key, value in exif.items():
            name = TAGS.get(key, key)
            exif[name] = exif.pop(key)

        if 'GPSInfo' in exif:
            for key in exif['GPSInfo'].keys():
                name = GPSTAGS.get(key,key)
                exif['GPSInfo'][name] = exif['GPSInfo'].pop(key)

    return exif

exif = get_exif('image.jpg')

The exif variable then contains all the metadata of the image: objective, aperture, speed, author… Our GPS data is stored in exif['GPSInfo'], in the following form:

{'GPSLongitude': ((19, 1), (31, 1), (5139, 100)),
 'GPSAltitudeRef': b'\x00',
 'GPSAltitude': (92709, 191),
 'GPSTimeStamp': ((13, 1), (18, 1), (42, 1)),
 'GPSHPositioningError': (10, 1),
 'GPSLatitudeRef': 'N',
 'GPSLatitude': ((63, 1), (40, 1), (5908, 100)),
 'GPSLongitudeRef': 'W'}

Getting GPS data

Sexagesimal form

Geographic coordinates are usually expressed in the sexagesimal system, or DMS for degrees (°), minutes (′) and seconds (″). The unit is the degree of angle (1 turn = 360°), then the minute of angle (1° = 60′), then the second of angle (1° = 3 600″).

Compared to the equatorial plane, the latitude is completed by a letter N (hemisphere) or S depending on whether one is located in the northern or southern hemisphere. Compared to the Greenwich meridian, the longitude is completed by a letter W or E depending on whether you are located in the west or east.

The data is directly accessible in exif['GPSInfo'] to be able to output this format:

def get_coordinates(info):
    for key in ['Latitude', 'Longitude']:
        if 'GPS'+key in info and 'GPS'+key+'Ref' in info:
            e = info['GPS'+key]
            ref = info['GPS'+key+'Ref']
            info[key] = ( str(e[0][0]/e[0][1]) + '°' +
                          str(e[1][0]/e[1][1]) + '′' +
                          str(e[2][0]/e[2][1]) + '″ ' +
                          ref )

    if 'Latitude' in info and 'Longitude' in info:
        return [info['Latitude'], info['Longitude']]

get_coordinates(exif['GPSInfo'])

We get:

['63.0°40.0′59.08″ N', '19.0°31.0′51.39″ W']

Decimal form

To obtain automated geographic data processing, a decimal format is often more convenient: minutes are divided by 60 and seconds by 3600 and added together. Latitude is negative in the Southern Hemisphere (S), and west of the Greenwich Meridian (W). The calculation is then very simple:

def get_decimal_coordinates(info):
    for key in ['Latitude', 'Longitude']:
        if 'GPS'+key in info and 'GPS'+key+'Ref' in info:
            e = info['GPS'+key]
            ref = info['GPS'+key+'Ref']
            info[key] = ( e[0][0]/e[0][1] +
                          e[1][0]/e[1][1] / 60 +
                          e[2][0]/e[2][1] / 3600
                        ) * (-1 if ref in ['S','W'] else 1)

    if 'Latitude' in info and 'Longitude' in info:
        return [info['Latitude'], info['Longitude']]

get_decimal_coordinates(exif['GPSInfo'])

We get:

[63.683077777777775, -19.530941666666667]

With GPSPhoto

Since the end of 2018, the Python library GPSPhoto allows to obtain directly the geographical coordinates of a photo.

It is installed with:

pip3 install GPSPhoto

We can now use:

from GPSPhoto import gpsphoto
gpsphoto.getGPSData('image.jpg')

We get:

{'Latitude': 63.683077777777775,
 'Longitude': -19.530941666666667,
 'Altitude': 485.3874345549738,
 'UTC-Time': '13:18:42',
 'Date': '09/03/2018'}

Nevertheless, according to the file, GPSPhoto tends to easily return errors that, in my case, make it impossible to automate a large number of photos:

ValueError: malformed node or string: <_ast.BinOp object at 0x10ea16320>

Automation

If you want to recover metadata from a set of photos, you can iterate to a folder (and its subfolders).

import os
points = []
for r, d, f in os.walk(path):
    for file in f:
        if file.lower().endswith(('.png', '.jpg', '.jpeg')):
            filepath = os.path.join(r, file)
            exif = get_exif(filepath)
            if exif is not None and 'GPSInfo' in exif:
                latlong = get_decimal_coordinates(exif['GPSInfo'])
                if latlong is not None:
                    points.append(latlong)

You can then export points to display it on a map, for example with Leaflet.