JavaScript-парсер для искателей сокровищ фотографических глубин

от автора

Первая дошедшая до нас фотокарточка была чёрно-белой и размытой. Потом в фотографию пришла резкость. Позже – цвет. Ещё один шаг вперёд – цифра. Популярность и распространение «светописи» постоянно росли и растут. Вот уже и коты делают селфи. Что дальше? А дальше (вернее – прямо сейчас) цифровые снимки, которые, помимо миллионов цветных точек, хранят информацию о глубине запечатлённого на них пространства.

Это открывает потрясающие возможности. Среди них – эффекты движения, такие, как параллакс и «наезд-отъезд». В «глубинах» снимков таятся новые подходы к художественным фильтрам, к настройке резкости, к редактированию изображений, к измерениям по фото. И это – только начало.

Сегодня мы поговорим о JavaScript-реализации парсера фотографий с поддержкой глубины. Он работает с графическими файлами формата eXtensible Device Metadata (XDM), извлекая из них встроенные метаданные и сохраняя полученные материалы в виде XML-файлов. Кроме того, программа умеет извлекать из XML сведения о цвете и глубине пространства. В результате, на выходе получаются XML-файлы, цветные изображения и файлы карт глубины.

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

Прежде чем рассматривать код, остановимся на формате XDM.

Формат XDM

На вход скрипта подаются XDM-файлы. В формате XDM метаданные хранятся в изображениях-контейнерах, при этом изображения совместимы с существующими приложениями для просмотра графики. Этот формат разработан для технологии Intel RealSense. Метаданные содержат технические сведения. А именно, это карта глубины, пространственное положение устройства и камеры, модель перспективы объектива, информация о производителе оборудования, облако точек. Вот, как выглядит цветное изображение (справа) и соответствующая ему карта глубины в формате XDM (слева), которая хранится в файле изображения в виде метаданных.


Цветное изображение и его карта глубины

Данные формата XDM нужно как-то интегрировать в файл-контейнер. Для этого используется стандарт Adobe XMP.

Стандарт Adobe XMP

Сейчас спецификация XDM предусматривает использование графических файлов-контейнеров четырёх форматов: JPEG, PNG, TIFF и GIF. Метаданные XDM сериализуются и внедряются в графический файл-контейнер. Способ хранения метаданных основан на стандарте Adobe Extensible Metadata Platform (XMP). Рассматриваемое здесь приложение рассчитано на использование контейнеров в формате JPEG. Кратко остановимся на том, как XMP-метаданные встраиваются в JPEG-файлы, и на том, как программа обрабатывает XMP-пакеты.

В стандарте XMP фрагменты данных маркируются 2-х байтовыми последовательностями. Маркеры типа 0xFFE0–0xFFEF обычно используются для данных приложений. Их имена имеют вид APPn. Такие маркеры принято начинать со строки, описывающей их назначение. Это – так называемая строка пространства имён или строка подписи. Маркер APP1 идентифицирует метаданные Exif и TIFF. Кодом APP13 маркируют данные формата Photoshop Image Resources. Они содержат IPTC-метаданные. На расположение XMP-пакета или пакетов указывают ещё один или несколько APP1-маркеров.

Вот как выглядит запись формата StandardXMP в JPEG-файле.

Поля записи формата StandardXMP

Смещение, байт Длина, байт Значение Имя Комментарии
0 2 0xFFE1 APP1 Маркер APP1 указывает на раздел метаданных
2 2 2 + 29 + длина XMP пакета Lp Размер в байтах, равный сумме размеров этого раздела и двух следующих
4 29 Строка ASCII без кавычек, заканчивающаяся нулевым символом namespace URI пространства имён XMP, используется как уникальный идентификатор: ns.adobe.com/xap/1.0
33 < 65503 XMP-пакет Обязательно использование кодировки UTF-8

Если после сериализации размер XMP-пакета оказывается больше, чем 64 Кб, его можно разделить на части и сохранить эти части в нескольких местах JPEG-файла. А именно, при таком подходе данные пакета будут представлены главным (StandardXMP) и расширенным (ExtendedXMP) сегментами. ExtendedXMP использует тот же формат записи, что и StandardXMP. Единственное исключение – в поле, хранящем сведения о пространстве имён (namespace), указывается http://ns.adobe.com/xmp/extension/.

Вот, как выглядят данные XMP-пакета, внедрённые в JPEG-файл в виде записей форматов StandardXMP и ExtendedXMP.


Записи форматов StandardXMP и ExtendedXMP в JPEG-файле

