Scrivere applicazioni di realtà aumentata utilizzando JSARToolKit

Ilmari Heikkinen

Introduzione

Questo articolo spiega come utilizzare la libreria JSARToolKit con l'API getUserMedia WebRTC per eseguire applicazioni di realtà aumentata sul web. Per il rendering, uso WebGL per via delle migliori prestazioni che offre. Il risultato finale di questo articolo è un'applicazione demo che inserisce un modello 3D sopra un indicatore di realtà aumentata nel video con webcam.

JSARToolKit è una libreria di realtà aumentata per JavaScript. Si tratta di una libreria open source rilasciata con licenza GPL e una porta diretta di FLARToolKit Flash che ho creato per la demo di Remixing Reality di Mozilla. Lo stesso FLARToolKit è la porta di Java NyARToolKit, che è una porta di C ARToolKit. E la strada è lunga, ma eccoci qui.

JSARToolKit funziona su elementi canvas. Poiché deve leggere l'immagine dal canvas, l'immagine deve provenire dalla stessa origine della pagina o deve utilizzare CORS per aggirare il criterio della stessa origine. In breve, imposta la proprietà crossOrigin sull'elemento immagine o video che vuoi utilizzare come texture su '' o 'anonymous'.

Quando passi un canvas a JSARToolKit per l'analisi, JSARToolKit restituisce un elenco di indicatori AR trovati nell'immagine e le matrici di trasformazione corrispondenti. Per disegnare un oggetto 3D sopra un indicatore, devi passare la matrice di trasformazione a qualsiasi libreria di rendering 3D che stai utilizzando, in modo che l'oggetto venga trasformato utilizzando la matrice. A questo punto, disegna il fotogramma del video nella scena WebGL e disegna l'oggetto sopra. È tutto.

Per analizzare il video con JSARToolKit, disegna il video su una tela, quindi passa la tela a JSARToolKit. Ripeti la procedura per ogni fotogramma per ottenere il tracciamento AR video. JSARToolKit è abbastanza veloce sui moderni motori JavaScript per farlo in tempo reale anche su frame video 640x480. Tuttavia, più grande è il fotogramma del video, maggiore sarà il tempo richiesto per l'elaborazione. Una buona dimensione dell'inquadratura è 320 x 240, ma se prevedi di utilizzare indicatori piccoli o multipli, è preferibile una dimensione 640 x 480.

Demo

Per visualizzare la demo della webcam, devi attivare WebRTC nel browser (su Chrome, vai alla sezione about:flags e attiva MediaStream). Devi anche stampare l'indicatore AR riportato di seguito. Puoi anche provare ad aprire l'immagine dell'indicatore sul tuo smartphone o tablet e a mostrarla alla webcam.

indicatore AR.
Indicatore AR.

Configurazione di JSARToolKit

L'API JSARToolKit è abbastanza simile a Java, quindi dovrai fare un piccolo contorsio per utilizzarla. L'idea alla base è che si dispone di un oggetto rilevatore che opera su un oggetto raster. Tra il rilevatore e il raster si trova un oggetto parametro della fotocamera che trasforma le coordinate del raster in coordinate della videocamera. Per ottenere gli indicatori rilevati dal rilevatore, esegui l'iterazione su di essi e copia le matrici di trasformazione nel codice.

Il primo passaggio consiste nel creare l'oggetto raster, l'oggetto parametro della fotocamera e l'oggetto rilevatore.

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

Utilizzo di getUserMedia per accedere alla webcam

Quindi, creerò un elemento video che riceve video dalla webcam tramite le API WebRTC. Per i video preregistrati, ti basta impostare l'attributo source dei video sull'URL del video. Se esegui il rilevamento degli indicatori da immagini fisse, puoi utilizzare un elemento immagine più o meno allo stesso modo.

Poiché WebRTC e getUserMedia sono ancora nuove tecnologie emergenti, devi rilevarle in base alle funzionalità. Per ulteriori dettagli, consulta l'articolo di Eric Bidelman su Acquisizione di audio e video in 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.");
  }
);

Rilevamento degli indicatori

Una volta che il rilevatore funziona correttamente, possiamo iniziare a fornirgli immagini per rilevare le matrici AR. Prima disegna l'immagine sul canvas dell'oggetto raster, quindi esegui il rilevatore sull'oggetto raster. Il rilevatore restituisce il numero di indicatori trovati nell'immagine.

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

L'ultimo passaggio consiste nell'eseguire l'iterazione degli indicatori rilevati e ottenere le matrici di trasformazione. Puoi usare le matrici di trasformazione per posizionare oggetti 3D sopra gli indicatori.

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

Mappatura matriciale

Ecco il codice per copiare le matrici JSARToolKit nelle matrici glMatrix (che sono FloatArrays a 16 elementi con la colonna di traduzione negli ultimi quattro elementi). Funziona per magia (leggi: Non so come vengono configurate le matrici ARToolKit. ipotizziamo che l'asse Y invertito. In ogni caso, questo pezzo di voodoo invertito fa funzionare una matrice JSARToolKit come una matrice glMatrix.

Per utilizzare la libreria con un'altra libreria, come Three.js, devi scrivere una funzione che converta le matrici ARToolKit nel formato matriciale della libreria. Devi anche eseguire l'hook nel metodo FLARParam.copyFotocameraMatrix. Il metodo copyFotocameraMatrix scrive la matrice prospettica FLARParam in una matrice di tipo 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;
}

Integrazione di Three.js

Three.js è un popolare motore 3D JavaScript. Ora vedremo come utilizzare l'output di JSARToolKit in Three.js. Hai bisogno di tre elementi: un riquadro a schermo intero su cui è disegnata l'immagine video, una videocamera con la matrice prospettica FLARParam e un oggetto con matrice di indicatori come sua trasformazione. Il seguente codice illustra l'integrazione.

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

Riepilogo

In questo articolo abbiamo esaminato le nozioni di base di JSARToolKit. Ora puoi creare la tua webcam utilizzando applicazioni di realtà aumentata con JavaScript.

L'integrazione di JSARToolKit con Three.js è un po' complicato, ma è sicuramente possibile. Non so con certezza se lo sto facendo correttamente nella mia demo, quindi fammi sapere se conosci un modo migliore per ottenere l'integrazione. Le patch sono ben accette :)

Riferimenti