使用 JSARToolKit 编写增强现实应用

Ilmari Heikkinen

简介

本文介绍了如何将 JSARToolKit 库与 WebRTC getUserMedia API 搭配使用,在 Web 上实现增强现实应用。在渲染方面,我使用的是 WebGL,因为它可以提供更高的性能。本文的最终结果是一个演示应用,该应用会在摄像头视频中的增强现实标记上放置 3D 模型。

JSARToolKit 是一个适用于 JavaScript 的增强现实库。它是一个根据 GPL 发布的开源库,也是我为 Mozilla Remixing Reality 演示制作的 Flash FLARToolKit 的直接移植。FLARToolKit 本身是 Java NyARToolKit 的移植版本,而 NyARToolKit 是 C ARToolKit 的移植版本。我们走了一段很长的路,但终于到了这里。

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

当您将画布传递给 JSARToolKit 进行分析时,JSARToolKit 会返回在图片中找到的 AR 标记的列表以及相应的转换矩阵。如需在标记上绘制 3D 对象,您需要将转换矩阵传递给您使用的任何 3D 渲染库,以便使用该矩阵转换对象。然后,在 WebGL 场景中绘制视频帧,并在其上绘制对象,即可顺利运行。

如需使用 JSARToolKit 分析视频,请在画布上绘制视频,然后将画布传递给 JSARToolKit。对每个帧执行此操作,即可实现视频 AR 跟踪。在现代 JavaScript 引擎上,JSARToolKit 的速度足够快,即使在 640x480 视频帧上,也能实时执行此操作。不过,视频帧越大,处理时间就越长。合适的视频帧大小为 320x240,但如果您打算使用小标记或多个标记,则最好使用 640x480。

演示

如需查看网络摄像头演示,您需要在浏览器中启用 WebRTC(在 Chrome 中,请前往 about:flags 并启用 MediaStream)。您还需要打印以下 AR 标记。您也可以尝试在手机或平板电脑上打开标记图片,然后将其展示给摄像头。

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 获取网络摄像头视频。对于预录制的视频,只需将视频的来源属性设置为视频网址即可。如果您要从静态图片中检测标记,可以使用 image 元素,方法大同小异。

由于 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 集成有点麻烦,但肯定是可行的。我不完全确定我在演示中是否做对了,如果您知道更好的集成方法,请告诉我。欢迎提供补丁 :)

参考