Первая дошедшая до нас фотокарточка была чёрно-белой и размытой. Потом в фотографию пришла резкость. Позже – цвет. Ещё один шаг вперёд – цифра. Популярность и распространение «светописи» постоянно росли и растут. Вот уже и коты делают селфи. Что дальше? А дальше (вернее – прямо сейчас) цифровые снимки, которые, помимо миллионов цветных точек, хранят информацию о глубине запечатлённого на них пространства.
Это открывает потрясающие возможности. Среди них – эффекты движения, такие, как параллакс и «наезд-отъезд». В «глубинах» снимков таятся новые подходы к художественным фильтрам, к настройке резкости, к редактированию изображений, к измерениям по фото. И это – только начало.
Сегодня мы поговорим о 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 и хотите обрабатывать фото с поддержкой глубины в своих проектах? Если так – значит вам сюда.
Автор: Intel