mapillary_download/lib/exifedit.py
2023-09-10 21:02:33 +02:00

246 lines
9.7 KiB
Python

import sys
import json
import datetime
import hashlib
import base64
import uuid
from lib.geo import normalize_bearing
from lib.exif import EXIF, verify_exif
from lib.pexif import JpegFile, Rational
import shutil
def create_mapillary_description(filename, username, email, userkey,
upload_hash, sequence_uuid,
interpolated_heading=None,
offset_angle=0.0,
timestamp=None,
orientation=None,
project="",
secret_hash=None,
external_properties=None,
verbose=False):
'''
Check that image file has the required EXIF fields.
Incompatible files will be ignored server side.
'''
# read exif
exif = EXIF(filename)
if not verify_exif(filename):
return False
if orientation is None:
orientation = exif.extract_orientation()
# write the mapillary tag
mapillary_description = {}
# lat, lon of the image, takes precedence over EXIF GPS values
mapillary_description["MAPLongitude"], mapillary_description["MAPLatitude"] = exif.extract_lon_lat()
# altitude of the image, takes precedence over EXIF GPS values, assumed 0 if missing
mapillary_description["MAPAltitude"] = exif.extract_altitude()
# capture time: required date format: 2015_01_14_09_37_01_000, TZ MUST be UTC
if timestamp is None:
timestamp = exif.extract_capture_time()
# The capture time of the image in UTC. Will take precedence over any other time tags in the EXIF
mapillary_description["MAPCaptureTime"] = datetime.datetime.strftime(timestamp, "%Y_%m_%d_%H_%M_%S_%f")[:-3]
# EXIF orientation of the image
mapillary_description["MAPOrientation"] = orientation
heading = exif.extract_direction()
if heading is None:
heading = 0.0
heading = normalize_bearing(interpolated_heading + offset_angle) if interpolated_heading is not None else normalize_bearing(heading + offset_angle)
# bearing of the image
mapillary_description["MAPCompassHeading"] = {"TrueHeading": heading, "MagneticHeading": heading}
# authentication
assert(email is not None or userkey is not None)
if email is not None:
mapillary_description["MAPSettingsEmail"] = email
if username is not None:
mapillary_description["MAPSettingsUsername"] = username
# use this if available, and omit MAPSettingsUsername and MAPSettingsEmail for privacy reasons
if userkey is not None:
mapillary_description["MAPSettingsUserKey"] = userkey
if upload_hash is not None:
settings_upload_hash = hashlib.sha256("%s%s%s" % (upload_hash, email, base64.b64encode(filename))).hexdigest()
# this is not checked in the backend right now, will likely be changed to have user_key instead of email as part
# of the hash
mapillary_description['MAPSettingsUploadHash'] = settings_upload_hash
# a unique photo ID to check for duplicates in the backend in case the image gets uploaded more than once
mapillary_description['MAPPhotoUUID'] = str(uuid.uuid4())
# a sequene ID to make the images go together (order by MAPCaptureTime)
mapillary_description['MAPSequenceUUID'] = str(sequence_uuid)
# The device model
mapillary_description['MAPDeviceModel'] = exif.extract_model()
# The device manufacturer
mapillary_description['MAPDeviceMake'] = exif.extract_make()
if upload_hash is None and secret_hash is not None:
mapillary_description['MAPVideoSecure'] = secret_hash
mapillary_description["MAPSettingsProject"] = project
# external properties (optional)
if external_properties is not None:
# externl proerties can be saved and searched in Mapillary later on
mapillary_description['MAPExternalProperties'] = external_properties
# write to file
if verbose:
print("tag: {0}".format(mapillary_description))
metadata = ExifEdit(filename)
metadata.add_image_description(mapillary_description)
metadata.add_orientation(orientation)
metadata.add_direction(heading)
metadata.write()
def add_mapillary_description(filename, username, email,
project, upload_hash, image_description,
output_file=None):
"""Add Mapillary description tags directly with user info."""
if username is not None:
# write the mapillary tag
image_description["MAPSettingsUploadHash"] = upload_hash
image_description["MAPSettingsEmail"] = email
image_description["MAPSettingsUsername"] = username
settings_upload_hash = hashlib.sha256("%s%s%s" % (upload_hash, email, base64.b64encode(filename))).hexdigest()
image_description['MAPSettingsUploadHash'] = settings_upload_hash
# if this image is part of a projet, the project UUID
image_description["MAPSettingsProject"] = project
assert("MAPSequenceUUID" in image_description)
if output_file is not None:
shutil.copy(filename, output_file)
filename = output_file
# write to file
json_desc = json.dumps(image_description)
metadata = ExifEdit(filename)
metadata.add_image_description(json_desc)
metadata.add_orientation(image_description.get("MAPOrientation", 1))
metadata.add_direction(image_description["MAPCompassHeading"]["TrueHeading"])
metadata.add_lat_lon(image_description["MAPLatitude"], image_description["MAPLongitude"])
date_time = datetime.datetime.strptime(image_description["MAPCaptureTime"]+"000", "%Y_%m_%d_%H_%M_%S_%f")
metadata.add_date_time_original(date_time)
metadata.write()
def add_exif_data(filename, data, output_file=None):
"""Add minimal exif data to an image"""
if output_file is not None:
shutil.copy(filename, output_file)
filename = output_file
metadata = ExifEdit(filename)
metadata.add_orientation(data.get("orientation", 1))
metadata.add_direction(data.get("bearing", 0))
metadata.add_lat_lon(data["lat"], data["lon"])
metadata.add_date_time_original(data["capture_time"])
metadata.add_camera_make_model(data["make"], data["model"])
metadata.write()
class ExifEdit(object):
def __init__(self, filename):
"""Initialize the object"""
self.filename = filename
self.ef = None
if (type(filename) is str) or (type(filename) is unicode):
self.ef = JpegFile.fromFile(filename)
else:
filename.seek(0)
self.ef = JpegFile.fromString(filename.getvalue())
try:
if (type(filename) is str) or (type(filename) is unicode):
self.ef = JpegFile.fromFile(filename)
else:
filename.seek(0)
self.ef = JpegFile.fromString(filename.getvalue())
except IOError:
etype, value, traceback = sys.exc_info()
print >> sys.stderr, "Error opening file:", value
except JpegFile.InvalidFile:
etype, value, traceback = sys.exc_info()
print >> sys.stderr, "Error opening file:", value
def add_image_description(self, dict):
"""Add a dict to image description."""
if self.ef is not None:
self.ef.exif.primary.ImageDescription = json.dumps(dict)
def add_orientation(self, orientation):
"""Add image orientation to image."""
self.ef.exif.primary.Orientation = [orientation]
def add_date_time_original(self, date_time):
"""Add date time original."""
self.ef.exif.primary.ExtendedEXIF.DateTimeOriginal = date_time.strftime('%Y:%m:%d %H:%M:%S')
"""Add subsecond if the value exists"""
if date_time.microsecond:
subsec = str(date_time.microsecond).zfill(6)
self.add_subsec_time_original(subsec)
#if date_time.microsecond:
# self.ef.exif.primary.ExtendedEXIF.SubSecTimeOriginal = str(date_time.microsecond).zfill(6)
def add_subsec_time_original(self, subsec):
"""Add subsecond."""
self.ef.exif.primary.ExtendedEXIF.SubSecTimeOriginal = subsec
def add_lat_lon(self, lat, lon):
"""Add lat, lon to gps (lat, lon in float)."""
self.ef.set_geo(float(lat), float(lon))
def add_camera_make_model(self, make, model):
''' Add camera make and model.'''
self.ef.exif.primary.Make = make
self.ef.exif.primary.Model = model
def add_dop(self, dop, perc=100):
"""Add GPSDOP (float)."""
self.ef.exif.primary.GPS.GPSDOP = [Rational(abs(dop * perc), perc)]
def add_altitude(self, altitude, precision=100):
"""Add altitude (pre is the precision)."""
ref = '\x00' if altitude > 0 else '\x01'
self.ef.exif.primary.GPS.GPSAltitude = [Rational(abs(altitude * precision), precision)]
self.ef.exif.primary.GPS.GPSAltitudeRef = [ref]
def add_direction(self, direction, ref="T", precision=100):
"""Add image direction."""
self.ef.exif.primary.GPS.GPSImgDirection = [Rational(abs(direction * precision), precision)]
self.ef.exif.primary.GPS.GPSImgDirectionRef = ref
def write(self, filename=None):
"""Save exif data to file."""
try:
if filename is None:
filename = self.filename
self.ef.writeFile(filename)
except IOError:
type, value, traceback = sys.exc_info()
print >> sys.stderr, "Error saving file:", value
def write_to_string(self):
"""Save exif data to StringIO object."""
return self.ef.writeString()
def write_to_file_object(self):
"""Save exif data to file object."""
return self.ef.writeFd()