386 lines
12 KiB
Python
386 lines
12 KiB
Python
|
#!/usr/bin/env python
|
||
|
|
||
|
import os
|
||
|
import sys
|
||
|
import exifread
|
||
|
import datetime
|
||
|
from lib.geo import normalize_bearing
|
||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||
|
|
||
|
def eval_frac(value):
|
||
|
return float(value.num) / float(value.den)
|
||
|
|
||
|
def exif_gps_fields():
|
||
|
'''
|
||
|
GPS fields in EXIF
|
||
|
'''
|
||
|
return [
|
||
|
["GPS GPSLongitude", "EXIF GPS GPSLongitude"],
|
||
|
["GPS GPSLatitude", "EXIF GPS GPSLatitude"]
|
||
|
]
|
||
|
|
||
|
|
||
|
def exif_datetime_fields():
|
||
|
'''
|
||
|
Date time fields in EXIF
|
||
|
'''
|
||
|
return [["EXIF DateTimeOriginal",
|
||
|
"Image DateTimeOriginal",
|
||
|
"EXIF DateTimeDigitized",
|
||
|
"Image DateTimeDigitized",
|
||
|
"EXIF DateTime"
|
||
|
"Image DateTime",
|
||
|
"GPS GPSDate",
|
||
|
"EXIF GPS GPSDate",
|
||
|
"EXIF DateTimeModified"]]
|
||
|
|
||
|
def format_time(time_string):
|
||
|
'''
|
||
|
Format time string with invalid time elements in hours/minutes/seconds
|
||
|
Format for the timestring needs to be "%Y_%m_%d_%H_%M_%S"
|
||
|
|
||
|
e.g. 2014_03_31_24_10_11 => 2014_04_01_00_10_11
|
||
|
'''
|
||
|
data = time_string.split("_")
|
||
|
hours, minutes, seconds = int(data[3]), int(data[4]), int(data[5])
|
||
|
date = datetime.datetime.strptime("_".join(data[:3]), "%Y_%m_%d")
|
||
|
date_time = date + datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)
|
||
|
return date_time
|
||
|
|
||
|
def gps_to_decimal(values, reference):
|
||
|
sign = 1 if reference in 'NE' else -1
|
||
|
degrees = eval_frac(values[0])
|
||
|
minutes = eval_frac(values[1])
|
||
|
seconds = eval_frac(values[2])
|
||
|
return sign * (degrees + minutes / 60 + seconds / 3600)
|
||
|
|
||
|
|
||
|
def get_float_tag(tags, key):
|
||
|
if key in tags:
|
||
|
return float(tags[key].values[0])
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
def get_frac_tag(tags, key):
|
||
|
if key in tags:
|
||
|
return eval_frac(tags[key].values[0])
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
def extract_exif_from_file(fileobj):
|
||
|
if isinstance(fileobj, (str, unicode)):
|
||
|
with open(fileobj) as f:
|
||
|
exif_data = EXIF(f)
|
||
|
else:
|
||
|
exif_data = EXIF(fileobj)
|
||
|
|
||
|
d = exif_data.extract_exif()
|
||
|
return d
|
||
|
|
||
|
def required_fields():
|
||
|
return exif_gps_fields() + exif_datetime_fields()
|
||
|
|
||
|
|
||
|
def verify_exif(filename):
|
||
|
'''
|
||
|
Check that image file has the required EXIF fields.
|
||
|
Incompatible files will be ignored server side.
|
||
|
'''
|
||
|
# required tags in IFD name convention
|
||
|
required_exif = required_fields()
|
||
|
exif = EXIF(filename)
|
||
|
required_exif_exist = exif.fields_exist(required_exif)
|
||
|
return required_exif_exist
|
||
|
|
||
|
|
||
|
def verify_mapillary_tag(filename):
|
||
|
'''
|
||
|
Check that image file has the required Mapillary tag
|
||
|
'''
|
||
|
return EXIF(filename).mapillary_tag_exists()
|
||
|
|
||
|
|
||
|
def is_image(filename):
|
||
|
return filename.lower().endswith(('jpg', 'jpeg', 'png', 'tif', 'tiff', 'pgm', 'pnm', 'gif'))
|
||
|
|
||
|
|
||
|
class EXIF:
|
||
|
'''
|
||
|
EXIF class for reading exif from an image
|
||
|
'''
|
||
|
def __init__(self, filename, details=False):
|
||
|
'''
|
||
|
Initialize EXIF object with FILE as filename or fileobj
|
||
|
'''
|
||
|
self.filename = filename
|
||
|
if type(filename) == str:
|
||
|
with open(filename, 'rb') as fileobj:
|
||
|
self.tags = exifread.process_file(fileobj, details=details)
|
||
|
else:
|
||
|
self.tags = exifread.process_file(filename, details=details)
|
||
|
|
||
|
|
||
|
def _extract_alternative_fields(self, fields, default=None, field_type=float):
|
||
|
'''
|
||
|
Extract a value for a list of ordered fields.
|
||
|
Return the value of the first existed field in the list
|
||
|
'''
|
||
|
for field in fields:
|
||
|
if field in self.tags:
|
||
|
if field_type is float:
|
||
|
value = eval_frac(self.tags[field].values[0])
|
||
|
if field_type is str:
|
||
|
value = str(self.tags[field].values)
|
||
|
if field_type is int:
|
||
|
value = int(self.tags[field].values[0])
|
||
|
return value, field
|
||
|
return default, None
|
||
|
|
||
|
|
||
|
def exif_name(self):
|
||
|
'''
|
||
|
Name of file in the form {lat}_{lon}_{ca}_{datetime}_{filename}
|
||
|
'''
|
||
|
lon, lat = self.extract_lon_lat()
|
||
|
ca = self.extract_direction()
|
||
|
if ca is None:
|
||
|
ca = 0
|
||
|
ca = int(ca)
|
||
|
date_time = self.extract_capture_time()
|
||
|
date_time = date_time.strftime("%Y-%m-%d-%H-%M-%S-%f")
|
||
|
date_time = date_time[:-3]
|
||
|
filename = '{}_{}_{}_{}_{}'.format(lat, lon, ca, date_time, os.path.basename(self.filename))
|
||
|
return filename
|
||
|
|
||
|
|
||
|
def extract_altitude(self):
|
||
|
'''
|
||
|
Extract altitude
|
||
|
'''
|
||
|
fields = ['GPS GPSAltitude', 'EXIF GPS GPSAltitude']
|
||
|
altitude, _ = self._extract_alternative_fields(fields, 0, float)
|
||
|
return altitude
|
||
|
|
||
|
|
||
|
def extract_capture_time(self):
|
||
|
'''
|
||
|
Extract capture time from EXIF
|
||
|
return a datetime object
|
||
|
TODO: handle GPS DateTime
|
||
|
'''
|
||
|
time_string = exif_datetime_fields()[0]
|
||
|
capture_time, time_field = self._extract_alternative_fields(time_string, 0, str)
|
||
|
|
||
|
# if "GPSDate" in time_field:
|
||
|
# return self.extract_gps_time()
|
||
|
|
||
|
if capture_time is 0:
|
||
|
# try interpret the filename
|
||
|
try:
|
||
|
capture_time = datetime.datetime.strptime(os.path.basename(self.filename)[:-4]+'000', '%Y_%m_%d_%H_%M_%S_%f')
|
||
|
except:
|
||
|
pass
|
||
|
else:
|
||
|
capture_time = capture_time.replace(" ", "_")
|
||
|
capture_time = capture_time.replace(":", "_")
|
||
|
capture_time = "_".join(["{0:02d}".format(int(ts)) for ts in capture_time.split("_") if ts.isdigit()])
|
||
|
capture_time = format_time(capture_time)
|
||
|
sub_sec = self.extract_subsec()
|
||
|
capture_time = capture_time + datetime.timedelta(seconds=float(sub_sec)/10**len(str(sub_sec)))
|
||
|
|
||
|
return capture_time
|
||
|
|
||
|
|
||
|
def extract_direction(self):
|
||
|
'''
|
||
|
Extract image direction (i.e. compass, heading, bearing)
|
||
|
'''
|
||
|
fields = ['GPS GPSImgDirection',
|
||
|
'EXIF GPS GPSImgDirection',
|
||
|
'GPS GPSTrack',
|
||
|
'EXIF GPS GPSTrack']
|
||
|
direction, _ = self._extract_alternative_fields(fields)
|
||
|
|
||
|
if direction is not None:
|
||
|
direction = normalize_bearing(direction, check_hex=True)
|
||
|
return direction
|
||
|
|
||
|
|
||
|
def extract_dop(self):
|
||
|
'''
|
||
|
Extract dilution of precision
|
||
|
'''
|
||
|
fields = ['GPS GPSDOP', 'EXIF GPS GPSDOP']
|
||
|
dop, _ = self._extract_alternative_fields(fields)
|
||
|
return dop
|
||
|
|
||
|
|
||
|
def extract_geo(self):
|
||
|
'''
|
||
|
Extract geo-related information from exif
|
||
|
'''
|
||
|
altitude = self.extract_altitude()
|
||
|
dop = self.extract_dop()
|
||
|
lon, lat = self.extract_lon_lat()
|
||
|
d = {}
|
||
|
if lon is not None and lat is not None:
|
||
|
d['latitude'] = lat
|
||
|
d['longitude'] = lon
|
||
|
if altitude is not None:
|
||
|
d['altitude'] = altitude
|
||
|
if dop is not None:
|
||
|
d['dop'] = dop
|
||
|
return d
|
||
|
|
||
|
def extract_gps_time(self):
|
||
|
'''
|
||
|
Extract timestamp from GPS field.
|
||
|
'''
|
||
|
gps_date_field = "GPS GPSDate"
|
||
|
gps_time_field = "GPS GPSTimeStamp"
|
||
|
gps_time = 0
|
||
|
if gps_date_field in self.tags and gps_time_field in self.tags:
|
||
|
date = str(self.tags[gps_date_field].values).split(":")
|
||
|
t = self.tags[gps_time_field]
|
||
|
gps_time = datetime.datetime(
|
||
|
year=int(date[0]),
|
||
|
month=int(date[1]),
|
||
|
day=int(date[2]),
|
||
|
hour=int(eval_frac(t.values[0])),
|
||
|
minute=int(eval_frac(t.values[1])),
|
||
|
second=int(eval_frac(t.values[2])),
|
||
|
)
|
||
|
microseconds = datetime.timedelta(microseconds=int( (eval_frac(t.values[2])%1) *1e6))
|
||
|
gps_time += microseconds
|
||
|
return gps_time
|
||
|
|
||
|
def extract_exif(self):
|
||
|
'''
|
||
|
Extract a list of exif infos
|
||
|
'''
|
||
|
width, height = self.extract_image_size()
|
||
|
make, model = self.extract_make(), self.extract_model()
|
||
|
orientation = self.extract_orientation()
|
||
|
geo = self.extract_geo()
|
||
|
capture = self.extract_capture_time()
|
||
|
direction = self.extract_direction()
|
||
|
d = {
|
||
|
'width': width,
|
||
|
'height': height,
|
||
|
'orientation': orientation,
|
||
|
'direction': direction,
|
||
|
'make': make,
|
||
|
'model': model,
|
||
|
'capture_time': capture
|
||
|
}
|
||
|
d['gps'] = geo
|
||
|
return d
|
||
|
|
||
|
|
||
|
def extract_image_size(self):
|
||
|
'''
|
||
|
Extract image height and width
|
||
|
'''
|
||
|
width, _ = self._extract_alternative_fields(['Image ImageWidth', 'EXIF ExifImageWidth'], -1, int)
|
||
|
height, _ = self._extract_alternative_fields(['Image ImageLength', 'EXIF ExifImageLength'], -1, int)
|
||
|
return width, height
|
||
|
|
||
|
|
||
|
def extract_image_description(self):
|
||
|
'''
|
||
|
Extract image description
|
||
|
'''
|
||
|
description, _ = self._extract_alternative_fields(['Image ImageDescription'], "{}", str)
|
||
|
return description
|
||
|
|
||
|
|
||
|
def extract_lon_lat(self):
|
||
|
if 'GPS GPSLatitude' in self.tags and 'GPS GPSLatitude' in self.tags:
|
||
|
lat = gps_to_decimal(self.tags['GPS GPSLatitude'].values,
|
||
|
self.tags['GPS GPSLatitudeRef'].values)
|
||
|
lon = gps_to_decimal(self.tags['GPS GPSLongitude'].values,
|
||
|
self.tags['GPS GPSLongitudeRef'].values)
|
||
|
elif 'EXIF GPS GPSLatitude' in self.tags and 'EXIF GPS GPSLatitude' in self.tags:
|
||
|
lat = gps_to_decimal(self.tags['EXIF GPS GPSLatitude'].values,
|
||
|
self.tags['EXIF GPS GPSLatitudeRef'].values)
|
||
|
lon = gps_to_decimal(self.tags['EXIF GPS GPSLongitude'].values,
|
||
|
self.tags['EXIF GPS GPSLongitudeRef'].values)
|
||
|
else:
|
||
|
lon, lat = None, None
|
||
|
return lon, lat
|
||
|
|
||
|
|
||
|
def extract_make(self):
|
||
|
'''
|
||
|
Extract camera make
|
||
|
'''
|
||
|
fields = ['EXIF LensMake', 'Image Make']
|
||
|
make, _ = self._extract_alternative_fields(fields, default='none', field_type=str)
|
||
|
return make
|
||
|
|
||
|
|
||
|
def extract_model(self):
|
||
|
'''
|
||
|
Extract camera model
|
||
|
'''
|
||
|
fields = ['EXIF LensModel', 'Image Model']
|
||
|
model, _ = self._extract_alternative_fields(fields, default='none', field_type=str)
|
||
|
return model
|
||
|
|
||
|
|
||
|
def extract_orientation(self):
|
||
|
'''
|
||
|
Extract image orientation
|
||
|
'''
|
||
|
fields = ['Image Orientation']
|
||
|
orientation, _ = self._extract_alternative_fields(fields, default=1, field_type=int)
|
||
|
if orientation not in [1, 3, 6, 8]:
|
||
|
return 1
|
||
|
return orientation
|
||
|
|
||
|
|
||
|
def extract_subsec(self):
|
||
|
'''
|
||
|
Extract microseconds
|
||
|
'''
|
||
|
fields = [
|
||
|
'Image SubSecTimeOriginal',
|
||
|
'EXIF SubSecTimeOriginal',
|
||
|
'Image SubSecTimeDigitized',
|
||
|
'EXIF SubSecTimeDigitized',
|
||
|
'Image SubSecTime',
|
||
|
'EXIF SubSecTime'
|
||
|
]
|
||
|
sub_sec, _ = self._extract_alternative_fields(fields, default=0, field_type=str)
|
||
|
sub_sec = int(sub_sec)
|
||
|
return sub_sec
|
||
|
|
||
|
|
||
|
def fields_exist(self, fields):
|
||
|
'''
|
||
|
Check existence of a list fields in exif
|
||
|
'''
|
||
|
for rexif in fields:
|
||
|
vflag = False
|
||
|
for subrexif in rexif:
|
||
|
if subrexif in self.tags:
|
||
|
vflag = True
|
||
|
if not vflag:
|
||
|
print("Missing required EXIF tag: {0} for image {1}".format(rexif[0], self.filename))
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
|
||
|
def mapillary_tag_exists(self):
|
||
|
'''
|
||
|
Check existence of Mapillary tag
|
||
|
'''
|
||
|
description_tag = "Image ImageDescription"
|
||
|
if description_tag in self.tags:
|
||
|
if "MAPSequenceUUID" in self.tags[description_tag].values:
|
||
|
return True
|
||
|
return False
|
||
|
|