JSARToolKit を使用した拡張現実アプリケーションの作成

HTML5 Rocks

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

はじめに

この記事では、JSARToolKit ライブラリと WebRTC getUserMedia API(リンク先はいずれも英語)を使ってウェブ上に拡張現実(AR)アプリケーションを実装する方法について説明します。ここでのレンダリングには、パフォーマンスが向上している WebGL を使用しています。この記事では最終的に、ウェブカメラ動画の拡張現実マーカー上に 3D モデルを合成するデモ アプリケーションを作成します。

JSARToolKit は JavaScript 用の拡張現実ライブラリで、GPL のもとで公開されるオープンソース ライブラリです。Mozilla Remixing Reality デモ(英語)用に、筆者が Flash の FLARToolKit から直接移植して作成しました。FLARToolKit 自体は Java の NyARToolKit を移植したもので、NyARToolKit は C 言語の ARToolKit(リンク先は英語)を移植したものです。長くなりましたが、以上が前置きです。

JSARToolKit は canvas 要素に対して動作します。このツールキットでは canvas 外の画像を読み取る必要があるので、画像はページと同じ生成元からのものである必要があります。または、同一生成元のポリシーによる制約を回避するには、CORS を使用する必要があります。つまり、テクスチャとして使用する image 要素や video 要素の crossOrigin プロパティを '' または 'anonymous' に設定します。

canvas を解析のため JSARToolKit に渡すと、画像内に見つかった AR マーカーのリストとそれに対応する変換行列が JSARToolKit から返されます。3D オブジェクトをマーカーの前面に描画するには、使用している任意の 3D レンダリング ライブラリにこの変換行列を渡し、オブジェクトが行列に従って変換されるようにします。次に WebGL シーンの中に動画フレームを描画し、その前面にオブジェクトを描画して完了です。

JSARToolKit を使って動画を解析するには、canvas 上に動画を描画した後、その canvas を JSARToolKit に渡します。フレームごとにこの手順を行うことで、動画の AR トラッキングができます。JSARToolKit では、最新の JavaScript エンジンにより、640x480 サイズの動画フレームにも対応できるだけの十分な速度でこの処理がリアルタイムに行われます。ただし、動画フレームが大きいほど処理に時間はかかります。最適な動画フレームのサイズは通常 320x240 ですが、使用するマーカーが小さかったり複数だったりする場合は 640x480 が適しています。

デモ

ウェブカメラのデモを見るには、ブラウザで WebRTC を有効にする必要があります(Chrome の場合は、about:flags にアクセスして MediaStream を有効にします)。また、下図の AR マーカーを印刷してください。携帯端末やタブレットでマーカー画像を開いてウェブカメラに映してみる方法でうまくいけば、それでもかまいません。

AR マーカー

以下が今回のデモになります。このデモでは、AR マーカーを使って画像のスライドショーを作成します。マーカーをカメラに向けると、画像が合成されます。マーカーをカメラのフレームから外してもう一度カメラに向けると、違う画像になります。

AR ウェブカメラのデモ(WebRTC を有効にできない場合は、完成後の動画をご覧ください)

JSARToolKit をセットアップする

JSARToolKit API は Java に沿った作りの API なので、使用するには少し細工が必要です。基本的な考え方としては、ラスター オブジェクト上で動作する検出オブジェクトを用意し、検出オブジェクトとラスター オブジェクトの間に、ラスター座標をカメラ座標に変換するカメラ パラメータ オブジェクトを用意します。検出されたマーカーを検出オブジェクトから取得するには、マーカーを繰り返し処理し、その変換行列をコードにコピーします。

最初のステップとして、ラスター オブジェクト、カメラ パラメータ オブジェクト、検出オブジェクトを作成します。

// Create a RGB raster object for the 2D canvas.
// JSARToolKit uses raster objects to read image data.
// Note that you need to set canvas.changed = true on every frame.
var raster = new NyARRgbRaster_Canvas2D(canvas);

// FLARParam is the thing used by FLARToolKit to set camera parameters.
// Here we create a FLARParam for images with 320x240 pixel dimensions.
var param = new FLARParam(320, 240);

// The FLARMultiIdMarkerDetector is the actual detection engine for marker detection.
// It detects multiple ID markers. ID markers are special markers that encode a number.
var detector = new FLARMultiIdMarkerDetector(param, 120);

// For tracking video set continue mode to true. In continue mode, the detector
// tracks markers across multiple frames.
detector.setContinueMode(true);

// Copy the camera perspective matrix from the FLARParam to the WebGL library camera matrix.
// The second and third parameters determine the zNear and zFar planes for the perspective matrix.
param.copyCameraMatrix(display.camera.perspectiveMatrix, 10, 10000);

