Как считывать и удалять метаданные из ваших фотографий с помощью Python +8


AliExpress RU&CIS

Те, кто знает Python, могут делать поистине удивительные вещи, например, создавать арт-объекты и игры и красивые карты, полнотекстовую поисковую машину и систему распознавания лиц. Применим Python и для вполне бытовых задач. Сегодня, специально к старту новых потоков курсов Python для веб-разработки и Fullstack-разработчик на Python поделимся с вами туториалом, как считывать, добавлять и удалять EXIF метаданные из фотографий с помощью Python.


Немного очевидного для тех кто не в теме — метаданные в фото

Массовое распространение смартфонов (по сути, портативных, оснащённых датчиками, всегда подключённых к сети компьютеров) привело к двум важным последствиям — влиянию на конфиденциальность и влиянию на безопасность, и это ещё не до конца изучено:

  1. У большого количества людей есть камера, которая обычно находится в пределах досягаемости.

  2. По умолчанию фотографии, сделанные на  камеру, могут выдавать конфиденциальную информацию, и многие пользователи не подозревают об этом.

Помимо данных изображения фотографии, сделанные с помощью смартфонов и современных цифровых камер, содержат метаданные, которые являются дополнительной информацией о фотографии. Эти метаданные хранятся в формате EXIF (сокращение от EXchangeable Image File format), который является постоянно развивающимся стандартом для информации, добавляемой к цифровым изображениям и звукозаписям.

На фотографиях EXIF ??может включать такую ??информацию, как:

  • Размеры и плотность пикселей фото.

  • Марка и модель устройства, на котором был сделан снимок.

  • Масштабирование, диафрагма, вспышка и другие настройки камеры при съёмке фотографии.

  • Ориентация устройства при фотографировании.

  • Когда было сделано фото.

  • Где было сделано фото.

  • В каком направлении была обращена камера.

  • Высота, на которой был сделан снимок.

Эти метаданные полезны для сортировки, каталогизации и поиска фотографий, поэтому был определён стандарт EXIF. Однако при этом возникают проблемы с конфиденциальностью и безопасностью, которые многие пользователи не принимают во внимание.

Одним из первых предостережений об EXIF ??является инцидент с конфиденциальностью в 2012 году, связанный с основателем антивирусной компании Джоном Макафи. Это было захватывающее время в жизни Макафи, поскольку он уклонялся от действий правоохранительных органов, но при этом давал журналистам эксклюзивные, пользующиеся большим спросом интервью. Один журналист, которому посчастливилось взять интервью, решил похвастаться своей удачей, опубликовав фотографию Макафи, но предварительно не удалив EXIF-данные. Они и раскрыли местонахождение Макафи и привели к его аресту.

Вы, вероятно, знаете о недавних историях, когда правоохранительные органы арестовывали подозреваемых, используя фотографии, размещённые в социальных сетях, потому что подозреваемые не принимали меры предосторожности и не «очищали» свои данные EXIF ??в процессе загрузки.

Я подозреваю, что вы как разработчики, заботящиеся о безопасности, вероятно, задаёте себе следующие вопросы:

  • Как я могу программно обнаружить и прочитать EXIF-метаданные ??с фотографии?

  • Как я могу программно изменять, добавлять или удалять EXIF-метаданные?

Если на вашем компьютере установлен Python 3.6 или более поздней версии, вы можете узнать это, выполнив приведённые ниже инструкции. Они охватывают пару пакетов Python, которые вы можете включить в свои приложения для извлечения, добавления, изменения или удаления EXIF-данных ??с фотографий. Попутно вы не только будете использовать свои навыки программирования, но и поработаете детективом!

Модуль exif

Существует ряд модулей Python, которые могут получить доступ к данным EXIF ??в цифровых фотографиях. В этой статье мы сосредоточимся на exif. Его API настолько питонический, что кажется, будто он является частью языка, а не модулем.

Чтобы установить exif, используйте pip, введя в командной строке следующее:

pip install exif

Если при вводе этой команды выдаётся сообщение об ошибке, попробуйте вместо этого использовать команду pip3 install exif. pip3 — это версия pip, менеджера пакетов Python, специально предназначенная для Python 3.

Если вы не поддерживаете какой-либо код на Python 2, но вам нужно использовать pip3 для установки пакетов Python, то вам следует подумать об обновлении Python.

Загрузка фотографий и проверка их на наличие данных EXIF

Давайте проверим exif в работе. Рассмотрим эти две фотографии, palmtree1.jpg и palmtree2.jpg:

Предположим, вам задали следующие вопросы:

  1. Были ли эти фотографии сделаны на одно устройство или на разные?

  2. Какое фото было сделано первым?

  3. Где были сделаны эти фотографии?

Чтобы ответить на эти вопросы, мы загрузим данные из этих фотографий в объекты exif-Image, а затем будем использовать эти объекты для проверки EXIF-метаданных:

from exif import Image

with open("./images/palm tree 1.jpg", "rb") as palm_1_file:
    palm_1_image = Image(palm_1_file)
    
with open("./images/palm tree 2.jpg", "rb") as palm_2_file:
    palm_2_image = Image(palm_2_file)
    
images = [palm_1_image, palm_2_image]

