The mass adoption of smartphones — essentially portable, sensor-rich, location-aware, always-networked computers — has given us two major consequences whose effects on privacy and security aren’t yet fully understood:
More people than ever have a camera that’s usually within arm’s reach.
By default, the photos taken with these cameras can give away sensitive information, and many users are unaware that it’s happening.
In addition to picture data, photos taken with smartphones and modern digital cameras contain metadata, which is additional information about the photo. This metadata is stored in a format called EXIF, which is short for EXchangeable Image File format, which is a continually evolving standard for information added to digital image and sound recordings.
In photos, EXIF can include information such as:
The dimensions and pixel density of the photo
The make and model of the device used to take the photo
Zoom, aperture, flash, and other camera settings when the photo was taken
The orientation of the device when the photo was taken
When the photo was taken
Where the photo was taken
Which direction the camera was facing
The altitude at which the photo was taken
This metadata is useful for sorting, cataloging, and searching through photos, which is why the EXIF standard was defined. However, it also introduces privacy and security concerns that many users don’t take into account.
One of the earliest cautionary tales about EXIF is the 2012 privacy incident involving antivirus company founder John McAfee. This was an exciting time in McAfee’s life, as he was evading law enforcement while still granting exclusive, much sought-after interviews to journalists. One journalist who was lucky enough to get an interview decided to show off his good fortune by posting a photo of McAfee without first removing its EXIF data. That data gave away his location and led to his arrest.
You’re probably aware of more recent stories where law enforcement has been arresting suspects using photos posted to a social media site that didn’t take the precaution of “scrubbing” their EXIF data during the upload process.
Being the security-conscious developers that I suspect you are, you’re probably asking yourself questions like these:
How can I programmatically detect and read the EXIF metadata from a photo?
How can I alter, add, or erase EXIF metadata programmatically?
If you have Python 3.6 or later installed on your computer, you can find out through the hands-on exercises below. They’ll cover a couple of Python packages that you can incorporate into your applications to extract, add, alter, or erase EXIF data from photos. Along the way, you’ll use not just your programming skills, but you’ll do some detective work as well!
The exif Module
There are a number of Python modules that can access the EXIF data in digital photos. For this article, we’ll focus on exif. Its API is so Pythonic that it feels almost as if it’s part of the language rather than a module.
To install exif, use pip by entering the following on the command line:
pip install exif
If entering this command gave you an error message, try using the command pip3 install exif instead. pip3 is the version of pip, the Python package manager, that is specifically for Python 3.
If you’re not maintaining any Python 2 code but have to use pip3 to install Python packages, you should consider upgrading your Python installation.
Loading photos and checking them for EXIF data
Let’s put exif to work. Consider these two photos, named palm tree 1.jpg and palm tree 2.jpg:
Photo 1 of palm tree beside a road and a small lake. This photo contains EXIF metadata - see if you can find where, when, and on what camera it was taken!
Photo 2 of palm tree beside a road and a small lake. This photo contains EXIF metadata - see if you can find where, when, and on what camera it was taken!
Suppose you’ve been asked these questions:
Were these photos taken on the same device or two different devices?
Which photo was taken first?
Where were these photos taken?
To answer these questions, we’ll load the data from these photos’ files into exif Image objects and then use those objects to examine the EXIF metadata:
https://pactforanimals.org/advert/semifinal-hockey-free-bemidji-state-vs-wisconsin-live-stream-ice-hockey-bridgeport-regional-semifinal-free/
https://pactforanimals.org/advert/hockey-free-massachusetts-vs-lake-superior-state-live-stream-ice-hockey-bridgeport-regional-semifinal-free/
https://pactforanimals.org/advert/semifinal-hockey-free-american-international-vs-north-dakota-live-stream-ice-hockey-fargo-regional-semifinal-free/
https://blog.goo.ne.jp/huas/e/5b9e3d661493b5b02d8aedcbc4b34520
https://www.deviantart.com/htfrttr/journal/How-to-Read-and-Remove-Metadata-from-Your-Photos-874408570
https://paiza.io/projects/bwUaMcCb_-r6LDU9LpqY6w
https://caribbeanfever.com/photo/albums/sadsasad-1
from exif import Image
with open("./images/palm tree 1.jpg", "rb") as palm_1_file:
palm_1_image = Image(palm_1_file)
with open("./images/palm tree 2.jpg", "rb") as palm_2_file:
palm_2_image = Image(palm_2_file)
images = [palm_1_image, palm_2_image]
The code opens each file as read-only. It reads the file’s data in binary format into a file object, which it then uses to instantiate an exif Image object. Finally, it puts the Image objects into an array so that we can iterate over them, performing the same operations on each photo’s EXIF metadata.
Let’s perform our first operation on each photo: confirming that they actually contain EXIF data. We’ll do this by checking each Image object’s has_exif property. For every image that contains EXIF data, we’ll use Image’s exif_version property to display the version of EXIF it’s using:
for index, image in enumerate(images):
if image.has_exif:
status = f"contains EXIF (version {image.exif_version}) information."
else:
status = "does not contain any EXIF information."
print(f"Image {index} {status}")
When run, this code produces the following results:
Image 0 contains EXIF (version 0220) information.
Image 1 contains EXIF (version 0232) information.
We now know that both photos contain EXIF information and use different versions of EXIF. It’s very likely that these photos were taken on different cameras.
What EXIF metadata is available in each photo?
Think of EXIF as a key-value data structure similar to a Python dictionary. EXIF key-value pairs are called tags, and each tag can contain either a string or numeric value. There are dozens of tags in the current EXIF standard (version 2.32), and anyone — from smartphone and camera manufacturers to photographers — is free to add their own.
Here’s a comprehensive list of EXIF tags that you’re likely to find in digital photos. It includes tags that aren’t in the EXIF standard, but are provided by a number of devices or software.
EXIF is one of those informal formats where manufacturers pick and choose which features they’ll implement. This means that photos produced by different cameras and smartphones will have different sets of EXIF tags.
You can generally count on smartphones embedding a number of often-used EXIF tags in the photos they take, including the make and model of the smartphone, the date and time when the photo was taken, the location where the phone was taken, and common camera settings.
Given the differences between devices and the availability of tools for editing EXIF data, one of the first things you should do when working with a photo’s EXIF data is to see which tags are available.
The Image objects provided by the exif module expose EXIF tags as properties of that object. This means that you can use Python’s built-in dir() function on an Image object to see which EXIF tags it has.
The following code displays the list of members of each Image object in our list of palm tree images:
image_members = []
for image in images:
image_members.append(dir(image))
for index, image_member_list in enumerate(image_members):
print(f"Image {index} contains {len(image_member_list)} members:")
print(f"{image_member_list}
")
You’ll see the following output:
Image 0 contains 53 members:
['_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'color_space', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'digital_zoom_ratio', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_datestamp', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'has_exif', 'light_source', 'make', 'max_aperture_value', 'metering_mode', 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'sensing_method', 'shutter_speed_value', 'subsec_time', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']
Image 1 contains 68 members:
['<unknown EXIF tag 316>', '<unknown EXIF tag 322>', '<unknown EXIF tag 323>', '<unknown EXIF tag 42080>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_dest_bearing', 'gps_dest_bearing_ref', 'gps_horizontal_positioning_error', 'gps_img_direction', 'gps_img_direction_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'gps_speed', 'gps_speed_ref', 'has_exif', 'lens_make', 'lens_model', 'lens_specification', 'make', 'maker_note', 'metering_mode', 'model', 'offset_time', 'offset_time_digitized', 'offset_time_original', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'scene_type', 'sensing_method', 'shutter_speed_value', 'software', 'subject_area', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']
As you can see, while both Image objects have a lot of common members, image 1 contains a few more than image 0. This means that image 1 has a few more EXIF tags than image 0. This is a strong indicator that image 0 and image 1 were taken on different devices.
You can use Python sets to determine the members that image 0 and image 1 have in common:
common_members = set(image_members[0]).intersection(set(image_members[1]))
common_members_sorted = sorted(list(common_members))
print("Image 0 and Image 1 have these members in common:")
print(f"{common_members_sorted}")
Running this code produces this output:
Image 0 and Image 1 have these members in common:
['_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'has_exif', 'make', 'metering_mode', 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'sensing_method', 'shutter_speed_value', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']
If you looked at the names of EXIF tags in the documentation (either the list of standard EXIF tags or the extended list), you may have noticed that EXIF tag names are in PascalCase while the EXIF properties in exif Image objects are in snake_case. This is because the authors of the exif module are striving to follow the Python Style Guide and designed Image to convert EXIF tag names into Pythonic property names.
Non-EXIF members of exif’s Image class
Most, but not all, of the members of any Image instances will be properties corresponding to EXIF tags in the image. However, there are a handful of members for working with EXIF metadata. They’re listed in the table below:
Class member Description
delete(attribute) Removes the EXIF tag specified by the string attribute from the image.
delete_all() Removes all EXIF tags from the image.
get(attribute, default=None) Returns the value for the EXIF tag specified by the string attribute. If the tag is not available or does contain a value, it returns the value specified by the default keyword argument.
get_file() Returns the current data for the image, formatted for writing to a binary file.
get_thumbnail() Returns the binary data for a thumbnail of the image.
has_exif A Boolean value that returns True if the image currently contains EXIF metadata.
set(attribute, value) Set the value for the EXIF tag specified by the string attribute. If the tag is not available or does contain a value, it returns the value specified by the default keyword argument.
We’ll cover most of these properties and methods in this article.
Getting Basic Photo Information
Let’s answer the first question about these photos. Were they taken on the same device or two different devices? We already have reason to believe that the answer is “no”.
Make and model of the device used to take the photo
Both images have a make and model tag, so let’s use those to determine what kind of devices were used to take these pictures:
for index, image in enumerate(images):
print(f"Device information - Image {index}")
print("----------------------------")
print(f"Make: {image.make}")
print(f"Model: {image.model}
")
Here’s the result of the code above:
Device information - Image 0
----------------------------
Make: motorola
Model: motorola one hyper
Device information - Image 1
----------------------------
Make: Apple
Model: iPhone 12 Pro
This confirms that the photos were taken with different devices, and we now know the specifics.
Additional and optional information about the devices
Let’s gather some more information about the devices used to take the photos, namely the details about their lenses and operating system versions.
Not all devices report the type of lens in their EXIF metadata, so we’ll use Image’s get() method, which is similar to the get() method used by Python’s dictionaries. Like Python dictionaries’ get() method, the get() method provided by exif’s Image object gracefully handles the case where the given key does not exist.
The code below uses get() to attempt to access the lens and operating system versions used in taking the photos. If a particular property doesn’t exist, its value will be displayed as “Unknown”:
for index, image in enumerate(images):
print(f"Lens and OS - Image {index}")
print("---------------------")
print(f"Lens make: {image.get('lens_make', 'Unknown')}")
print(f"Lens model: {image.get('lens_model', 'Unknown')}")
print(f"Lens specification: {image.get('lens_specification', 'Unknown')}")
print(f"OS version: {image.get('software', 'Unknown')}
")
Here’s its output:
Lens and OS - Image 0
---------------------
Lens make: Unknown
Lens model: Unknown
Lens specification: Unknown
OS version: Unknown
Lens and OS - Image 1
---------------------
Lens make: Apple
Lens model: iPhone 12 Pro back triple camera 4.2mm f/1.6
Lens specification: (1.5399999618512084, 6.0, 1.6, 2.4)
OS version: 14.3
Note that the phone used to take image 0 (the Motorola One Hyper), doesn’t provide the lens_make, lens_model, lens_specification, or software properties. If we had tried to access them directly (for example, image.lens_make), the result would have been an error. The get() method allowed us to provide an alternate value for those non-existent properties.
Date and time when the photo was taken
The next question is: which photo was taken first?
We can find out by looking at each photo’s datetime_original property, which specifies the date and time when the photo was taken. The processor in a smartphone is fast enough to record the exact fraction of a second when it takes a photo, and that fraction is stored in the subsec_time_original property.
Some phones also record an offset_time property, which we can use to determine datetime’s offset from UTC.
The following code displays the date and time when each of the photos was taken. If the photo includes time offset data, the code will also display that value:
for index, image in enumerate(images):
print(f"Date/time taken - Image {index}")
print("-------------------------")
print(f"{image.datetime_original}.{image.subsec_time_original} {image.get('offset_time', '')}
")
Here are the results:
Date/time taken - Image 0
-------------------------
2021:01:22 15:08:46.327211
Date/time taken - Image 1
-------------------------
2021:01:22 15:08:59.383 -05:00
As you can see from the output, image 0 was taken first, and image 1 was taken 13 seconds later.
Determining Where the Photo Was Taken
Digital cameras have been recording the date and time into their photos from the beginning, but it wasn’t until the smartphone that recording the location where the photo was taken became commonplace. In this section, we’ll look at accessing the GPS coordinates in a photo’s metadata, formatting those coordinates, and making those coordinates more comprehensible by locating them on a map and converting them into the names of the country, region, and city where the photo was taken.
Getting the photo’s GPS coordinates
The EXIF format specifies a number of tags beginning with “gps”, which contain useful geolocation information, including the latitude and longitude where the photo was taken:
for index, image in enumerate(images):
print(f"Coordinates - Image {index}")
print("---------------------")
print(f"Latitude: {image.gps_latitude} {image.gps_latitude_ref}")
print(f"Longitude: {image.gps_longitude} {image.gps_longitude_ref}
")
Here’s the output for the code above:
Coordinates - Image 0
---------------------
Latitude: (28.0, 0.0, 1.56) N
Longitude: (82.0, 26.0, 59.04) W
Coordinates - Image 1
---------------------
Latitude: (28.0, 0.0, 1.54) N
Longitude: (82.0, 26.0, 58.75) W
Note that the gps_latitude and gps_longitude properties return the latitude and longitude as a tuple of three values, which are:
Degrees
Minutes (1/60th of a degree)
Seconds (1/60th of a minute, or 1/3600th of a degree)
Latitude specifies the angular distance from the equator, which can be either north or south. The gps_latitude_ref property indicates this direction, which is either N or S.
Longitude specifies the angular distance from the meridian, which can be either east or west. The gps_longitude_ref property indicates this direction, which is either E or W.
You may have noticed the slight difference between the coordinates reported in the photos. This is expected; even the same device, located at the same spot, will report slightly different coordinates at different times. The discrepancy in coordinates reported by the phones is on the order of a fraction of a second, which translates to about 25 feet, or 7.6 meters.
Formatting latitude and longitude
Let’s define a couple of functions to format the latitude and longitude information returned by Image into standard formats:
Degrees, minutes, and seconds. In this format, the latitude of image 0 would be written as 28.0° 0.0' 1.56" N.
Decimal degrees. In this format, the latitude of image 0 would be written as 28.000433333333334. North latitudes and east longitudes are represented with positive values, while south latitudes and west longitudes are represented with negative values.
Here’s the definitions for those functions, as well as some code that makes use of them:
def format_dms_coordinates(coordinates):
return f"{coordinates[0]}° {coordinates[1]}' {coordinates[2]}""
def dms_coordinates_to_dd_coordinates(coordinates, coordinates_ref):
decimal_degrees = coordinates[0] +
coordinates[1] / 60 +
coordinates[2] / 3600
if coordinates_ref == "S" or coordinates_ref == "W":
decimal_degrees = -decimal_degrees
return decimal_degrees
for index, image in enumerate(images):
print(f"Coordinates - Image {index}")
print("---------------------")
print(f"Latitude (DMS): {format_dms_coordinates(image.gps_latitude)} {image.gps_latitude_ref}")
print(f"Longitude (DMS): {format_dms_coordinates(image.gps_longitude)} {image.gps_longitude_ref}
")
print(f"Latitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)}")
print(f"Longitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)}
")
Here’s the output:
Coordinates - Image 0
---------------------
Latitude (DMS): 28.0° 0.0' 1.56" N
Longitude (DMS): 82.0° 26.0' 59.04" W
Latitude (DD): 28.000433333333334
Longitude (DD): -82.44973333333334
Coordinates - Image 1
---------------------
Latitude (DMS): 28.0° 0.0' 1.54" N
Longitude (DMS): 82.0° 26.0' 58.75" W
Latitude (DD): 28.000427777777777
Longitude (DD): -82.44965277777779
Displaying photo locations on a map
You probably don’t know the latitude and longitude of your home, even when rounded to the nearest degree. There are a couple of simple ways to convert the EXIF location data into something easier for humans to understand.
One way is to use Python’s built-in webbrowser module to open a new browser tab for each photo, using the decimal version of each photo’s EXIF coordinates as the parameters for a Google Maps URL. We’ll create a utility function named draw_map_for_location() that does this:
def draw_map_for_location(latitude, latitude_ref, longitude, longitude_ref):
import webbrowser
decimal_latitude = dms_coordinates_to_dd_coordinates(latitude, latitude_ref)
decimal_longitude = dms_coordinates_to_dd_coordinates(longitude, longitude_ref)
url = f"https://www.google.com/maps?q={decimal_latitude},{decimal_longitude}"
webbrowser.open_new_tab(url)
for index, image in enumerate(images):
draw_map_for_location(image.gps_latitude,
image.gps_latitude_ref,
image.gps_longitude,
image.gps_longitude_ref)
When you run it, two new browser tabs will open, and each one will show a Google Map indicating where the corresponding photo was taken.
Displaying the city, region, and country where a photo was taken
Another way to display the location of a photo is to use reverse geocoding, which is the process of converting geographic coordinates into an address or the name of a place. In order to do this, we’ll need to install two Python modules:
reverse_geocoder, a simple offline reverse geocoder that uses internal tables to convert a set of coordinates into a collection of city and state/province names and country codes. Install this by entering pip install reverse_geocoder on the command line.
pycountry, a country lookup utility that we’ll use to convert country codes into their corresponding names. Install this by entering pip install pycountry on the command line.
Once you’ve installed the modules above, use the following code to convert the photo coordinates into something more recognizable:
import reverse_geocoder as rg
import pycountry
for index, image in enumerate(images):
print(f"Location info - Image {index}")
print("-----------------------")
decimal_latitude = dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)
decimal_longitude = dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)
coordinates = (decimal_latitude, decimal_longitude)
location_info = rg.search(coordinates)[0]
location_info['country'] = pycountry.countries.get(alpha_2=location_info['cc'])
print(f"{location_info}
")
The code uses reverse_geocoder’s search() method to convert each photo’s decimal latitude and longitude into a collection of the following information:
City, town, or village name
Major administrative region, which is typically a state or province
Minor administrative region, which is typically a county or district
Country code
It then uses pycountry’s get() method to convert the country code provided by reverse_geocoder into a tuple containing the corresponding common and official country names.
Here’s its output:
Location info - Image 0
-----------------------
{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', name='United States', numeric='840', official_name='United States of America')}
Location info - Image 1
-----------------------
{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', name='United States', numeric='840', official_name='United States of America')}
Other Useful Sensor Information
As I mentioned at the start of this article, smartphones are packed with sensors. These include a magnetometer, barometer, and accelerometer. In combination with GPS, these sensors provide extra information that is added to each photo’s EXIF metadata.
What direction was the camera facing?
The magnetometer senses magnetic fields, including the giant one generated by the Earth. Its primary purpose is to be the phone’s compass and determine the direction in which the phone is pointing. That information is written into EXIF as a compass heading every time you take a picture.
Let’s determine which direction I was facing when I took each of these photos:
The shore of a small lake, with the lake to the left. This photo contains EXIF metadata - see if you can find which direction the camera was facing!
The shore of a small lake, with the lake directly ahead. This photo contains EXIF metadata - see if you can find which direction the camera was facing!
Bushes, a street with a parked bicycle, and houses. This photo contains EXIF metadata - see if you can find which direction the camera was facing!
The shore of a small lake, with the lake to the right. This photo contains EXIF metadata - see if you can find which direction the camera was facing!
We’ll use the following exif Image properties to determine the direction in which the camera was pointed:
gps_img_direction: The compass heading (that is, direction) that the camera was facing when the picture was taken, expressed in decimal degrees. 0° is north, 90° is east, 180° is south, and 270° is west.
gps_img_direction_ref: The reference point for gps_img_direction. This can be either T, which means that 0° refers to true or geographic north, or M, which means that 0° refers to magnetic north. Most of the time, true north is used.
The code below displays the camera direction for the four lake photos. It makes use of a couple of utility functions:
degrees_to_direction(): This function converts compass headings into cardinal directions (e.g., N, NE, NNE, and so on).
format_direction_ref(): This function turns the value in gps_img_direction_ref into a human-friendly string.
def degrees_to_direction(degrees):
COMPASS_DIRECTIONS = [
"N",
"NNE",
"NE",
"ENE",
"E",
"ESE",
"SE",
"SSE",
"S",
"SSW",
"SW",
"WSW",
"W",
"WNW",
"NW",
"NNW"
]
compass_directions_count = len(COMPASS_DIRECTIONS)
compass_direction_arc = 360 / compass_directions_count
return COMPASS_DIRECTIONS[int(degrees / compass_direction_arc) % compass_directions_count]
def format_direction_ref(direction_ref):
direction_ref_text = "(true or magnetic north not specified)"
if direction_ref == "T":
direction_ref_text = "True north"
elif direction_ref == "M":
direction_ref_text = "Magnetic north"
return direction_ref_text
# Import images
lake_images = []
for i in range(1, 5):
filename = f"lake {i}.jpg"
with open(f"./images/{filename}", "rb") as current_file:
lake_images.append(Image(current_file))
# Display camera direction for each image
for index, image in enumerate(lake_images):
print(f"Image direction - Image {index}")
print("-------------------------")
print(f"Image direction: {degrees_to_direction(image.gps_img_direction)} ({image.gps_img_direction}°)")
print(f"Image direction ref: {format_direction_ref(image.gps_img_direction_ref)}
")
When you run the code, you’ll see this output:
Image direction - Image 0
-------------------------
Image direction: ENE (78.416259765625°)
Image direction ref: True north
Image direction - Image 1
-------------------------
Image direction: N (1.174224853515625°)
Image direction ref: True north
Image direction - Image 2
-------------------------
Image direction: SSE (178.46739196870607°)
Image direction ref: True north
Image direction - Image 3
-------------------------
Image direction: W (273.8248136315229°)
Image direction ref: True north
What was the altitude where the photo was taken?
In addition to providing location coordinates, GPS can also be used to determine altitude. Some smartphones are equipped with barometers (which detect air pressure), which they use to increase the accuracy of the altitude measurement.
Let’s find out the altitudes where these photos were taken:
The view from the deck of an infinity pool, with palm trees and a beach in the background. This photo contains EXIF metadata - see if you can find the altitude at which this photo was taken!
A lush tropical forest, with dome-shaped hills in the background. This photo contains EXIF metadata - see if you can find the altitude at which this photo was taken!
We’ll make use of these properties of exif’s Image object to determine altitude:
We’ll use the following exif Image properties:
gps_altitude: The altitude reported by the camera, expressed in meters.
gps_altitude_ref: The reference point for gps_altitude. This value is either 0, which means that the value in gps_altitude refers to meters above sea level, or 1, which means that the value in gps_altitude refers to meters below sea level.
The following code displays the altitude reported by the phone at the moment each of the photos was taken. It utilizes one utility function, format_altitude(), which specifies whether the given altitude is above or below sea level:
def format_altitude(altitude, altitude_ref):
altitude_ref_text = "(above or below sea level not specified)"
if altitude_ref == 0:
altitude_ref_text = "above sea level"
elif altitude_ref == 1:
altitude_ref_text = "below sea level"
return f"{altitude} meters {altitude_ref_text}"
# Import images
altitude_images = []
for i in range(1, 3):
filename = f"altitude {i}.jpg"
with open(f"./images/{filename}", "rb") as current_file:
altitude_images.append(Image(current_file))
# Display camera altitude for each image
for index, image in enumerate(altitude_images):
print(f"Altitude - Image {index}")
print( "------------------")
print(f"{format_altitude(image.gps_altitude, image.gps_altitude_ref)}
")
Here’s the resulting output:
Altitude - Image 0
------------------
14.025835763206075 meters above sea level
Altitude - Image 1
------------------
359.13079847908745 meters above sea level
I could tell you where I took these photos, but you already know how to find out for yourself.
Was the photographer moving?
Smartphones use a combination of GPS locations over time and the accelerometer to determine the phone’s speed and the direction in which it’s moving. Some phones provide this information as part of the EXIF metadata in photos.
My Motorola One Hyper doesn’t write any speed-related metadata to the photos it takes, but my iPhone does. This data can be accessed via these two exif Image object methods:
gps_speed: The speed reported by the camera, expressed as a number.
gps_speed_ref: The speed units used for the value in gps_speed. This value can be K for kilometers per hour, M for miles per hour, or N for nautical miles per hour, or “knots”.
Consider the following photos:
A small lake at sunset, with bird silhouttes. This photo contains EXIF metadata - see if you can find the speed at which the photographer was moving when it was taken!
A street, as seen from the passenger seat of a moving car. This photo contains EXIF metadata - see if you can find the speed at which the photographer was moving when it was taken!
A residential street, as seen from the handlebars of a bicycle. This photo contains EXIF metadata - see if you can find the speed at which the photographer was moving when it was taken!
Here’s code that prints out the recorded speed at the time each photo was taken. It includes a utlility function, format_speed_ref(), which specifies the units of the reported speed:
def format_speed_ref(speed_ref):
speed_ref_text = "(speed units not specified)"
if speed_ref == "K":
speed_ref_text = "km/h"
elif speed_ref == "M":
speed_ref_text = "mph"
elif speed_ref == "N":
speed_ref_text = "knots"
return speed_ref_text
# Import images
speed_images = []
for i in range(1, 4):
filename = f"speed {i}.jpg"
with open(f"./images/speed {i}.jpg", "rb") as current_file:
speed_images.append(Image(current_file))
for index, image in enumerate(speed_images):
print(f"Speed - Image {index}")
print("---------------")
print(f"Speed: {image.gps_speed} {format_speed_ref(image.gps_speed_ref)}
")
Here are the speeds it reports:
Speed - Image 0
---------------
Speed: 0.0 km/h
Speed - Image 1
---------------
Speed: 20.19736291335287 km/h
Speed - Image 2
---------------
Speed: 5.520932607215793 km/h
Updating EXIF Data and Saving It
So far, we’ve limited ourselves to simply reading the EXIF metadata from photos. The next step is to make changes to that data and then save the results as a new photo file.
Updating a photo’s coordinates
Let’s start with this photo:
- Hello guys, Im back again with an interesting post. I will share Netflix Premium Mod Apk. If youre looking for Netflix Movies/TV Shows
- Walikota Magelang, Sigit Widyonindito seusai shalat Idul Adha, hari jumat (31/07/2020) bersama jajarannya melakukan safari
- By the time the third act comes about, visuals are the only thing left to enjoy, as the journey to Kong’s home is perhaps the silliest
- Any mixed doubles National Olympic Committee that qualified for the 2020 or 2021 world mixed doubles championships but has not yet qualified