Turn Your Photo Library Into a Location-Based Search Engine Using EXIF Metadata
Developer Service

Developer Service @devasservice

About: Indie Hacker, Full Stack Developer, Content Creator 👨‍💻 Creating software products and teaching upcoming entrepreneurs on my blog 👨‍🏫

Location:
Netherlands
Joined:
Oct 30, 2021

Turn Your Photo Library Into a Location-Based Search Engine Using EXIF Metadata

Publish Date: Aug 21
0 0

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  
Enter fullscreen mode Exit fullscreen mode

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.


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"))

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.


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!")
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.


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.

Simple Approach: Exact Coordinate Search

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:

import csv

# Define the target location (London, UK coordinates)
target_lat, target_lon = 51.5074, -0.1278

# Open and read the photo index CSV file that contains photo metadata
with open("photo_index.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)

    # Process each row (photo) in the CSV file
    for row in reader:
        # Check if both latitude and longitude data exist for this photo
        if row["latitude"] and row["longitude"]:
            # Perform exact coordinate match - check if photo was taken at exactly the target location
            if float(row["latitude"]) == target_lat and float(row["longitude"]) == target_lon:
                print(row["filename"])
Enter fullscreen mode Exit fullscreen mode

Limitation: This approach only works if the coordinates exactly match, which is rare in real-world GPS data.

Advanced Approach: Radius-Based Search

A more practical solution is to search for photos within a certain radius of a location. The haversine formula is commonly used to calculate the great-circle distance between two points on the Earth:

import math
import csv

def haversine(lat1, lon1, lat2, lon2):
    """
    Calculate the great-circle distance between two points on Earth's surface
    using the Haversine formula.

    Args:
        lat1, lon1: Latitude and longitude of first point (in decimal degrees)
        lat2, lon2: Latitude and longitude of second point (in decimal degrees)

    Returns:
        Distance in kilometers between the two points

    Formula: d = R * c
    where R is Earth's radius and c is the angular distance in radians
    """
    R = 6371  # Earth's radius in kilometers

    # Convert latitude and longitude from degrees to radians
    phi1, phi2 = math.radians(lat1), math.radians(lat2)

    # Calculate differences in latitude and longitude (in radians)
    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)

    # Haversine formula components:
    # a = sin²(Δφ/2) + cos(φ1) * cos(φ2) * sin²(Δλ/2)
    a = math.sin(delta_phi/2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda/2)**2

    # c = 2 * atan2(√a, √(1-a)) - angular distance in radians
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

    # Final distance = Earth radius * angular distance
    return R * c

# Define the target location (London, UK coordinates)
target_lat, target_lon = 51.5074, -0.1278  # London coordinates in decimal degrees

# Set the search radius in kilometers
radius_km = 10

# Open and read the photo index CSV file
with open("photo_index.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)  # Read CSV as dictionary with column headers as keys

    # Process each row (photo) in the CSV file
    for row in reader:
        # Check if both latitude and longitude data exist for this photo
        if row["latitude"] and row["longitude"]:
            # Calculate distance from target location to photo location
            dist = haversine(target_lat, target_lon, float(row["latitude"]), float(row["longitude"]))

            # If photo is within the specified radius, display it
            if dist <= radius_km:
                print(row["filename"], f"{dist:.2f} km away")
Enter fullscreen mode Exit fullscreen mode

This will return all photos within 10 km of the target coordinates, for example:

england-london-bridge.jpg 3.70 km away
Enter fullscreen mode Exit fullscreen mode

Tools and Libraries for Spatial Queries

For more advanced use cases or large datasets, several Python libraries and database features can simplify the process:

  • geopy – Geocoding and distance calculations.
  • shapely – Geometry operations and spatial queries in Python.
  • SQL spatial queries – Use databases like PostgreSQL with PostGIS for efficient radius searches, polygon queries, or bounding boxes.

By combining EXIF metadata indexing with spatial searches, you can quickly find photos taken near landmarks, cities, or even a friend’s house. This opens the door to building personal mapping tools or automated photo albums sorted by location.


Conclusion

Indexing photos using EXIF metadata transforms your photo collection from a static archive into a searchable, organized library. By extracting GPS coordinates, timestamps, and camera information, you can locate photos based on location, date, or device.

By combining this indexing with spatial searches, you gain the ability to find photos within a radius, track journeys over time, or group images by location. This allows for turning raw data into actionable insights.

Leveraging the EXIF metadata, you can turn a simple collection of images into a powerful, location-aware photo library, making lost memories instantly findable and your workflow dramatically more efficient.

Source code available here: https://github.com/nunombispo/Photo-Library-EXIF-Metadata-Article


Follow me on Twitter: https://twitter.com/DevAsService

Follow me on Instagram: https://www.instagram.com/devasservice/

Follow me on TikTok: https://www.tiktok.com/@devasservice

Follow me on YouTube: https://www.youtube.com/@DevAsService

Comments 0 total

    Add comment