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.
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/
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
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
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']
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
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.
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.
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
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