Чтение заголовка CR2 (Raw Canon Image) с использованием Python - PullRequest
10 голосов
/ 13 сентября 2010

Я пытаюсь извлечь дату / время, когда снимок был сделан с CR2 (формат Canon для необработанных снимков).

Я знаю спецификацию CR2 и знаю, что могу использовать модуль Python struct для извлечения фрагментов из двоичного буфера.

Вкратце, в спецификации сказано, что в теге 0x0132 / 306 я могу найти строку длиной 20 - дату и время.

Я пытался получить этот тег, используя:

struct.unpack_from(20*'s', buffer, 0x0132)

но я получаю

('\x00', '\x00', "'", '\x88, ...[and more crap])

Есть идеи?

Редактировать

Большое спасибо за тщательные усилия! Ответы феноменальны, и я многое узнал об обработке двоичных данных.

Ответы [ 3 ]

7 голосов
/ 13 сентября 2010

Учли ли вы заголовок, который должен (в соответствии со спецификацией) предшествовать блоку IFD, о котором вы говорите?

Я просмотрел спецификацию, и она говорит, что первый блок IFD следует за 16 байтами.заголовок.Таким образом, если мы читаем байты 16 и 17 (со смещением 0x10 hex), мы должны получить количество записей в первом блоке IFD.Тогда нам просто нужно искать в каждой записи, пока мы не найдем соответствующий идентификатор тега, который (как я читал) дает нам смещение в байтах вашей строки даты / времени.

Это работает для меня:

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"
6 голосов
/ 13 сентября 2010

0x0132 - это не смещение, это номер тега даты.CR2 или TIFF, соответственно, является форматом на основе каталога.Вы должны найти запись по указанному вами (известному) тегу.

Редактировать : Хорошо, прежде всего, вам нужно прочитать, если данные файла сохранены с использованиемформат little или big-endian.Первые восемь байтов задают заголовок, а первые два байта этого заголовка определяют порядковый номер.Структурный модуль Python позволяет вам обрабатывать небольшие и большие порядковые данные, добавляя префикс формата в '<' или '>'.Таким образом, предполагая, что data является буфером, содержащим ваше изображение CR2, вы можете обрабатывать порядковый номер с помощью

header = data[:8]
endian_flag = "<" if header[:2] == "II" else ">"

В спецификации формата указывается, что первый каталог файла изображения начинается со смещением относительно начала файла,со смещением, указанным в последних 4 байтах заголовка.Таким образом, чтобы получить смещение к первому 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.После number_of_entries * 12 байтов будет еще одно смещение длиной 4 байта, сообщающее вам, где искать следующий каталог.Это в основном то, как вы работаете с изображениями TIFF и CR2.

"Волшебство" здесь заключается в том, чтобы заметить, что при каждой из 12-байтовых записей поля первые два байта будут идентификатором тега.И вот где вы ищите свой тег 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

Вы, очевидно, захотите реорганизовать эту распаковку в общую функцию и, вероятно, обернутьВесь формат в хороший класс, но это выходит за рамки этого примера.Вы также можете сократить распаковку, объединив несколько строк формата в одну, получив больший кортеж, содержащий все поля, которые вы можете распаковать, в различные переменные, которые я оставил для ясности.

4 голосов
/ 06 ноября 2011

Я обнаружил, что EXIF.py из https://github.com/ianare/exif-py читает данные EXIF ​​из файлов .CR2.Кажется, что поскольку файлы .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)
...