使用Python读取CR2(佳能原始图像)头部
我正在尝试从CR2格式的照片中提取拍摄的日期和时间(CR2是佳能相机的原始照片格式)。
我知道CR2的规格,也知道可以使用Python的struct模块来从二进制数据中提取信息。
简单来说,规格说明中提到在标签0x0132 / 306
里可以找到一个长度为20的字符串,也就是日期和时间。
我试着用以下代码获取这个标签:
struct.unpack_from(20*'s', buffer, 0x0132)
但是我得到了
('\x00', '\x00', "'", '\x88, ...[and more crap])
有没有什么想法?
编辑
非常感谢大家的详细解答!这些回答太棒了,我学到了很多关于处理二进制数据的知识。
3 个回答
我发现EXIF.py这个工具可以从.CR2文件中读取EXIF数据,地址在https://github.com/ianare/exif-py。因为.CR2文件是基于.TIFF文件的,所以EXIF.py可以兼容使用。
import EXIF
import time
# Change the filename to be suitable for you
f = open('../DCIM/100CANON/IMG_3432.CR2', 'rb')
data = EXIF.process_file(f)
f.close()
date_str = data['EXIF DateTimeOriginal'].values
# We have the raw data
print date_str
# We can now convert it
date = time.strptime(date_str, '%Y:%m:%d %H:%M:%S')
print date
然后这段代码会输出:
2011:04:30 11:08:44
(2011, 4, 30, 11, 8, 44, 5, 120, -1)
0x0132 不是偏移量,而是日期的标签编号。CR2 或 TIFF 格式是基于目录的格式。你需要根据你要找的(已知的)标签查找相应的条目。
编辑:
首先,你需要判断文件数据是用小端格式还是大端格式保存的。前八个字节指定了文件头,而文件头的前两个字节则指定了字节序。Python 的 struct 模块可以通过在格式字符串前加上 '<' 或 '>' 来处理小端和大端数据。所以,假设 data
是一个包含你的 CR2 图像的缓冲区,你可以通过以下方式处理字节序:
header = data[:8]
endian_flag = "<" if header[:2] == "II" else ">"
格式说明中提到,第一个图像文件目录(IFD)是相对于文件开头的偏移量,这个偏移量在文件头的最后四个字节中指定。所以,要获取第一个 IFD 的偏移量,你可以使用类似下面的代码:
ifd_offset = struct.unpack("{0}I".format(endian_flag), header[4:])[0]
现在你可以继续读取第一个 IFD。你会在指定的文件偏移量找到目录中的条目数量,这个数量占用两个字节。因此,你可以使用以下代码读取第一个 IFD 中的条目数量:
number_of_entries = struct.unpack("{0}H".format(endian_flag), data[ifd_offset:ifd_offset+2])[0]
每个字段条目占用 12 个字节,所以你可以计算 IFD 的长度。在条目数量乘以 12 字节之后,会有另一个 4 字节的偏移量,告诉你下一个目录的位置。这就是处理 TIFF 和 CR2 图像的基本方法。
这里的“魔法”在于注意到每个 12 字节的字段条目的前两个字节是标签 ID。这就是你查找标签 0x0132 的地方。所以,假设你知道第一个 IFD 在文件中的偏移量是 ifd_offset,你可以通过以下方式扫描第一个目录:
current_position = ifd_offset + 2
for field_offset in xrange(current_position, number_of_entries*12, 12):
field_tag = struct.unpack("{0}H".format(endian_flag), data[field_offset:field_offset+2])[0]
field_type = struct.unpack("{0}H".format(endian_flag), data[field_offset+2:field_offset+4])[0]
value_count = struct.unpack("{0}I".format(endian_flag), data[field_offset+4:field_offset+8])[0]
value_offset = struct.unpack("{0}I".format(endian_flag), data[field_offset+8:field_offset+12])[0]
if field_tag == 0x0132:
# You are now reading a field entry containing the date and time
assert field_type == 2 # Type 2 is ASCII
assert value_count == 20 # You would expect a string length of 20 here
date_time = struct.unpack("20s", data[value_offset:value_offset+20])
print date_time
显然,你会想把这个解包过程重构成一个通用函数,并可能把整个格式封装成一个漂亮的类,但这超出了这个例子的范围。你还可以通过将多个格式字符串组合成一个,来缩短解包过程,从而得到一个包含所有字段的大元组,可以解包成不同的变量,我为了清晰起见没有包含这一部分。
你有没有考虑到在你提到的IFD块之前应该有一个头部(根据规范)?
我查了一下规范,它说第一个IFD块是在16字节的头部之后。所以如果我们读取第16和第17个字节(在0x10这个位置),我们应该能得到第一个IFD块中的条目数量。接下来,我们只需要逐个检查这些条目,直到找到一个匹配的标签ID,这个ID能告诉我们日期/时间字符串的字节偏移。
这个方法对我有效:
from struct import *
def FindDateTimeOffsetFromCR2( buffer, ifd_offset ):
# Read the number of entries in IFD #0
(num_of_entries,) = unpack_from('H', buffer, ifd_offset)
print "ifd #0 contains %d entries"%num_of_entries
# Work out where the date time is stored
datetime_offset = -1
for entry_num in range(0,num_of_entries-1):
(tag_id, tag_type, num_of_value, value) = unpack_from('HHLL', buffer, ifd_offset+2+entry_num*12)
if tag_id == 0x0132:
print "found datetime at offset %d"%value
datetime_offset = value
return datetime_offset
if __name__ == '__main__':
with open("IMG_6113.CR2", "rb") as f:
buffer = f.read(1024) # read the first 1kb of the file should be enough to find the date / time
datetime_offset = FindDateTimeOffsetFromCR2(buffer, 0x10)
print unpack_from(20*'s', buffer, datetime_offset)
我示例文件的输出是:
ifd #0 contains 14 entries
found datetime at offset 250
('2', '0', '1', '0', ':', '0', '8', ':', '0', '1', ' ', '2', '3', ':', '4', '5', ':', '4', '6', '\x00')
[编辑] - 一个修订过的/更详细的示例
from struct import *
recognised_tags = {
0x0100 : 'imageWidth',
0x0101 : 'imageLength',
0x0102 : 'bitsPerSample',
0x0103 : 'compression',
0x010f : 'make',
0x0110 : 'model',
0x0111 : 'stripOffset',
0x0112 : 'orientation',
0x0117 : 'stripByteCounts',
0x011a : 'xResolution',
0x011b : 'yResolution',
0x0128 : 'resolutionUnit',
0x0132 : 'dateTime',
0x8769 : 'EXIF',
0x8825 : 'GPS data'};
def GetHeaderFromCR2( buffer ):
# Unpack the header into a tuple
header = unpack_from('HHLHBBL', buffer)
print "\nbyte_order = 0x%04X"%header[0]
print "tiff_magic_word = %d"%header[1]
print "tiff_offset = 0x%08X"%header[2]
print "cr2_magic_word = %d"%header[3]
print "cr2_major_version = %d"%header[4]
print "cr2_minor_version = %d"%header[5]
print "raw_ifd_offset = 0x%08X\n"%header[6]
return header
def FindDateTimeOffsetFromCR2( buffer, ifd_offset, endian_flag ):
# Read the number of entries in IFD #0
(num_of_entries,) = unpack_from(endian_flag+'H', buffer, ifd_offset)
print "Image File Directory #0 contains %d entries\n"%num_of_entries
# Work out where the date time is stored
datetime_offset = -1
# Go through all the entries looking for the datetime field
print " id | type | number | value "
for entry_num in range(0,num_of_entries):
# Grab this IFD entry
(tag_id, tag_type, num_of_value, value) = unpack_from(endian_flag+'HHLL', buffer, ifd_offset+2+entry_num*12)
# Print out the entry for information
print "%04X | %04X | %08X | %08X "%(tag_id, tag_type, num_of_value, value),
if tag_id in recognised_tags:
print recognised_tags[tag_id]
# If this is the datetime one we're looking for, make a note of the offset
if tag_id == 0x0132:
assert tag_type == 2
assert num_of_value == 20
datetime_offset = value
return datetime_offset
if __name__ == '__main__':
with open("IMG_6113.CR2", "rb") as f:
# read the first 1kb of the file should be enough to find the date/time
buffer = f.read(1024)
# Grab the various parts of the header
(byte_order, tiff_magic_word, tiff_offset, cr2_magic_word, cr2_major_version, cr2_minor_version, raw_ifd_offset) = GetHeaderFromCR2(buffer)
# Set the endian flag
endian_flag = '@'
if byte_order == 0x4D4D:
# motorola format
endian_flag = '>'
elif byte_order == 0x4949:
# intel format
endian_flag = '<'
# Search for the datetime entry offset
datetime_offset = FindDateTimeOffsetFromCR2(buffer, 0x10, endian_flag)
datetime_string = unpack_from(20*'s', buffer, datetime_offset)
print "\nDatetime: "+"".join(datetime_string)+"\n"