Have you ever tried to find that one vacation photo you took years ago, only to scroll endlessly through thousands of images with no luck? What many people don’t realize is that most photos already come with a hidden trail of breadcrumbs that can solve this problem: EXIF metadata.

Every time you snap a photo with a smartphone or digital camera, extra information gets embedded into the file, details like the date, camera settings, and, in many cases, the exact GPS coordinates of where the picture was taken. This hidden metadata is called EXIF (Exchangeable Image File Format), and it’s more powerful than it looks. While smartphones often automatically organize your photos, many of us still have massive collections stored on a NAS or external drives, where sorting and searching manually can feel impossible.

By extracting EXIF data, you can do much more than just learn which lens or exposure setting was used. You can index, organize, and search your entire photo library in ways that go far beyond filenames and folders. Want to pull up every photo taken in Paris? Or quickly filter for shots within 10 kilometers (about 6 miles) of Central Park? With EXIF indexing, that becomes not only possible but straightforward.

In this article, we’ll explore how to extract EXIF metadata, build an index of your photos, including those on NAS drives, and run location-based searches to find exactly what you’re looking for.


What is EXIF Metadata?

When you take a photo, your camera doesn’t just capture light, it also records a set of descriptive details about the image, known as EXIF metadata. EXIF stands for Exchangeable Image File Format, and it’s a standardized way of embedding extra information directly into the image file itself.

Think of EXIF as the "digital notebook" your camera keeps for each shot. Some of the most common fields include:

  • Timestamp – the exact date and time the photo was taken.
  • Camera information – make, model, lens, focal length, aperture, shutter speed, ISO.
  • GPS coordinates – latitude, longitude, and sometimes altitude, if location services were enabled.

Among these, the GPS data is especially powerful for organizing and searching photos. Cameras and smartphones typically store coordinates in a format based on degrees, minutes, and seconds. For example:

Latitude: 40° 46′ 56.62″ N  
Longitude: 73° 58′ 0.85″ W  
Altitude: 15.0 m  

This information can be converted into decimal degrees (e.g., 40.7824, -73.9669), which is a more convenient format for indexing and performing calculations like distance searches.

EXIF isn’t just technical clutter inside your photos. It’s a hidden layer of context that tells you when and where a picture was taken, and with what gear, making it a goldmine for indexing and retrieval.


💡
Sometimes, the best code is the shortest code, without sacrificing readability.

That’s why I created the Python One-Liner Cheat Sheet - a free, downloadable PDF packed with popular Python tricks, idiomatic expressions, and productivity boosters.

Extracting EXIF Data from Photos

Now that we know what EXIF metadata is, the next step is learning how to actually extract it from photos. Python offers several libraries that make this easy:

  • exif – simple and modern library to read and write EXIF data, including GPS coordinates and altitude.
  • exifread – lightweight library for reading EXIF metadata from JPEG and TIFF files.
  • Pillow – popular imaging library that can read and manipulate images, including EXIF tags.
  • piexif – designed for both reading and writing EXIF data, useful if you need to modify metadata.

For modern projects, exif is often the most straightforward and Pythonic choice.

Reading GPS Coordinates and Altitude with exif

Here’s a minimal Python script that reads GPS coordinates and altitude from an image using exif:

from exif import Image

def convert_to_decimal_degrees(gps_coord, ref):
    """Convert GPS coordinates from (degrees, minutes, seconds) to decimal degrees"""
    if gps_coord is None:
        return None
    
    degrees, minutes, seconds = gps_coord
    decimal_degrees = degrees + (minutes / 60.0) + (seconds / 3600.0)
    
    # Apply negative sign based on reference
    if ref in ['S', 'W']:
        decimal_degrees = -decimal_degrees
    
    return decimal_degrees

def get_gps_from_image(image_path):
    with open(image_path, 'rb') as img_file:
        img = Image(img_file)

    if not img.has_exif or not img.gps_latitude or not img.gps_longitude:
        return None  # No GPS data available

    # Convert to decimal degrees
    lat = convert_to_decimal_degrees(img.gps_latitude, img.gps_latitude_ref)
    lon = convert_to_decimal_degrees(img.gps_longitude, img.gps_longitude_ref)

    # Get altitude if available
    alt = img.gps_altitude if hasattr(img, 'gps_altitude') else None
    if alt is not None and hasattr(img, 'gps_altitude_ref') and img.gps_altitude_ref == 1:
        alt = -alt  # Altitude reference 1 = below sea level

    return (lat, lon, alt)

# Example usage
print(get_gps_from_image("england-london-bridge.jpg"))

Let's take the example of this photo:

Tower Bridge

This function returns a tuple like:

(51.504105555555554, -0.074575, 77.88)  # Latitude, Longitude, Altitude in meters

If the image didn't have any geo-location metadata, it would return None.

Handling Missing or Corrupted EXIF Data

Not every photo will have usable EXIF metadata. For example:

  • Some cameras or photo-editing software strip metadata to save space.
  • Privacy-focused apps (like messaging platforms) often remove GPS coordinates.
  • Altitude may not always be recorded, even if latitude and longitude exist.
  • In rare cases, EXIF data may be partially corrupted.

