Écrire des applications de réalité augmentée à l'aide de JSARToolKit

Ilmari Heikkinen

Introduction

Cet article explique comment utiliser la bibliothèque JSARToolKit avec l'API getUserMedia WebRTC pour créer des applications de réalité augmentée sur le Web. Pour le rendu, j'utilise WebGL, car il offre des performances accrues. Le résultat de cet article est une application de démonstration qui place un modèle 3D au-dessus d'un repère en réalité augmentée dans une vidéo avec une webcam.

JSARToolKit est une bibliothèque de réalité augmentée pour JavaScript. Il s'agit d'une bibliothèque Open Source publiée sous la licence GPL. Elle permet de transférer directement le FLARToolKit Flash que j'ai réalisé pour la démo de Remixing Reality de Mozilla. FLARToolKit lui-même est un portage du NyARToolKit Java, qui est un port de l’ARToolKit C. Long chemin, mais nous y voilà.

JSARToolKit agit sur les éléments de canevas. Étant donné qu'elle doit lire l'image hors du canevas, celle-ci doit provenir de la même origine que la page ou utiliser CORS pour contourner la règle d'origine commune. En résumé, définissez la propriété crossOrigin de l'élément image ou vidéo que vous souhaitez utiliser comme texture sur '' ou 'anonymous'.

Lorsque vous transmettez un canevas à JSARToolKit à des fins d'analyse, ce dernier renvoie une liste de repères de RA trouvés dans l'image ainsi que les matrices de transformation correspondantes. Pour dessiner un objet 3D au-dessus d'un repère, vous devez transmettre la matrice de transformation à la bibliothèque de rendu 3D que vous utilisez afin que votre objet soit transformé à l'aide de la matrice. Ensuite, dessinez l'image vidéo dans votre scène WebGL, puis dessinez l'objet par-dessus. Le tour est joué.

Pour analyser une vidéo à l'aide de JSARToolKit, dessinez-la sur un canevas, puis transmettez le canevas à JSARToolKit. Faites-le pour chaque image et vous bénéficiez d'un suivi vidéo en réalité augmentée. JSARToolKit est suffisamment rapide sur les moteurs JavaScript modernes pour effectuer cette opération en temps réel, même sur des images vidéo de 640 x 480. Cependant, plus l'image vidéo est grande, plus le traitement est long. La taille d'image vidéo est de 320 x 240 pixels, mais si vous prévoyez d'utiliser de petits ou plusieurs repères, le format 640 x 480 est préférable.

Démonstration

Pour regarder la démonstration avec la webcam, vous devez activer WebRTC dans votre navigateur (dans Chrome, accédez à about:flags, puis activez MediaStream). Vous devez également imprimer le repère en RA ci-dessous. Vous pouvez également essayer d'ouvrir l'image du repère sur votre téléphone ou votre tablette pour la montrer à la webcam.

Repère en RA
Repère de RA

Configuration de JSARToolKit

Comme l'API JSARToolKit est assez proche de Java, vous devrez apporter quelques contraintes pour l'utiliser. L'idée de base est que vous disposez d'un objet détecteur qui agit sur un objet matriciel. Un objet de paramètre de caméra qui transforme les coordonnées matricielles en coordonnées de la caméra se trouve entre le détecteur et la trame. Pour obtenir les marqueurs détectés à partir du détecteur, vous les itérez et vous copiez leurs matrices de transformation dans votre code.

La première étape consiste à créer l'objet matriciel, l'objet paramètre de la caméra et l'objet détecteur.

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

Utiliser getUserMedia pour accéder à la webcam

Je vais maintenant créer un élément vidéo qui récupère la vidéo de la webcam via les API WebRTC. Pour les vidéos préenregistrées, il vous suffit de définir l'attribut source de la vidéo sur l'URL de la vidéo. Si vous détectez des repères à partir d'images fixes, vous pouvez utiliser un élément image de la même manière.

WebRTC et getUserMedia étant encore de nouvelles technologies émergentes, vous devez les utiliser pour les détecter. Pour en savoir plus, consultez l'article d'Eric Bidelman sur la capture audio et vidéo au format 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.");
  }
);

Détecter des repères

Une fois que le détecteur fonctionne avec "a-ok", nous pouvons commencer à lui fournir des images pour détecter les matrices de RA. Commencez par dessiner l'image sur le canevas de l'objet matriciel, puis exécutez le détecteur sur cet objet matriciel. Le détecteur renvoie le nombre de repères trouvés dans l'image.

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

La dernière étape consiste à itérer les repères détectés et à obtenir leurs matrices de transformation. Vous utilisez les matrices de transformation pour placer des objets 3D au-dessus des repères.

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

Mise en correspondance matricielle

Voici le code permettant de copier les matrices JSARToolKit dans des matrices glMatrix (qui sont des FloatArrays à 16 éléments avec la colonne de traduction dans les quatre derniers éléments). Cela fonctionne par magie. Lisez : "Je ne sais pas comment les matrices ARToolKit sont configurées. Je suppose que l'axe Y inversé est inversé.) Quoi qu'il en soit, grâce à ce vaudou inverse, la matrice JSARToolKit fonctionne de la même manière qu'une glMatrix.

Pour utiliser la bibliothèque avec une autre bibliothèque, telle que Three.js, vous devez écrire une fonction qui convertit les matrices ARToolKit au format matriciel de la bibliothèque. Vous devez également créer un hook à la méthode FLARParam.copyCameraMatrix. La méthode copyCameraMatrix écrit la matrice de perspective FLARParam dans une matrice de style 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;
}

Intégration de Three.js

Three.js est un moteur 3D JavaScript populaire. Je vais vous expliquer comment utiliser la sortie JSARToolKit dans Three.js. Vous avez besoin de trois éléments: un quad plein écran sur lequel est dessinée l'image vidéo, une caméra avec la matrice de perspective FLARParam et un objet avec une matrice de repères comme transformation. Je vais vous guider tout au long de l'intégration dans le code ci-dessous.

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

Résumé

Dans cet article, nous avons passé en revue les principes de base de JSARToolKit. Vous êtes maintenant prêt à créer vos propres applications de réalité augmentée utilisant une webcam avec JavaScript.

L'intégration de JSARToolKit à Three.js est un peu pénible, mais c'est tout à fait possible. Je ne suis pas certain à 100% que ma démonstration fonctionne correctement. Veuillez m'indiquer si vous connaissez un meilleur moyen d'obtenir l'intégration. Les correctifs sont les bienvenus :)

Références