Thursday, April 18, 2019

Extracting location information from Photos

Photos exported from digital cameras often contain meta-data in Exif format (Exchangeable Image File Format). For images taken with cellphone cameras, this info typically also includes (GPS) location information of where the photo was taken.

Inspired by this previous post on the mapping of GPS lat/lon coordinates from Google+ location data to a rough description of the location, we could also use the location encoded in the photo itself.

We are using again the reverse geocoding service from OpenStreetMap to find the names of the country and locality in which the GPS coordinates are included in.

For the purpose of public posting, reducing the accuracy of the GPS location to the granularity of the city town or village provides some increased confidentiality of where the picture was taken compared to the potentially meter/centimeter resolution accuracy of GPS data that generally allows to pinpoint the location down to a building and street address.

Fractional numbers are represented as ratios of integers in Exif. For example the number 0.5 could be encoded as the tuple (5, 10). The coordinates in the Exif location meta-data are represented in the DMS (Degrees Minutes Seconds) format which needs to to be converted into the DD (decimal degree) format used by most GIS systems including OpenStreetMap.


#!/usr/bin/env python

import sys

import geopy
import PIL.Image 
import PIL.ExifTags
import pycountry

geocoder = geopy.geocoders.Nominatim(user_agent='gplus_migration', timeout=None)

def get_location_hashtags(latlon):
  """Reverse geo-code lat/lon coordinates ISO-code / country / municipality names."""
  hashtags = []
  if latlon:
    addr = geocoder.reverse((latlon[0], latlon[1])).raw
    if 'address' in addr:
      addr = addr['address']
      cc = addr['country_code'].upper()
      hashtags.append(cc)
      hashtags.append(pycountry.countries.get(alpha_2=cc).name.replace(' ',''))
      for location in ['city', 'town', 'village']:
        if location in addr:
          hashtags.append(addr[location].replace(' ', ''))
  return hashtags

def get_exif_info(img):
  """Decode Exif data in image."""
  ret = {}
  info = img._getexif()
  if not info:
    return ret
  for tag, value in info.items():
    decoded = PIL.ExifTags.TAGS.get(tag, tag)
    ret[decoded] = value
  return ret

def get_gps_info(info):
  """Decode GPSInfo sub-tags in Exif data."""
  ret = {}
  if not info or not 'GPSInfo' in info:
    return ret
  for tag, value in info['GPSInfo'].items():
    decoded = PIL.ExifTags.GPSTAGS.get(tag, tag)
    ret[decoded] = value
  return ret

def degrees_from_ratios(ratios):
  """Convert from Exif d/m/s array of ratios to floating point representation."""
  f = [(float(r[0]) / float(r[1])) for r in ratios]
  return f[0] + f[1] / 60.0 + f[2] / 3600.0

def get_latlon(gps_info):
  """Extract the GPS coordinates from the GPS Exif data and convert into fractional coordinates."""
  lat = gps_info.get('GPSLatitude', None)
  lat_hemi = gps_info.get('GPSLatitudeRef', None)
  lon = gps_info.get('GPSLongitude', None)
  lon_hemi = gps_info.get('GPSLongitudeRef', None)
  if lat and lat_hemi and lon and lon_hemi:
    return (degrees_from_ratios(lat) * (-1 if lat_hemi == 'S' else 1),
            degrees_from_ratios(lon) * (-1 if lon_hemi == 'W' else 1))
  else:
    return None

def get_camera(info):
  """Get Camera make & model as another example of Exif data."""
  if 'Make' in info and 'Model' in info:
    return '%s %s' % (info['Make'], info['Model'])
  else:
    return None
  
#------------------------------------------------------

for filename in sys.argv[1:]:
  image = PIL.Image.open(filename)
  exif_info = get_exif_info(image)
  gps_info = get_gps_info(exif_info)
  latlon = get_latlon(gps_info)
  print ('%s : %s %s' % (filename, get_camera(exif_info), get_location_hashtags(latlon)))

No comments:

Post a Comment