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 WebRTC getUserMedia 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 un video de cámara web.

JSARToolKit es una biblioteca de realidad aumentada para JavaScript. Es una biblioteca de código abierto lanzada bajo la GPL y un puerto directo de FLARToolKit de Flash que hice para la demostración de realidad de Remixing de Mozilla. FLARToolKit en sí es el puerto de Java NyARToolKit, que es un puerto de C ARToolKit. Largo camino, pero aquí estamos.

JSARToolKit opera en elementos lienzo. Como se necesita leer la imagen del lienzo, esta debe provenir del mismo origen que la página o usar CORS para evadir la política del mismo origen. En resumen, 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 su análisis, JSARToolKit devuelve una lista de los marcadores de RA que se encuentran en la imagen y las matrices de transformación correspondientes. Para dibujar un objeto 3D sobre un marcador, debes pasar la matriz de transformación a cualquier biblioteca de renderización 3D que estés usando para transformar el objeto con la matriz. Luego, dibuja el fotograma en tu escena de WebGL y dibuja el objeto encima de eso. Eso es todo.

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

Demostración

Para ver la demostración de la cámara web, debes habilitar WebRTC en el navegador (en Chrome, ve a "Acerca de las marcas" y habilita MediaStream). También debes imprimir el marcador de RA que aparece a continuación. También puedes intentar abrir la imagen del marcador en tu teléfono o tablet y mostrarla a la cámara web.

Marcador de RA.
Marcador de RA.

Configuración de 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 de trama. Entre el detector y la trama, hay un objeto de parámetro de la cámara que transforma coordenadas de trama en coordenadas de la cámara. Para obtener los marcadores detectados del detector, itera sobre ellos y copia sus matrices de transformación en tu código.

El primer paso es crear el objeto de trama, el objeto de parámetro de la cámara y el objeto 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 obtiene el video de la cámara web a través de las APIs de WebRTC. Para los videos pregrabados, solo debes definir el atributo fuente del video como la URL del video. Si realizas la detección de marcadores a partir de imágenes estáticas, puedes utilizar un elemento de imagen de la misma manera.

Dado que WebRTC y getUserMedia siguen siendo tecnologías emergentes nuevas, debes detectarlas con las funciones. Para obtener más información, 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

Cuando el detector esté funcionando correctamente, podremos comenzar a proporcionarle 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 encuentran 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 encima de 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 las matrices de JSARToolKit a las matrices de glMatrix (que son FloatArrays de 16 elementos con la columna de traducción en los últimos cuatro elementos). Funciona por magia (lee: No sé cómo se configuran las matrices de ARToolKit, Supongo que el eje Y invertido). De todos modos, este vudú que revierte los signos hace que una matriz JSARToolKit funcione de la misma manera 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 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 popular de JavaScript en 3D. Voy a explicar cómo usar JSARToolKit en Three.js. Necesitas tres elementos: un cuadrante de pantalla completa con la imagen de video dibujada, una cámara con la matriz de perspectiva FLARParam y un objeto con una matriz de marcador como su transformación. A continuación, te guiaremos en el proceso de integración.

// 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, vimos los conceptos básicos de JSARToolKit. Ya tienes todo listo para compilar tus propias aplicaciones de realidad aumentada con cámara web y JavaScript.

Integrar JSARToolKit con Three.js es un poco complicado, pero sí es posible. No sé al 100% 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