Geolocating a Folder of Images with Python

I’m not sure how useful this will be, but as I recently built it in another language (I plan on blogging that soon as well), I thought I’d take a stab at building it in Python. Given a folder of images, can I use Python to grab the Exif information and then using that, figure out where the photos were taken using a reverse geocoding service? Here’s what I built.

First – Get the Images

Ok, the first step is simple, just get a list of images from a directory:

INPUT = "./sources"  files = os.listdir(INPUT)  for file in files: 	print(file) 

Woot! I’m a Python Master!

Get the Exif info

For the next step, I knew I needed to get the Exif info. For that I used the Pillow library, which has a handy getexif() method:

img = PIL.Image.open(os.path.join(INPUT, file))  img_exif = img.getexif() 

However, this doesn’t return what I expected:

{296: 2, 282: 72.0, 256: 4000, 257: 3000, 34853: 754, 34665: 248, 271: 'samsung', 272: 'Galaxy S24 Ultra', 305: 'S928U1UES4AXKF', 274: 1, 306: '2025:01:06 07:35:55', 531: 1, 283: 72.0} 

From what I can tell, the getexif method returns only a subset of exif information, not all the possible tags that may exist. That makes some sense as different hardware/services may write ad hoc exif data.

I did some Googling and this StackOverflow answer provided an interesting hack. This code will search for the GPSInfo tag and return the numeric value:

GPSINFO_TAG = next( 	tag for tag, name in PIL.ExifTags.TAGS.items() if name == "GPSInfo" )  # should be 34853 

You can then combine this to get the right value:

gpsinfo = img_exif.get_ifd(GPSINFO_TAG) 

Whew. This returns a dictionary that looks like so:

{1: 'N', 2: (47.0, 30.0, 2.952359), 3: 'E', 4: (19.0, 2.0, 42.89964), 5: 0, 6: 143.0} 

This is the direction and GPS info in degrees and minutes returned in a Python dictionary. To convert the values, I did this:

if len(gpsinfo): 	latitude = f"{gpsinfo[2][0]}°{gpsinfo[2][1]}'{gpsinfo[2][2]}" {gpsinfo[1]}"         	longitude = f"{gpsinfo[4][0]}°{gpsinfo[4][1]}'{gpsinfo[4][2]}" {gpsinfo[3]}" 

I then needed to convert the longitude and latitude to decimal. For that… I turned to AI. Yep, I cheated. But guess what, it worked? Gemini spit out this function:

def dms_to_dd(dms_str): 	"""Converts a DMS string to decimal degrees.  	Args: 		dms_str: A string representing the angle in DMS format  				 (e.g., "36°57'9" N", "110°4'21" W").  	Returns: 		The angle in decimal degrees. 	"""  	parts = re.split(r'[^dw.]+', dms_str.strip()) 	degrees = float(parts[0]) 	minutes = float(parts[1]) if len(parts) > 1 else 0 	seconds = float(parts[2]) if len(parts) > 2 else 0 	direction = parts[3].upper() if len(parts) > 3 else 'E'  # Default to East  	dd = degrees + minutes / 60 + seconds / 3600 	if direction in ('S', 'W'): 		dd *= -1 	return dd 

As I had recently did something just like this, I was able to eyeball it and confirm it made sense. I will say the comment is slightly off as the input isn’t the full address, but only one portion.

Reverse Geocoding

Now that I had a location, I needed to reverse geocode, which is basically, "Given a location, what the heck is there?" Mapbox has an excellent free API for this, so I built a quick wrapper:

def reverseGeoCode(lon, lat): 	res = requests.get(f"https://api.mapbox.com/search/geocode/v6/reverse?longitude={lon}&latitude={lat}&access_token={MAPBOX_KEY}") 	return res.json() 

And called it like so:

loc = reverseGeoCode(dms_to_dd(longitude), dms_to_dd(latitude)) 

This returns a great deal of information in GeoJSON format which I’m familiar with because of my time at HERE. In my mind, I was imagining printing results for public consumption, so while a heck of a lot of data was returned, I simply wanted a formatted address:

print(loc["features"][1]["properties"]["place_formatted"]) 

You can check the Mapbox docs if you want to see more of the results, but that worked just fine for me.

The Results

When I ran it, I got exactly what I expected:

20250108_101553.jpg Bratislava, Bratislava, Slovakia -------------------------------------------------------------------------------- laf.jpg Lafayette, Louisiana, United States -------------------------------------------------------------------------------- 20250107_160104.jpg Győr, Győr-Moson-Sopron, Hungary -------------------------------------------------------------------------------- 20250111_170109.jpg Grein, Upper Austria, Austria -------------------------------------------------------------------------------- 20250112_154722.jpg Salzburg, Salzburg, Austria -------------------------------------------------------------------------------- 20250106_073555.jpg Budapest, Hungary -------------------------------------------------------------------------------- 

And if you’re curious, here is each of those images, in the same order as above. (I edited the laf.jpg one to not have my precise address. Sorry. 😉 The complete source code for this script will be at the end.

Bratislava, Bratislava, Slovakia

Lafayette, Louisiana, United States

Győr, Győr-Moson-Sopron, Hungary

Grein, Upper Austria, Austria

Salzburg, Salzburg, Austria

Budapest, Hungary

The Script

import os  import PIL.Image  import PIL.ExifTags import re  import requests  MAPBOX_KEY = os.environ.get('MAPBOX_KEY')  def reverseGeoCode(lon, lat): 	res = requests.get(f"https://api.mapbox.com/search/geocode/v6/reverse?longitude={lon}&latitude={lat}&access_token={MAPBOX_KEY}") 	return res.json()  def dms_to_dd(dms_str): 	"""Converts a DMS string to decimal degrees.  	Args: 		dms_str: A string representing the angle in DMS format  				 (e.g., "36°57'9" N", "110°4'21" W").  	Returns: 		The angle in decimal degrees. 	"""  	parts = re.split(r'[^dw.]+', dms_str.strip()) 	degrees = float(parts[0]) 	minutes = float(parts[1]) if len(parts) > 1 else 0 	seconds = float(parts[2]) if len(parts) > 2 else 0 	direction = parts[3].upper() if len(parts) > 3 else 'E'  # Default to East  	dd = degrees + minutes / 60 + seconds / 3600 	if direction in ('S', 'W'): 		dd *= -1 	return dd  # Credit: https://stackoverflow.com/a/73745613/52160 GPSINFO_TAG = next( 	tag for tag, name in PIL.ExifTags.TAGS.items() if name == "GPSInfo" )  # should be 34853  INPUT = "./sources"  files = os.listdir(INPUT)  for file in files: 	print(file) 	img = PIL.Image.open(os.path.join(INPUT, file))  	img_exif = img.getexif() 	 	gpsinfo = img_exif.get_ifd(GPSINFO_TAG)  	if len(gpsinfo): 		latitude = f"{gpsinfo[2][0]}°{gpsinfo[2][1]}'{gpsinfo[2][2]}" {gpsinfo[1]}"         		longitude = f"{gpsinfo[4][0]}°{gpsinfo[4][1]}'{gpsinfo[4][2]}" {gpsinfo[3]}"  		loc = reverseGeoCode(dms_to_dd(longitude), dms_to_dd(latitude)) 		print(loc["features"][1]["properties"]["place_formatted"])  	print('-' * 80) 

Raymond Camden

Posted in: JavaScript

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.