Python – Working with exif and geopy

Sample Python code for using the exif and geopy modules with jpeg images.
November 04, 2022

Today, Im showing how to use the exif and geopy modules to read and work with jpeg images.

Python – Working with exif and geopy

Sample Python code for using the exif and geopy modules with jpeg images.
November 04, 2022

Today, Im showing how to use the exif and geopy modules to read and work with jpeg images.


Python – Working with exif and geopy

Sample Python code for using the exif and geopy modules with jpeg images.
November 04, 2022

Today, Im showing how to use the exif and geopy modules to read and work with jpeg images.


Python – Working with exif and geopy

Consider for a moment the title image for this article. It’s a picture of a very nice bike trail running through the woods. But when was a taken, where, and with which camera? How can I search all my pictures for more that I have taken at this same location, or the same town, or during the same week?

Will, in this article, I will be using the Python 'exif' and 'geopy' modules to find these answers.

I will also show how to setup a 'Data Cashe' so we don’t request the same data from the Internet multiple times.

Throughout this artical, you can click on the links ( underlined text ) for more information about these sub-topics.


What you will need:

To run the sample code in this article, you will need to install the 'exif' and 'geopy' modules. These are NOT part of the Python Standard Library ( they do not come per-packaged with Python ) and will need to be installed on your computer manually. They are 100% FREE.

The command to install them is:
pip install exif geopy

On some computers, you may need to uses this command instead:
pip3 install exif geopy

For more information about pip, see: https://pip.pypa.io/en/stable/installation/


So, where and when was the picture taken?

The following code sample will use the exif module to read the Meta data from a picture file. This data will include, among other very useful things, the longitude and latitude, the time, and information about the camera. In some cases, it may also include info about the subject ( what or who is in the Picture ).

from exif import Image

filename = "IMG_20200905_150106976.jpg"
with open(filename, 'rb') as image_file:
  my_image = Image(image_file)
  if my_image.has_exif:
    for item in my_image.list_all():
      try:  
        print( item, my_image[item] )
      except:
        pass

make motorola
model moto g(6) play
x_resolution 72.0
y_resolution 72.0
resolution_unit ResolutionUnit.INCHES
software jeter-user 9 PPPS29.118-68-9 329ef release-keys
datetime 2020:09:05 15:01:07
y_and_c_positioning 1
_exif_ifd_pointer 244
_gps_ifd_pointer 2464
compression 6
jpeg_interchange_format 2808
jpeg_interchange_format_length 37919
exposure_time 0.001589825119236884
f_number 2.0
exposure_program ExposureProgram.NORMAL_PROGRAM
photographic_sensitivity 50
exif_version 0220
datetime_original 2020:09:05 15:01:07
datetime_digitized 2020:09:05 15:01:07
shutter_speed_value 9.29
aperture_value 2.0
brightness_value -1.0
exposure_bias_value 0.0
max_aperture_value 2.0
metering_mode MeteringMode.CENTER_WEIGHTED_AVERAGE
flash Flash(flash_fired=False, flash_return=FlashReturn.NO_STROBE_RETURN_DETECTION_FUNCTION, flash_mode=FlashMode.COMPULSORY_FLASH_SUPPRESSION, flash_function_not_present=False, red_eye_reduction_supported=False, reserved=0)
focal_length 3.519
color_space ColorSpace.SRGB
pixel_x_dimension 3120
pixel_y_dimension 4160
custom_rendered 0
exposure_mode ExposureMode.AUTO_EXPOSURE
white_balance WhiteBalance.AUTO
digital_zoom_ratio 1.0
scene_capture_type SceneCaptureType.STANDARD
contrast 0
saturation Saturation.LOW
sharpness Sharpness.SOFT
gps_version_id 2
gps_latitude_ref N
gps_latitude (47.0, 22.0, 41.0009)
gps_longitude_ref W
gps_longitude (121.0, 23.0, 27.2676)
gps_altitude_ref GpsAltitudeRef.ABOVE_SEA_LEVEL
gps_altitude 836.0
gps_timestamp (22.0, 1.0, 6.0)
gps_map_datum WGS-84

Warning (from warnings module):
  File "/home/joe/.local/lib/python3.10/site-packages/exif/_image.py", line 127
    return self.__getattr__(item)
RuntimeWarning: ASCII tag contains 0 fewer bytes than specified
gps_processing_method ASCII
gps_datestamp 2020:09:05


About the warning messages.

When you run this script, you might see one, or both, of the following warning messages. These are caused by a known bug in the software of some cameras, which saves the Meta data incorrectly. These warnings do not affect the quality of the data, and can be safely ignored.

xxxx is not a valid TiffByteOrder
ASCII tag contains x fewer bytes than specified



