Недавно у меня возникла необходимость отсканировать большое количество фотографий в хорошем качестве. Это фото с семейного альбома. Я приобрел принтер Epson Perfection V600 Photo. Он отлично сканирует, но иногда остается большое количетсво мелких соринок и прочих проблем сканирования. Где то цвета не те, где то фото обрезалось при сканировании. Для этого я решил делать по несколько сканов одной фотографии в разных положениях и потом соединять их в одну.

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


Мой сканер.


Общая схема работы

Общая идея работы была такая, сделать 4 фото, объединить их в одну. Для этого я буду делать сразу сканы 4 фото параллельно. Это позволяет сделать сканер. Они автоматически будут разделены.

На схеме синим подписаны фотографии.

Я делаю сканы без сжатия, в tif формате. Получается нам нужно сделать 4 перехода, как показано выше. У нас будет 4 фото на 4 сканирования => 16 фото.

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

> tree scan
scan
├── 1
│   ├── 1.tif
│   ├── 5.tif
│   ├── 7.tif
│   └── 9.tif
└── 2
    ├── 2.tif
    ├── 4.tif
    ├── 6.tif
    └── 8.tif

Дальше нужно соединить фото в папке и сохранить ее в другую папку.

Photoshop автоматизация

Я помнил, что в Photoshop можно сделать автоматизацию. Но перед этим нужно было как их объединить. Оказалось, что уже есть возможность загрузить и объединить несколько фото. Это Load Files into Stack.

Going to File > Scripts > Load Files into Stack.

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

Мой скрипт

На основе этого был сделан скрип, в котором нужно указать папку scan. После этого он будет автоматически объединять все фото в папке и применять параметры объединения. И сохраняет в папку output. Кстати, для создания скрипта использовались функции Photoshop по логированию всех действий в файл. По этому такие странные имена в функции parse. Вывод идет логов в файл ScriptingListenerJS.log на рабочем столе.

Я оставлю комментарии прямо в коде скрипта. Для запуска нужно сохранить это код с расширением .jsx

// Не отображать окна для подтверждения
app.displayDialogs = DialogModes.NO;

// Загрузить уже существующий скрипт
loadLayersFromScript = true;
var SCRIPTS_FOLDER = decodeURI(
  app.path + "/" + localize("$$$/ScriptingSupport/InstalledScripts=Presets/Scripts"),
);
$.evalFile(new File(SCRIPTS_FOLDER + "/Load Files into Stack.jsx"));

// Куда нужно сохранять изображения
var output = "/Users/grishy/Desktop/result/Untitled-";
// Начальный индекс для фото
// Получается Untitled-1 Untitled-2 Untitled-3
var startInx = 1;

// Выбираем папку с папками (scan)
var dirFolder = Folder.selectDialog("Select a folder to process");
// Фильтр, чтобы выбрать только папки
var dirList = dirFolder.getFiles(function (f) {
  return f instanceof Folder;
});

// Начать объединения всех фото
var len = dirList.length;
for (var i = 0; i < len; i++) {
  var el = dirList[i];

  // Вывести сообщение в консоль
  $.writeln("> Selected photos (" + i + 1 + "/" + len + "): " + el);

  parse(Folder(el));
  startInx++;
}