When building your index, always check for missing values and decide how to handle them, for instance, skipping photos without GPS tags, or indexing only the fields that are available.

For a practical use case of EXIF, check out my other article, "How to Build an Email-Based Environmental Monitoring System with FastAPI and Postmark".


Building an Index of Photos

Extracting EXIF data from a single photo is useful, but the real power comes when you apply it to your entire photo library. By creating an index, you can quickly search and filter images without repeatedly scanning every file.

The typical workflow is:

  • Loop through all files in a given directory (and subdirectories).
  • Extract EXIF metadata from each photo using exif.
  • Store the results in a structured format for later searching.

Storing Metadata in CSV

Here’s a Python example that scans a directory and writes the extracted EXIF metadata into a CSV file:

import os
import csv
from pathlib import Path
from exif import Image

def convert_to_decimal_degrees(gps_coord, ref):
    """Convert GPS coordinates from (degrees, minutes, seconds) to decimal degrees"""
    if gps_coord is None:
        return None
    
    degrees, minutes, seconds = gps_coord
    decimal_degrees = degrees + (minutes / 60.0) + (seconds / 3600.0)
    
    # Apply negative sign based on reference
    if ref in ['S', 'W']:
        decimal_degrees = -decimal_degrees
    
    return decimal_degrees

def extract_metadata(image_path):
    """Extract filename, timestamp, GPS coordinates, altitude, and camera info."""
    with open(image_path, 'rb') as img_file:
        img = Image(img_file)

    # Filename
    filename = Path(image_path).name

    # Timestamp
    timestamp = getattr(img, "datetime_original", "Unknown")

    # Camera model
    camera = getattr(img, "model", "Unknown")

    # GPS data - convert to decimal degrees
    lat = convert_to_decimal_degrees(
        getattr(img, "gps_latitude", None),
        getattr(img, "gps_latitude_ref", "N")
    )
    lon = convert_to_decimal_degrees(
        getattr(img, "gps_longitude", None),
        getattr(img, "gps_longitude_ref", "E")
    )
    
    # Altitude
    alt = getattr(img, "gps_altitude", None)
    if alt is not None and getattr(img, "gps_altitude_ref", 0) == 1:
        alt = -alt  # below sea level

    return {
        "filename": filename,
        "timestamp": str(timestamp),
        "latitude": lat,
        "longitude": lon,
        "altitude": alt,
        "camera": str(camera),
    }

def build_index(photo_dir, output_csv="photo_index.csv"):
    """Build an index of photos with their metadata"""
    with open(output_csv, "w", newline="", encoding="utf-8") as f:
        fieldnames = ["filename", "timestamp", "latitude", "longitude", "altitude", "camera"]
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()

        for root, _, files in os.walk(photo_dir):
            for file in files:
                if file.lower().endswith((".jpg", ".jpeg", ".tiff")):
                    try:
                        metadata = extract_metadata(os.path.join(root, file))
                        writer.writerow(metadata)
                        print(f"Processed: {file}")
                    except Exception as e:
                        print(f"Failed to process {file}: {e}")

# Example usage - process current directory
print("Building photo index from current directory...")
build_index("photos")
print("Photo index created successfully!")

This script generates a photo_index.csv file with rows like:

filename,timestamp,latitude,longitude,altitude,camera
england-london-bridge.jpg,2018:08:22 13:13:41,51.504105555555554,-0.074575,77.88,Pixel 2
germany-garching-heide.jpg,2018:08:29 19:31:19,48.268274999999996,11.603361111111111,540.05,Pixel 2
irland-dingle.jpg,2012:09:16 16:58:02,52.139276657230475,-10.274594797178132,,DMC-FX60
italy-garda-lake-sailing-club.jpg,2018:09:16 11:08:41,45.877630555555555,10.857161111111111,71.95,Pixel 2
japan-katsura-river.jpg,2016:11:12 16:13:18,35.014377,135.669015,0.0,MI 5
taiwan-jiufen.jpg,2016:04:04 19:35:38,25.10820386111111,121.8439483611111,279.0,GT-I9505
turkey-bodrum.jpg,2018:10:18 18:16:32,37.02995277777778,27.41326388888889,79.19,Pixel 2

CSV vs Database Indexing

There are multiple ways to store the index, each with pros and cons:

  • CSV or JSON
    • ✅ Easy to read, portable, no setup required.
    • ❌ Searching can be slow for large collections (tens of thousands of photos).
  • SQLite (or Postgres for larger setups)
    • ✅ Efficient queries, support for filtering, sorting, and even spatial queries.
    • ✅ Scales better for very large photo libraries.
    • ❌ Requires a bit more setup and knowledge of SQL.

For small to medium personal collections, a CSV or JSON file is perfectly fine. For larger archives or a search engine interface, consider a database backend.

Full source code is available at the end of the article.


Searching Photos by Location

Once you have a structured index of your photos with GPS data, the next step is searching by location. There are different approaches depending on how precise or flexible you want the search to be.

The most basic method is to match photos that have the exact latitude and longitude. This is straightforward but rarely practical, since GPS coordinates can have minor variations: