Pisanie aplikacji rzeczywistości rozszerzonej za pomocą JSARToolKit

Ilmari Heikkinen

Wstęp

Ten artykuł dotyczy używania biblioteki JSARToolKit z interfejsem WebRTC getUserMedia API do tworzenia aplikacji wykorzystujących rzeczywistość rozszerzoną w internecie. Do renderowania używam WebGL ze względu na jego lepszą wydajność. Efektem końcowym tego artykułu jest aplikacja demonstracyjna, która umieszcza model 3D na znaczniku rzeczywistości rozszerzonej w filmie z kamery internetowej.

JSARToolKit to biblioteka JavaScriptu w rzeczywistości rozszerzonej. Jest to biblioteka open source wydana na licencji spółek społecznościowych i bezpośredni port wtyczki Flash FLARToolKit, która została przeze mnie utworzona na potrzeby demonstracji remiksowania w programie Mozilla. Sam plik FLARToolKit jest portem Java NyARToolKit, który jest portem pakietu C ARToolKit. To była długa droga, ale jesteśmy na miejscu.

JSARToolKit działa na elementach canvas. Obraz musi odczytywać obraz z obszaru roboczego, dlatego musi pochodzić z tego samego pochodzenia co strona lub użyć CORS, aby obejść zasadę tego samego pochodzenia. W skrócie ustaw właściwość crossOrigin-obrazu lub elementu wideo, którego chcesz użyć jako tekstury, na '' lub 'anonymous'.

Gdy przekazujesz płótno do JSARToolKit w celu analizy, JSARToolKit zwraca listę znaczników AR znalezionych na obrazie oraz odpowiednich macierzy transformacji. Aby narysować obiekt 3D na znaczniku, należy przekazać macierz przekształceń do dowolnej biblioteki renderowania 3D, co spowoduje przekształcenie obiektu przy użyciu macierzy. Następnie narysuj klatkę wideo w scenie WebGL i narysuj na niej obiekt – i gotowe.

Aby przeanalizować film za pomocą JSARToolKit, narysuj film na płótnie, a następnie przekaż go do JSARToolKit. Powtórz te czynności dla każdej klatki, a uzyskasz możliwość śledzenia obrazu AR. JSARToolKit jest wystarczająco szybki w nowoczesnych silnikach JavaScript, aby robić to w czasie rzeczywistym nawet w przypadku klatek wideo o rozdzielczości 640 x 480. Jednak im większa klatka wideo, tym dłużej trwa przetwarzanie. Dobry rozmiar klatki filmu to 320 x 240, ale jeśli zamierzasz używać małych lub wielu znaczników, zalecamy rozmiar 640 x 480.

Pokaz

Aby wyświetlić prezentację kamery internetowej, musisz włączyć WebRTC w przeglądarce (w Chrome kliknij about:flags i włącz MediaStream). Musisz też wydrukować znacznik AR poniżej. Możesz też otworzyć zdjęcie znacznika na telefonie lub tablecie i pokazać je kamerze internetowej.

Znacznik AR.
Znacznik AR.

Konfigurowanie JSARToolKit

Interfejs API JSARToolKit jest dość podobny do języka Java, więc korzystanie z niego wymaga trochę przerobienia. Podstawowy założenie jest takie, że wykrywacz ma obiekt, który działa na obiekcie rastrowym. Pomiędzy detektorem a rastrem znajduje się obiekt parametrów kamery, który przekształca współrzędne rastrowania na współrzędne kamery. Aby pobrać wykryte znaczniki z detektora, wykonaj iterację i skopiuj ich macierze przekształceń do swojego kodu.

Pierwszym krokiem jest utworzenie obiektu rastrowego, obiektu z parametrami kamery i obiektu wzorca do wykrywania treści.

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

Uzyskiwanie dostępu do kamery internetowej przy użyciu getUserMedia

Następnie utworzę element wideo, który przez interfejsy API WebRTC przesyła obraz z kamery internetowej. W przypadku nagrań wideo po prostu ustaw atrybut źródła filmu jako adres URL filmu. W przypadku wykrywania znaczników na zdjęciach nieruchomych możesz w ten sam sposób korzystać z elementów graficznych.

WebRTC i getUserMedia to wciąż nowe technologie, więc musisz je wykrywać. Więcej informacji znajdziesz w artykule Erica Bidelmana o rejestrowaniu dźwięku i wideo w 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.");
  }
);

Wykrywanie znaczników

Gdy detektor będzie działać prawidłowo, możemy zacząć przesyłać do niego obrazy, aby wykrywać macierze AR. Najpierw narysuj obraz na obiektie rastrowym, a następnie uruchom wzorzec na obiekcie rastrowym. Wzorzec do wykrywania treści zwraca liczbę znaczników znalezionych na obrazie.

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

Ostatnim krokiem jest powtórzenie wykrytych znaczników i uzyskanie ich macierzy transformacji. Matryce przekształceń służą do umieszczania obiektów 3D na znacznikach.

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

Mapowanie macierzy

Oto kod, który pozwala skopiować macierze JSARToolKit do macierzy glMatrix (czyli 16-elementowych obiektów FloatArrays z kolumną translacji w ostatnich 4 elementach). To działa magia (przeczytaj: Nie wiem, jak są skonfigurowane macierze ARToolKit Przypuszczam, że odwrócono oś Y). W każdym razie to odwracalne voodoo sprawia, że macierz JSARToolKit działa tak samo jak glMatrix.

Aby użyć tej biblioteki z inną biblioteką, np. Three.js, musisz napisać funkcję, która konwertuje macierze ARToolKit na jej format macierzy. Musisz też dołączyć do metody FLARParam.copyCameraMatrix. Metoda copyCameraMatrix zapisuje macierz perspektywy FLARParam do macierzy w stylu 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;
}

Integracja z Three.js

Three.js to popularny mechanizm 3D JavaScript. Teraz pokażę, jak używać danych wyjściowych JSARToolKit w języku Three.js. Potrzebujesz 3 elementów: kwadratu z narysowanym obrazem wideo, aparatu z matrycą perspektywy FLARParam i przekształconym obiektem z matrycą znaczników. W poniższym kodzie przeprowadzę Cię przez proces integracji.

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

Podsumowanie

W tym artykule omówiliśmy podstawy działania JSARToolKit. Teraz możesz tworzyć własne aplikacje wykorzystujące JavaScript do obsługi kamery internetowej.

Integracja JSARToolKit z Three.js może być kłopotliwa, ale z pewnością jest możliwa. Nie mam całkowitej pewności, czy robię to prawidłowo w mojej wersji demonstracyjnej, dlatego daj mi znać, jeśli wiesz, jak można przeprowadzić integrację. Poprawki są mile widziane :)

Źródła