Код открывает каждый файл только для чтения. Он считывает данные файла в двоичном формате в объект файла, который затем используется для создания экземпляра объекта exif-Image. Наконец, он помещает объекты Image в массив, чтобы мы могли перебирать их, выполняя одни и те же операции с EXIF-метаданными каждой фотографии.

Давайте выполним нашу первую операцию: убедимся, что фото действительно содержат EXIF-данные. Мы сделаем это, проверив свойство has_exif каждого объекта Image. Для каждого изображения, содержащего EXIF-данные, мы будем использовать свойство изображения exif_version, чтобы отобразить версию EXIF:

for index, image in enumerate(images):
    if image.has_exif:
        status = f"contains EXIF (version {image.exif_version}) information."
    else:
        status = "does not contain any EXIF information."
    print(f"Image {index} {status}")

При запуске этот код даёт следующие результаты:

Image 0 contains EXIF (version 0220) information.
Image 1 contains EXIF (version 0232) information.

Теперь мы знаем, что обе фотографии содержат информацию EXIF ??и используют разные версии EXIF. Скорее всего, эти фотографии были сделаны на разные камеры.

Какие метаданные EXIF ??доступны на каждой фотографии?

Думайте о EXIF ??как о структуре данных «ключ-значение», подобной словарю Python. Пары «ключ-значение» EXIF ??называются тегами, и каждый тег может содержать строковое или числовое значение. В текущем стандарте EXIF ??(версия 2.32) есть десятки тегов, и каждый — от производителей смартфонов и камер до фотографов — может добавлять свои собственные теги.

Вот полный список тегов EXIF, которые вы, вероятно, найдёте на цифровых фотографиях. Кроме того, тут есть теги, не входящие в стандарт EXIF, но предоставляемые рядом устройств или программного обеспечения.

EXIF — один из тех неформальных форматов, где производители сами выбирают, какие функции они будут реализовывать. Это означает, что фотографии, снятые разными камерами и смартфонами, будут иметь разные наборы тегов EXIF.

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

Учитывая различия между устройствами и наличие инструментов для редактирования данных EXIF, первое, что вы должны сделать при работе с EXIF данными ??фотографии, — это посмотреть, какие теги доступны.

Объекты Image, предоставляемые модулем exif, представляют теги EXIF ??как свойства этого объекта. Это означает, что вы можете использовать встроенную функцию Python dir() для объекта Image, чтобы увидеть, какие теги EXIF ??у него есть.

Следующий код отображает список тегов каждого объекта Image в нашем списке изображений:

image_members = []

for image in images:
    image_members.append(dir(image))

for index, image_member_list in enumerate(image_members):
    print(f"Image {index} contains {len(image_member_list)} members:")
    print(f"{image_member_list}\n")

Вы увидите следующий результат:

