Source code for photo_tagging

import pyexiv2 as exiv # see not about pyexiv2 in notes.txt
from ast import literal_eval
from depth_temp_log_io import *
from configuration import *
from gps_log_io import *
from common import *
# That namespace url doesn't really exist. The custom tags seem to work
# without it. Perhaps I should figure out if I really need it or not.
exiv.xmp.register_namespace('http://svarchiteuthis.com/benthicphoto/', 'BenthicPhoto')

[docs]class image_directory(object): def __init__(self, dir_path): if os.path.isdir(dir_path): jpgs = [ os.path.join(dir_path,f) for f in os.listdir(dir_path) if f.lower().endswith('.jpg') ] else: raise ValueError("%s is not a directory." % dir_path) self.path = dir_path self.images = [ image_file(img) for img in jpgs ] self.images.sort(key=lambda i: i.datetime) # sort the images by datetime of the image self.image_count = len( self.images ) def __shift_datetimes__(self, time_delta_obj, verbose=True): """ Shift the 'date original' values of all photos in the directory. See the warnings in the image_file.__set_datetime__ method doc string. You should be careful about using this method. """ for img in self.images: new_dt = img.__shift_datetime__( time_delta_obj, verbose=verbose ) @property def local_datetimes(self): return [ x.datetime for x in self.images ] @property def utc_datetimes(self): return [ x.utc_datetime for x in self.images ] @property def exif_depths(self): d_list = [] for img in self.images: if img.exif_depth: d_list.append(img.exif_depth * -1) else: d_list.append(0.0) return np.array(d_list)
[docs] def depth_plot(self): """ Create a plot of the depth profile with photo times and depths marked. """ drs = dive_record_set( min(self.local_datetimes), max(self.local_datetimes) ) y = -1 * drs.depth_time_array[:,0] # depths * -1 to make negative values x = drs.depth_time_array[:,1] # datetimes fig = plt.figure() # imported from matplotlib ax = fig.add_subplot(111) ax.plot_date(x,y,marker='.',linestyle='-',tz=pytz.timezone(LOCAL_TIME_ZONE) ) # LOCAL_TIME_ZONE from configuration.py) ax.plot(self.local_datetimes,self.exif_depths,'r*',markersize=10,picker=5) plt.xlabel('Date and Time') plt.ylabel('Depth (meters)') fig.suptitle('Photos with Depth and Time') #print "Before def onpick" def onpick(event): global ann try: ann.remove() except NameError: pass ind = event.ind[0] fname = os.path.basename( self.images[ind].file_path ) ann_text = "Photo: %s\ndepth: %g\ndate: %s" % ( fname, self.exif_depths[ind], self.local_datetimes[ind].strftime('%Y/%m/%d %H:%M:%S') ) ann = ax.annotate(ann_text, xy=(self.local_datetimes[ind], self.exif_depths[ind]), xytext=(-20,-20), textcoords='offset points', ha='center', va='top', bbox=dict(boxstyle='round,pad=0.2', fc='yellow', alpha=0.3), arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.5', color='red')) plt.draw() print "Photo: %s, index: %i, depth: %g, date: %s" % ( fname, ind, self.exif_depths[ind], self.local_datetimes[ind].strftime('%Y/%m/%d %H:%M:%S') ) #print "Before mpl_connect" fig.canvas.mpl_connect('pick_event', onpick) plt.show() #print "after plt show"
[docs] def depth_temp_tag(self,verbose=False): """ Depth tag all the photos in the directory. """ for img in self.images: img.depth_temp_tag(verbose)
[docs]class image_file(object): """ An object to make accessing image files and metadata easier. """ def __init__(self,img_path): if os.path.exists(img_path): self.file_path = img_path md = exiv.ImageMetadata(img_path) md.read() self.md = md else: raise ValueError( "The file %s does not exist." % (img_path,) ) def __repr__(self): return "Image file: %s" % (self.file_path,) def __get_exiv_tag(self,tag_string): """ Try to get a pyexiv2 tag. If the tag doesn't exist, return None. """ try: return self.md[tag_string] except KeyError: return None def __get_exiv_tag_value(self,tag_string): """ Try to get a pyexiv2 tag value. If the tag doesn't exist, return None. """ try: return self.md[tag_string].value except KeyError: return None def __get_exiv_tag_human_value(self,tag_string): """ Try to get a pyexiv2 tag human value. If the tag doesn't exist, return None. """ try: return self.md[tag_string].human_value except KeyError: return None
[docs] def exif_dict(self, exclude_panasonic_keys=True): """ Return a dict with all exif and xmp keys and values. """ exif_dict = {} for key in self.md.xmp_keys: if self.__get_exiv_tag_value(key): exif_dict.update( { key : self.__get_exiv_tag_value(key) } ) for key in self.md.exif_keys: if not ( exclude_panasonic_keys and 'Panasonic' in key.split('.') ): if self.__get_exiv_tag_human_value(key): exif_dict.update( { key : self.__get_exiv_tag_human_value(key)[:100] } ) return exif_dict
@property def file_name(self): return os.path.basename(self.file_path) @property
[docs] def datetime(self): """ Try to get a datetime object for the image's creation from the Exif.Photo.DateTimeOriginal value via pyexiv2. """ if self.__get_exiv_tag_value('Exif.Photo.DateTimeOriginal').tzname(): return self.__get_exiv_tag_value('Exif.Photo.DateTimeOriginal') else: return make_aware_of_local_tz( self.__get_exiv_tag_value('Exif.Photo.DateTimeOriginal') )
@property def utc_datetime(self): if self.datetime: return utc_from_local(self.datetime) else: return None @property def exif_direction(self): if self.__get_exiv_tag_value('Exif.GPSInfo.GPSImgDirection'): return float( self.__get_exiv_tag_value('Exif.GPSInfo.GPSImgDirection') ) @property def exif_lat_tag(self): return self.__get_exiv_tag('Exif.GPSInfo.GPSLatitude') @property def exif_latref_tag(self): return self.__get_exiv_tag('Exif.GPSInfo.GPSLatitudeRef') @property def exif_lon_tag(self): return self.__get_exiv_tag('Exif.GPSInfo.GPSLongitude') @property def exif_lonref_tag(self): return self.__get_exiv_tag('Exif.GPSInfo.GPSLongitudeRef') @property def exif_depth_tag(self): return self.__get_exiv_tag('Exif.GPSInfo.GPSAltitude') @property def exif_depth(self): try: ret_val = float( self.__get_exiv_tag_value('Exif.GPSInfo.GPSAltitude') ) except TypeError: try: ret_val = self.__get_exiv_tag_value('Exif.GPSInfo.GPSAltitude').to_float() except AttributeError: ret_val = None return ret_val @property def __exif_depth_temp_dict(self): """ This is a bit of a hack. I couldn't find a good place to store temperature data in the exif so I went with storing a python dictionary as a string in Exif.Photo.UserComment. I think I'm going to stop using this and store this stuff in custom xmp tags instead. UserComment is accessible to many photo management apps so it seems likely to get corrupted. I made it a private method but maybe I should have just deleted it. """ try: dstr = self.md['Exif.Photo.UserComment'].value return literal_eval(dstr) except KeyError: return None @property def __exif_temperature(self): """ This just exposes the temperature value from the hack mentioned in the doc string for exif_depth_temp_dict. I'm going to stop writing to this tag so don't be surprised if this returns nothing. Actually, I think I may just make it a private method because I don't want to delete it. """ if self.exif_depth_temp_dict: return self.exif_depth_temp_dict['temp'] else: return None @property def xmp_temperature(self): return self.__get_exiv_tag_value('Xmp.BenthicPhoto.temperature') @property def xmp_temp_units(self): return self.__get_exiv_tag_value('Xmp.BenthicPhoto.temp_units') @property def xmp_substrate(self): return self.__get_exiv_tag_value('Xmp.BenthicPhoto.substrate') @property def xmp_habitat(self): return self.__get_exiv_tag_value('Xmp.BenthicPhoto.habitat') @property
[docs] def position(self): """ Look at the exif data and return a position object (as defined in gps_log_io). Return None if there's no GPSInfo in the exif. """ if self.exif_lat_tag and self.exif_lon_tag and self.exif_latref_tag and self.exif_lonref_tag: lat = latitude.from_exif_coord(self.exif_lat_tag.value,self.exif_latref_tag.value) lon = longitude.from_exif_coord(self.exif_lon_tag.value,self.exif_lonref_tag.value) return position(lat,lon) else: return None
def __set_datetime__(self,dt_obj): """ Set the date original in the exif. I don't think you want to do this but I did want to once. If you lose the origination time for your image you can not sync it to your gps track or your depth log so leave this alone unless you're sure you know what you're doing. If you screw up your data don't come crying to me. I tried to warn you. """ key = 'Exif.Photo.DateTimeOriginal' self.md[key] = exiv.ExifTag(key,dt_obj) self.md.write() return self.datetime def __shift_datetime__(self,time_delta_obj,verbose=True): """ Shift the 'date original' in the exif by the given time delta. See the warnings in the doc string of __set_datetime__ method. You should be careful with this. """ current_dt = self.datetime self.__set_datetime__( current_dt + time_delta_obj ) if verbose: print "datetime of %s changed from %s to %s." % ( self.file_name, current_dt.strftime('%X, %x'), self.datetime.strftime('%X, %x') ) return self.datetime def __set_exif_position(self,pos,verbose=False): """ Set the relevant exif tags to match the position object handed in. The position object is defined over in gps_log_io.py """ pre = 'Exif.GPSInfo.GPS' add_dict = {pre+'Latitude': pos.lat.exif_coord, pre+'LatitudeRef': pos.lat.hemisphere, pre+'Longitude': pos.lon.exif_coord, pre+'LongitudeRef': pos.lon.hemisphere } for k,v in add_dict.iteritems(): if verbose: print "%s = %s" % (str(k),str(v)) self.md[k] = exiv.ExifTag(k,v) self.md.write() return True def __set_exif_depth_temp(self,depth,temp,verbose=False): from pyexiv2.utils import Rational if depth < 0: # This can happen because there's a bit of slop in the conversion from pressure to depth if verbose: print "Given depth was a negative value." depth = 0 if not depth: return None if not temp: temp = 0.0 # temperature isn't important at this point so if it's not there we'll just call it zero pre = 'Exif.GPSInfo.GPS' #dt_str = "{'depth':%g,'temp':%g}" % (depth,temp) dfrac = Fraction.from_float(depth).limit_denominator() add_dict = {pre+'Altitude': Rational(dfrac.numerator,dfrac.denominator), pre+'AltitudeRef': bytes(1), } #'Exif.Photo.UserComment': dt_str } for k,v in add_dict.iteritems(): if verbose: print "%s = %s" % (str(k),str(v)) self.md[k] = exiv.ExifTag(k,v) self.md.write() return True def __set_xmp_depth_temp(self,depth,temp): if not depth: return None if not temp: temp = 0.0 # temperature isn't important at this point so if it's not there we'll just call it zero pre = 'Xmp.BenthicPhoto.' self.md[pre+'depth'] = str(depth) self.md[pre+'depth_units'] = 'meters' self.md[pre+'temperature'] = str(temp) self.md[pre+'temp_units'] = 'celsius' self.md.write() def set_xmp_substrate(self, subst_str): pre = 'Xmp.BenthicPhoto.' self.md[pre+'substrate'] = subst_str self.md.write() def set_xmp_habitat(self, subst_str): pre = 'Xmp.BenthicPhoto.' self.md[pre+'habitat'] = subst_str self.md.write() @property
[docs] def logger_depth(self): """ Get the logged depth out of the db that matches the photo's timestamp. """ if self.utc_datetime: depth = get_depth_for_time(self.utc_datetime,reject_threshold=30) return depth else: return None
@property
[docs] def logger_temp(self): """ Get the logged temperature out of the db that matches the photo's timestamp. """ if self.utc_datetime: temp = get_temp_for_time(self.utc_datetime,reject_threshold=30) return temp else: return None
[docs] def depth_temp_tag(self,verbose=False): """ Get the depth and temp readings out of the db that match the photo's origination time (considering that the photo's time stamp is in the local timezone and the logs are in UTC) and write those values to the image's exif data. """ self.__set_exif_depth_temp(self.logger_depth,self.logger_temp,verbose=verbose) self.__set_xmp_depth_temp(self.logger_depth,self.logger_temp) if self.exif_depth_tag: return self.exif_depth_tag.value else: return None
[docs] def geotag(self,verbose=True): """ Get a position that matches the time of creation for the image out of the database and set the exif data accordingly. We assume that the photo timestamp is local and the gps position is utc. """ pos = get_position_for_time(self.utc_datetime,verbose=verbose) if verbose and pos: print "-------------------GeoTagg--------------------------------" print "%s is going to get set to %s as %s, %s" % ( os.path.basename( self.file_path ), unicode( pos ), str(pos.lat.exif_coord), str(pos.lon.exif_coord) ) print "%s, %s in dms" % ( str(pos.lat.dms), str(pos.lon.dms) ) if pos: self.__set_exif_position(pos,verbose) return self.position
def __compare_position__(self): """ This is just for testing. Check to see if the value stored in the db matches what we display after conversion. I want to make sure I'm not throwing away precision in coordinate conversions. """ pos = get_position_for_time(self.utc_datetime,verbose=True) print " db says: %s, %s \nexif says: %s, %s" % ( pos.lat.nmea_string, pos.lon.nmea_string, self.position.lat.nmea_string, self.position.lon.nmea_string ) if pos.lat.nmea_string == self.position.lat.nmea_string: print "Latitudes match" if pos.lon.nmea_string == self.position.lon.nmea_string: print "Longitudes match"
[docs] def remove_geotagging(self): """ You probably won't need to do this but I did a few times during testing. """ geokeys = ['Latitude','LatitudeRef','Longitude','LongitudeRef'] pre = 'Exif.GPSInfo.GPS' for key in [pre+gk for gk in geokeys]: if self.md.__contains__(key): self.md.__delitem__(key) self.md.write()
[docs] def remove_depthtagging(self): """ You probably won't need to do this but I did a few times during testing. """ geokeys = ['Altitude','AltitudeRef'] pre = 'Exif.GPSInfo.GPS' for key in [pre+gk for gk in geokeys]: if self.md.__contains__(key): self.md.__delitem__(key) self.md.write()
[docs] def remove_temptagging(self): """ You probably won't need to do this but I did a few times during testing. """ geokeys = ['depth','depth_units','temperature','temp_units'] pre = 'Xmp.BenthicPhoto.' for key in [pre+gk for gk in geokeys]: if self.md.__contains__(key): self.md.__delitem__(key) self.md.write()
[docs] def remove_substratetagging(self): """ You probably won't need to do this but I did a few times during testing. """ key = 'Xmp.BenthicPhoto.substrate' if self.md.__contains__(key): self.md.__delitem__(key) self.md.write()
[docs] def remove_all_tagging(self): """ You probably won't need to do this but I did a few times during testing. """ self.remove_geotagging() self.remove_depthtagging() self.remove_temptagging() self.remove_substratetagging()
def exif_tag_jpegs(photo_dir): for fname in os.listdir(photo_dir): if fname.lower().endswith('.jpg'): imf = image_file( os.path.join(photo_dir,fname) ) imf.depth_temp_tag() imf.geotag() if imf.exif_depth_tag: dstr = imf.exif_depth_tag.human_value else: dstr = 'None' if imf.exif_temperature: tstr = "%g C" % imf.exif_temperature else: tstr = 'None' print "Image: %s - Depth: %s, Temp %s, Position: %s" % (fname,dstr,tstr,imf.position) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Tag a photos with position, depth, and temperature from a gps and a Sensus Ultra depth and temperature logger.') parser.add_argument('photo_dir', nargs='?', type=str, help='The directory that contains photos you would like tagged.') args = parser.parse_args() exif_tag_jpegs(args.photo_dir) #### Pretty much just testing garbage below here ####### #### Why don't I delete it? Good question. ####### def check_time_tags(img): md = get_photo_metadata(img) timetags = [tag for tag in md.exif_keys if tag.find('Time')<>-1] for t in timetags: print "%s: %s" % (t,md[t]) def read_gps_crap(img): md = get_photo_metadata(img_path) try: gpstag = md['Exif.Image.GPSTag'].human_value except KeyError: gpstag = 'not set' try: lat = md['Exif.GPSInfo.GPSLatitude'].human_value except KeyError: lat = 'not set' try: lon = md['Exif.GPSInfo.GPSLongitude'].human_value except KeyError: lon = 'not set' print "GPSTag: %s, Lat: %s, Lon: %s" % ( str(gpstag), str(lat), str(lon) ) def read_gps_crap_from_dir(dir): for fname in os.listdir(dir): if fname.lower().endswith('.jpg'): read_gps_crap(os.path.join(dir,fname)) def shift_time_for_photos(direc,time_delta): for fname in os.listdir(direc): if fname.lower().endswith('.jpg'): imf = image_file( os.path.join( direc,fname ) ) orig_time = imf.datetime imf.__set_datetime__( orig_time + time_delta ) print "Changed %s from %s to %s." % ( fname, orig_time.strftime('%H:%M'), imf.datetime.strftime('%H:%M') ) def photo_times_for_dir(dir): for fname in os.listdir(dir): if fname.lower().endswith('.jpg'): img = os.path.join(dir,fname) md = get_photo_metadata(img) ptime = get_photo_datetime(md) if ptime: ending = ptime.strftime('%Y-%m-%d %H:%M:%S') else: ending = 'no time tag' print "%s: %s" % (fname,ending) def get_photo_metadata(img_path): md = exiv.ImageMetadata(img_path) md.read() return md def get_photo_datetime(md): """If I find inconsistency in exif tags, I may have to get a little more creative here.""" try: ptime = md['Exif.Photo.DateTimeOriginal'].value except KeyError: ptime = False return ptime