Criação de aplicativos de realidade aumentada usando o JSARToolKit

Ilmari Heikkinen

Introdução

Este artigo aborda o uso da biblioteca JSARToolKit com a API getUserMedia do WebRTC para criar aplicativos de realidade aumentada na Web. Para renderização, estou usando o WebGL devido ao aumento de desempenho que ele oferece. O resultado final deste artigo é um aplicativo de demonstração que coloca um modelo 3D sobre um marcador de realidade aumentada em um vídeo de webcam.

O JSARToolKit é uma biblioteca de realidade aumentada para JavaScript. É uma biblioteca de código aberto lançada sob a GPL e uma porta direta do FLARToolKit do Flash que eu fiz para a demonstração Remixing Reality do Mozilla. O FLARToolKit é uma porta do NyARToolKit Java, que é uma porta do ARToolKit C. Um longo caminho, mas cá estamos nós.

O JSARToolKit opera em elementos de tela. Como a imagem precisa ser lida fora da tela, ela precisa vir da mesma origem da página ou usar CORS para contornar a política de mesma origem. Resumindo, defina a propriedade crossOrigin no elemento de imagem ou vídeo que você quer usar como uma textura para '' ou 'anonymous'.

Quando você transmite uma tela para análise ao JSARToolKit, ele retorna uma lista de marcadores de RA encontrados na imagem e as matrizes de transformação correspondentes. Para desenhar um objeto 3D sobre um marcador, transmita a matriz de transformação para a biblioteca de renderização 3D que você está usando para que o objeto seja transformado usando a matriz. Em seguida, renderize o frame de vídeo na sua cena do WebGL e o objeto em cima dele.

Para analisar o vídeo usando o JSARToolKit, desenhe o vídeo em uma tela e transmita a tela para o JSARToolKit. Faça isso para cada frame e você terá o rastreamento de RA em vídeo. O JSARToolKit é rápido o suficiente em mecanismos modernos de JavaScript para fazer isso em tempo real, mesmo em frames de vídeo de 640x480. No entanto, quanto maior o frame do vídeo, mais tempo ele leva para ser processado. Um bom tamanho de frame de vídeo é 320 x 240, mas se você pretende usar marcadores pequenos ou vários marcadores, 640 x 480 é preferível.

Demonstração

Para conferir a demonstração da webcam, você precisa ativar o WebRTC no navegador. No Chrome, acesse about:flags e ative o MediaStream. Você também precisa imprimir o marcador de RA abaixo. Você também pode abrir a imagem do marcador no smartphone ou tablet e mostrar para a webcam.

Marcador de RA.
Marcador de RA.

Como configurar o JSARToolKit

A API JSARToolKit é bastante semelhante ao Java, então você terá que fazer algumas contorções para usá-la. A ideia básica é que você tem um objeto detector que opera em um objeto raster. Entre o detector e o raster, há um objeto de parâmetro da câmera que transforma as coordenadas do raster em coordenadas da câmera. Para receber os marcadores detectados do detector, você itera sobre eles e copia as matrizes de transformação para o código.

A primeira etapa é criar o objeto raster, o objeto de parâmetro da câmera e o 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);

Como usar getUserMedia para acessar a webcam

Em seguida, vou criar um elemento de vídeo que recebe vídeo da webcam pelas APIs WebRTC. Para vídeos pré-gravados, basta definir o atributo de origem do vídeo como o URL dele. Se você estiver fazendo a detecção de marcadores de imagens estáticas, poderá usar um elemento de imagem da mesma forma.

Como o WebRTC e o getUserMedia ainda são tecnologias emergentes, é necessário detectar o recurso. Para mais detalhes, consulte o artigo de Eric Bidelman sobre Como capturar áudio e vídeo no 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.");
  }
);

Como detectar marcadores

Depois que o detector estiver funcionando corretamente, vamos começar a alimentar imagens para detectar matrizes de RA. Primeiro, desenhe a imagem na tela do objeto raster e, em seguida, execute o detector no objeto raster. O detector retorna o número de marcadores encontrados na imagem.

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

A última etapa é iterar pelos marcadores detectados e receber as matrizes de transformação. Você usa as matrizes de transformação para colocar objetos 3D sobre os 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);
}

Mapeamento de matriz

Confira o código para copiar matrizes do JSARToolKit para matrizes do glMatrix (que são FloatArrays de 16 elementos com a coluna de tradução nos quatro últimos elementos). Funciona por magia (leia-se: eu não sei como as matrizes do ARToolKit são configuradas. O eixo Y invertido é minha suposição.) De qualquer forma, esse pouco de magia de inversão de sinal faz com que uma matriz JSARToolKit funcione da mesma forma que uma glMatrix.

Para usar a biblioteca com outra, como a Three.js, você precisa escrever uma função que converta as matrizes do ARToolKit para o formato de matriz da biblioteca. Você também precisa se conectar ao método FLARParam.copyCameraMatrix. O método copyCameraMatrix grava a matriz de perspectiva FLARParam em uma matriz do 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;
}

Integração do three.js

O Three.js é um mecanismo 3D JavaScript conhecido. Vou mostrar como usar a saída do JSARToolKit no Three.js. Você precisa de três coisas: um quadrângulo de tela cheia com a imagem do vídeo desenhada nele, uma câmera com a matriz de perspectiva FLARParam e um objeto com a matriz de marcador como transformação. Vou explicar a integração no código abaixo.

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

Resumo

Neste artigo, abordamos os princípios básicos do JSARToolKit. Agora você está pronto para criar seus próprios aplicativos de realidade aumentada com webcam usando JavaScript.

A integração do JSARToolKit com o Three.js é um pouco complicada, mas é possível. Não tenho 100% de certeza se estou fazendo isso certo na minha demonstração. Informe se você conhece uma maneira melhor de fazer a integração. Patches são bem-vindos :)

Referências