Criação de aplicativos de realidade aumentada com JSARToolKit

HTML5 Rocks

This article discusses APIs that are not yet fully standardized and still in flux. Be cautious when using experimental APIs in your own projects.

Introdução

Este artigo é sobre o uso da biblioteca JSARToolKit (link em inglês) com a API getUserMedia WebRTC (link em inglês) para executar aplicativos de realidade aumentada na Web. Para a renderização, estou usando o WebGL pelo maior desempenho que ele oferece. O resultado final deste artigo é um aplicativo de demonstração que coloca um modelo 3D em cima de um marcador de realidade aumentada no vídeo da webcam.

JSARToolKit é uma biblioteca de realidade aumentada para JavaScript. É uma biblioteca de código aberto disponibilizada por GPL e uma porta direta do Flash FLARToolKit (link em inglês) que desenvolvi para a demonstração de remixagem de realidade (link em inglês) do Mozilla. A própria FLARToolKit é uma porta do Java NyARToolKit (link em inglês) que é uma porta do C ARToolKit (link em inglês). Foi um longo caminho, mas aqui estamos.

A JSARToolKit opera nos elementos do canvas. Como é necessário ler a imagem a partir do canvas, a imagem precisa vir da mesma origem que a página ou usar CORS (link em inglês) para contornar a política de mesma origem. Resumidamente, defina a propriedade crossOrigin na imagem ou elemento de vídeo pretendido como uma textura para '' ou "anonymous".

Quando você passa um canvas para JSARToolKit para análise, o JSARToolKit retorna uma lista de marcadores AR localizados na imagem e as matrizes de transformação correspondentes. Para desenhar um objeto 3D em cima de um marcador, você transfere a matriz de transformação para qualquer biblioteca de renderização 3D que estiver usando para que o seu objeto seja transformado usando a matriz. Depois, desenhe o quadro do vídeo no cenário WebGL e desenhe o objeto em cima dele e está pronto.

Para analisar o vídeo usando o JSARToolKit, desenhe o vídeo em um canvas e, em seguida, passe o canvas para o JSARToolKit. Faça isso para cada quadro e você tem o rastreamento AR de vídeo. O JSARToolKit é rápido o suficiente para fazer isso em tempo real nos mecanismos JavaScript modernos , até mesmo em quadros de vídeo de 640x480. Contudo, quanto maior o quadro de vídeo, mais demorado é o processamento. 320x240 é um bom tamanho de quadro de vídeo, mas se você planeja usar marcadores pequenos ou vários marcadores, prefira 640x480.

Demonstração

Para visualizar a demonstração de webcam, é necessário ter o WebRTC ativado no navegador (no Google Chrome, acesse about:flags e ative o MediaStream). Também é necessário imprimir o marcador AR abaixo. Você também pode tentar abrir a imagem do marcador no telefone ou tablet e mostrá-la na webcam.

Marcador AR.

Segue uma demonstração do que estamos almejando. Esta demonstração cria uma apresentação de slides de imagens usando os marcadores AR. Mostre um marcador para a câmera, que ela colocará a foto nele. Mova o marcador para fora da vista da câmera e mostre-o novamente, que a imagem muda.

Demonstração de AR na webcam (link em inglês) (se não tiver WebRTC, esta é uma versão com o vídeo predefinido.

Como configurar o JSARToolKit

A API JSARToolKit é muito parecida com a do Java, por isso é necessário um pouco de ginástica para usá-la. A ideia básica é que você tenha um objeto detector que opere em um objeto de varredura. Entre o detector e a varredura está um objeto de parâmetro da câmera que transforma as coordenadas da varredura em coordenadas da câmera. Para obter os marcadores detectados, você itera sobre eles e copia as matrizes de transformação sobre seu código.

O primeiro passo é criar o objeto de varredura, o objeto de parâmetro da câmera e o 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);

Como usar o getUserMedia para acessar a webcam

Na sequência, vou criar um elemento de vídeo que obtém o vídeo da webcam pelas APIs WebRTC. Para vídeos pré-gravados, configure apenas o atributo de origem do vídeo para o URL do vídeo. Se estiver fazendo a detecção de marcador das imagens estáticas, você pode usar um elemento de imagem da mesma maneira.

Como o WebRTC e o getUserMedia são tecnologias que surgiram recentemente, é necessário detectá-las com elementos. Para obter mais detalhes, consulte o artigo de Eric Bidelman sobre Captura de áudio e vídeo em HTML5 (link em inglês).

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

Quando o detector estiver funcionando corretamente, podemos começar alimentá-lo com imagens para detectar as matrizes AR. Primeiro, desenhe a imagem no canvas do objeto de varredura, e então, execute o detector no objeto de varredura. O detector retorna o número de marcadores localizados 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 os marcadores detectados e obter suas matrizes de transformação. Use as matrizes de transformação para colocar os 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 < 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

Este é o código para copiar as matrizes do JSARToolKit sobre as matrizes do glMatrix (que são FloatArrays [link em inglês] de 16 elementos com coluna de translação nos últimos quatro elementos). Funciona como por mágica (leia-se: não sei como as matrizes do ARToolKit são configuradas. Meu palpite é eixo Y invertido.) De qualquer maneira, esse feitiço de inversão de sinal faz a matriz JSAToolKit funcionar da mesma forma que uma glMatrix.

Para usar a biblioteca com outra biblioteca, como Three.js, é preciso escrever uma função que converta as matrizes ARToolKit para a o formato da matriz da biblioteca. Você também precisa conhecer bastante o método FLARParam.copyCameraMatrix. O método copyCameraMatrix escreve a matriz da perspectiva FLAParam em uma matriz 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 de Three.js

Three.js é um mecanismo popular de JavaScript 3D. Vou mostrar como usar a saída do JSRToolkit em Three.js. São necessárias três coisas: um quadrângulo de tela cheia com a imagem de vídeo desenhada nele, uma câmera com a matriz de perspectiva FLAParam e um objeto com matriz de marcador conforme a transformação ocorre. Vou guiá-los pela integração do código abaixo.

Este é um link (em inglês) para a demonstração do Three.js em ação. Ela tem uma saída de depuração ativada, assim, é possível ver parte do funcionamento interno da biblioteca JSARToolKit.

// 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<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, passamos pelo básico da JSARToolKit. Agora, você está pronto para construir seus próprios aplicativos de realidade aumentada usando a webcam com o JavaScript.

A integração da JSARToolKit com a Three.js é um tanto chatinha, mas certamente é possível. Não tenho certeza se minha demonstração está certa, avisem-me se conhecerem uma forma melhor de conseguir a integração. Aceito correções :) (link em inglês)

Referências

Comments

0