Наступает тот момент, когда фотографий становится не много, а катастрофически много.
Предыстория
Решил я в один из дней отсортировать весь свой архив цифровых фото, накопленный за 20 лет, и понял, что всего за это время накопил 112 000 фотографий на 435 гигабайт.
Причем, некоторая часть из них, лежит более менее сортировано, например фотографии с зеркальной камеры, по папкам с названиями, и датами, а другая часть фотографий, которая была импортирована с iphone/android никак не названа и не отсортирована, часто это просто гигантская папка на 10 гигабайт, с парой тысяч файлов внутри, и удалить жалко и сортировать невозможно.
Начал искать инструменты автоматической сортировки и понял, что все хорошие сервисы, вроде Picasa уже купил и закрыл Google, можно конечно все выгрузить к ним на Google.Photos однако там есть проблемы поиск ищет далеко не все и вообще не хватает где-то половины функций от того чтобы было в Picasa, а если вы еще переживаете за то, что ваши фотографии будут распознавать и использовать, то выкладывать в веб это вообще не ваш путь.
В итоге было решено написать небольшой скрипт, который все отсортирует, сначала думал о shell скрипте, однако поняв, что нужен будет EXIF решил вернуться к старому доброму PHP.
Задача №1 — Разложить все файлы по датам
Сначала я пошел самым простым путем, возьмем все файлы, посмотрим дату создания и раскидаем по вложенным путям:
$file_list = $files->getDirContents($config['photos.unsorted']);
foreach ($file_list as $key => $value) {
moveImageFile($value);
}
function moveImageFile($filename) {
$dt= new DateTime();
$dt->setTimestamp(filectime($filename));
$start_path = $this->config['photos'];
$year = $start_path."\Year".$dt->format('Y');
if (!is_dir($year)) mkdir($year);
$month = $year."\\".$dt->format('Y-m-F');
if (!is_dir($month)) mkdir($month);
$path = $month."\\".$dt->format('Y-m-d');
if (!is_dir($path)) mkdir($path);
}
$full_path = getUniqueFilename($filename, $path, $dt, 0);
copy($filename, $full_path);
Нашлось несколько проблем:
- Часть файлов имели неправильную дату создания
- Если делать copy, новый файл создается текущей датой
- Файлы могут иметь дубли, с одинаковым временем создания
Задача №2 — Получаем дату из Exif
Было решено брать дату из EXIF, для файлов делать rename и touch ставить дату из exif, а также проверять файлы на дубликаты с помощью md5.
В принципе PHP уже имеет в наборе библиотек расширение exif, поэтому ничего сверхъестественного не предвиделось
$dt = DateTime::createFromFormat('Y:m:d H:i:s', $exif['DateTime']);
$start_path = $this->config['photos.exif'];
$is_exif = true;
if (md5_file($filename) == md5_file($full_path)) return false;
rename($filename, $full_path);
touch($full_path, $dt->getTimestamp());
Все бы хорошо, 500 гигабайт фотографий было отсортировано и очищено от дубликатов за пару часов, но тут я вспомнил о старых папках, которые содержали в себе название региона, где была проведена фотосессия, и подумал, почему бы не получать названия городов из геоданных?
Задача №3 — Страны, города и регионы из геоданных EXIF
Координаты легко найти в файлах, они лежат в Exif в GPSLongitude и GPSLatitude, но нельзя забывать, что хранятся они там в градусах, минутах и секундах, поэтому нужно использовать функции преобразования координат в десятичную систему счисления.
function getGps($exifCoord, $hemi) {
$degrees = count($exifCoord) > 0 ? $this->gps2Num($exifCoord[0]) : 0;
$minutes = count($exifCoord) > 1 ? $this->gps2Num($exifCoord[1]) : 0;
$seconds = count($exifCoord) > 2 ? $this->gps2Num($exifCoord[2]) : 0;
$flip = ($hemi == 'W' or $hemi == 'S') ? -1 : 1;
return $flip * ($degrees + $minutes / 60 + $seconds / 3600);
}
Второй вопрос, а что делать с координатами, как получить название города?
На помощь приходит Geocoder от Yandex, но будьте внимательны с лимитами и условиями использования.
$url = "https://geocode-maps.yandex.ru/1.x/";
$apikey = require('../config/apikey.php');
$json = array(
'geocode' => $lon.",".$lat,
'kind' => 'locality',
'apikey' => $apikey,
'results' =>'1',
'skip' => '0',
'format' => 'json'
);
$response = file_get_contents($url."?".http_build_query($json));
Чтобы не убивать Yandex миллионами запросов, кэшируем данные в MySql, округляя координаты до 3х знаков после запятой, то есть 43.161 — 19.182 этого достаточно, чтобы определить город, и тем самым на 110 000 фотографий у меня получилось всего 1500 геометок.
Внешний вид папок примерно такой:
- D:\photos\photos_exif\Year2019\2019-09-September\2019-09-23-Босния и Герцеговина, Республика Сербская, Фоча\
- D:\photos\photos_exif\Year2019\2019-08-August\2019-08-25-Албания, область Дуррес, Круя\
- D:\photos\photos_exif\Year2018\2018-10-October\2018-10-06-Россия, Московская область, Балашиха\
Вместо заключения
На самом деле если взяться за этот продукт, можно делать его месяцами, мне понадобился день, на то, чтобы написать скрипт, и оптимизировать хранение фотографий.
Из планов: добавление геотегов в существующие фотографии, пересортировка текущего архива фотографий, поиск дубликатов среди пережатых изображений.
Все файлы проекта доступны в GitHub
Не бейте меня сильно, это первый мой полностью open source проект, если что-то разместил или написал не так, скажите, и да сейчас все заточено под среду исполнения Windows с кодировкой 1251.
Комментарии (17)
Taraflex
03.10.2019 21:37если что-то разместил или написал не так
Да х… с оформлением. Но почему конфиг на php с жестко зашитым расположением вместо cli интерфейса?
packagist.org/?query=comman%20line%20parser
Ну и базу sqlite взять, чтобы более портабельно было.Vseznaut Автор
03.10.2019 23:18Да, согласен надо CLI сделать и SqlLite просто пока делал для личного использования все адреса вообще были хардкодом вшиты. Запишу в Todo
Moskus
03.10.2019 22:37Напомню, на всякий случай, о существовании www.sentex.net/~mwandel/jhead
Vseznaut Автор
03.10.2019 23:25На самом деле, когда уже написал, нашел несколько инструментов:
www.sno.phy.queensu.ca/~phil/exiftool — ExifTool
www.geckoandfly.com/7987/how-to-change-exif-data-date-and-camera-properties-with-free-editor
arslan.io/2018/04/18/tips-tricks-to-batch-edit-exif-metadata-of-photos
en.wikipedia.org/wiki/Comparison_of_digital_image_metadata_editors
Как обычно, сначала пишем код, потом неожиданно находятся десятки аналогов :)
UksusoFF
03.10.2019 22:38+2FastStone Image Viewer умеет раскидывать по дате из EXIF.
Проблемы начинаются когда обнаруживаешь что в фотках нет EXIF, или там стоит 1990 год, или фотки с нескольких устройств с одного мероприятия.muxa_ru
03.10.2019 22:49+1или фотки с нескольких устройств с одного мероприятия.
И одно ещё в московском времени, а другое уже в берлинском
saintbyte
03.10.2019 23:13Вы не поверите у меня такая структура уже 10 лет и я делаю это руками. Просто дисциплина. Автоматизация конечно хорошо — но проблема в том потом приходиться разгребать беспристрастные результаты алгоритма. Для полного счастья у меня камера и смена батарей сбрасывает время.
Vseznaut Автор
03.10.2019 23:28Это да, когда все сделано руками супер. Вопрос лишь в том, чтобы не забывать это делать день за днем, плюс фотографии с телефона, которые прилетают сразу по 1000 штук с разных локаций и дат и сортировать их руками очень не хочется.
shoorick
04.10.2019 10:05У меня сначала фотки сбрасываются с аппарата и сортируются самодельным перловым скриптом, а затем уже я просматриваю, что получилось, переименовываю получившиеся папки, объединяя некоторые из них (если одно мероприятие длится больше одного дня). Под годы, месяцы и дни — отдельные папки (например, ~/photo/2019/10/04 — мне так удобнее, чем видеть в одном месте кучу папок за все дни)
NickyX3
04.10.2019 15:04+1Автор не столкнулся с корявыми датами в EXIF?
Во-первых хорошо бы брать DateTimeOriginal, ибо DateTime в EXIF часто не бывает
Во-вторых там частенько бывают кривые даты, формат которых зависит от производителей камер-телефонов-прошивки, что добавляет проблем, ниже функция нормализации, по крайней на тех 200к фото, которые у нас есть отработала все варианты. Фото совершенно из разных источников
function normalizeExifDateTimeOriginal( $date ) { $parsed = preg_replace('/\D+/', '#', $date); $exploded = explode('#', $parsed); $parts = array(); foreach ($exploded as $part) { $parts[] = intval($part); } $counts = count($parts); if ( $parts[1] > 12 ) { $parts[1] = intval(substr($parts[1], 0, 2)); } if ( $counts >= 3 ) { switch ($counts) { case 6: $normalized = vsprintf("%04d-%02d-%02d %02d:%02d:%02d", $parts); break; case 5: $normalized = vsprintf("%04d-%02d-%02d %02d:%02d:00", $parts); break; case 4: $normalized = vsprintf("%04d-%02d-%02d %02d:00:00", $parts); break; case 3: $normalized = vsprintf("%04d-%02d-%02d 00:00:00", $parts); break; default: $normalized = vsprintf("%04d-%02d-%02d 00:00:00", $parts); break; } } else { $normalized = date('c'); } return $normalized; }
muxa_ru
Ещё будет полезно писать в базу хэши md5 и периодически проверять не карапнулся ли файл
Vseznaut Автор
Саму базу файлов я пока никак не веду, в БД только кэш геометок