getUserMedia を使ってウェブカメラにアクセスする

次に、WebRTC API によりウェブカメラ動画を取得する video 要素を作成します。録画済みの動画の場合は、動画のソース属性を動画の URL に設定するだけです。静止画像からマーカー検出を行う場合は、今回と同様の方法で image 要素を使用できます。

WebRTC と getUserMedia はまだ新しい技術なので、これらを検出する機能が必要です。詳しくは、HTML5 での音声/動画の取得(リンク先は英語)に関する Eric Bidelman 氏の記事をご覧ください。

var video = document.createElement('video');
video.width = 320;
video.height = 240;

var getUserMedia = function(t, onsuccess, onerror) {
  if (navigator.getUserMedia) {
    return navigator.getUserMedia(t, onsuccess, onerror);
  } else if (navigator.webkitGetUserMedia) {
    return navigator.webkitGetUserMedia(t, onsuccess, onerror);
  } else if (navigator.mozGetUserMedia) {
    return navigator.mozGetUserMedia(t, onsuccess, onerror);
  } else if (navigator.msGetUserMedia) {
    return navigator.msGetUserMedia(t, onsuccess, onerror);
  } else {
    onerror(new Error("No getUserMedia implementation found."));
  }
};

var URL = window.URL || window.webkitURL;
var createObjectURL = URL.createObjectURL || webkitURL.createObjectURL;
if (!createObjectURL) {
  throw new Error("URL.createObjectURL not found.");
}

getUserMedia({'video': true},
  function(stream) {
    var url = createObjectURL(stream);
    video.src = url;
  },
  function(error) {
    alert("Couldn't access webcam.");
  }
);

マーカーを検出する

検出オブジェクトが問題なく動作するようになったら、AR の行列を検出するために画像のフィードを開始できます。まずラスター オブジェクトの canvas に画像を描画し、次にラスター オブジェクト上で検出オブジェクトを実行します。検出オブジェクトからは画像内で見つかったマーカーの数が返されます。

// Draw the video frame to the raster canvas, scaled to 320x240.
canvas.getContext('2d').drawImage(video, 0, 0, 320, 240);

// Tell the raster object that the underlying canvas has changed.
canvas.changed = true;

// Do marker detection by using the detector object on the raster object.
// The threshold parameter determines the threshold value
// for turning the video frame into a 1-bit black-and-white image.
//
var markerCount = detector.detectMarkerLite(raster, threshold);

最後のステップとして、検出されたマーカーを繰り返し処理し、その変換行列を取得します。この変換行列は、3D オブジェクトをマーカーの前面に配置するために使用します。

// Create a NyARTransMatResult object for getting the marker translation matrices.
var resultMat = new NyARTransMatResult();

var markers = {};

// Go through the detected markers and get their IDs and transformation matrices.
for (var idx = 0; idx < markerCount; idx++) {
  // Get the ID marker data for the current marker.
  // ID markers are special kind of markers that encode a number.
  // The bytes for the number are in the ID marker data.
  var id = detector.getIdMarkerData(idx);

  // Read bytes from the id packet.
  var currId = -1;
  // This code handles only 32-bit numbers or shorter.
  if (id.packetLength <= 4) {
    currId = 0;
    for (var i = 0; i < id.packetLength; i++) {
      currId = (currId << 8) | id.getPacketData(i);
    }
  }

  // If this is a new id, let's start tracking it.
  if (markers[currId] == null) {
    markers[currId] = {};
  }
  // Get the transformation matrix for the detected marker.
  detector.getTransformMatrix(idx, resultMat);

  // Copy the result matrix into our marker tracker object.
  markers[currId].transform = Object.asCopy(resultMat);
}

行列のマッピング

JSARToolKit の行列を glMatrix の行列(16 個の要素からなる FloatArray、最後の 4 要素が変換列)にコピーするコードは次のとおりです。これで希望の結果が得られます(注: ARToolKit の行列のセットアップ方法については筆者は詳しくありませんが、Y 軸の反転が関係しているように思います)。この符号を反転させるトリックにより、JSARToolKit の行列が glMatrix と同じように動作します。

このライブラリを別のライブラリ(Three.js など)とともに使用するには、ARToolKit の行列をライブラリの行列形式に変換する関数を記述する必要があります。また、FLARParam.copyCameraMatrix メソッドに接続する必要もあります。copyCameraMatrix メソッドにより、FLARParam 透視変換行列が glMatrix 形式の行列に書き換えられます。

