Skip to content

Instantly share code, notes, and snippets.

@rruntsch
Created August 29, 2021 19:29
Show Gist options
  • Save rruntsch/60871695678551dafd3adaf5c69dadad to your computer and use it in GitHub Desktop.
Save rruntsch/60871695678551dafd3adaf5c69dadad to your computer and use it in GitHub Desktop.
"""
Name: c_photo_exif.py
Author: Randy Runtsch
Date: March 20, 2021
Update Date: August 28, 2021
Description: The c_photo_exif class opens all image files in the specified
folder, reads the Exif data from each file. It then gets the sunrise
and sunset times of the day the photo was taken. It then writes a subset
of the Exif data, and the sunrise and sunset times, as a row in a CSV file.
GPS Tags: https://www.opanda.com/en/pe/help/gps.html#GPSLongitude
"""
import csv
import exifread
import os
import datetime
#from pytz import timezone
class c_photo_exif:
def __init__(self, input_photo_folder, output_csv_file_nm):
# Create the output CSV file with headers and process all of the
# photo files.
self._rows_processed = 0
csv_writer = self.init_csv_file(output_csv_file_nm)
self.process_photos(input_photo_folder, csv_writer)
def process_photos(self, input_photo_folder, csv_writer):
# Process all of the image files contained in the input folder.
rows = 0
for subdirs, dirs, files in os.walk(input_photo_folder):
for photo_file_nm in files:
# Process only files with a .jpg extension.
if photo_file_nm[-4:] == '.jpg':
self._rows_processed += 1
photo_file_nm_full = input_photo_folder + '/' + photo_file_nm
self.process_photo(photo_file_nm, photo_file_nm_full, csv_writer)
def process_photo(self, photo_file_nm, photo_file_nm_full, csv_writer):
# Get a subset of EXIF values from the photo file. Call function write_csv_file_row()
# to write the values as a row to a CSV file.
photo_file = open(photo_file_nm_full, 'rb')
tags = exifread.process_file(photo_file, strict = False)
camera_make = tags['Image Make'].printable
camera_model = tags['Image Model'].printable
photo_date_time = tags['EXIF DateTimeOriginal'].printable
focal_length_raw = tags['EXIF FocalLength'].printable
shutter_speed = tags['EXIF ExposureTime'].printable
f_stop_raw = tags['EXIF FNumber'].printable
exposure_bias = tags['EXIF ExposureBiasValue'].printable
white_balance = tags['EXIF WhiteBalance'].printable
iso = tags['EXIF ISOSpeedRatings'].printable
# Handle errors for GPS tags, since they may not exist in all records.
try:
gps_latitude = tags['GPS GPSLatitude']
except:
gps_latitude = "0"
try:
gps_longitude = tags['GPS GPSLongitude']
except:
gps_longitude = "0"
# The GPS GPSLongitudeRef tag stores "W" for west and "E" for east.
# Every coordinate west of the Prime Meridian is negative longitude,
# while every coordinate to its right is positive.
try:
gps_longitude_ref = tags['GPS GPSLongitudeRef'].printable
except:
gps_longitude_ref = ""
latitude = self.calculate_coordinate(gps_latitude, "")
longitude = self.calculate_coordinate(gps_longitude, gps_longitude_ref)
# Convert some iPhone values from fraction string to decimal.
f_stop = self.calculate_fraction(f_stop_raw)
focal_length = self.calculate_fraction(focal_length_raw)
# Exif date-time values contain colons in the date part (for example, 2021:01:20). Replace
# the first two colons with hyphens (-) so that they will be treated as date-time values
# in other programs, such as Tableau. This will leave the colons in place in the time portion
# of the value.
photo_date_time = photo_date_time.replace(':', '-', 2)
self.write_csv_file_row(csv_writer, photo_file_nm, photo_date_time, \
camera_make, camera_model, focal_length, shutter_speed, f_stop, exposure_bias, iso, white_balance, \
latitude, longitude)
def calculate_fraction(self, fraction_string):
# iPhones store f stop values as a fraction. For example, 8/5.
# If the value contains a slash, divide the numerator by the denominator
# and return the result. Else, simply return the original value.
return_val = fraction_string
if fraction_string.find("/") != -1:
fraction_list = fraction_string.split("/")
return_val = int(fraction_list[0]) / int(fraction_list[1])
return return_val
def calculate_coordinate(self, gps_string, gps_longitude_ref):
# Convert a GPS latitude or longitude coordinate string to a numeric decimal value.
# A GPS coordinate is composed of these comma-separated strings:
# - Degrees
# - Minutes (optional)
# - Seconds (optional)
if gps_string == "0":
return 0
degrees = gps_string.values[0]
minutes_raw = gps_string.values[1]
seconds_raw = gps_string.values[2]
minutes = (minutes_raw.numerator / minutes_raw.denominator) / 60
seconds = (seconds_raw.numerator / seconds_raw.denominator) / 3600
coordinate = degrees + minutes + seconds
# Change longitude in the Western Hempispher to negative values.
if gps_longitude_ref == "W":
coordinate = -abs(coordinate)
return coordinate
def init_csv_file(self, output_csv_file_nm):
# Open the CSV output file for write (create a new file and overwrite
# the existing file), write its header, and return its handle.
headers = ['File Name', 'Photo Date Time', 'Camera Make', 'Camera Model', \
'Focal Length', 'Shutter Speed', 'F Stop', 'Exposure Bias', 'ISO', 'White Balance', 'Latitude', 'Longitude']
csv_file = open(output_csv_file_nm, 'w', encoding='utf-8', newline='')
csv_writer = csv.DictWriter(csv_file, headers)
csv_writer.writeheader()
return csv_writer
def write_csv_file_row(self, csv_writer, file_name, photo_date_time, \
camera_make, camera_model, focal_length, shutter_speed, f_stop, exposure_bias, iso, white_balance, \
latitude, longitude):
# Assemble the record of Exif values by column and write it to the CSV file.
row = {'File Name' : file_name, 'Photo Date Time' : photo_date_time, \
'Camera Make' : camera_make, 'Camera Model' : camera_model, \
'Focal Length' : focal_length, 'Shutter Speed' : shutter_speed, \
'F Stop' : f_stop, 'Exposure Bias': exposure_bias, 'ISO' : iso, 'White Balance' : white_balance, \
'Latitude' : latitude, 'Longitude' : longitude}
csv_writer.writerow(row)
def get_rows_processed(self):
return self._rows_processed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment