📸

WebARでカメラ情報を取得できるRaw Camera Accessについての調査録

2021/12/18に公開

はじめに

TL;DR

Raw Camera Access を用いることによって WebXR Device API からカメラ画像やカメラ内部パラメータ-を取得できる。
現在(2021/12)は実験的機能として AR がサポートされている Chrome for Android のみで提供されている。

概要

この記事はWebXR アドベントカレンダーの 18 日目の記事です。

私は WebAR を使った開発をすることがあるのですが、
その中で「カメラの情報を取得したい」という課題を抱えておりました。
その中で色々調べて発見した謎の WebXR Raw Camera Access Feature(以下 Raw Camera Access)について紹介していきます。

扱う内容

本記事では以下の内容を扱います。

  • Raw Camera Access の概要
  • Raw Camera Access の現状
  • Babylon.js を使った使用方法の例

また本記事では基本事項を確認せずに技術スタックを使用する場合があります。
したがって基本的な Web フロントエンドの知識や TypeScript の文法などを
理解していることが望ましいです。

対象読者

以下に示すような方には、本記事で触れる内容が役に立つと考えます。

  • WebAR でカメラ画像を取得したい人
  • WebXR Device API で何ができるのかを知りたい人

また詳細は後述しますが、今回扱う API はかなり最近出てきた実験的な APIです。
試すときは自己責任でお願いします。
そして現在(2021/12)この API が使えるのは Chrome for Android のみと発表されているので、
iOS で WebXR をやられている方はごめんなさい。

想定環境

筆者が用いた環境を以下に示します。

  • 開発機:Windows 10 Home
  • デバッグ機:Pixel 4a 5G(Android12), Nexus 5X(Android8.1)
  • Node.js 16.x
  • Babylon.js 5.0.0.preview-60
  • TypeScript
  • Vite 2

Raw Camera Access の現状調査

Raw Camera Access とはいったい何か

Raw Camera Access とは、その名の通り
WebXR Device API 経由でカメラ画像テクスチャを取得し、
それを WebAR アプリケーションで利用するための機能です。

https://chromestatus.com/feature/5759984304390144

なんだか普通に実装されてそうな機能ですが、
実はセキュリティの観点から長らく実装されていませんでした。
代替として getUserMedia が考えられますが、
普通にスマホでやってしまうと getUserMedia と WebXR Device API がカメラのストリームを奪い合ってしまうため
厳しい選択です。

上に載せた Chrome Platform Status の Platform Support Explanation を一部引用します。

This will be supported on platforms where Chrome supports AR. Currently, this is only Android. WebView support is planned. There are no technical restrictions specific to this API preventing it from being implemented on other platforms.

これによると、Raw Camera Access Feature は
AR がサポートされている Chrome でサポートされる予定であり、
現在(2021/11/27)時点では Chrome for Android のみとのことです。

API の情報整理と利用例

もともと Raw Camera Access は次の GitHub に提案内容がありました。
最終コミットが 2021 年の 6 月なので、Raw Camera Access が実験的にリリースされる前ですね。

https://github.com/immersive-web/raw-camera-access/blob/main/explainer.md

この提案内容の中には、すでに Raw Camera Access を有効にする方法や、
それを使ってカメラ画像とカメラ内部パラメータを計算・取得する方法が記載されています。
具体的に内容を抜粋してみます。

まず Raw Camera Access を有効にするためには、
XRSession を取得する際に feature として"camera-access"を require します。

GitHubより抜粋
const session = await navigator.xr.requestSession("immersive-ar", {
  requiredFeatures: ["camera-access"]
});

そしてカメラ画像を取得するためのサンプルコードは次のようになっていました。

GitHubより抜粋(ちょっと改変)
// ... in rAFcb ...
let viewerPose = xrFrame.getViewerPose(xrRefSpace);
for (const view of viewerPose.views) {
  if (view.camera) {
    const cameraTexture = binding.getCameraImage(view.camera);
  }
}

最終的にcameraTextureに格納されたものがカメラのテクスチャとなります。
このテクスチャはWebGLTextureオブジェクトで、WebGL API で扱うことができる画像データです。

またコメントでも記載されている通り、このコードは
xrSession.requestAnimationFrameコールバック内で実行される必要があります。
このコールバックは通常の Web API の requestAnimationFrame とは異なり、WebXR 専用のものですので注意が必要です。

このコード中に存在するview.camerabinding.getCameraImageなどは
通常型宣言には組まれていないため、TypeScript で扱う場合や型補完を効かせたい場合には
次の例にあるような型宣言をします。

GitHubより抜粋
partial interface XRView {
  // Non-null iff there exists an associated camera that perfectly aligns with the view:
  [SameObject] readonly attribute XRCamera? camera;
};

interface XRCamera {
  // Dimensions of the camera image:
  readonly attribute long width;
  readonly attribute long height;
};

partial interface XRWebGLBinding {
  // Access to the camera texture itself:
  WebGLTexture? getCameraImage(XRCamera camera);
};

続いてカメラ内部パラメータを取得するサンプルを見ていきましょう。
コードを抜粋します。

GitHubから抜粋
function getCameraIntrinsics(projectionMatrix, viewport) {
  const p = projectionMatrix;

  // Principal point in pixels (typically at or near the center of the viewport)
  let u0 = (1 - p[8]) * viewport.width / 2 + viewport.x;
  let v0 = (1 - p[9]) * viewport.height / 2 + viewport.y;

  // Focal lengths in pixels (these are equal for square pixels)
  let ax = viewport.width / 2 * p[0];
  let ay = viewport.height / 2 * p[5];

  // Skew factor in pixels (nonzero for rhomboid pixels)
  let gamma = viewport.width / 2 * p[4];

  // Print the calculated intrinsics:
  const intrinsicString = (
    "intrinsics: u0=" + u0 + " v0=" + v0 + " ax=" + ax + " ay=" + ay +
    " gamma=" + gamma + " for viewport {width=" +
    viewport.width + ",height=" + viewport.height + ",x=" +
    viewport.x + ",y=" + viewport.y + "}");

  console.log("projection:", Array.from(projectionMatrix).join(", "));
  console.log(intrinsicString);
}

引数に取っているprojectionMatrixはカメラ射影行列のことで、
XRView.projectionMatrixのようにして取得できる行列オブジェクトです。
行列といってもFloat32Array型なので実態は配列ですね。

viewportXRViewport型のオブジェクトで、以下のような定義になっています。

interface XRViewport {
  height: number;
  width: number;
  x: number;
  y: number;
}

WebGLLayerWebGLRenderingContextから取得できるのですが、
自分の環境では XRWebGLLayer から取得した viewport の画素数と
カメラ画像の画素数に違いが出てしまっていました。
つまりカメラ画像の intrinsics を取得するためには Viewport を自分で作成しなくてはいけないみたいで、
次のようにすることで解決しました。

const viewport: XRViewport = {
  x: 0,
  y: 0,
  width: view.camera.width,
  height: view.camera.height,
};

関数中にあるu0,v0が光学中心、ax,ayが焦点距離を表わし、
これらの値を算出できます。

Babylon.js での利用

今回、サンプルまでは用意できませんでしたが、
実際に TypeScript で書いた Babylon.js の WebAR モードで試すことができましたので
雰囲気だけご紹介します。

まず Babylon.js で XRSession を初期化・取得するためには
_scene.createDefaultXRExperienceAsyncメソッドを使用して次のようにします。

this._xrExperience = await this._scene.createDefaultXRExperienceAsync({
  uiOptions: {
    sessionMode: "immersive-ar",
    referenceSpaceType: "unbounded",
    optionalFeatures: ["camera-access"],
  },
});

ポイントはoptionalFeaturesとしてcamera-accessを指定していることです。
これは WebXR Device API をそのまま使ったときと見た目は似ていますね。
またカメラ情報は requestAnimationFrame コールバック内で処理する必要があるため、
Babylon.js の XRSessionManager からコールバックを登録しましょう。

this._sessionManager = this._xrExperience.baseExperience.sessionManager;
this._sessionManager.onXRFrameObservable.add((frame) => {
  // camera operations ...
});

続いて Raw Camera Access を用いてカメラ情報を取得します。

コールバック内
if (!this._xrExperience || !this._sessionManager) {
  return;
}

const viewerPose = frame.getViewerPose(this._sessionManager.referenceSpace);
if (!viewerPose) {
  return;
}

const view = viewerPose.views[0];
if (!view) {
  return;
}

// calc camera intrinsics
const viewport: XRViewport = {
  x: 0,
  y: 0,
  width: (view as any).camera.width,
  height: (view as any).camera.height,
};

const projectionMatrix = view.projectionMatrix;
const intrinsics = this.getCameraIntrinsics(projectionMatrix, viewport); // 後述

// get camera image data
const gl = this._scene.getEngine()._gl;
const xrWebGLBinding = new XRWebGLBinding(this._sessionManager.session, gl);

const webglTexture = (xrWebGLBinding as any).getCameraImage((view as any).camera);

コード内で用いているthis.getCameraIntrinsicsメソッドは
サンプルコードを改変して以下のように定義しました。

private static getCameraIntrinsics(projectionMatrix: Float32Array, viewport: XRViewport) {
  const p = projectionMatrix;

  // Principal point in pixels (typically at or near the center of the viewport)
  const u0 = ((1 - p[8]) * viewport.width) / 2 + viewport.x;
  const v0 = ((1 - p[9]) * viewport.height) / 2 + viewport.y;

  // Focal lengths in pixels (these are equal for square pixels)
  const ax = (viewport.width / 2) * p[0];
  const ay = (viewport.height / 2) * p[5];

  return {
    principalOffset: {
      x: u0,
      y: v0,
    },
    focalLength: {
      x: ax,
      y: ay,
    },
  };
}

使用時の注意

最後に使用時の注意です。
前述のとおり Raw Camera Access は AR をサポートしているデバイスの Chrome for Android で提供されている機能です。
現在(2021/12/14 時点)ではその WebXR Incubation という flag を有効にしないと実行できません。
自分はこれに数日悩まされたので、みなさんは気を付けてください......。

WebXR Incubation は、Chrome の url にchrome://flagsと打ち込んで「WebXR」と検索することで見つけることができます。

img

おわりに

WebXR Device API でカメラ情報を取得できる Raw Camera Access をご紹介しました。
元々自分はカメラ内部パラメータを取得したかったものの、難しいのかなと考えていました。
なので Raw Camera Access を発見してかなり興奮したのを覚えています。
ただネット上にあまり情報がなく、ほぼ公式から出ている情報のみで手探りで進めていました。

WebAR は Unity やネイティブ AR アプリよりも
開発環境や API が整備されていないのが現状だと考えます。
Web 上で扱える情報が増えてアプリの幅が広がることで、
将来 Web という手軽なプラットフォームでリッチな AR 体験ができるのではないでしょうか。

本記事の内容が少しでも皆さんのお力になれれば幸いです。

参考文献

https://chromestatus.com/feature/5759984304390144

https://github.com/immersive-web/raw-camera-access/blob/main/explainer.md

https://zenn.dev/drumath2237/scraps/935e96c3058f42

GitHubで編集を提案

Discussion