Image 0 contains 53 members:
['_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'color_space', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'digital_zoom_ratio', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_datestamp', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'has_exif', 'light_source', 'make', 'max_aperture_value', 'metering_mode', 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'sensing_method', 'shutter_speed_value', 'subsec_time', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']

Image 1 contains 68 members:
['<unknown EXIF tag 316>', '<unknown EXIF tag 322>', '<unknown EXIF tag 323>', '<unknown EXIF tag 42080>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_dest_bearing', 'gps_dest_bearing_ref', 'gps_horizontal_positioning_error', 'gps_img_direction', 'gps_img_direction_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'gps_speed', 'gps_speed_ref', 'has_exif', 'lens_make', 'lens_model', 'lens_specification', 'make', 'maker_note', 'metering_mode', 'model', 'offset_time', 'offset_time_digitized', 'offset_time_original', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'scene_type', 'sensing_method', 'shutter_speed_value', 'software', 'subject_area', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']

Как вы можете видеть, в то время как оба объекта Image имеют много общих тегов, изображение 1 содержит немного больше, чем изображение 0. Это означает, что изображение 1 имеет несколько больше тегов EXIF, чем изображение 0. Это сильный индикатор того, что изображение 0 и изображение изображение 1 было снято на разные устройства.

Вы можете использовать стандартный метод set() для определения общих элементов изображения 0 и изображения 1:

common_members = set(image_members[0]).intersection(set(image_members[1]))
common_members_sorted = sorted(list(common_members))
print("Image 0 and Image 1 have these members in common:")
print(f"{common_members_sorted}")

Запуск этого кода даёт следующий результат:

Image 0 and Image 1 have these members in common:
['_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'components_configuration', 'datetime', 'datetime_digitized', 'datetime_original', 'delete', 'delete_all', 'exif_version', 'exposure_bias_value', 'exposure_mode', 'exposure_program', 'exposure_time', 'f_number', 'flash', 'flashpix_version', 'focal_length', 'focal_length_in_35mm_film', 'get', 'get_file', 'get_thumbnail', 'gps_altitude', 'gps_altitude_ref', 'gps_latitude', 'gps_latitude_ref', 'gps_longitude', 'gps_longitude_ref', 'has_exif', 'make', 'metering_mode', 'model', 'orientation', 'photographic_sensitivity', 'pixel_x_dimension', 'pixel_y_dimension', 'resolution_unit', 'scene_capture_type', 'sensing_method', 'shutter_speed_value', 'subsec_time_digitized', 'subsec_time_original', 'white_balance', 'x_resolution', 'y_resolution']

Если вы смотрели EXIF-теги в документации (либо список стандартных тегов EXIF, либо расширенный список), вы могли заметить, что имена тегов EXIF находятся в PascalCase, а свойства EXIF в объектах exif-Image — в snake_case. Это связано с тем, что авторы модуля exif стремятся следовать руководству по стилю Python и разработали Image для преобразования имён тегов EXIF в имена свойств Python.

Члены класса Image exif, не являющиеся тегами EXIF

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


Члены класса

Описание

delete(attribute)

Удаляет из изображения тег EXIF, указанный в строковом атрибуте.

delete_all

Удаляет все теги EXIF из изображения.

get(attribute, default=None)

Возвращает значение тега EXIF, заданное строковым атрибутом. Если тег недоступен или содержит значение, он возвращает значение, указанное в аргументе ключевого слова по умолчанию.

get_file

Возвращает текущие данные изображения, отформатированные для записи в двоичный файл.

get_thumbnail

Возвращает двоичные данные для эскиза изображения.

has_exif

Логическое значение, которое возвращает True, если изображение в настоящее время содержит метаданные EXIF.

set(attribute, value)

Установить значение для тега EXIF, заданное строковым атрибутом. Если тег недоступен или содержит значение, он возвращает значение, указанное в аргументе ключевого слова по умолчанию.

В этой статье мы рассмотрим большинство из этих свойств и методов.

Получение основной информации о фотографиях

Ответим на первый вопрос об этих фотографиях. Были ли они сняты на одно устройство или на разные? У нас уже есть основания полагать, что ответ — «нет».

Марка и модель устройства, на котором было сделано фото.

У обоих изображений есть теги марки и модели, поэтому давайте с их помощью определим, какие устройства использовались для создания этих снимков:

for index, image in enumerate(images):
    print(f"Device information - Image {index}")
    print("----------------------------")
    print(f"Make: {image.make}")
    print(f"Model: {image.model}\n")

Вот результат кода выше:

Device information - Image 0
----------------------------
Make: motorola
Model: motorola one hyper

Device information - Image 1
----------------------------
Make: Apple
Model: iPhone 12 Pro

Это подтверждает, что фотографии были сделаны на разных устройствах, и теперь мы знаем детали.

Дополнительная информация об устройствах

Давайте соберём дополнительную информацию об устройствах, которые используются для фотосъёмки, а именно об их объективах и версиях операционной системы.

Не все устройства сообщают тип линзы в своих EXIF-метаданных, поэтому мы будем использовать метод Image get(), который аналогичен методу get(), используемому в Python. Подобно методу get() в Python, метод get(), предоставляемый объектом exif Image, изящно обрабатывает случай, когда данный ключ не существует.

В приведённом ниже коде используется get(), чтобы попытаться получить доступ к версиям объектива и операционной системы, используемым при съёмке фотографий. Если определённого свойства не существует, его значение будет отображаться как «Неизвестно»:

for index, image in enumerate(images):
    print(f"Lens and OS - Image {index}")
    print("---------------------")
    print(f"Lens make: {image.get('lens_make', 'Unknown')}")
    print(f"Lens model: {image.get('lens_model', 'Unknown')}")
    print(f"Lens specification: {image.get('lens_specification', 'Unknown')}")
    print(f"OS version: {image.get('software', 'Unknown')}\n")

Вот его результат:

Lens and OS - Image 0
---------------------
Lens make: Unknown
Lens model: Unknown
Lens specification: Unknown
OS version: Unknown

Lens and OS - Image 1
---------------------
Lens make: Apple
Lens model: iPhone 12 Pro back triple camera 4.2mm f/1.6
Lens specification: (1.5399999618512084, 6.0, 1.6, 2.4)
OS version: 14.3

Обратите внимание, что телефон, используемый для съёмки изображения 0 (Motorola One Hyper), не предоставляет свойства lens_make, lens_model, lens_specification или software. Если бы мы попытались получить к ним доступ напрямую (например, image.lens_make), результатом была бы ошибка. Метод get() позволил нам предоставить альтернативное значение для этих несуществующих свойств.

Дата и время, когда была сделана фотография

Следующий вопрос: какое фото было сделано первым?

Мы можем узнать это, просмотрев свойство datetime_original каждой фотографии, которое указывает на дату и время, когда фотография была сделана. Процессор смартфона достаточно быстр, чтобы записывать точную долю секунды, когда он делает фотографию, и это значение сохраняется в свойстве subsc_time_original.

Некоторые телефоны также записывают свойство offset_time, которое мы можем использовать для определения смещения datetime относительно UTC.

Следующий код отображает дату и время, когда была сделана каждая из фотографий. Если фотография включает данные о временном сдвиге, код также отобразит это значение:

for index, image in enumerate(images):
    print(f"Date/time taken - Image {index}")
    print("-------------------------")
    print(f"{image.datetime_original}.{image.subsec_time_original} {image.get('offset_time', '')}\n")

Вот результаты:

Date/time taken - Image 0
-------------------------
2021:01:22 15:08:46.327211 

Date/time taken - Image 1
-------------------------
2021:01:22 15:08:59.383 -05:00

Как видно из вывода, изображение 0 было снято первым, а изображение 1 было снято через 13 секунд.

Определение места, где была сделана фотография

В этом разделе мы рассмотрим доступ к GPS-координатам в метаданных фотографии, форматирование этих координат и упрощение понимания этих координат путём их размещения на карте и преобразования их в названия страны, региона и города, где фото было сделано.

Получение GPS-координат фотографии

Формат EXIF определяет ряд тегов, начинающихся с «gps», которые содержат полезную информацию о геолокации, включая широту и долготу, где была сделана фотография:

for index, image in enumerate(images):
    print(f"Coordinates - Image {index}")
    print("---------------------")
    print(f"Latitude: {image.gps_latitude} {image.gps_latitude_ref}")
    print(f"Longitude: {image.gps_longitude} {image.gps_longitude_ref}\n")

Вот результат кода выше:

Coordinates - Image 0
---------------------
Latitude: (28.0, 0.0, 1.56) N
Longitude: (82.0, 26.0, 59.04) W

Coordinates - Image 1
---------------------
Latitude: (28.0, 0.0, 1.54) N
Longitude: (82.0, 26.0, 58.75) W

Обратите внимание, что свойства gps_latitude и gps_longitude возвращают широту и долготу в виде кортежа из трёх значений:

  1. Градусы.

  2. Минуты (1/60 градуса).

  3. Секунды (1/60 минуты или 1/3600 градуса).

Широта определяет угловое расстояние от экватора, которое может быть северным или южным. Свойство gps_latitude_ref указывает это направление, которое может быть N или S.

Долгота определяет угловое расстояние от меридиана, которое может быть восточным или западным. Свойство gps_longitude_ref указывает это направление: E или W.

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

Форматирование широты и долготы

Давайте определим пару функций для форматирования информации о широте и долготе, возвращаемой Image, в стандартные форматы:

  • Градусы, минуты и секунды. В этом формате широта изображения 0 будет записана как 28.0 ° 0.0 '1.56 "N.

  • Десятичные градусы. В этом формате широта изображения 0 будет записана как 28,000433333333334. Северные широты и восточные долготы представлены положительными значениями, в то время как южные широты и западные долготы представлены отрицательными значениями.

Вот определения этих функций, а также некоторый код, который их использует:

def format_dms_coordinates(coordinates):
    return f"{coordinates[0]}° {coordinates[1]}\' {coordinates[2]}\""

def dms_coordinates_to_dd_coordinates(coordinates, coordinates_ref):
    decimal_degrees = coordinates[0] +                       coordinates[1] / 60 +                       coordinates[2] / 3600
    
    if coordinates_ref == "S" or coordinates_ref == "W":
        decimal_degrees = -decimal_degrees
    
    return decimal_degrees

for index, image in enumerate(images):
    print(f"Coordinates - Image {index}")
    print("---------------------")
    print(f"Latitude (DMS): {format_dms_coordinates(image.gps_latitude)} {image.gps_latitude_ref}")
    print(f"Longitude (DMS): {format_dms_coordinates(image.gps_longitude)} {image.gps_longitude_ref}\n")
    print(f"Latitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)}")
    print(f"Longitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)}\n")

Вот результат:

Coordinates - Image 0
---------------------
Latitude (DMS): 28.0° 0.0' 1.56" N
Longitude (DMS): 82.0° 26.0' 59.04" W

Latitude (DD): 28.000433333333334
Longitude (DD): -82.44973333333334

Coordinates - Image 1
---------------------
Latitude (DMS): 28.0° 0.0' 1.54" N
Longitude (DMS): 82.0° 26.0' 58.75" W

Latitude (DD): 28.000427777777777
Longitude (DD): -82.44965277777779

Отображение местоположения фотографий на карте

Вы, вероятно, не знаете широту и долготу своего дома, даже если округлить до ближайшего градуса. Есть несколько простых способов преобразовать данные о местоположении EXIF во что-то более лёгкое для понимания.

Один из способов — использовать встроенный в Python модуль webbrowser для открытия новой вкладки браузера для каждой фотографии, используя десятичную версию EXIF-координат каждой фотографии в качестве параметров URL-адреса Google Maps. Мы создадим служебную функцию с именем draw_map_for_location(), которая сделает следующее:

def draw_map_for_location(latitude, latitude_ref, longitude, longitude_ref):
    import webbrowser
    
    decimal_latitude = dms_coordinates_to_dd_coordinates(latitude, latitude_ref)
    decimal_longitude = dms_coordinates_to_dd_coordinates(longitude, longitude_ref)
    url = f"https://www.google.com/maps?q={decimal_latitude},{decimal_longitude}"
    webbrowser.open_new_tab(url)

for index, image in enumerate(images):
    draw_map_for_location(image.gps_latitude, 
                          image.gps_latitude_ref, 
                          image.gps_longitude,
                          image.gps_longitude_ref)

Когда вы запустите его, откроются две новые вкладки браузера, на каждой из которых будет отображаться карта Google, указывающая, где была сделана соответствующая фотография.

Отображение города, региона и страны, где был сделан снимок

Другой способ отобразить местоположение фотографии — использовать обратное геокодирование, то есть процесс преобразования географических координат в адрес или название места. Для этого нам нужно установить два модуля Python:

  • reverse_geocoder — простой автономный обратный геокодер, который использует внутренние таблицы для преобразования набора координат в набор названий городов и штатов/провинций и кодов стран. Установите его, введя pip install reverse_geocoder в командной строке.

  • pycountry — утилита поиска стран, которую мы будем использовать для преобразования кодов стран в их соответствующие названия. Установите это, введя pip install pycountry в командной строке.

После того как вы установили указанные выше модули, используйте следующий код, чтобы преобразовать координаты фотографии в нечто более узнаваемое:

import reverse_geocoder as rg
import pycountry

for index, image in enumerate(images):
    print(f"Location info - Image {index}")
    print("-----------------------")
    decimal_latitude = dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)
    decimal_longitude = dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)
    coordinates = (decimal_latitude, decimal_longitude)
    location_info = rg.search(coordinates)[0]
    location_info['country'] = pycountry.countries.get(alpha_2=location_info['cc'])
    print(f"{location_info}\n")

