Last active
March 31, 2016 02:11
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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