Рассмотрим три функции.

  • Функция findMarker анализирует JPEG-файл в поиске маркера 0xFFE1, начиная с заданной позиции. Содержимое файла представлено параметром функции buffer, позиция – параметром position. Если маркер найден – функция вернёт его адрес, если не найден – значение -1.
  • Функция findHeader занимается поиском пространств имён StandardXMP (http://ns.adobe.com/xap/1.0/) и ExtendedXMP (http://ns.adobe.com/xmp/extension/) в JPEG-файле. Ей передаются, опять же, буфер с данными файла (buffer) и позиция, с которой надо начинать поиск (position). Если совпадение найдено – функция вернёт строку, соответствующую обнаруженному пространству имён. Если нет – будет возвращена пустая строка.
  • Функция findGUID занимается поиском GUID, который хранится в элементе xmpNote:HasExtendedXMP в JPEG-файле (параметр buffer), начиная с переданного ей места в файле (position) и заканчивая позицией в файле, вычисляемой как position+zize-1. Найдя искомый элемент, она возвращает его адрес.

Вот код этих функций.

// Возвращает позицию в файле (buffer),в которой содержится маркер 0xFFE1, начиная поиск с заданного места (position) // Возвращает -1, если совпадений не найдено function findMarker(buffer, position) {     var index;     for (index = position; index < buffer.length; index++) {         if ((buffer[index] == marker1) && (buffer[index + 1] == marker2))             return index;     }     return -1; }  // Возвращает строку, указывающую на пространство имён, либо – пустую строку, если ничего не найдено.  function findHeader(buffer, position) {     var string1 = buffer.toString('ascii', position + 4, position + 4 + header1.length);     var string2 = buffer.toString('ascii', position + 4, position + 4 + header2.length);     if (string1 == header1)         return header1;     else if (string2 == header2)         return header2;     else         return noHeader; }  // Возвращает адрес GUID function findGUID(buffer, position, size) {     var string = buffer.toString('ascii', position, position + size - 1);     var xmpNoteString = "xmpNote:HasExtendedXMP=";     var GUIDPosition = string.search(xmpNoteString);     var returnPos = GUIDPosition + position + xmpNoteString.length + 1;     return returnPos; }

128-битный GUID хранится в виде 32-байтовой шестнадцатеричной ASCII-строки в каждом сегменте ExtendedXMP, за пространством имён. Он же хранится и в StandardXMP-сегменте, как значение свойства xmpNote:HasExtendedXMP. Благодаря этому мы можем обнаруживать неподходящие или изменённые ExtendedXMP-сегменты.

XML

Метаданные формата XMP можно внедрять непосредственно в XML-документы. В соответствии со спецификацией XDM, структуру данных XML можно задать так, как показано в таблице.

XML-представление XMP-данных

Графический файл содержит вышеописанные элементы в формате RDF/XML. Нужно отметить, что изображение-контейнер является внешним, по отношению к XDM-данным, объектом. Оно остаётся совместимым с обычными приложениями для просмотра графики, не поддерживающими XDM.

Вот фрагмент кода, в котором продемонстрировано ядро парсера. Именно здесь осуществляется анализ входного JPEG-файла, поиск APP1-маркера 0xFFE1. Если маркер найден, выполняется поиск строковых представлений пространств имён StandardXMP и ExtendedXMP. Если найдено первое, вычисляется размер метаданных и их начальный адрес, данные извлекаются и создаётся XML-файл StandardXMP. Если найдено второе, процедура повторяется, но формируется уже XML-файл ExtendedXMP. На выходе приложения оказываются два XML-файла.

// Главная функция для разбора XDM-файла function xdmParser(xdmFilePath) {  try {      //Получаем размер JPEG-файла в байтах      var fileStats = fs.statSync(xdmFilePath);      var fileSizeInBytes = fileStats["size"];       var fileBuffer = new Buffer(fileSizeInBytes);          //Получаем дескриптор JPEG-файла      var xdmFileFD = fs.openSync(xdmFilePath, 'r');       //Читаем JPEG-файл в двоичный буфер      fs.readSync(xdmFileFD, fileBuffer, 0, fileSizeInBytes, 0);       var bufferIndex, segIndex = 0, segDataTotalLength = 0, XMLTotalLength = 0;      for (bufferIndex = 0; bufferIndex < fileBuffer.length; bufferIndex++) {          var markerIndex = findMarker(fileBuffer, bufferIndex);          if (markerIndex != -1) {                 // Найден маркер 0xFFE1              var segHeader = findHeader(fileBuffer, markerIndex);              if (segHeader) {                  // Найден заголовок                  // Если заголовок найти не удалось, ищем следующий такой маркер, а этот пропускаем                     // segIndex начинается с 0, А НЕ с 1                  var segSize = fileBuffer[markerIndex + 2] * 16 * 16 + fileBuffer[markerIndex + 3];                  var segDataStart;                   // 2-->segSize длиной 2-байта                     // 1-->учтём последний 0 в конце заголовка, один байт                  segSize -= (segHeader.length + 2 + 1);                  // 2-->0xFFE1 длиной 2-байта                  // 2-->segSize длиной 2 байта                  // 1-->учтём последний 0 в конце заголовка, один байт                  segDataStart = markerIndex + segHeader.length + 2 + 2 + 1;                                   if (segHeader == header1) {                         // StandardXMP                      var GUIDPos = findGUID(fileBuffer, segDataStart, segSize);                      var GUID = fileBuffer.toString('ascii', GUIDPos, GUIDPos + 32);                      var segData_xap = new Buffer(segSize - 54);                      fileBuffer.copy(segData_xap, 0, segDataStart + 54, segDataStart + segSize);                      fs.appendFileSync(outputXAPFile, segData_xap);                  }                  else if (segHeader == header2) {                         // ExtendedXMP                      var segData = new Buffer(segSize - 40);                      fileBuffer.copy(segData, 0, segDataStart + 40, segDataStart + segSize);                      XMLTotalLength += (segSize - 40);                      fs.appendFileSync(outputXMPFile, segData);                  }                  bufferIndex = markerIndex + segSize;                  segIndex++;                  segDataTotalLength += segSize;              }          }          else {                 // Больше маркеров нет, остановим цикл              break;          };      }  } catch(ex) {   console.log("Something bad happened! " + ex);  } }

Вот фрагмент кода, который анализирует XML-файл и формирует цветное изображение и его карту глубины. Потом этими данными можно пользоваться для обработки фото с поддержкой глубины. Здесь всё очень просто. Функция xmpMetadataParser() ищет атрибут IMAGE:DATA и извлекает соответствующие ему данные в JPEG-файл. Получается цветное изображение. Если найдено несколько таких атрибутов, будет создано несколько JPEG-файлов. Кроме того, функция выполняет поиск атрибута DEPTHMAP:DATA и извлекает соответствующие данные в PNG-файл. Это и есть карта глубины. Если найдено несколько таких атрибутов, соответственно, создаётся несколько PNG-файлов. На выходе получаем один или несколько JPEG- и PNG-файлов.

// Обработка XMP-метаданных и поиск атрибутов, соответствующих цветным изображениям и картам глубины function xmpMetadataParser() {     var imageIndex = 0, depthImageIndex = 0, outputPath = "";     parser = sax.parser();      // Когда нужный атрибут найден, извлекаем данные     parser.onattribute = function (attr) {         if ((attr.name == "IMAGE:DATA") || (attr.name == "GIMAGE:DATA")) {             outputPath = inputJpgFile.substring(0, inputJpgFile.length - 4) + "_" + imageIndex + ".jpg";             var atob = require('atob'), b64 = attr.value, bin = atob(b64);             fs.writeFileSync(outputPath, bin, 'binary');             imageIndex++;         } else if ((attr.name == "DEPTHMAP:DATA") || (attr.name == "GDEPTH:DATA")) {             outputPath = inputJpgFile.substring(0, inputJpgFile.length - 4) + "_depth_" + depthImageIndex + ".png";             var atob = require('atob'), b64 = attr.value, bin = atob(b64);             fs.writeFileSync(outputPath, bin, 'binary');             depthImageIndex++;         }     };      parser.onend = function () {         console.log("All done!")     } }  // Обработка XMP-метаданных function processXmpData(filePath) {     try {         var file_buf = fs.readFileSync(filePath);         parser.write(file_buf.toString('utf8')).close();     } catch (ex) {         console.log("Something bad happened! " + ex);     } }

Итоги

Итак, XDM-файлы разобраны, превращены в JPEG и PNG, в цветные изображения и карты глубины. Всё это сделано исключительно средствами нашего скрипта, без привлечения дополнительных библиотек. Хотите внедрить в свой веб-проект инструменты для обработки фото с поддержкой глубины? JavaScript-парсер, о котором мы рассказали, способен стать фундаментом, на котором подобные инструменты можно построить.

P.S. Пишете на Java и хотите обрабатывать фото с поддержкой глубины в своих проектах? Если так – значит вам сюда.

ссылка на оригинал статьи https://habrahabr.ru/post/278401/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *