ブラウザでMiDaS深度推定を動かした
画像から深度マップを生成するAIモデル、MiDaSをブラウザで動かした。
サーバー不要。画像をドロップするだけで、深度情報が取得できる。CPU処理だから遅いけど、動く。
なぜ作ったか
写真に奥行き情報を追加したかった。
平面の写真を3D空間に配置する時、どこが手前でどこが奥なのか分かれば、立体的に表現できる。
midori264やmidori265で、深度情報を使った3D表現を実装していた。でも、深度マップは手動で作っていた。
手動だと、写真1枚に30分以上かかる。100枚の写真があったら、50時間。現実的じゃない。
AIで自動化したかった。
MiDaSは、Intelが公開している深度推定モデル。学習済みモデルが使える。
Pythonならすぐに使えるけど、ブラウザで動かしたかった。写真展示で使うなら、ユーザーのPCで動く方が便利。
ONNX形式に変換すれば、onnxruntime-webで動くと知った。試してみた。
MiDaSとは
MiDaSは、単眼カメラの画像から深度を推定するAIモデル。
通常、深度情報を取得するには、ステレオカメラ(2つのレンズ)やLiDARセンサーが必要。
でも、MiDaSは1枚の画像だけで深度を推定できる。
学習済みモデルが複数公開されている:
- ベーシックモデル(256px、512px、1024px)
- DPT Hybridモデル(384px、512px、1024px)
- DPT Largeモデル(384px、512px、1024px)
精度はDPT Largeが最も高い。でも、処理時間も長い。
ONNX変換
MiDaSのモデルは、元々PyTorch形式。
ブラウザで動かすには、ONNX形式に変換する必要がある。
Pythonで変換スクリプトを書いた:
import torch
from midas.model_loader import load_model
# モデルを読み込み
model = load_model("DPT_Hybrid")
model.eval()
# ダミー入力を作成
dummy_input = torch.randn(1, 3, 384, 384)
# ONNX形式にエクスポート
torch.onnx.export(
model,
dummy_input,
"midas_model_DPT_Hybrid_384.onnx",
opset_version=11,
input_names=["input"],
output_names=["output"]
)
これで、ONNXファイルが生成される。
9種類のモデル全てをONNX形式に変換した。サイズは30MB~180MB。
onnxruntime-webでの実行
onnxruntime-webは、Microsoftが開発しているONNXランタイムのブラウザ版。
WebAssemblyで動く。CPUで推論を実行できる。
async function loadModel() {
if (!session) {
session = await ort.InferenceSession.create(modelPath);
}
return session;
}
モデルを読み込むのは、これだけ。
executionProvidersを指定しないと、デフォルトでCPU(WebAssembly)で実行される。
画像の前処理
MiDaSに入力する前に、画像を前処理する必要がある。
リサイズ
モデルの入力サイズに合わせてリサイズ。384×384pxのモデルなら、画像を384×384pxに縮小する。
const offCanvas = document.createElement("canvas");
offCanvas.width = inferenceWidth;
offCanvas.height = inferenceHeight;
const offCtx = offCanvas.getContext("2d");
offCtx.drawImage(imageElement, 0, 0, inferenceWidth, inferenceHeight);
Canvasで描画し直すだけ。簡単。
正規化
ピクセル値を正規化する。
RGB値は0~255。これを0~1に変換してから、平均と標準偏差で標準化する。
const r = data[dataIndex] / 255;
const normalized = (r - mean[0]) / std[0];
ここでハマった。
モデルによって、正規化のパラメータが違う。
ベーシックモデル(512px、1024px):
- mean = [0.485, 0.456, 0.406]
- std = [0.229, 0.224, 0.225]
DPT系モデル:
- mean = [0.5, 0.5, 0.5]
- std = [0.5, 0.5, 0.5]
最初、全部同じパラメータで処理していた。DPTモデルの結果がおかしかった。真っ黒な深度マップが出力された。
ドキュメントを読んで、モデルごとにパラメータが違うと知った。修正したら、正しく動いた。
CHWフォーマットへの変換
画像データは通常、HWC形式(高さ×幅×チャンネル)で格納されている。
ONNXモデルの入力は、CHW形式(チャンネル×高さ×幅)。
並び替える必要がある。
const floatData = new Float32Array(width * height * 3);
for (let h = 0; h < height; h++) {
for (let w = 0; w < width; w++) {
const pixelIndex = h * width + w;
const dataIndex = pixelIndex * 4;
const r = data[dataIndex] / 255;
const g = data[dataIndex + 1] / 255;
const b = data[dataIndex + 2] / 255;
// CHW形式に変換
floatData[pixelIndex] = (r - mean[0]) / std[0];
floatData[width * height + pixelIndex] = (g - mean[1]) / std[1];
floatData[2 * width * height + pixelIndex] = (b - mean[2]) / std[2];
}
}
R、G、Bの順に、全ピクセルを並べる。
最初、この並び替えを間違えた。RGBRGBRGBの順に並べていた。
結果がおかしい。よく見たら、入力形式が違った。修正したら、正しい深度マップが出力された。
推論の実行
Tensorを作成してから、推論を実行する。
const tensor = new ort.Tensor(
"float32",
floatData,
[1, 3, height, width]
);
const feeds = { "input": tensor };
const results = await session.run(feeds);
session.run()で推論が実行される。
CPU処理だから、時間がかかる。512×512pxで約12秒。1024×1024pxだと、45秒以上。
でも、動く。これだけでも嬉しかった。
結果の後処理
推論結果は、生の深度値。最小値と最大値が画像ごとに違う。
正規化して、0~255に変換する。
const outputData = outputTensor.data;
let min = Infinity, max = -Infinity;
for (let i = 0; i < outputData.length; i++) {
if (outputData[i] < min) min = outputData[i];
if (outputData[i] > max) max = outputData[i];
}
const ratio = 255 / (max - min);
const normalizedValue = (outputData[i] - min) * ratio;
これで、0~255の範囲に収まる。
Infernoカラーマップ
深度値をグレースケールで表示するより、カラーマップで可視化した方が見やすい。
Infernoカラーマップを実装した。深い部分は暗い青、浅い部分は明るい黄色。
科学的な可視化でよく使われる配色。視覚的に分かりやすい。
5つのキーポイントで色を定義して、線形補間で中間色を計算する。
深度の違いが直感的に分かるようになった。
9種類のモデル対応
MiDaSには複数のモデルがある。
全部試してみたかった。9種類のモデルをONNX変換して、ブラウザで切り替えられるようにした。
ドロップダウンで選択すると、モデルが切り替わる。
256pxモデル: 約5秒(速い、精度は普通)
384px DPT Hybrid: 約8秒(バランス良い)
512pxモデル: 約12秒(精度良い、やや遅い)
1024px DPT Large: 約45秒(最高精度、かなり遅い)
用途に応じて選べる。
写真展示で使うなら、384pxで十分。8秒なら、待てる。
ハマったポイント
処理時間の長さ
CPU処理は、やっぱり遅い。
512×512pxで12秒。これだと、実用的とは言えない。
ユーザーが画像をドロップして、12秒待つ。その間、何も表示されないと、フリーズしたと思われる。
進捗表示を追加した。「推論中: 3.5秒」とリアルタイムで表示。
const startTime = Date.now();
const timerId = setInterval(() => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
timingDiv.textContent = `推論中: ${elapsed}秒`;
}, 100);
0.1秒ごとに更新。ユーザーが「ちゃんと動いている」と分かる。
でも、やっぱり12秒は長い。GPU版(midori262)を作ることにした。
メモリ管理
最初、複数の画像を連続で処理すると、ブラウザが重くなった。
メモリリークしているのかと思った。Canvasを使い回すようにしたけど、あまり改善しない。
よく調べたら、ONNXSessionをキャッシュしすぎていた。
モデルを切り替えるたびに、古いsessionを破棄するようにした。
modelSelector.addEventListener("change", () => {
session = null; // 古いセッションを破棄
updateModelInfo(modelSelector.value);
});
これで、メモリ使用量が安定した。
正規化パラメータの違い
前述の通り、モデルによって正規化パラメータが違う。
これを知らずに実装して、DPTモデルが動かなかった。
GitHubのIssueを読んで、パラメータの違いに気づいた。修正して、全モデルが動くようになった。
使ってみて
実際にCPU版を使ってみて、処理時間の遅さを実感した。
384×384pxで約8秒。複数の画像を処理するには、時間がかかりすぎる。
でも、ブラウザで深度推定ができること自体が面白かった。サーバー不要。画像をドロップするだけ。
ポイントは以下の3つ:
- MiDaSモデルをONNX形式に変換してonnxruntime-webで実行(CPUのみ)
- 9種類のモデルに対応(256px~1024px、精度と速度のトレードオフ)
- Infernoカラーマップで深度を可視化(直感的に理解しやすい)
CPU処理は遅いけど、技術的には動く。次のステップ(WebGPU版)への基礎になった。
同じようなブラウザAI推論を試している方の参考になれば嬉しいです。
まとめ
今回は、MiDaS深度推定モデルをブラウザで実行しました。
ポイントは以下の4つ:
- MiDaSモデルをONNX形式に変換(PyTorchから)
- onnxruntime-webでCPU推論を実行(サーバー不要)
- 9種類のモデルに対応(精度と速度を選択可能)
- 正規化パラメータの違いに注意(モデルごとに異なる)
CPU処理は遅いけど(512px: 約12秒)、ブラウザで深度推定ができることが分かった。
次のmidori262では、WebGPUで10倍高速化した。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。