LoginSignup
12
11

More than 5 years have passed since last update.

JavaScriptでThetaの360°JPEGファイルからXMPメタデータを取得

Last updated at Posted at 2016-01-26

はじめに

冬休みにThetaの360°画像をぼかすWebアプリSphereBlur.com作った際に、JPEG画像から姿勢情報を取得する必要があった。Thetaの開発者向けサイトによると、天頂補正情報は機種によってEXIFもしくはPhoto Sphere XMP Metadata準拠のXMPに保存されているとのこと。

https://developers.theta360.com/en/docs/introduction/

EXIF XMP
RICOH THETA OK NG
RICOH THETA m15 / RICOH THETA S OK OK

いろいろ調べたが、EXIFにどのような形式で保存されているのか分からなかったので、とりあえずXMPから姿勢情報を取得することにした。XMPの方はこちらに詳細な情報が記載されている。

デモ

https://jsbin.com/tovowo/edit?js,output

ソースコード解説

まず、ローカルにあるファイルをJavaScriptで読み込むために、<input type="file">エレメントを使って得られたファイルからFileReader.readAsArrayBuffer()を使ってArrayBufferに読み込む。

var fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/jpeg';
fileInput.name = 'files[]';
fileInput.addEventListener('change', function () {
  var fileReader = new FileReader();
  fileReader.onload = function(e) {
    var arrayBuffer = e.target.result;
    var poseInfo = getPoseInfo(arrayBuffer);
    if (!poseInfo) {
      alert('Failed to get the pose info.');
      return;
    }
    alert('heading: ' + poseInfo.heading + '\n' +
          'pitch: ' + poseInfo.pitch + '\n' +
          'roll: ' + poseInfo.roll);
  };
  fileReader.readAsArrayBuffer(fileInput.files[0]);
}, false);
document.body.appendChild(fileInput);

JPEGファイルは、FFD8で始まって、「2バイトのマーカー値、2バイトのセグメントサイズ値、セグメントの中身」というセグメントが複数個並んでいる。マーカー値がFFDAのセグメントまでくるとこれ以降にはイメージデータが入っているので、このマーカーが出るまで順番に読んでいく。
XMPのデータはFFE1というマーカーで始まるAPP1と呼ばれるセグメントに入っている。ただし、XMP以外にEXIFも別のAPP1セグメントに入っているので、isXmp()で先頭を確認する。

function getPoseInfo(arrayBuffer) {
    var pos = 0;
    try {
      if (read2Bytes() != 0xFFD8)
        return null;
      while (pos + 4 < arrayBuffer.byteLength) {
        var marker = read2Bytes();
        var size = read2Bytes();
        if (marker == 0xFFDA)
          break;
        if (marker == 0xFFE1) {
          if (isXmp()) {
            var dom = getXmp(size);
            if (!dom)
              return null;
            var list = getLeafs(dom);
            return getHadingPitchRoll(list);
          }
        }
        pos += size - 2;
      }
    } catch (e) {
      console.error(e.toString());
    }
    return null;
    // 後略
}

read2Bytes()では、Uint8Arrayを使ってarrayBufferから2バイト読んでいる。

    function read2Bytes() {
      var bytes =  new Uint8Array(arrayBuffer, pos, 2);
      pos += 2;
      return bytes[0] * 0x100 + bytes[1];
    }

XMPを格納したセグメントはhttp://ns.adobe.com/xap/1.0/\0で始まるので、それを確認する。ちなみに、EXIFはExif\0\0で始まる。

    function isXmp() {
      var bytes = new Uint8Array(arrayBuffer, pos, 29);
      var t = String.fromCharCode.apply(null, bytes);
      return t == 'http://ns.adobe.com/xap/1.0/\0';
    }

先頭のhttp://ns.adobe.com/xap/1.0/\0を読み飛ばした部分から、<x:xmpmetaで始まって</x:xmpmeta>で終わる領域を抜き出して、DOMParserでパースする。

    function getXmp(size) {
      var xmpStr = String.fromCharCode.apply(
          null,
          new Uint8Array(arrayBuffer, pos + 29, size - 31));
      var startPos = xmpStr.search('<x:xmpmeta ');
      var endPos = xmpStr.search('</x:xmpmeta>');
      if (startPos == -1 || endPos == -1)
        return null;
      xmpStr = xmpStr.substr(startPos, endPos - startPos + 12);
      var dom = (new DOMParser()).parseFromString(
          xmpStr, 'text/xml');
      return dom;
    }

getLeafs(node)では、XMLの葉の要素だけをリストにして返している。

    function getLeafs(node) {
      var list = [];
      if (node.childNodes.length == 1 &&
          node.childNodes[0].nodeType == 3) {
        return [[node.nodeName, node.childNodes[0].nodeValue]];
      }
      for (var i = 0; i < node.childNodes.length; ++i)
        list = list.concat(getLeafs(node.childNodes[i]));
      return list;
    }

getHadingPitchRoll()で、必要となる姿勢情報だけを取り出して返している。

    function getHadingPitchRoll(list) {
      var heading = 0, pitch = 0, roll = 0;
      list.forEach(function(item) {
        if (item[0] == 'GPano:PoseHeadingDegrees')
          heading = item[1];
        if (item[0] == 'GPano:PosePitchDegrees')
          pitch = item[1];
        if (item[0] == 'GPano:PoseRollDegrees')
          roll = item[1];
      });
      return {heading: heading, pitch: pitch, roll: roll};
    }

参考リンク

12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11