function copyMarkerMatrix(arMat, glMat) {
  glMat[0] = arMat.m00;
  glMat[1] = -arMat.m10;
  glMat[2] = arMat.m20;
  glMat[3] = 0;
  glMat[4] = arMat.m01;
  glMat[5] = -arMat.m11;
  glMat[6] = arMat.m21;
  glMat[7] = 0;
  glMat[8] = -arMat.m02;
  glMat[9] = arMat.m12;
  glMat[10] = -arMat.m22;
  glMat[11] = 0;
  glMat[12] = arMat.m03;
  glMat[13] = -arMat.m13;
  glMat[14] = arMat.m23;
  glMat[15] = 1;
}

Three.js を組み込む

Three.js は広く使われている JavaScript 3D エンジンです。以下では、Three.js での JSARToolKit 出力の使用方法について説明します。ここで必要になるのは、動画画像が描画される 4 分割フルスクリーン、FLARParam 透視変換行列を使用するカメラ、変換用のマーカー行列を使用するオブジェクトの 3 つです。下記のコードで、Three.js を組み込む手順を示します。

実際に動作する Three.js のデモについては、こちらをご覧ください。デバッグ出力が有効になっているので、JSARToolKit ライブラリの内部動作を確認できます。

// I'm going to use a glMatrix-style matrix as an intermediary.
// So the first step is to create a function to convert a glMatrix matrix into a Three.js Matrix4.
THREE.Matrix4.prototype.setFromArray = function(m) {
  return this.set(
    m[0], m[4], m[8], m[12],
    m[1], m[5], m[9], m[13],
    m[2], m[6], m[10], m[14],
    m[3], m[7], m[11], m[15]
  );
};

// glMatrix matrices are flat arrays.
var tmp = new Float32Array(16);

// Create a camera and a marker root object for your Three.js scene.
var camera = new THREE.Camera();
scene.add(camera);

var markerRoot = new THREE.Object3D();
markerRoot.matrixAutoUpdate = false;

// Add the marker models and suchlike into your marker root object.
var cube = new THREE.Mesh(
  new THREE.CubeGeometry(100,100,100),
  new THREE.MeshBasicMaterial({color: 0xff00ff})
);
cube.position.z = -50;
markerRoot.add(cube);

// Add the marker root to your scene.
scene.add(markerRoot);

// Next we need to make the Three.js camera use the FLARParam matrix.
param.copyCameraMatrix(tmp, 10, 10000);
camera.projectionMatrix.setFromArray(tmp);


// To display the video, first create a texture from it.
var videoTex = new THREE.Texture(videoCanvas);

// Then create a plane textured with the video.
var plane = new THREE.Mesh(
  new THREE.PlaneGeometry(2, 2, 0),
  new THREE.MeshBasicMaterial({map: videoTex})
);

// The video plane shouldn't care about the z-buffer.
plane.material.depthTest = false;
plane.material.depthWrite = false;

// Create a camera and a scene for the video plane and
// add the camera and the video plane to the scene.
var videoCam = new THREE.Camera();
var videoScene = new THREE.Scene();
videoScene.add(plane);
videoScene.add(videoCam);

...

// On every frame do the following:
function tick() {
  // Draw the video frame to the canvas.
  videoCanvas.getContext('2d').drawImage(video, 0, 0);
  canvas.getContext('2d').drawImage(videoCanvas, 0, 0, canvas.width, canvas.height);

  // Tell JSARToolKit that the canvas has changed.
  canvas.changed = true;

  // Update the video texture.
  videoTex.needsUpdate = true;

  // Detect the markers in the video frame.
  var markerCount = detector.detectMarkerLite(raster, threshold);
  for (var i=0; i<markerCount; i++) {
    // Get the marker matrix into the result matrix.
    detector.getTransformMatrix(i, resultMat);

    // Copy the marker matrix to the tmp matrix.
    copyMarkerMatrix(resultMat, tmp);

    // Copy the marker matrix over to your marker root object.
    markerRoot.matrix.setFromArray(tmp);
  }

  // Render the scene.
  renderer.autoClear = false;
  renderer.clear();
  renderer.render(videoScene, videoCam);
  renderer.render(scene, camera);
}

まとめ

この記事では、JSARToolKit の基本事項について説明しました。この記事を参考に、ウェブカメラを使用した JavaScript の拡張現実アプリケーションを作成してみてください。

JSARToolKit を Three.js に組み込むのは少し面倒ですが、必ずできます。このデモで採用している方法が正しいとは限らないので、より効率良く組み込める方法があればぜひ教えてください。修正パッチも歓迎します(リンク先は英語)。

参考資料

Comments

0