Turning geo data into something human readable.

OK, so now that we have the longitude and latitude for this picture, let’s turn this into something we can actually work with. To do this, we will be using something called a reverse-geolocator. This is included in the geopy module. This take the longitude and latitude, will search the OpenStreetMaps service ( a FREE services ), and return human readable information about the location.

from geopy.geocoders import Nominatim

lref = "47.378055805555555, -121.39090766666668"

geolocator = Nominatim(user_agent="Python testing geopy module")
location = geolocator.reverse( lref, language="en")
for item,value in location.raw.items():
    print( item, value )

place_id 273410602
licence Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright
osm_type way
osm_id 885665981
lat 47.3782853
lon -121.3913336
display_name Palouse to Cascades State Park Trail, Hyak, Kittitas County, Washington, 98068, United States
address {'road': 'Palouse to Cascades State Park Trail', 'hamlet': 'Hyak', 'county': 'Kittitas County', 'state': 'Washington', 'ISO3166-2-lvl4': 'US-WA', 'postcode': '98068', 'country': 'United States', 'country_code': 'us'}
boundingbox ['47.3725151', '47.3921056', '-121.3945985', '-121.3901047']


About the OpenStreetMaps services

The 'geopy' module requests data from the OpenStreetMaps Internet services. This service is an initiative to provide FREE geographic data, such as street maps and reverse-geolocating data, to anyone in the world. There is no need to create an account to uses it, and it does not require any kind of registration key.

If you are intrested in learning more about the Open Streep Maps Foundation, or wish to make a contrubution to help keep the servers running, please visit the foundation webpage at https://wiki.osmfoundation.org/wiki/Main_Page


Creating a Data Cashe.

Ok, the above code works grate if we are working with only one picture. But what if we have a thousand or more? And what if we run the script multiple times? After all, we tend to take more than one pictures in the same place, and run such a script multiple times. We don’t want to hammer the OpenStreetMap servers with the same request over, and over, and over again!!

The following code example will create something called a 'Data Cashe' file. This is a file where previous requests are saved, and used to prevent asking the Internet servers about the same location multiple times.

Note: This code will create a file called geodata.json. Be sure to backup this file, along with all your other data, when you do your usual system backups.


Putting it all together.

Now let’s put it all together into something, more or less, useful.

The sample script bellow will find all the Picture files in your home/Pictures folder, read the Meta data from the files, get the geo-location information, and write a text file containing all that information. You could then uses another Python script, or even a simple text editor, to search that text file for any pictures taken in a city, or during a particular time frame.

The first time you run the sample code bellow, it will be very slow. It will need to ask the OpenStreetMaps server about each location. But as it runs, you will notices that it becomes faster and faster as the Data Cashe builds. The second time you run the script, it will be VERY fast, because most of the data will be in the Data Cashe.

This sample code bellow will create a text file called PictureData.txt, which you can search for information. You could add additional code to write this data to an SQL table or a Json file to make it more usable, but that is beyond the scope of this article.

Hey, You didn’t think that I was going to do all the work for you did you?

"""
testinggeopy.py
Written by Joe Roten, October 30, 2022

Example script showing how to uses the 'geopy' and 'exif' modules,
  to read Meta data from a Picture file (jpeg),
  and lookup the geo-location of where the picture was taken.

This script will read the Meta data of each file in the Pictures folder,
  look up it's geo-location data from the OpenStreetMaps servers,
  and append the output to a text file.

Also shows how to uses a Data Cashe to prevent multiple requests
  to the OpenStreetMaps servers for the same locations.

To run this code, you will need to install the 'exif' and
  'geopy' modules manually, as they are not part of the
  Python Sandard Modules. The command to do this is:
      pip install exif geopy

Note: Don't worry about any warning messages saying
   'ASCII tag contains x fewer bytes than specified'.
   'xxxx is not a valid TiffByteOrder'.
This is caused by a known bug in the software of some cameras,
  and will not affect the quallity of the output data.

Reminder: Don't forget to backup the
  Data Cashe file ( "geodata.json" ) when you do
  your usuall system backups. It's valuable data  :-)

For documentation, see http://www.gsw7.net/K700018.php

"""

import os, json
from exif import Image
from geopy.geocoders import Nominatim



# The next few lines may need to be changed, depending on your system.

HomeDir = os.path.expanduser( "~" ) # User's home directory.

# Folder where Picture files are saved. Usually home/Pictures.
PictureDir = os.path.join( HomeDir, "Pictures" ) 
print( "I think your Picture folder is ", PictureDir )

# File where Data Cashe is to be saved.
DataCasheFile = os.path.join( HomeDir, "geodata.json" ) 

