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?
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)
.