function parse(inputFolder) {
  // Берем все фото
  var filesList = inputFolder.getFiles("*.tif");

  $.writeln("> Selected photos:");
  $.writeln(filesList);

  if (filesList == null) return;

  // Создаем объединение
  loadLayers.createSmartObject = true;
  loadLayers.intoStack(filesList, true);

  // =======================================================
  // Обрезаем пустые места с краев, crop
  var idtrim = stringIDToTypeID("trim");
  var desc4 = new ActionDescriptor();
  var idtrimBasedOn = stringIDToTypeID("trimBasedOn");
  var idtrimBasedOn = stringIDToTypeID("trimBasedOn");
  var idtransparency = stringIDToTypeID("transparency");
  desc4.putEnumerated(idtrimBasedOn, idtrimBasedOn, idtransparency);
  var idtop = stringIDToTypeID("top");
  desc4.putBoolean(idtop, true);
  var idbottom = stringIDToTypeID("bottom");
  desc4.putBoolean(idbottom, true);
  var idleft = stringIDToTypeID("left");
  desc4.putBoolean(idleft, true);
  var idright = stringIDToTypeID("right");
  desc4.putBoolean(idright, true);
  executeAction(idtrim, desc4, DialogModes.NO);

  // =======================================================
  // Объединяем их по среднему значению в каждом пикселе
  var idapplyImageStackPluginRenderer = stringIDToTypeID("applyImageStackPluginRenderer");
  var desc2 = new ActionDescriptor();
  var idimageStackPlugin = stringIDToTypeID("imageStackPlugin");
  var idavrg = charIDToTypeID("avrg");
  desc2.putClass(idimageStackPlugin, idavrg);
  var idname = stringIDToTypeID("name");
  desc2.putString(idname, "Mean");
  executeAction(idapplyImageStackPluginRenderer, desc2, DialogModes.NO);

  // =======================================================
  // Сохраняем фото в папку, как png
  var idsave = stringIDToTypeID("save");
  var desc4 = new ActionDescriptor();
  var idas = stringIDToTypeID("as");
  var desc5 = new ActionDescriptor();
  var idmethod = stringIDToTypeID("method");
  var idPNGMethod = stringIDToTypeID("PNGMethod");
  var idquick = stringIDToTypeID("quick");
  desc5.putEnumerated(idmethod, idPNGMethod, idquick);
  var idPNGInterlaceType = stringIDToTypeID("PNGInterlaceType");
  var idPNGInterlaceType = stringIDToTypeID("PNGInterlaceType");
  var idPNGInterlaceNone = stringIDToTypeID("PNGInterlaceNone");
  desc5.putEnumerated(idPNGInterlaceType, idPNGInterlaceType, idPNGInterlaceNone);
  var idPNGFilter = stringIDToTypeID("PNGFilter");
  var idPNGFilter = stringIDToTypeID("PNGFilter");
  var idPNGFilterAdaptive = stringIDToTypeID("PNGFilterAdaptive");
  desc5.putEnumerated(idPNGFilter, idPNGFilter, idPNGFilterAdaptive);
  var idcompression = stringIDToTypeID("compression");
  desc5.putInteger(idcompression, 6);
  var idPNGFormat = stringIDToTypeID("PNGFormat");
  desc4.putObject(idas, idPNGFormat, desc5);
  var idin = stringIDToTypeID("in");
  desc4.putPath(idin, new File(output + startInx + ".png"));
  var iddocumentID = stringIDToTypeID("documentID");
  desc4.putInteger(iddocumentID, 223);
  var idlowerCase = stringIDToTypeID("lowerCase");
  desc4.putBoolean(idlowerCase, true);
  var idsaveStage = stringIDToTypeID("saveStage");
  var idsaveStageType = stringIDToTypeID("saveStageType");
  var idsaveSucceeded = stringIDToTypeID("saveSucceeded");
  desc4.putEnumerated(idsaveStage, idsaveStageType, idsaveSucceeded);
  executeAction(idsave, desc4, DialogModes.NO);

  return;
  // =======================================================
  // Закрываем фото, для экономии памяти
  var idclose = stringIDToTypeID("close");
  var desc3 = new ActionDescriptor();
  var idsaving = stringIDToTypeID("saving");
  var idyesNo = stringIDToTypeID("yesNo");
  var idno = stringIDToTypeID("no");
  desc3.putEnumerated(idsaving, idyesNo, idno);
  var iddocumentID = stringIDToTypeID("documentID");
  desc3.putInteger(iddocumentID, 258);
  var idforceNotify = stringIDToTypeID("forceNotify");
  desc3.putBoolean(idforceNotify, true);
  executeAction(idclose, desc3, DialogModes.NO);
}

Запуск

После первого запуска я получил проблемы нехватки места...

Во втором месте, я решил замерить, как оно по потреблению памяти. Я решил не закрывать фото, стало интересно, как оно будет по потреблению памяти.

Кстати, оказалось что есть плагин для VSCode для отладки таких скриптов. Они могу запускать их в Debug режиме. Мне очень помогло в разработки скрипта.

Результат