使用 JSARToolKit 编写增强现实应用

Ilmari Heikkinen

简介

本文介绍了如何将 JSARToolKit 库与 WebRTC getUserMedia API 结合使用,以在网页上运行增强现实应用。我之所以使用 WebGL 进行渲染,是因为它提高了性能。本文的最终结果是一款演示应用,它可以将 3D 模型置于摄像头视频的增强现实标记之上。

JSARToolKit 是 JavaScript 的增强现实库。它是按照 GPL 发布的开源库,是我为 Mozilla 混剪现实演示构建的 Flash FLARToolKit 的直接移植。FLARToolKit 本身是 Java NyARToolKit 的移植,后者是 C ARToolKit 的端口。很久,但现在我们到了。

JSARToolKit 在画布元素上运行。由于图片需要从画布中读取图片,因此图片需要与网页来自同一来源,或使用 CORS 规避同源政策。简而言之,在您要用作纹理的图片或视频元素上,将 crossOrigin 属性设为 '''anonymous'

当您将画布传递给 JSARToolKit 进行分析时,JSARToolKit 会返回在图片和相应转换矩阵中找到的 AR 标记列表。要在标记上绘制 3D 对象,请将转换矩阵传递给您使用的任何 3D 渲染库,以便使用此矩阵转换对象。然后,在 WebGL 场景中绘制视频帧,并在视频帧之上绘制对象,就大功告成了。

要使用 JSARToolKit 分析视频,请在画布上绘制视频,然后将画布传递给 JSARToolKit。每一帧都这样,视频增强现实跟踪就设置好了。在现代 JavaScript 引擎上,JSARToolKit 的处理速度甚至足以在 640x480 的视频帧上实时执行此操作。但是,视频帧越大,处理所需的时间就越长。320x240 是合适的视频帧尺寸,但如果您希望使用较小的标记或多个标记,最好使用 640x480。

演示

如要观看摄像头演示,您需要在浏览器中启用 WebRTC(在 Chrome 中,请访问 about:flags 并启用 MediaStream)。您还需要打印出下方的 AR 标记。您也可以尝试在手机或平板电脑上打开标记图像,并展示给摄像头。

增强现实标记。
AR 标记。

设置 JSARToolKit

JSARToolKit API 与 Java 非常相似,因此您需要在使用时进行一些调整。基本思路是,检测器对象运行在光栅对象上。相机参数对象位于检测器和光栅之间,用于将光栅坐标转换为相机坐标。为了从检测器中获取检测到的标记,您需要遍历这些标记,并将其转换矩阵复制到您的代码中。

第一步是创建光栅对象、相机参数对象和检测器对象。

// 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 获取摄像头视频。对于预先录制的视频,只需将视频的来源属性设为视频网址即可。如果您从静态图片中检测标记,则可以用与此大致相同的方式使用图片元素。

由于 WebRTC 和 getUserMedia 仍是新兴技术,因此您需要对它们进行功能检测。有关详情,请参阅 Eric Bidelman 有关在 HTML5 中捕获音频和视频的文章。

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 矩阵。先将图片绘制到光栅对象画布上,然后在光栅对象上运行检测器。检测器会返回在图片中找到的标记数量。

// 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 &lt; 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 个元素的 FloatArrays,最后四个元素是转换列)的代码。它的工作原理十分强大(注意:我不知道 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 输出。您需要三项内容:上面绘制有视频图片的全屏四边形、带有 FLARParam 透视矩阵的相机,以及带有作为转换矩阵的标记矩阵的对象。我将通过以下代码向您介绍相关集成。

// 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&lt;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 集成有些麻烦,但完全有可能实现。我无法 100% 确定我在演示中的操作是否正确,因此如果您知道更好的实现集成的方法,请告诉我。欢迎提供补丁 :)

参考编号