# Text file where the output is to be recorded.
PictureDataFile = os.path.join( HomeDir, "PictureData.txt" ) 




def dms_to_dd(d, m, s, ref="" ):
    """Convert long/lat from DMS to Degrees. Returns a Float value. """
    dd = d + float(m)/60 + float(s)/3600
    r = ref.upper().strip()
    if r=="S" or r=="W": dd = dd * -1
    return dd


def geolookup(latitude, longitude, DataCasheFile, verbose=False):
    """Return a Dict containing the geo-location data for a given latitude/longitude. """
    # Also uses a Data Cashe to prevent multiple calls to the OpenStreetMaps servers.
    # Note: A global list called 'geodata' must be created BEFORE this is called.
    
    global geodata
    reply = {}

    # If first time, load the data from the Data Cashe File ( if exists ).
    if len(geodata)<1:
      if os.path.exists(DataCasheFile):
        with open(DataCasheFile, 'r') as f: geodata = json.load(f)

    # Convert latitude/longitude to a string.
    lref = str(latitude) + ", " + str(longitude)

    # Try to lookup the location in the Data Cashe.
    # If it's found, reply = place, and set FoundItFlag to True. 
    FoundItFlag = False
    for place in geodata:
      if place.get('lref') == lref:
        reply = place
        FoundItFlag = True

    # If location not in the Data Cashe, look it up using geopy,
    #    and add the new location to the Data Cashe File.
    if FoundItFlag == False:
      geolocator = Nominatim(user_agent="Python testing geopy module")
      location = geolocator.reverse( lref, language="en")
      reply = location.raw.get("address")
      reply["title"] = location.raw.get("display_name")
      reply['lref'] = lref
      geodata.append( reply )
      with open(DataCasheFile, 'w') as f: json.dump(geodata, f, indent=1)
      if verbose: print( reply )
       
    return reply
    



if __name__ == "__main__":

    # Create a global list called geodata. 
    geodata = []

    # Clear the output file.
    with open(PictureDataFile, "w") as output: output.write("")  

    # For Loops; Looping through each file in the Picture folder.
    for root, dirs, files in os.walk( PictureDir ):
      for file in files:
          
        # Get the full filename (path/file) of the Picture.  
        FullFileName = os.path.join( root, file )

        # Create a Dict to hold the data for the Picture.
        exifData = { "FullFileName": FullFileName }

        # Use exif to get the Meta data from the Picture file.
        #   Add the data to exifData.
        with open(FullFileName, 'rb') as image_file:
          my_image = Image(image_file)
          if my_image.has_exif:
            for item in my_image.list_all():
              try:  
                exifData[item] = my_image[item]
              except:
                pass


        # If there is Geo data in the Dict....
        if "gps_longitude" in exifData:
            
          # Convert longitude/latitude from DMS format, to Degrees (float).
          t = exifData.get("gps_longitude")
          longitude = dms_to_dd( t[0], t[1], t[2], exifData.get("gps_longitude_ref") )
          t = exifData.get("gps_latitude")
          latitude = dms_to_dd( t[0], t[1], t[2], exifData.get("gps_latitude_ref") )

          # Merge the geo-location data into exifData.
          geoLocationData = geolookup( latitude, longitude, DataCasheFile, verbose=True )
          exifData = { **exifData, **geoLocationData } # How to merge 2 Dict.


        # Append the results for this Picture to the Picture Data (text) File.
        with open(PictureDataFile, "a") as output:
          output.write( str( exifData ) + "\n\n")

        # ** Add your code here if you wish to do additional
        #      processing with the data.
        #  All data for the Picture is now in the Dict ( exifData ).

     # End of the For loops.

# End-Of-code.

Click HERE to download the above sample code.

Upgrading the modules:

From time to time, updated versions of these Python Modules are released. Just as they where installed manually, you should upgrade them manually. So, you will probably want to add an event on your calendar, to run the following commands, every 6 months or so. These commands will upgrade these modules to the most current versions.

# Bring pip to the most current version:
pip install --upgrade pip

# Bring exif and geopy to the most current version:
pip install --upgrade exif geopy



In concussion.

And that concludes what I have to say about this subject. I hope that someone out there find’s this useful.

Everyone have a good day, and stay terrestrial.

Last updated: 2022-11-04



Written by Joe Roten

Computer tech, Graphic Artist, Photographer, Writer, Educator, Programmer, Jack of many trades, Social gadfly, and Scholar without portfolio. http://www.gsw7.net/joe/

Written by Joe Roten

http://www.gsw7.net/joe/

As always

The information on my website is FREE.
But donations to help pay for Coffee and Beer are always welcomed.
Thanks.