Search code examples
pythongeolocationmetadatajpegexif

Adding GPS location to EXIF using Python (slots not recognised by Windows 10 + nasty workaround required)


I searched for some Python package that is able to read and edit EXIF data. Finally, I got the package exif to work on Windows and Ubuntu (since I use the same scripts in both OS).

I wrote the following function to add longitude and latitude to .jpg images:

import exif
import numpy as np

def dec_to_dms(dec):
    '''
    Convert decimal degrees to degrees-minutes-seconds
    
    Parameters
    ----------
    dec : float
        Input coordinate in decimal degrees.
    
    Returns
    -------
    list
        Coordinate in degrees-minutes-seconds.
    '''
    degree = np.floor(dec)
    minutes = dec % 1.0 * 60
    seconds = minutes % 1.0 * 60
    minutes = np.floor(minutes)
    
    return [degree, minutes, seconds]

def add_coords(path, coordinates, replace = False):
    '''
    Add coordinates to the Exif data of a .jpg file.

    Parameters
    ----------
    path : str
        Full path to the image file.
    coordinates : list or tuple of float
        Latitude and longitude that shall be added to the image file.
    replace : bool
        Replace existing coordinates if the image alread contains values for
        the repective Exif tags.

    Returns
    -------
    None.
    '''
    with open(path, "rb") as f:
        
        img = exif.Image(f)
    
    lat = None
    
    if img.has_exif:
        if "gps_latitude" in img.list_all():
            lat = img.gps_latitude
            lon = img.gps_longitude
            
            ### While theoretically valid coordinates, (0.0, 0.0, 0.0) will be
            ### replaced since apparently, for some images (0.0, 0.0, 0.0) is
            ### set when no coordinates were specified.
            if lat == (0.0, 0.0, 0.0) or lon == (0.0, 0.0, 0.0):
                lat = lon = None
    
    if lat is None or replace:
        lat = tuple(dec_to_dms(coordinates[0]))
        lon = tuple(dec_to_dms(coordinates[1]))
        
        try:
            img.gps_latitude = lat
            img.gps_longitude = lon
        
        except:
            ###---------------------------------------------------------------|
            ### This is a quick and dirty workaround for current shortcomings
            ### of the exif package
            from PIL import Image
            EXPLIMG = "D:/switchdrive/PlantApp/img/Species/EXAMPLE_ANDROID.jpg"
            example_image = Image.open(EXPLIMG)
            example_exif = example_image.getexif()
            example_image.close()
            
            print(path)
            
            with Image.open(path) as current_image:
                current_image.save(path, exif = example_exif)
            
            with open(path, "rb") as f:
                img = exif.Image(f)
            
            img.gps_latitude = lat
            img.gps_longitude = lon
            ###---------------------------------------------------------------|
        
        with open(path, "wb") as f:
            
            f.write(img.get_file())
    
    return

Unfortunately, the exif package cannot add coordinates to image metadata which do not already contain the respective slots. This is why I used some template metadata from another image and overwrite the image in case my image does not contain lon and lat.

Now, when I apply the function, it appears the coordinates are not recognised by every program/OS(?)

E.g., when I drag and drop the image on websites such as pic2map.com, it appears the images are placed correctly. However, when in view the image details on Windows 10 Enterprise, I get

GPS-------------------------
Altitude 0

with no additional longitude and latitude or similar information.

The template image, for example, has lon and lat that are displayed in the Windows image properties as

GPS-------------------------
Latitude 46; 55; 41.577500000000013856
Longitude 6; 44; 40.51680000000001325

and I would expect the result to look similar for the images where I "manually" added coordinates.

Also, the workaround using some template image is a terrible solution, imo. Is there some way to add coordinates without using a template image and preferably in a way that makes even Windows 10 recognise the GPS coordinates are present in the metadata?


Solution

  • There is indeed a way to write EXIF data without resorting to a template image. One way to achieve this is to use the piexif package. This package allows you to create a dictionary of EXIF tags and their values, which you can then insert into your image file.

    Here's an example of how you can add GPS information to an image using piexif:

    import piexif
    from PIL import Image
    import numpy as np
    
    def dec_to_dms(dec):
        '''
        Convert decimal degrees to degrees-minutes-seconds
        
        Parameters
        ----------
        dec : float
            Input coordinate in decimal degrees.
        
        Returns
        -------
        list
            Coordinate in degrees-minutes-seconds.
        '''
        degree = np.floor(dec)
        minutes = dec % 1.0 * 60
        seconds = minutes % 1.0 * 60
        minutes = np.floor(minutes)
        
        return (degree, minutes, seconds)
    
    def add_coords(path, coordinates):
        '''
        Add coordinates to the Exif data of a .jpg file.
    
        Parameters
        ----------
        path : str
            Full path to the image file.
        coordinates : list or tuple of float
            Latitude and longitude that shall be added to the image file.
    
        Returns
        -------
        None.
        '''
        img = Image.open(path)
    
        exif_dict = piexif.load(img.info['exif'])
    
        # Latitude and Longitude
        lat_deg, lat_min, lat_sec = dec_to_dms(coordinates[0])
        lon_deg, lon_min, lon_sec = dec_to_dms(coordinates[1])
    
        # Set GPS Info
        exif_dict['GPS'][piexif.GPSIFD.GPSLatitude] = [(abs(int(lat_deg)),1), (int(lat_min),1), (int(lat_sec),1)]
        exif_dict['GPS'][piexif.GPSIFD.GPSLatitudeRef] = 'N' if coordinates[0] >= 0 else 'S'
        exif_dict['GPS'][piexif.GPSIFD.GPSLongitude] = [(abs(int(lon_deg)),1), (int(lon_min),1), (int(lon_sec),1)]
        exif_dict['GPS'][piexif.GPSIFD.GPSLongitudeRef] = 'E' if coordinates[1] >= 0 else 'W'
        
        exif_bytes = piexif.dump(exif_dict)
        
        img.save(path, exif=exif_bytes)
    

    Here, dec_to_dms function takes a decimal degree and converts it to a tuple of degrees, minutes, and seconds, which is formatted for piexif to be able to use it.

    Please note, the piexif.GPSIFD.GPSLatitudeRef and piexif.GPSIFD.GPSLongitudeRef fields are required by the EXIF standard to denote whether the latitude and longitude are North or South and East or West, respectively.

    I suggest you to test this code snippet and observe if Windows 10 and other platforms recognize the GPS coordinates inserted using this method.

    This code should work on both Windows and Ubuntu.

    Also, please note that if the image doesn't have any EXIF information, piexif.load(img.info['exif']) will raise a KeyError. You would have to create a new EXIF dictionary in this case. Here is an example of how you can do that:

    try:
        exif_dict = piexif.load(img.info['exif'])
    except KeyError:
        exif_dict = {'0th':{},'Exif':{},'GPS':{},'1st':{},'thumbnail':None}
    

    You can add this right after opening the image with Image.open(path).