Escritura de aplicaciones de realidad aumentada con JSARToolKit

Ilmari Heikkinen

Introducción

En este artículo, se explica cómo usar la biblioteca JSARToolKit con la API de getUserMedia de WebRTC para crear aplicaciones de realidad aumentada en la Web. Para la renderización, uso WebGL debido al mayor rendimiento que ofrece. El resultado final de este artículo es una aplicación de demostración que coloca un modelo 3D sobre un marcador de realidad aumentada en el video de la cámara web.

JSARToolKit es una biblioteca de realidad aumentada para JavaScript. Es una biblioteca de código abierto publicada bajo la GPL y un puerto directo del FLARToolKit de Flash que hice para la demo de Remixing Reality de Mozilla. FLARToolKit es un puerto de NyARToolKit de Java, que es un puerto de ARToolKit de C. Fue un largo camino, pero aquí estamos.

JSARToolKit opera en elementos de lienzo. Como debe leer la imagen del lienzo, esta debe provenir del mismo origen que la página o usar CORS para evitar la política del mismo origen. En pocas palabras, establece la propiedad crossOrigin en el elemento de imagen o video que deseas usar como textura en '' o 'anonymous'.

Cuando pasas un lienzo a JSARToolKit para el análisis, JSARToolKit muestra una lista de marcadores de RA que se encuentran en la imagen y las matrices de transformación correspondientes. Para dibujar un objeto 3D sobre un marcador, pasas la matriz de transformación a cualquier biblioteca de renderización 3D que uses para que tu objeto se transforme con la matriz. Luego, dibuja el fotograma de video en tu escena de WebGL y dibuja el objeto encima.

Para analizar un video con JSARToolKit, dibuja el video en un lienzo y, luego, pásalo a JSARToolKit. Haz esto para cada fotograma y tendrás el seguimiento de RA en video. JSARToolKit es lo suficientemente rápido en los motores de JavaScript modernos para hacerlo en tiempo real, incluso en fotogramas de video de 640 × 480. Sin embargo, cuanto más grande sea el fotograma de video, más tiempo tardará en procesarse. Un buen tamaño de fotogramas de video es de 320 × 240, pero si esperas usar marcadores pequeños o varios, es preferible 640 × 480.

Demostración

Para ver la demostración de la cámara web, debes tener habilitado WebRTC en tu navegador (en Chrome, ve a about:flags y habilita MediaStream). También debes imprimir el marcador de RA que aparece a continuación. También puedes abrir la imagen del marcador en tu teléfono o tablet y mostrársela a la cámara web.

Marcador de RA.
Marcador de RA.

Cómo configurar JSARToolKit

La API de JSARToolKit es bastante similar a Java, por lo que tendrás que hacer algunas contorsiones para usarla. La idea básica es que tienes un objeto detector que opera en un objeto raster. Entre el detector y el ráster, hay un objeto de parámetro de cámara que transforma las coordenadas del ráster en coordenadas de la cámara. Para obtener los marcadores detectados del detector, debes iterar sobre ellos y copiar sus matrices de transformación en tu código.

El primer paso es crear el objeto de raster, el objeto de parámetros de la cámara y el objeto de detector.

// 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);

Cómo usar getUserMedia para acceder a la cámara web

A continuación, crearé un elemento de video que obtenga video de la cámara web a través de las APIs de WebRTC. En el caso de los videos grabados previamente, solo debes establecer el atributo de origen del video en la URL del video. Si realizas la detección de marcadores a partir de imágenes fijas, puedes usar un elemento de imagen de la misma manera.

Como WebRTC y getUserMedia aún son tecnologías emergentes nuevas, debes detectarlas. Para obtener más detalles, consulta el artículo de Eric Bidelman sobre cómo capturar audio y video en 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.");
  }
);

Cómo detectar marcadores

Una vez que el detector esté en funcionamiento, podemos comenzar a alimentarlo con imágenes para detectar matrices de RA. Primero, dibuja la imagen en el lienzo del objeto de trama y, luego, ejecuta el detector en el objeto de trama. El detector muestra la cantidad de marcadores que se encontraron en la imagen.

// 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);

El último paso es iterar a través de los marcadores detectados y obtener sus matrices de transformación. Las matrices de transformación se usan para colocar objetos 3D sobre los marcadores.

// 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);
}

Asignación de matrices

Este es el código para copiar matrices de JSARToolKit en matrices de glMatrix (que son FloatArrays de 16 elementos con la columna de traducción en los últimos cuatro elementos). Funciona de forma mágica (es decir, no sé cómo se configuran las matrices de ARToolKit). Mi suposición es que el eje Y está invertido). De cualquier manera, este poco de magia de inversión de signo hace que una matriz de JSARToolKit funcione igual que una glMatrix.

Para usar la biblioteca con otra, como Three.js, debes escribir una función que convierta las matrices de ARToolKit al formato de matriz de la biblioteca. También debes conectarte al método FLARParam.copyCameraMatrix. El método copyCameraMatrix escribe la matriz de perspectiva de FLARParam en una matriz de estilo 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;
}

Integración de Three.js

Three.js es un motor 3D popular de JavaScript. Explicaré cómo usar el resultado de JSARToolKit en Three.js. Necesitas tres elementos: un cuádruple de pantalla completa con la imagen de video dibujada en él, una cámara con la matriz de perspectiva de FLARParam y un objeto con la matriz de marcadores como su transformación. Te explicaré la integración en el siguiente código.

// 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);
}

Resumen

En este artículo, repasamos los conceptos básicos de JSARToolKit. Ya está todo listo para que crees tus propias aplicaciones de realidad aumentada con JavaScript y una cámara web.

Integrar JSARToolKit con Three.js es un poco complicado, pero es posible. No sé con certeza si lo estoy haciendo bien en mi demostración, así que avísame si conoces una mejor manera de lograr la integración. Se aceptan parches :)

Referencias