В коде используется метод search() из reverse_geocoder для преобразования десятичных значений широты и долготы каждой фотографии в набор следующей информации:

  • Название города, посёлка или деревни.

  • Главный административный регион, который обычно представляет собой штат или провинцию.

  • Незначительный административный район, который обычно является округом или районом.

  • Код страны.

Затем он использует метод get() из pycountry для преобразования кода страны, предоставленного reverse_geocoder, в кортеж, содержащий соответствующие общие и официальные названия стран.

Вот его результат:

Location info - Image 0
-----------------------
{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', name='United States', numeric='840', official_name='United States of America')}

Location info - Image 1
-----------------------
{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', name='United States', numeric='840', official_name='United States of America')}

Другая полезная информация о датчиках

Как я уже упоминал в начале этой статьи, смартфоны снабжены датчиками. К ним относятся магнитометр, барометр и акселерометр. В сочетании с GPS эти датчики предоставляют дополнительную информацию, которая добавляется к метаданным EXIF каждой фотографии.

В каком направлении была обращена камера?

Магнитометр воспринимает магнитные поля, в том числе создаваемые Землёй. Его основная цель — быть компасом телефона и определять направление, на которое он указывает. Эта информация записывается в EXIF в виде компасного курса каждый раз, когда вы делаете снимок.

Давайте определим, в каком направлении я смотрел, когда делал каждую из этих фотографий:


Мы будем использовать следующие свойства exif-Image, чтобы определить направление, в котором была направлена камера:

  • gps_img_direction: направление по компасу, то есть направление, в котором смотрела камера, когда был сделан снимок, выраженный в десятичных градусах. 0 ° — север, 90 ° — восток, 180 ° — юг и 270 ° — запад.

  • gps_img_direction_ref: контрольная точка для gps_img_direction. Это может быть либо T, что означает, что 0° относится к истинному или географическому северу, либо M, что означает, что 0 ° относится к магнитному северу. В большинстве случаев используется истинный север.

В приведённом ниже коде показано направление камеры для четырёх фотографий озера. Он использует несколько служебных функций:

  • degrees_to_direction(): эта функция преобразует направления по компасу в основные направления (например, N, NE, NNE и т. д.);

  • format_direction_ref(): эта функция превращает значение в gps_img_direction_ref  понятную для человека строку.

def degrees_to_direction(degrees):
    COMPASS_DIRECTIONS = [
        "N",
        "NNE",
        "NE",
        "ENE",
        "E", 
        "ESE", 
        "SE", 
        "SSE",
        "S", 
        "SSW", 
        "SW", 
        "WSW", 
        "W", 
        "WNW", 
        "NW", 
        "NNW"
    ]
    
    compass_directions_count = len(COMPASS_DIRECTIONS)
    compass_direction_arc = 360 / compass_directions_count
    return COMPASS_DIRECTIONS[int(degrees / compass_direction_arc) % compass_directions_count]

def format_direction_ref(direction_ref):
    direction_ref_text = "(true or magnetic north not specified)"
    if direction_ref == "T":
        direction_ref_text = "True north"
    elif direction_ref == "M":
        direction_ref_text = "Magnetic north"
    return direction_ref_text

# Import images
lake_images = []
for i in range(1, 5):
    filename = f"lake {i}.jpg"
    with open(f"./images/{filename}", "rb") as current_file:
        lake_images.append(Image(current_file))

# Display camera direction for each image
for index, image in enumerate(lake_images):
    print(f"Image direction - Image {index}")
    print("-------------------------")
    print(f"Image direction: {degrees_to_direction(image.gps_img_direction)} ({image.gps_img_direction}°)")
    print(f"Image direction ref: {format_direction_ref(image.gps_img_direction_ref)}\n")

Когда вы запустите код, вы увидите следующий результат:

Image direction - Image 0
-------------------------
Image direction: ENE (78.416259765625°)
Image direction ref: True north

Image direction - Image 1
-------------------------
Image direction: N (1.174224853515625°)
Image direction ref: True north

Image direction - Image 2
-------------------------
Image direction: SSE (178.46739196870607°)
Image direction ref: True north

Image direction - Image 3
-------------------------
Image direction: W (273.8248136315229°)
Image direction ref: True north

На какой высоте был сделан снимок?

В дополнение к предоставлению координат местоположения GPS также может использоваться для определения высоты. Некоторые смартфоны оснащены барометрами (которые определяют давление воздуха), которые они используют для повышения точности измерения высоты.

Давайте выясним, на каких высотах были сделаны эти фотографии:

Мы воспользуемся этими свойствами объекта Exif Image для определения высоты:

  • gps_altitude: высота в метрах;

  • gps_altitude_ref: контрольная точка для gps_altitude. Это значение равно 0, что означает, что значение в gps_altitude относится к метрам над уровнем моря, или 1, что означает, что значение в gps_altitude относится к метрам ниже уровня моря.

Следующий код отображает высоту, сообщённую телефоном в момент съёмки каждой из фотографий. Он использует одну служебную функцию, format_altitude(), которая определяет, находится ли заданная высота над или под уровнем моря:

def format_altitude(altitude, altitude_ref):
    altitude_ref_text = "(above or below sea level not specified)"
    if altitude_ref == 0:
        altitude_ref_text = "above sea level"
    elif altitude_ref == 1:
        altitude_ref_text = "below sea level"
    return f"{altitude} meters {altitude_ref_text}"

# Import images
altitude_images = []
for i in range(1, 3):
    filename = f"altitude {i}.jpg"
    with open(f"./images/{filename}", "rb") as current_file:
        altitude_images.append(Image(current_file))
        
# Display camera altitude for each image
for index, image in enumerate(altitude_images):
    print(f"Altitude - Image {index}")
    print( "------------------")
    print(f"{format_altitude(image.gps_altitude, image.gps_altitude_ref)}\n")

Вот результат:

Altitude - Image 0
------------------
14.025835763206075 meters above sea level

Altitude - Image 1
------------------
359.13079847908745 meters above sea level

Я мог бы сказать вам, где я сделал эти фотографии, но вы уже знаете, как это выяснить.

Двигался ли фотограф?

Смартфоны используют комбинацию местоположений GPS с течением времени и акселерометра, чтобы определить скорость телефона и направление, в котором он движется. Некоторые телефоны предоставляют эту информацию как часть метаданных EXIF на фотографиях.

Мой Motorola One Hyper не записывает метаданные, связанные со скоростью, но мой iPhone это делает. Доступ к этим данным можно получить с помощью этих двух методов объекта exif Image:

  • gps_speed: скорость, выраженная в виде числа;

  • gps_speed_ref: единицы скорости, используемые для значения в gps_speed. Это значение может быть: K — для километров в час; M — для миль в час; N — для морских миль в час, или «узлов».

Рассмотрим следующие фото:

Вот код, который выводит записанную скорость во время съёмки каждой фотографии. Он включает функцию универсальности, format_speed_ref(), которая определяет единицы сообщаемой скорости:

def format_speed_ref(speed_ref):
    speed_ref_text = "(speed units not specified)"
    if speed_ref == "K":
        speed_ref_text = "km/h"
    elif speed_ref == "M":
        speed_ref_text = "mph"
    elif speed_ref == "N":
        speed_ref_text = "knots"
    return speed_ref_text

# Import images
speed_images = []
for i in range(1, 4):
    filename = f"speed {i}.jpg"
    with open(f"./images/speed {i}.jpg", "rb") as current_file:
        speed_images.append(Image(current_file))
    
for index, image in enumerate(speed_images):
    print(f"Speed - Image {index}")
    print("---------------")
    print(f"Speed: {image.gps_speed} {format_speed_ref(image.gps_speed_ref)}\n")

Вот результат:

Speed - Image 0
---------------
Speed: 0.0 km/h

Speed - Image 1
---------------
Speed: 20.19736291335287 km/h

Speed - Image 2
---------------
Speed: 5.520932607215793 km/h

Редактирование EXIF данных и их сохранение

Пока что мы ограничились простым чтением метаданных EXIF с фотографий. Следующим шагом является внесение изменений в эти данные, а затем сохранение результатов в виде нового файла фотографии.

Редактирование координат фотографии

Начнём с этой фотографии:

К настоящему времени вы должны знать, как читать координаты из EXIF и использовать эти координаты, чтобы открыть карту Google, чтобы показать местоположение, где фото снято.

Вот код, который это сделает. Он использует служебную функцию draw_map_for_location(), которую мы определили ранее:

with open(f"./images/hotel original.jpg", "rb") as hotel_file:
    hotel_image = Image(hotel_file)
    
# Read the GPS data
print("Original coordinates")
print("--------------------")
print(f"Latitude: {hotel_image.gps_latitude} {hotel_image.gps_latitude_ref}")
print(f"Longitude: {hotel_image.gps_longitude} {hotel_image.gps_longitude_ref}\n")

# Open a Google Map showing the location represented by these coordinates
draw_map_for_location(hotel_image.gps_latitude,
                      hotel_image.gps_latitude_ref,
                      hotel_image.gps_longitude,
                      hotel_image.gps_longitude_ref

Он выводит следующее:

Original coordinates
--------------------
Latitude: (28.0, 21.0, 58.44) N
Longitude: (81.0, 33.0, 34.29) W

Кроме того, он открывает новую вкладку браузера, показывающую карту Google, которая отображает отели Swan и Dolphin, которые находятся в нескольких минутах ходьбы от Walt Disney World во Флориде.

Давайте сделаем вещи немного интереснее, изменив координаты, встроенные в данные EXIF ??фотографии, чтобы они сообщали, что она была сделана в Зоне 51. Если вы не слышали об этом месте, это военный объект в Неваде, в котором, как считают теоретики теории заговора, правительство США хранит тела пришельцев и космический корабль, захваченные в 1950-х годах. Его координаты: 37,0 ° 14 '3,6 "северной широты, 115 ° 48' 23,99" западной долготы.

Вы видели, что чтение значения тега EXIF ??с использованием объекта Exif Image — это просто чтение значения в соответствующем свойстве. Точно так же редактирование его значения — это просто вопрос присвоения нового значения этому свойству. В этом случае мы присвоим значения, определяющие координаты Зоны 51, свойствам изображения gps_latitude, gps_latitude_ref, gps_longitude и gps_longitude_ref:

# Boring. Let's change those coordinates to Area 51!
hotel_image.gps_latitude = (37.0, 14, 3.6)
hotel_image.gps_latitude_ref = 'N'
hotel_image.gps_longitude = (115, 48, 23.99)
hotel_image.gps_longitude_ref = 'W'

# Read the revised GPS data
print("Revised coordinates")
print("-------------------")
print(f"Latitude: {hotel_image.gps_latitude} {hotel_image.gps_latitude_ref}")
print(f"Longitude: {hotel_image.gps_longitude} {hotel_image.gps_longitude_ref}\n")

# Open a Google Map showing the location represented by the revised coordinates
draw_map_for_location(hotel_image.gps_latitude,
                      hotel_image.gps_latitude_ref,
                      hotel_image.gps_longitude,
                      hotel_image.gps_longitude_ref

Выполнение кода приведёт к следующему результату …

Revised coordinates
-------------------
Latitude: (37.0, 14.0, 3.6) N
Longitude: (115.0, 48.0, 23.99) W

... и он откроет новую вкладку с картой Google, в которой отображается Зона 51.

Заполнение неиспользуемых тегов EXIF

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

Примечание: модуль exif записывает только теги в спецификации EXIF, но не дополнительные теги, включённые поставщиками. «Официальные» теги в спецификации EXIF ??подчёркнуты.

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

Вот несколько тегов EXIF, которые вы, возможно, захотите заполнить, так как они удобны для управления библиотекой фотографий. Они оба принимают строковые значения:

  • ImageDescription: описание фотографии. В объектах Exif Image это отображается как свойство image_description.

  • Copyright: уведомление об авторских правах на фотографию. В объектах Exif Image это свойство copyright.

Вот код, который заполняет эти теги:

hotel_image.image_description = "The Dolphin Hotel in Orlando, viewed at sunset from the Swan Hotel"
hotel_image.copyright = "Copyright 2021 (Your name here)"

print(f"Description: {hotel_image.image_description}")
print(f"Copyright: {hotel_image.copyright}")

Выполнение кода даёт следующий результат:

Description: The Dolphin Hotel in Orlando, viewed at sunset from the Swan Hotel
Copyright: Copyright 2021 (Your name here)

Сохранение фото с обновленными данными

Теперь, когда мы отредактировали данные EXIF на картинке, давайте сохраним их как новый файл с названием hotel_updated.jpg.

with open('./images/hotel updated.jpg', 'wb') as updated_hotel_file:
    updated_hotel_file.write(hotel_image.get_file())

Этот код создаёт файловый объект updated_hotel_file для записи двоичных данных в файл с именем hotel_updated.jpg. Затем он использует метод Image get_file() для получения данных об изображении отеля в сериализуемой форме и записывает эти данные в файл.

Теперь у вас должен быть новый файл: hotel_updated.jpg. Загрузим его и убедимся, что изменённые и добавленные данные сохранены:

with open(f"./images/hotel updated.jpg", "rb") as hotel_file:
    hotel_image = Image(hotel_file)
    
print("Coordinates")
print("-----------")
print(f"Latitude: {hotel_image.gps_latitude} {hotel_image.gps_latitude_ref}")
print(f"Longitude: {hotel_image.gps_longitude} {hotel_image.gps_longitude_ref}\n")

print("Other info")
print("----------")
print(f"Description: {hotel_image.image_description}")
print(f"Copyright: {hotel_image.copyright}")

# Open a Google Map showing the location represented by these coordinates
draw_map_for_location(hotel_image.gps_latitude,
                      hotel_image.gps_latitude_ref,
                      hotel_image.gps_longitude,
                      hotel_image.gps_longitude_ref

Код выдаст вам этот результат …

Coordinates
-----------
Latitude: (37.0, 14.0, 3.6) N
Longitude: (115.0, 48.0, 23.99) W

Other info
----------
Description: The Dolphin Hotel in Orlando, viewed at sunset from the Swan Hotel
Copyright: Copyright 2021 (Your name here)

... и откроется ещё одна вкладка браузера, отображающая Зону 51 на карте.

Удаление данных EXIF и сохранение «очищенной» фотографии

Предположим, что вместо изменения данных о местоположении на фотографии отеля мы решили удалить теги. Есть два способа сделать это.

Первый способ — использовать метод delete(), предоставляемый объектом Exif-Image. Давайте используем его, чтобы удалить данные о широте:

hotel_image.delete('gps_latitude')
hotel_image.delete('gps_latitude_ref')

print("Latitude data")
print("-------------")
print(f"gps_latitude: {hotel_image.get('gps_latitude', 'Not found')}")
print(f"gps_latitude_ref: {hotel_image.get('gps_latitude_ref', 'Not found')}")

Вот результат кода выше:

Latitude data
-------------
gps_latitude: Not found
gps_latitude_ref: Not found

Второй способ — использовать оператор Python del, который удаляет объекты из памяти. В этом случае мы будем использовать его для удаления атрибутов hotel_image, в которых хранятся данные о долготе:

del hotel_image.gps_longitude
del hotel_image.gps_longitude_ref

print("Longitude data")
print("--------------")
print(f"gps_longitude: {hotel_image.get('gps_longitude', 'Not found')}")
print(f"gps_longitude_ref: {hotel_image.get('gps_longitude_ref', 'Not found')}")

Вот результат кода выше:

Longitude data
--------------
gps_longitude: Not found
gps_longitude_ref: Not found

Теперь, когда мы удалили данные о местоположении с фотографии, давайте сохраним её под новым именем, hotel_without_location_data.jpg:

with open('./images/hotel without location data.jpg', 'wb') as updated_hotel_file:
    updated_hotel_file.write(hotel_image.get_file())

Наконец, если вы хотите просто удалить все данные EXIF с фотографии, вы можете использовать метод delete_all() объекта exif Image:

hotel_image.delete_all()

dir(hotel_image)
['<unknown EXIF tag 322>',
 '<unknown EXIF tag 323>',
 '_segments',
 'delete',
 'delete_all',
 'get',
 'get_file',
 'get_thumbnail',
 'has_exif']

Опять же, как только вы использовали delete_all() для удаления всех тегов EXIF, вам нужно сохранить эти изменения. Приведённый ниже код сохраняет наше изображение со всеми удалёнными тегами как hotel_without_tags.jpg:

with open('./images/hotel without tags.jpg', 'wb') as updated_hotel_file:
    updated_hotel_file.write(hotel_image.get_file())

Практические и технические соображения

Причины удаления метаданных EXIF

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

Одно из очевидных соображений — это конфиденциальность. Метаданные GPS для отдельной фотографии могут определить ваше местоположение в определённую дату и время. Что гораздо более ценно, так это совокупный GPS из чьей-то камеры, поскольку он содержит шаблоны, которые можно проанализировать, чтобы определить, где вы живёте, работаете и какие места вы часто посещаете в повседневной жизни. Вот почему более авторитетные социальные сети удаляют эту информацию, когда вы публикуете свои фотографии в Интернете. Помните, что это означает лишь то, что ваши фотографии в галерее социальной сети не содержат данных GPS. Нет гарантии, что служба не записала информацию о местоположении чтобы передать её рекламодателю перед её удалением.

Другие метаданные EXIF, такие как марка, модель, настройки и даже ориентация камеры, могут использоваться для связывания набора фотографий с конкретным человеком.

Некоторые фотографы больше озабочены секретностью, чем конфиденциальностью, когда речь идёт о метаданных их фотографий. Они не хотят разглашать настройки камеры, которые использовали для фотосъёмки.

Причины редактирования или добавления метаданных EXIF

Одна из первоначальных причин, по которой был разработан формат метаданных EXIF, заключалась в том, чтобы упростить автоматизацию процесса каталогизации, сортировки и поиска в коллекции цифровых фотографий. EXIF позволяет искать фотографии, сделанные в течение определенного периода, с помощью определённого устройства или в определённом месте. Другие теги, такие как тег copyright, позволяют искать фотографии, сделанные определённым фотографом, а также защищать его авторские права.

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

Метаданные EXIF ??особенно полезны в мобильных приложениях, которые записывают одновременные комбинации визуальной информации, информации о дате/времени и местоположении. Например, от техника может потребоваться сделать фотографии выполненных ремонтных работ «до» и «после» с указанием того, где, когда и как были выполнены эти работы.

Другие вещи, которые следует учитывать

Как мы видели в различных тегах, записанных устройствами iPhone и Android, используемыми для фотосъёмки в этой статье, разные камеры записывают разные наборы тегов EXIF. Программное обеспечение для редактирования фотографий часто записывает информацию в разные теги или добавляет свои собственные теги. Эти дополнительные метаданные часто могут указывать на то, что фотография была отредактирована.

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

Наконец, есть сообщения, которые отображаются в мобильных приложениях, использующих камеру, при первом использовании. Обычно они говорят что-то вроде «Это приложение использует камеру. Вы согласны? », Но большинство из них не даёт понять, что камера добавляет местоположение и другие метаданные к каждой сделанной фотографии. Если вы пишете мобильные приложения, использующие камеру, вы можете сообщить об этом пользователю.

Знание Python — это полезный hard skill не только для программиста, но и для аналитика и даже маркетолога. С ним вы можете сами написать простой парсер, написать небольшое приложение под свои задачи и даже увеличить свой доход, выполняя заказы на фрилансе. Если вы хотите дополнительно прокачать себя и освоить этот язык под чутким руководством наших опытных менторов — обратите внимание на курс Fullstack-разработчик на Python и Python для веб-разработки.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы




Комментарии (2):

  1. nixtonixto
    /#22917378

    Для удаления EXIF перед публикацией изображения, можно просто загрузить картинку, например, на Радикал. Он её автоматически переконвертирует в WEBP, в котором не будет EXIF. Или на любой аналогичный хостинг.

    Хотя, не уверен — возможно, мои программы просмотра не видят EXIF в WEBP, а сайт при конвертации его переносит в новый файл.

  2. BubaVV
    /#22934650

    Еще есть rasterio, очень удобная штука