diff --git a/facemovie/Facemoviefier.py b/facemovie/Facemoviefier.py index df618de..5c2822b 100644 --- a/facemovie/Facemoviefier.py +++ b/facemovie/Facemoviefier.py @@ -105,4 +105,4 @@ class Facemoviefier(): if __name__ == '__main__': my_job = Facemoviefier() my_job.run() - print "Facemovie finished finally !" \ No newline at end of file + print "Facemovie finished finally !" diff --git a/facemovie/lib/__init__.py b/facemovie/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/facemovie/lib/exif.py b/facemovie/lib/exif.py new file mode 100755 index 0000000..a90b3b3 --- /dev/null +++ b/facemovie/lib/exif.py @@ -0,0 +1,675 @@ +## =========================================================================== +## NAME: exif +## TYPE: python script +## CONTENT: library for parsing EXIF headers +## =========================================================================== +## AUTHORS: rft Robert F. Tobler +## =========================================================================== +## HISTORY: +## +## 10-Aug-01 11:14:20 rft last modification +## 09-Aug-01 16:51:05 rft created +## =========================================================================== + +import string + +ASCII = 0 +BINARY = 1 + +## --------------------------------------------------------------------------- +## 'Tiff' +## This class provides the Exif header as a file-like object and hides +## endian-specific data access. +## --------------------------------------------------------------------------- + +class Tiff: + def __init__(self, data, file = None): + self.data = data + + self.file = file + self.endpos = len(data) + + self.pos = 0 + if self.data[0:2] == "MM": + self.S0 = 1 ; self.S1 = 0 + self.L0 = 3 ; self.L1 = 2 ; self.L2 = 1 ; self.L3 = 0 + else: + self.S0 = 0 ; self.S1 = 1 + self.L0 = 0 ; self.L1 = 1 ; self.L2 = 2 ; self.L3 = 3 + + def seek(self, pos): + self.pos = pos + if self.pos > self.endpos: + self.data += self.file.read( self.endpos - self.pos ) + + def tell(self): + return self.pos + + def read(self, len): + old_pos = self.pos + self.pos = self.pos + len + if self.pos > self.endpos: + self.data += self.file.read( self.endpos - self.pos ) + return self.data[old_pos:self.pos] + + def byte(self, signed = 0): + pos = self.pos + self.pos = pos + 1 + if self.pos > self.endpos: + self.data += self.file.read( self.endpos - self.pos ) + hi = ord(self.data[pos]) + if hi > 127 and signed: hi = hi - 256 + return hi + + def short(self, signed = 0): + pos = self.pos + self.pos = pos + 2 + if self.pos > self.endpos: + self.data += self.file.read( self.endpos - self.pos ) + hi = ord(self.data[pos+self.S1]) + if hi > 127 and signed: hi = hi - 256 + return (hi<<8)|ord(self.data[pos+self.S0]) + + def long(self, signed = 0): + pos = self.pos + self.pos = pos + 4 + if self.pos > self.endpos: + self.data += self.file.read( self.endpos - self.pos ) + hi = ord(self.data[pos+self.L3]) + if hi > 127 and not signed: hi = long(hi) + return (hi<<24) | (ord(self.data[pos+self.L2])<<16) \ + | (ord(self.data[pos+self.L1])<<8) | ord(self.data[pos+self.L0]) + +## --------------------------------------------------------------------------- +## 'Type', 'Type...' +## A small hierarchy of objects that knows how to read each type of tag +## field from a tiff file, and how to pretty-print each type of tag. +## +## The method 'read' is used to read a tag with a given count from the +## supplied tiff file. +## +## The method 'str_table' is used to pretty-print the value table of a +## tag if no special format for this tag is present. +## --------------------------------------------------------------------------- + +class Type: + def str_table(self, table): + result = [] + for val in table: result.append(self.str_value(val)) + return string.join(result, ", ") + def str_value(self, val): + return str(val) + +class TypeByte(Type): + def __init__(self): self.name = "BYTE" ; self.len = 1 + def read(self, tiff, count): + result = [] + for i in range(0, count): table.append(tiff.byte()) + return table + +class TypeAscii: + def __init__(self): self.name = "ASCII" ; self.len = 1 + def read(self, tiff, count): + return tiff.read(count-1) + def str_table(self, table): + return string.strip(table) + +class TypeShort(Type): + def __init__(self): self.name = "SHORT" ; self.len = 2 + def read(self, tiff, count): + table = [] + for i in range(0, count): table.append(tiff.short()) + return table + +class TypeLong(Type): + def __init__(self): self.name = "LONG" ; self.len = 4 + def read(self, tiff, count): + table = [] + for i in range(0, count): table.append(tiff.long()) + return table + +class TypeRatio(Type): + def __init__(self): self.name = "RATIO" ; self.len = 8 + def read(self, tiff, count): + table = [] + for i in range(0, count): table.append((tiff.long(), tiff.long())) + return table + def str_value(self, val): + return "%d/%d" %(val[0], val[1]) + +class TypeSByte(Type): + def __init__(self): self.name = "SBYTE" ; self.len = 1 + def read(self, tiff, count): + table = [] + for i in range(0, count): table.append(tiff.byte(signed=1)) + return table + +class TypeUndef(TypeByte): + def __init__(self): self.name = "UNDEF" ; self.len = 1 + def read(self, tiff, count): + return tiff.read(count) + def str_table(self, table): + result = map( lambda x: str(ord(x)), table ) + # this next line is somehow much more efficient than using str() + return '[ ' + string.join( result, ',' ) + ' ]' + +class TypeSShort(Type): + def __init__(self): self.name = "SSHORT" ; self.len = 2 + def read(self, tiff, count): + table = [] + for i in range(0, count): table.append(tiff.short(signed=1)) + return table + +class TypeSLong(Type): + def __init__(self): self.name = "SLONG" ; self.len = 4 + def read(self, tiff, count): + table = [] + for i in range(0, count): table.append(tiff.short(signed=1)) + return table + +class TypeSRatio(TypeRatio): + def __init__(self): self.name = "SRATIO" ; self.len = 8 + def read(self, tiff, count): + table = [] + for i in range(0, count): + table.append((tiff.long(signed=1), tiff.long(signed=1))) + return table + +class TypeFloat: + def __init__(self): self.name = "FLOAT" ; self.len = 4 + def read(self, tiff, count): + return tiff.read(4 * count) + +class TypeDouble: + def __init__(self): self.name = "DOUBLE" ; self.len = 8 + def read(self, tiff, count): + return tiff.read(8 * count) + +TYPE_MAP = { + 1: TypeByte(), + 2: TypeAscii(), + 3: TypeShort(), + 4: TypeLong(), + 5: TypeRatio(), + 6: TypeSByte(), + 7: TypeUndef(), + 8: TypeSShort(), + 9: TypeSLong(), + 10: TypeSRatio(), + 11: TypeFloat(), + 12: TypeDouble(), +} + +## --------------------------------------------------------------------------- +## 'Tag' +## A tag knows about its name and an optional format. +## --------------------------------------------------------------------------- + +class Tag: + def __init__(self, name, format = None): + self.name = name + self.format = format + +## --------------------------------------------------------------------------- +## 'Format', 'Format...' +## A small hierarchy of objects that provide special formats for certain +## tags in the EXIF standard. +## +## The method 'str_table' is used to pretty-print the value table of a +## tag. It gets the table of tags that have already been parsed as a +## parameter in order to handle vendor specific extensions. +## --------------------------------------------------------------------------- + +class Format: + def str_table(self, table, value_map): + result = [] + for val in table: result.append(self.str_value(val)) + return string.join(result, ", ") + +class FormatMap: + def __init__(self, map, make_ext = {}): + self.map = map + self.make_ext = make_ext + def str_table(self, table, value_map): + if len(table) == 1: + key = table[0] + else: + key = table + value = self.map.get(key) + if not value: + make = value_map.get("Make") + if make: value = self.make_ext.get(make,{}).get(key) + if not value: value = `key` + return value + +class FormatRatioAsFloat(Format): + def str_value(self, val): + if val[1] == 0: return "0.0" + return "%g" % (val[0]/float(val[1])) + +class FormatRatioAsBias(Format): + def str_value(self, val): + if val[1] == 0: return "0.0" + if val[0] > 0: return "+%3.1f" % (val[0]/float(val[1])) + if val[0] < 0: return "-%3.1f" % (-val[0]/float(val[1])) + return "0.0" + +def format_time(t): + if t > 0.5: return "%g" % t + if t > 0.1: return "1/%g" % (0.1*int(10/t+0.5)) + return "1/%d" % int(1/t+0.5) + +class FormatRatioAsTime(Format): + def str_value(self, val): + if val[1] == 0: return "0.0" + return format_time(val[0]/float(val[1])) + +class FormatRatioAsApexTime(Format): + def str_value(self, val): + if val[1] == 0: return "0.0" + return format_time(pow(0.5, val[0]/float(val[1]))) + +## --------------------------------------------------------------------------- +## The EXIF parser is completely table driven. +## --------------------------------------------------------------------------- + +## --------------------------------------------------------------------------- +## Nikon 99x MakerNote Tags http://members.tripod.com/~tawba/990exif.htm +## --------------------------------------------------------------------------- +NIKON_99x_MAKERNOTE_TAG_MAP = { + 0x0001: Tag('MN_0x0001'), + 0x0002: Tag('MN_ISOSetting'), + 0x0003: Tag('MN_ColorMode'), + 0x0004: Tag('MN_Quality'), + 0x0005: Tag('MN_Whitebalance'), + 0x0006: Tag('MN_ImageSharpening'), + 0x0007: Tag('MN_FocusMode'), + 0x0008: Tag('MN_FlashSetting'), + 0x000A: Tag('MN_0x000A'), + 0x000F: Tag('MN_ISOSelection'), + 0x0080: Tag('MN_ImageAdjustment'), + 0x0082: Tag('MN_AuxiliaryLens'), + 0x0085: Tag('MN_ManualFocusDistance', FormatRatioAsFloat() ), + 0x0086: Tag('MN_DigitalZoomFactor', FormatRatioAsFloat() ), + 0x0088: Tag('MN_AFFocusPosition', + FormatMap({ + '\00\00\00\00': 'Center', + '\00\01\00\00': 'Top', + '\00\02\00\00': 'Bottom', + '\00\03\00\00': 'Left', + '\00\04\00\00': 'Right', + })), + 0x008f: Tag('MN_0x008f'), + 0x0094: Tag('MN_Saturation', + FormatMap({ + 0: '0', + 1: '1', + 2: '2', + -3: 'B&W', + -2: '-2', + -1: '-1', + })), + 0x0095: Tag('MN_NoiseReduction'), + 0x0010: Tag('MN_DataDump'), + 0x0011: Tag('MN_0x0011'), + 0x0e00: Tag('MN_0x0e00'), +} + +## --------------------------------------------------------------------------- +## 'MakerNote...' +## This currently only parses Nikon E990, and Nikon E995 MakerNote tags. +## Additional objects with a 'parse' function can be placed here to add +## support for other cameras. This function adds the pretty-printed +## information in the MakerNote to the 'value_map' that is supplied. +## --------------------------------------------------------------------------- + +class MakerNoteTags: + def __init__(self, tag_map): + self.tag_map = tag_map + def parse(self, tiff, mode, tag_len, value_map): + num_entries = tiff.short() + if verbose_opt: print num_entries, 'tags' + for field in range(0, num_entries): + parse_tag(tiff, mode, value_map, self.tag_map) + +NIKON_99x_MAKERNOTE = MakerNoteTags(NIKON_99x_MAKERNOTE_TAG_MAP) + +## --------------------------------------------------------------------------- +## 'MAKERNOTE_MAP' +## Interpretation of the MakerNote tag indexed by 'Make', 'Model' pairs. +## --------------------------------------------------------------------------- + +MAKERNOTE_MAP = { + ('NIKON', 'E990'): NIKON_99x_MAKERNOTE, + ('NIKON', 'E995'): NIKON_99x_MAKERNOTE, +} + +## --------------------------------------------------------------------------- +## 'TAG_MAP' +## This is the map of tags that drives the parser. +## --------------------------------------------------------------------------- + +TAG_MAP = { + 0x00fe: Tag('NewSubFileType'), + 0x0100: Tag('ImageWidth'), + 0x0101: Tag('ImageLength'), + 0x0102: Tag('BitsPerSample'), + 0x0103: Tag('Compression'), + 0x0106: Tag('PhotometricInterpretation'), + 0x010a: Tag('FillOrder'), + 0x010d: Tag('DocumentName'), + 0x010e: Tag('ImageDescription'), + 0x010f: Tag('Make'), + 0x0110: Tag('Model'), + 0x0111: Tag('StripOffsets'), + 0x0112: Tag('Orientation'), + 0x0115: Tag('SamplesPerPixel'), + 0x0116: Tag('RowsPerStrip'), + 0x0117: Tag('StripByteCounts'), + 0x011a: Tag('XResolution'), + 0x011b: Tag('YResolution'), + 0x011c: Tag('PlanarConfiguration'), + 0x0128: Tag('ResolutionUnit', + FormatMap({ + 1: 'Not Absoulute', + 2: 'Inch', + 3: 'Centimeter' + })), + 0x012d: Tag('TransferFunction'), + 0x0131: Tag('Software'), + 0x0132: Tag('DateTime'), + 0x013b: Tag('Artist'), + 0x013e: Tag('WhitePoint'), + 0x013f: Tag('PrimaryChromaticities'), + 0x0142: Tag('TileWidth'), + 0x0143: Tag('TileLength'), + 0x0144: Tag('TileOffsets'), + 0x0145: Tag('TileByteCounts'), + 0x014a: Tag('SubIFDs'), + 0x0156: Tag('TransferRange'), + 0x015b: Tag('JPEGTables'), + 0x0201: Tag('JPEGInterchangeFormat'), + 0x0202: Tag('JPEGInterchangeFormatLength'), + 0x0211: Tag('YCbCrCoefficients'), + 0x0212: Tag('YCbCrSubSampling'), + 0x0213: Tag('YCbCrPositioning'), + 0x0214: Tag('ReferenceBlackWhite'), + 0x828d: Tag('CFARepeatPatternDim'), + 0x828e: Tag('CFAPattern'), + 0x828f: Tag('BatteryLevel'), + 0x8298: Tag('Copyright'), + 0x829a: Tag('ExposureTime', FormatRatioAsTime() ), + 0x829d: Tag('FNumber', FormatRatioAsFloat() ), + 0x83bb: Tag('IPTC_NAA'), + 0x8773: Tag('InterColorProfile'), + 0x8822: Tag('ExposureProgram', + FormatMap({ + 0: 'Unidentified', + 1: 'Manual', + 2: 'Program Normal', + 3: 'Aperture Priority', + 4: 'Shutter Priority', + 5: 'Program Creative', + 6: 'Program Action', + 7: 'Portrait Mode', + 8: 'Landscape Mode', + })), + 0x8824: Tag('SpectralSensitivity'), + 0x8825: Tag('GPSInfo'), + 0x8827: Tag('ISOSpeedRatings'), + 0x8828: Tag('OECF'), + 0x8829: Tag('Interlace'), + 0x882a: Tag('TimeZoneOffset'), + 0x882b: Tag('SelfTimerMode'), + 0x8769: Tag('ExifOffset'), + 0x9000: Tag('ExifVersion'), + 0x9003: Tag('DateTimeOriginal'), + 0x9004: Tag('DateTimeDigitized'), + 0x9101: Tag('ComponentsConfiguration'), + 0x9102: Tag('CompressedBitsPerPixel'), + 0x9201: Tag('ShutterSpeedValue', FormatRatioAsApexTime() ), + 0x9202: Tag('ApertureValue', FormatRatioAsFloat() ), + 0x9203: Tag('BrightnessValue'), + 0x9204: Tag('ExposureBiasValue', FormatRatioAsBias() ), + 0x9205: Tag('MaxApertureValue', FormatRatioAsFloat() ), + 0x9206: Tag('SubjectDistance'), + 0x9207: Tag('MeteringMode', + FormatMap({ + 0: 'Unidentified', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot', + }, + make_ext = { + 'NIKON': { 5: 'Matrix' }, + })), + 0x9208: Tag('LightSource', + FormatMap({ + 0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten', + 10: 'Flash', + 17: 'Standard light A', + 18: 'Standard light B', + 19: 'Standard light C', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 255: 'Other' + })), + 0x9209: Tag('Flash', + FormatMap({ + 0: 'no', + 1: 'fired', + 5: 'fired (?)', # no return sensed + 7: 'fired (!)', # return sensed + 9: 'fill fired', + 13: 'fill fired (?)', + 15: 'fill fired (!)', + 16: 'off', + 24: 'auto off', + 25: 'auto fired', + 29: 'auto fired (?)', + 31: 'auto fired (!)', + 32: 'not available' + })), + 0x920a: Tag('FocalLength', FormatRatioAsFloat()), + 0x920b: Tag('FlashEnergy'), + 0x920c: Tag('SpatialFrequencyResponse'), + 0x920d: Tag('Noise'), + 0x920e: Tag('FocalPlaneXResolution'), + 0x920f: Tag('FocalPlaneYResolution'), + 0x9210: Tag('FocalPlaneResolutionUnit', + FormatMap({ + 1: 'Inch', + 2: 'Meter', + 3: 'Centimeter', + 4: 'Millimeter', + 5: 'Micrometer', + })), + 0x9211: Tag('ImageNumber'), + 0x9212: Tag('SecurityClassification'), + 0x9213: Tag('ImageHistory'), + 0x9214: Tag('SubjectLocation'), + 0x9215: Tag('ExposureIndex'), + 0x9216: Tag('TIFF_EPStandardID'), + 0x9217: Tag('SensingMethod'), + 0x927c: Tag('MakerNote'), + 0xa001: Tag('ColorSpace'), + 0xa002: Tag('ExifImageWidth'), + 0xa003: Tag('ExifImageHeight'), + 0xa005: Tag('Interoperability_IFD_Pointer'), +} + +def parse_tag(tiff, mode, value_map, tag_map): + tag_id = tiff.short() + type_no = tiff.short() + count = tiff.long() + + tag = tag_map.get(tag_id) + if not tag: tag = Tag("Tag0x%x" % tag_id) + + type = TYPE_MAP[type_no] + + if verbose_opt: + print "%30s:" % tag.name, + if verbose_opt > 1: print "%6s %3d" % (type.name, count), + + pos = tiff.tell() + tag_len = type.len * count + if tag_len > 4: + tag_offset = tiff.long() + tiff.seek(tag_offset) + if verbose_opt > 1: print "@%03x :" % tag_offset, + else: + if verbose_opt > 1: print " :", + + if tag.name == 'MakerNote': + makernote = MAKERNOTE_MAP.get((value_map['Make'],value_map['Model'])) + if makernote: + makernote.parse(tiff, mode, tag_len, value_map) + value_table = None + else: + value_table = type.read(tiff, count) + else: + value_table = type.read(tiff, count) + + if value_table: + if mode == ASCII: + if tag.format: + val = tag.format.str_table(value_table, value_map) + else: + val = type.str_table(value_table) + else: + val = value_table + value_map[tag.name] = val + if verbose_opt: + if value_map.has_key(tag.name): print val, + print + + tiff.seek(pos+4) + +def parse_ifd(tiff, mode, offset, value_map): + tiff.seek(offset) + num_entries = tiff.short() + if verbose_opt > 1: + print "%30s: %3d @%03x" % ("IFD", num_entries, offset) + for field in range(0, num_entries): + parse_tag(tiff, mode, value_map, TAG_MAP) + offset = tiff.long() + return offset + +def parse_tiff(tiff, mode): + value_map = {} + order = tiff.read(2) + if tiff.short() == 42: + offset = tiff.long() + while offset > 0: + offset = parse_ifd(tiff, mode, offset, value_map) + + if offset == 0: # special handling to get + if value_map.has_key('ExifOffset'): # next EXIF IFD + offset = value_map['ExifOffset'] + if mode == ASCII: + offset = int(offset) + else: + offset = offset[0] + del value_map['ExifOffset'] + return value_map + + +def parse_tiff_fortiff(tiff, mode): + + """Parse a real tiff file, not an EXIF tiff file.""" + + value_map = {} + order = tiff.read(2) + if tiff.short() == 42: + offset = tiff.long() + + # build a list of small tags, we don't want to parse the huge stuff + stags = [] + while offset > 0: + tiff.seek(offset) + num_entries = tiff.short() + + if verbose_opt > 1: + print "%30s: %3d @%03x" % ("IFD", num_entries, offset) + + for field in range(0, num_entries): + pos = tiff.tell() + + tag_id = tiff.short() + type_no = tiff.short() + length = tiff.long() + valoff = tiff.long() + #print TAG_MAP[ tag_id ].name, length + + if tag_id == 0x8769: + if mode == ASCII: + valoff = int(valoff) + else: + valoff = valoff[0] + stags += [ (tag_id, valoff) ] + + elif length < 1024: + stags += [ (tag_id, pos) ] + + offset = tiff.long() + + # IMPORTANT: we read the 0st ifd only for this. + # The second is reserved for the thumbnail, whatever is in there + # we ignore. + break + + for p in stags: + (tag_id, pos) = p + + if tag_id == 0x8769: + parse_ifd(tiff, mode, pos, value_map) + else: + tiff.seek( pos ) + parse_tag(tiff, mode, value_map, TAG_MAP) + + return value_map + + +## --------------------------------------------------------------------------- +## 'parse' +## This is the function for parsing the EXIF structure in a file given +## the path of the file. +## The function returns a map which contains all the exif tags that +## were found, indexed by the name of the tag. The value of each tag +## is already converted to a nicely formatted string. +## --------------------------------------------------------------------------- +def parse(path_name, verbose = 0, mode = 0): + global verbose_opt + verbose_opt = verbose + try: + file = open(path_name, "rb") + data = file.read(12) + if data[0:4] == '\377\330\377\341' and data[6:10] == 'Exif': + # JPEG + length = ord(data[4]) * 256 + ord(data[5]) + if verbose > 1: + print '%30s: %d' % ("EXIF header length",length) + tiff = Tiff(file.read(length-8)) + value_map = parse_tiff(tiff, mode) + elif data[0:2] in [ 'II', 'MM' ] and ord(data[2]) == 42: + # Tiff + tiff = Tiff(data,file) + tiff.seek(0) + value_map = parse_tiff_fortiff(tiff, mode) + else: + # Some other file format, sorry. + value_map = {} + + file.close() + except IOError: + value_map = {} + + return value_map + +## =========================================================================== diff --git a/test/exif_test.py b/test/exif_test.py new file mode 100755 index 0000000..dc42475 --- /dev/null +++ b/test/exif_test.py @@ -0,0 +1,9 @@ +import sys +sys.path.append("../facemovie/lib") +import exif + +path = "../data/input/Axel/2012-01-15-06h13m41DSCN9849.JPG" + +plop = exif.parse(path)['DateTime'] +print plop +