Skip to content

Instantly share code, notes, and snippets.

@jjones646
Last active March 31, 2016 02:11
Show Gist options
  • Save jjones646/74f0db7178e14cb74346 to your computer and use it in GitHub Desktop.
Save jjones646/74f0db7178e14cb74346 to your computer and use it in GitHub Desktop.
Set an image's metadata for panoramic viewing using a different image as reference.
#!/usr/bin/env python2
import re
import struct
import argparse
import StringIO
import pyexiv2 as pe
from pprint import PrettyPrinter
from os.path import abspath, isfile
def query_yes_no(question, default=None):
""" Ask a yes/no question and return the answer.
"""
valid = {'yes': True, 'y': True, 'no': False, 'n': False}
prompt = '[y/n]'
if default == 'yes':
prompt = '[Y/n]'
elif default == 'no':
prompt = '[y/N]'
while True:
print(question + ' ' + prompt + ' ')
c = raw_input().lower()
if default is not None and c == '':
return valid[default]
elif c in valid:
return valid[c]
else:
print("Please select a choice <[yes/y]|[no/n]>\n")
def to_deg(value, loc):
if value < 0:
loc_value = loc[0]
elif value > 0:
loc_value = loc[1]
else:
loc_value = ""
abs_value = abs(value)
deg = int(abs_value)
t1 = (abs_value - deg) * 60
min = int(t1)
sec = round((t1 - min) * 60, 5)
return (deg, min, sec, loc_value)
def show_meta(md):
mdKeys = md.iptc_keys + md.exif_keys + md.xmp_keys
print(19 * '=' + ' BEGIN METADATA ' + 19 * '=')
try:
maxLen = len(max(mdKeys, key=len))
for k in mdKeys:
v = md[k].value
print('{}{} => {}'.format(k, ' ' * (maxLen - len(k)), v))
except:
pass
print(20 * '=' + ' END METADATA ' + 20 * '=')
def get_image_info(data):
''' http://stackoverflow.com/questions/1507084/how-to-check-dimensions-of-all-images-in-a-directory-using-python#answer-3175473
'''
data = str(data)
size = len(data)
height = -1
width = -1
content_type = ''
# handle GIFs
if (size >= 10) and data[:6] in ('GIF87a', 'GIF89a'):
# Check to see if content_type is correct
content_type = 'image/gif'
w, h = struct.unpack("<HH", data[6:10])
width = int(w)
height = int(h)
# See PNG 2. Edition spec (http://www.w3.org/TR/PNG/)
# Bytes 0-7 are below, 4-byte chunk length, then 'IHDR'
# and finally the 4-byte width, height
elif ((size >= 24) and data.startswith('\211PNG\r\n\032\n') and (data[12:16] == 'IHDR')):
content_type = 'image/png'
w, h = struct.unpack(">LL", data[16:24])
width = int(w)
height = int(h)
# Maybe this is for an older PNG version.
elif (size >= 16) and data.startswith('\211PNG\r\n\032\n'):
# Check to see if we have the right content type
content_type = 'image/png'
w, h = struct.unpack(">LL", data[8:16])
width = int(w)
height = int(h)
# handle JPEGs
elif (size >= 2) and data.startswith('\377\330'):
content_type = 'image/jpeg'
jpeg = StringIO.StringIO(data)
jpeg.read(2)
b = jpeg.read(1)
try:
while (b and ord(b) != 0xDA):
while (ord(b) != 0xFF):
b = jpeg.read(1)
while (ord(b) == 0xFF):
b = jpeg.read(1)
if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
jpeg.read(3)
h, w = struct.unpack(">HH", jpeg.read(4))
break
else:
jpeg.read(int(struct.unpack(">H", jpeg.read(2))[0]) - 2)
b = jpeg.read(1)
width = int(w)
height = int(h)
except struct.error:
pass
except ValueError:
pass
return content_type, width, height
def set_gps_location(md, lat, lng):
"""Adds GPS position as EXIF metadata
file_name -- image file
lat -- latitude (as float)
lng -- longitude (as float)
"""
lat_deg = to_deg(lat, ["S", "N"])
lng_deg = to_deg(lng, ["W", "E"])
# convert decimal coordinates into degrees, munutes and seconds
exiv_lat = (pe.Rational(lat_deg[0] * 60 + lat_deg[1], 60), pe.Rational(lat_deg[2] * 100, 6000), pe.Rational(0, 1))
exiv_lng = (pe.Rational(lng_deg[0] * 60 + lng_deg[1], 60), pe.Rational(lng_deg[2] * 100, 6000), pe.Rational(0, 1))
# set the fields and return the metadata object
md["Exif.GPSInfo.GPSLatitude"] = exiv_lat
md["Exif.GPSInfo.GPSLatitudeRef"] = lat_deg[3]
md["Exif.GPSInfo.GPSLongitude"] = exiv_lng
md["Exif.GPSInfo.GPSLongitudeRef"] = lng_deg[3]
md["Exif.Image.GPSTag"] = 654
md["Exif.GPSInfo.GPSMapDatum"] = "WGS-84"
md["Exif.GPSInfo.GPSVersionID"] = '2 0 0 0'
return md
def main(args):
"""Main login function for the script
"""
mdSrc = pe.ImageMetadata(args.infile.name)
mdSrc.read()
if not isinstance(args.dst_files, list):
args.dst_files = [args.dst_files]
if args.dst_files[0] is None:
show_meta(mdSrc)
print('No changes made')
return
# iterate over all files
for f in args.dst_files:
# get the metadata for destination image
mdDst = pe.ImageMetadata(f.name)
mdDst.read()
# copy over the metadata from the source to the destination files
for k in mdSrc.exif_keys:
try:
if re.compile('gps', re.I).search(k):
mdDst[k] = pe.ExifTag(k, mdSrc[k].value)
except pe.exif.ExifValueError:
pass
for k in mdSrc.xmp_keys:
try:
mdDst[k] = pe.XmpTag(k, mdSrc[k].value)
except:
pass
# now, overwrite the required xmp metadata values
mdDst['Xmp.GPano:UsePanoramaViewer'] = pe.XmpTag('Xmp.GPano.UsePanoramaViewer', 'True')
mdDst['Xmp.GPano.SourcePhotosCount'] = pe.XmpTag('Xmp.GPano.SourcePhotosCount', str(4))
mdDst['Xmp.GPano.PoseHeadingDegrees'] = pe.XmpTag('Xmp.GPano.PoseHeadingDegrees', str(350.0))
mdDst['Xmp.GPano.InitialViewHeadingDegrees'] = pe.XmpTag('Xmp.GPano.InitialViewHeadingDegrees', str(90.0))
mdDst['Xmp.GPano.InitialViewPitchDegrees'] = pe.XmpTag('Xmp.GPano.InitialViewPitchDegrees', str(0.0))
mdDst['Xmp.GPano.InitialViewRollDegrees'] = pe.XmpTag('Xmp.GPano.InitialViewRollDegrees', str(0.0))
mdDst['Xmp.GPano.InitialHorizontalFOVDegrees'] = pe.XmpTag('Xmp.GPano.InitialHorizontalFOVDegrees', str(80.0))
# set the CroppedAreaImageWidthPixels & CroppedAreaImageHeightPixels to the actual image size
with open(f.name, 'r') as fdata:
imageData = fdata.read()
(imgType, imgW, imgH) = get_image_info(imageData)
mdDst['Xmp.GPano.CroppedAreaImageWidthPixels'] = pe.XmpTag('Xmp.GPano.CroppedAreaImageWidthPixels', str(imgW))
mdDst['Xmp.GPano.CroppedAreaImageHeightPixels'] = pe.XmpTag('Xmp.GPano.CroppedAreaImageHeightPixels', str(imgH))
mdDst['Xmp.GPano.FullPanoWidthPixels'] = pe.XmpTag('Xmp.GPano.FullPanoWidthPixels', str(imgW))
mdDst['Xmp.GPano.FullPanoHeightPixels'] = pe.XmpTag('Xmp.GPano.FullPanoHeightPixels', str(imgH))
# and set these to zero
mdDst['Xmp.GPano.CroppedAreaLeftPixels'] = pe.XmpTag('Xmp.GPano.CroppedAreaLeftPixels', str(0))
mdDst['Xmp.GPano.CroppedAreaTopPixels'] = pe.XmpTag('Xmp.GPano.CroppedAreaTopPixels', str(0))
# set exposure lock to true
mdDst['Xmp.GPano.ExposureLockUsed'] = pe.XmpTag('Xmp.GPano.ExposureLockUsed', 'True')
if args.gps:
set_gps_location(mdDst, args.gps[0], args.gps[1])
show_meta(mdDst)
if query_yes_no('Would you like to write the above metadata to {}?'.format(f.name), default='no'):
# write the metadata to the image
mdDst.write()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Set image metadata.')
parser.add_argument('infile', type=argparse.FileType('r'), help='the image to set the metadata from')
parser.add_argument('-d', '--destination', dest='dst_files', type=argparse.FileType('rw'), help='the file that will be overwritten with the given file\'s metadata')
parser.add_argument('-g', '--gps', dest='gps', type=float, nargs=2, help='the longitude and latitude coordinates to set for the output image')
args = parser.parse_args()
main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment