Хочу поделиться своим опытом сортировки фотографий с помощью скрипта на PHP
Наступает тот момент, когда фотографий становится не много, а катастрофически много.

Предыстория


Решил я в один из дней отсортировать весь свой архив цифровых фото, накопленный за 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)


  1. muxa_ru
    03.10.2019 20:09
    +1

    Ещё будет полезно писать в базу хэши md5 и периодически проверять не карапнулся ли файл


    1. Vseznaut Автор
      03.10.2019 23:16

      Саму базу файлов я пока никак не веду, в БД только кэш геометок


  1. Taraflex
    03.10.2019 21:37

    если что-то разместил или написал не так

    Да х… с оформлением. Но почему конфиг на php с жестко зашитым расположением вместо cli интерфейса?
    packagist.org/?query=comman%20line%20parser
    Ну и базу sqlite взять, чтобы более портабельно было.


    1. Vseznaut Автор
      03.10.2019 23:18

      Да, согласен надо CLI сделать и SqlLite просто пока делал для личного использования все адреса вообще были хардкодом вшиты. Запишу в Todo


  1. Moskus
    03.10.2019 22:37

    Напомню, на всякий случай, о существовании www.sentex.net/~mwandel/jhead


    1. 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

      Как обычно, сначала пишем код, потом неожиданно находятся десятки аналогов :)


  1. UksusoFF
    03.10.2019 22:38
    +2

    FastStone Image Viewer умеет раскидывать по дате из EXIF.
    Проблемы начинаются когда обнаруживаешь что в фотках нет EXIF, или там стоит 1990 год, или фотки с нескольких устройств с одного мероприятия.


    1. muxa_ru
      03.10.2019 22:49
      +1

      или фотки с нескольких устройств с одного мероприятия.

      И одно ещё в московском времени, а другое уже в берлинском



      1. Vseznaut Автор
        03.10.2019 23:27
        +1

        Время можно исправить внутри Exif это намного эффективнее, чем править названия файлов.


        1. muxa_ru
          03.10.2019 23:39

          Не готов изменять ни имя файла, ни экзифы.


          Это может поломать механизм дедупликации.


          Мало ли из каких бэкапов придётся восстанавливаться :)


  1. saintbyte
    03.10.2019 23:13

    Вы не поверите у меня такая структура уже 10 лет и я делаю это руками. Просто дисциплина. Автоматизация конечно хорошо — но проблема в том потом приходиться разгребать беспристрастные результаты алгоритма. Для полного счастья у меня камера и смена батарей сбрасывает время.


    1. Vseznaut Автор
      03.10.2019 23:28

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


    1. shoorick
      04.10.2019 10:05

      У меня сначала фотки сбрасываются с аппарата и сортируются самодельным перловым скриптом, а затем уже я просматриваю, что получилось, переименовываю получившиеся папки, объединяя некоторые из них (если одно мероприятие длится больше одного дня). Под годы, месяцы и дни — отдельные папки (например, ~/photo/2019/10/04 — мне так удобнее, чем видеть в одном месте кучу папок за все дни)



  1. 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;
    }
    


    1. Vseznaut Автор
      04.10.2019 15:06

      Я сделал проще, не удалось из даты создать что-то нормальное, значит даты нет, берем дату файла. А так вообще да, надо копать какие там ошибки попадаются, и обходить их, спасибо!


      1. NickyX3
        04.10.2019 15:07

        Там внутри есть fallback на текущую дату $normalized = date('c');