AI Dev Lab
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

DepthCast ができるまで — 1枚の写真をAIの深度推定でブラウザだけで立体にする

DepthCast は、写真を1枚入れるだけで 奥行き (深度) を AI が推定し、手前と奥で動かす量を変えて立体 (視差・パララックス) アニメ にする Webサービスです。深度推定モデル Depth Anything を transformers.js で読み込み、視差の描画は WebGL2 のシェーダで行います。深度推定も描画もすべてブラウザの中で完結し、写真はサーバーに送りません。赤青メガネで見るアナグリフ画像や、ぐるりと動く3Dの動画 (WebM) も書き出せます。ラボの「AIが主役」路線、オンデバイスML の1本目として作りました。

なぜこの形にしたか

「写真の3D化」は本来ステレオカメラや3Dスキャンが要る領域でしたが、単眼深度推定 (monocular depth estimation) の精度がここ数年で一気に上がり、1枚の普通の写真から「どこが手前でどこが奥か」を読み取れるようになりました。Depth Anything はその代表格で、量子化した小型版なら数十MBでブラウザに載る規模です。

ステレオでも3Dモデルでもない、スマホで撮っただけの1枚が揺れて立体に見える — この「え、これ動くの?」の驚きが体験の核なので、深度マップという中間生成物も画面に見せる方針にしました。AIが写真から奥行きを読み取った結果を、その場で目で確認できます。

visual direction

アナグリフ / 3Dシネマ。

  • チャコール地 (#16181d) に、アナグリフ赤 (#ef4b54) とシアン (#2fc6d6) の2色ずれ
  • 上部にフィルムのパーフォレーション (送り穴) を敷いて「映写・シネマ」のメタファー
  • 赤青の色は「赤青3Dメガネ」= 深度・立体という題材そのものから引いた。既存の紙・パステル・和紙系とは別軸のダーク基調

実装の見どころ

1. 深度推定は transformers.js に、視差描画は自前シェーダに分ける

深度の推定は Depth Anything (Xenova/depth-anything-small-hf) に任せ、そこから先の「奥行きをどう動かして立体に見せるか」は自前で書きました。パイプラインは1行です。

const pipe = await pipeline("depth-estimation", "Xenova/depth-anything-small-hf", {
  quantized: true,
})
const out = await pipe(imageURL)
// out.depth は min-max 正規化済みのグレースケール (0 = 奥, 255 = 手前)

出力は画像サイズに揃った深度マップ。これを WebGL のテクスチャに渡して、ピクセルごとに「手前ほど大きくずらす」だけで視差が生まれます。

2. 視差は「後ろ向きに反復サンプリング」するフラグメントシェーダ

素朴に「深度に比例して前景をずらす」と、ずらした先に穴 (occlusion hole) が開きます。DepthCast は逆に、出力側の各ピクセルから深度を見て入力座標を引き戻す backward warping を数回反復して、穴の少ない視差を作っています。

vec2 uv = vUv;
vec2 p = uv;
for (int i = 0; i < 5; i++) {
  float dep = texture(uDepth, p).r;      // その位置の深度
  p = uv + (dep - 0.5) * uAmp * uOffset; // 深度に比例してサンプル位置を補正
}
outColor = texture(uImage, p);

uOffset は視点の動き (円運動・左右・上下)、uAmp は立体の強さ。5回まわすと、手前と奥の境目でも破綻しにくくなります。

3. 動きは cos/sin のオフセットだけで作る

「ぐるり」「左右」「上下」の3モードは、時間 t から uOffset を作るだけです。重い再計算はなく、深度テクスチャは一度アップロードすれば使い回します。

// orbit: 円を描く / horizontal: 左右 / vertical: 上下
ox = Math.cos(t * omega); oy = Math.sin(t * omega)

4. 赤青3D (アナグリフ) は左右2視点を1枚に合成

左目 (offset -1) と右目 (offset +1) を別々にレンダリングし、左目から赤チャンネル、右目からシアン (緑+青) を取って1枚に重ねると、赤青メガネで立体に見える静止画になります。

comb.data[i]     = left.data[i]      // R  ← 左目
comb.data[i + 1] = right.data[i + 1] // G  ← 右目
comb.data[i + 2] = right.data[i + 2] // B  ← 右目

動く3Dは canvas.captureStream(30)MediaRecorder に流して WebM で書き出します。

苦労したところ

  • transformers.js を Turbopack 経由で import すると落ちる: await import("@xenova/transformers") を直接書くと、Next.js 16 のチャンクローダーがモジュールを取り違えて Cannot convert undefined or null to object で死ぬ。対処は CDN の ESM (cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2/dist/transformers.min.js) を変数化した URL で side-load し、バンドラを経由しないこと。翻訳の word-warp で最初に踏んだ罠と同じで、ML系サービスはこの経路で統一しています。

  • サンプル画像がベタ塗りだと精度が出ない: 公開直後、用意したお手本 (コードで描いた平面的なイラスト) で「精度が悪い」と指摘を受けました。原因はモデルではなく サンプル側でした。深度推定モデルは、テクスチャの粗密・陰影・かすみ (大気遠近)・重なり といった手がかりから奥行きを読みます。フラットな色面はその手がかりが乏しく、深度マップがガタつく。そこでサンプルを 山あいの霧 (奥ほど霞む多層の稜線)・並木道 (消失点への線遠近)・机の上 (手前に大きく陰影を付けた球) に描き直し、全面に連続グラデと微細なノイズを入れて平面を排除しました。実際に撮った写真ならもともと精度は出るので、「実写真を入れると立体感が上がる」ガイドも画面に明示しています。合成サンプルは AI の実力を示せない、という教訓です。

  • 深度マップの見た目は自動テストでは担保できない: 「深度が near→far の幅を持つ」「アナグリフに左右のずれがある」までは自動で検証できますが、立体アニメが自然に見えるかは実機で目視するしかありません。ここは正直に実機確認前提にしています。

今後の拡張

  • 深度からのメッシュ書き出し: 深度マップを高さ場として glTF / OBJ に変換し、3Dビューアで回せるように
  • 被写界深度 (ボケ) の後付け: 深度を使って背景だけぼかすポートレート風フィルタ
  • バッチ処理: 複数枚をまとめて3D化して連番WebMに

このサービスから言える事

「写真を立体にするには特別な機材が要る」という前提は、単眼深度推定がブラウザで動くようになった時点で崩れています。DepthCast はその変化を、モデルのダウンロードと数十行のシェーダだけで、誰の端末の中でも起こせる形にしたものです。深度推定は3D化だけでなく、ボケ・切り抜き・合成の土台にもなるので、この1枚から読み取った奥行きは今後いろいろな加工の入口になります。

[ ./next_action ]

読んだら、 DepthCast を実際に動かす。

この開発ログは DepthCast をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。