はじめに
最近はHugging Faceで面白そうなAIモデルを見つけて遊ぶことにハマっていまして、Appleが開発した単眼で深度推定するモデルDepth Proとやらを試してました。
これがいとも簡単に良さげな深度画像を生成してくれるもんですから、単に1枚の画像でやって終わり。じゃなくて動画全フレームに対して生成してこれを元になんちゃって空間ビデオをつくってMeta Quest 3で見てみた。という内容です。
追記:2025/07/14
これを踏まえて本当の空間ビデオに変換してみた続編の記事もありますのでぜひこちらも。
Depth Proについて
自分は深度推定に関しては素人ですが、Depth Proは既存モデルと比べて細かい部分の精度が良いことに加えて、あっという間に生成できる、というのがすごいみたい。
標準的なのGPUなら0.3秒で生成できるとのこと。まあ標準的なGPUがどのレベルかって話ではありますが。
引用:https://arxiv.org/pdf/2410.02073
導入手順
GitHubに書いてある手順通り、conda上に環境を作ってインストールすると簡単に実行できました。さすがApple。
インストール
まずはお好きなディレクトリ上でgit cloneしてディレクトリ移動
git clone https://github.com/apple/ml-depth-pro.git
cd ml-depth-pro
新しいconda環境を作成して起動。
conda create -n depth-pro -y python=3.9
conda activate depth-pro
以下を実行して編集可能モードでインストールを開始。
pip install -e .
推論モデルのダウンロード
Linux環境の場合
Linux系コマンドが使える環境であればそのまま以下のシェルスクリプトを実行すると、checkpoints
というフォルダが作成されてその中に1.7GBほどの推論モデルdepth_pro.pt
がダウンロードされるみたいです。
source get_pretrained_models.sh
Windows環境の場合
windows環境の方は手動でやったほうが簡単だと思います。
ml-depth-proディレクトリの場所でcheckpoints
フォルダを自分で作成し、
ここにあるdepth_pro.pt
をダウンロードしてcheckpoints
フォルダに入れるだけです。
実行
以下の環境で実行してます。
項目 | 内容 |
---|---|
CPU | Intel Core i9-14900K |
メモリ | DDR4 64GB |
GPU | NVIDIA GeForce RTX 3090 (VRAM24GB) |
OS | Windows 11 |
動作確認
まずはサンプル画像で動作確認しましょう。
depth-pro-run -i ./data/example.jpg
ロードには少し時間がかかりますが、推論を開始してからは宣伝通り一瞬で終わりましたね。
結果は以下のようになったと思います。
空間ビデオの作成
さて、ここからが本番です。といってもそんなに難しいことはしてなくて、このDepth Proを独自のスクリプトから呼んで動画分のフレーム(画像)に対して深度推定するだけです。
一つのスクリプトで全フレームの深度推定から空間ビデオ作成まで書いてもいいのですが、右目視点と左目視点を合体させたステレオ画像をつくる過程でちょっとした調整が必要になり、不変の深度画像はいったん保存しておいて使いまわしたいので以下の2つのスクリプトに分けます。
- 動画から深度画像の生成・保存
- 保存した深度画像とカラー画像を元にしてステレオ画像へ。それらを結合してなんちゃって空間ビデオの作成
ほぼChatGPTに書いてもらいました。
1. 動画から深度画像の生成・保存
import cv2
import torch
import numpy as np
from PIL import Image
from pathlib import Path
from tqdm import tqdm
import depth_pro
def preprocess_frame(frame: np.ndarray, transform, device=None):
frame_rgb = frame[..., ::-1] if frame.shape[2] == 3 else frame
img_pil = Image.fromarray(frame_rgb)
image_tensor = transform(img_pil).unsqueeze(0)
return image_tensor.to(device) if device else image_tensor
def generate_stereo_video(input_video):
video_name = str(Path(input_video).stem)
output_folder_path = Path("./output/") / video_name
output_folder_path.mkdir(parents=True, exist_ok=True)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
model, transform = depth_pro.create_model_and_transforms()
model = model.to(device)
model.eval()
cap = cv2.VideoCapture(input_video)
if not cap.isOpened():
raise RuntimeError(f"動画の読み込みに失敗しました: {input_video}")
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f"Total frames: {total_frames}")
frame_idx = 0
with tqdm(total=total_frames, desc="Processing frames", unit="frame") as pbar:
while True:
ret, frame = cap.read()
if not ret:
break
# RGB画像として保存するための変換
frame_rgb = frame[..., ::-1] # BGR → RGB
image_tensor = preprocess_frame(frame, transform, device=device)
with torch.no_grad():
prediction = model.infer(image_tensor, f_px=None)
# 深度画像作成
depth = prediction["depth"].squeeze().cpu().numpy()
depth_normalized = ((depth - depth.min()) / (depth.max() - depth.min()) * 255).astype(np.uint8)
depth_image = Image.fromarray(depth_normalized)
# カラー画像をPIL形式で
color_image = Image.fromarray(frame_rgb)
# 保存パスの設定(深度+カラー)
depth_path = output_folder_path / f"{video_name}_depth_{frame_idx:06d}.png"
color_path = output_folder_path / f"{video_name}_rgb_{frame_idx:06d}.png"
# 保存
depth_image.save(depth_path)
color_image.save(color_path)
frame_idx += 1
pbar.update(1)
cap.release()
# 実行例
if __name__ == "__main__":
input_video = "sample.mp4" # 入力動画
generate_stereo_video(input_video)
変換したい動画を指定してスクリプトを実行します。
実行すると、output
フォルダの中に動画のファイル名のフォルダの中に全フレームのカラー画像とその深度画像が連番で保存されます。
私の環境だと、WQHD画質1枚あたり1.7秒ほどかかったので、例えば60fpsの1分の動画であれば、単純計算で6120秒 = 約1時間40分ほどかかります。思ってた以上に時間かかりますね。なのでcuda対応のGPUがないと完走できないと思います。
注意:Windows Game Bar等でスクリーンキャプチャした動画は可変フレームレートになる影響で、のちの処理を終えて完成した動画を再生すると音声と映像がズレる場合があります。そのときは以下を実行してフレームレートを固定にすると同期するようになります。
$ ffmpeg -i input.mp4 -vf "fps=30" -vsync cfr -c:v libx264 -crf 18 output_fixed.mp4
2. 保存した深度画像とカラー画像を元にしてステレオ画像へ。それらを結合してなんちゃって空間ビデオの作成
import cv2
import numpy as np
from PIL import Image
from pathlib import Path
from tqdm import tqdm
import subprocess
import glob
def extract_fps_and_audio(source_video, temp_audio_path="temp_audio.aac"):
# FPS取得
probe = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", source_video],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
fps_text = probe.stdout.strip()
if '/' in fps_text:
num, den = map(int, fps_text.split('/'))
fps = num / den
else:
fps = float(fps_text)
# 音声抽出
subprocess.run([
"ffmpeg", "-y", "-i", source_video, "-vn", "-acodec", "copy", temp_audio_path
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return fps, temp_audio_path
def apply_horizontal_shift(rgb_img, shift_map):
"""
深度シフトをNumPyのベクトル演算で高速に適用
"""
h, w, c = rgb_img.shape
x_coords = np.arange(w)[None, :] # shape: (1, w)
x_map = np.clip(x_coords - shift_map, 0, w - 1)
indices_y = np.arange(h)[:, None]
shifted_rgb = rgb_img[indices_y, x_map]
return shifted_rgb
def create_spatial_video_frames(input_folder, max_pixel_shift):
input_folder = Path(input_folder)
rgb_files = sorted(glob.glob(str(input_folder / "*_rgb_*.png")))
depth_files = sorted(glob.glob(str(input_folder / "*_depth_*.png")))
assert len(rgb_files) == len(depth_files), "RGB画像とDepth画像の数が一致しません。"
frames = []
for rgb_path, depth_path in tqdm(zip(rgb_files, depth_files), total=len(rgb_files), desc="Creating stereo frames"):
rgb_img = np.array(Image.open(rgb_path))
depth_img = np.array(Image.open(depth_path)).astype(np.float32) / 255.0
h, w = depth_img.shape
shift_map = (depth_img * max_pixel_shift).astype(np.int32)
# ベクトル化された高速処理
right_eye = apply_horizontal_shift(rgb_img, shift_map)
left_eye = rgb_img
stereo_img = np.concatenate((left_eye, right_eye), axis=1)
frames.append(stereo_img)
return frames
def save_video(frames, output_path, fps):
h, w, _ = frames[0].shape
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(str(output_path), fourcc, fps, (w, h))
for frame in tqdm(frames, desc="Saving video"):
out.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
out.release()
def mux_audio_with_video(video_path, audio_path, final_output_path):
subprocess.run([
"ffmpeg", "-y", "-i", video_path, "-i", audio_path,
"-c:v", "copy", "-c:a", "aac", "-strict", "experimental", final_output_path
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def create_spatial_video_with_audio(input_folder, source_video, max_pixel_shift):
print("抽出中:FPSと音声...")
fps, audio_path = extract_fps_and_audio(source_video)
print(f"FPS: {fps}, 音声ファイル: {audio_path}")
# 出力ファイル名に max_pixel_shift を付加
input_folder = Path(input_folder)
base_name = input_folder.name
output_video_path = f"{base_name}_spatial_shift{max_pixel_shift}.mp4"
print("空間ビデオフレームを生成中...")
frames = create_spatial_video_frames(input_folder, max_pixel_shift)
temp_video_path = "temp_video.mp4"
print("動画を保存中(音声なし)...")
save_video(frames, temp_video_path, fps)
print("音声を合成中...")
mux_audio_with_video(temp_video_path, audio_path, output_video_path)
print(f"🎬 空間ビデオ(音声付き)を保存しました: {output_video_path}")
# クリーンアップ
Path(temp_video_path).unlink(missing_ok=True)
Path(audio_path).unlink(missing_ok=True)
# 実行例
if __name__ == "__main__":
create_spatial_video_with_audio(
input_folder="./output/sample", # 前処理した生成されたフォルダ名
source_video="sample.mp4", # 元の動画
max_pixel_shift=20 # 視差の最大シフト量
)
先ほど処理した動画のファイル名、フォルダ名を指定して実行します。
max_pixel_shift
は視差の最大シフト量を表しています。つまり、この値が大きければ大きいほど左右の視差が大きくなるので、理論的にはより立体感が増しますが、大きくしすぎるとあまりにも不自然になるので個人的には20〜25ぐらいがちょうどいいのではと思ってます。
こちらをいろんな値で試して生成できるよう、わざわざ1度深度画像をまるごと生成して保存しておいたというわけです。
いざ視聴
今回はMeta Quest 3で見ます。こちらはBOBOVRでカスタムしてます。
Meta Questにサイドバイサイドの画像・動画を入れても認識してくれないので、サードパーティー製のアプリでないとお手製の空間ビデオを見ることができません(Appleの本物の空間ビデオはMeta Quest標準アプリで対応してます)。今回は4XVR Video Palyer
を使いました。これ無料なのにすごい高機能。
Meta Horizon Storeでアプリをダウンロードして開きます。
生成した動画をQuestに送るときは、PCとQuestを有線で接続して、Quest側の通知欄に出てくるUSB接続を許可
みたいなのを選択するとPC側で認識してくれるようになります。
下のようなパスのダウンロードフォルダにでも入れましょう。
すると4XVR Video Palyer側で認識してくれるはずです。(添付画像は白塗りしてます)
動画を選択して再生し、画面外でトリガーを引くとシークバー等が出てきます。そこの赤丸の部分を選択して、
フラットの3D FSBS
を選択しましょう(おそらく既に選択されていると思いますが)。
これでなんちゃって空間ビデオが楽しめます。
これは推しの櫻坂46。
感想
出来としては、圧倒的立体感とまではいきませんが、なんとなく飛び出して見えるぐらいですかね。
正直シーンによってかなり左右されちゃいます。少なくとも画質が良くないとダメです。
この砂地に二人が寝ているシーンはかなりリアルに立体的に見えました(2D画像だと伝わりませんが)。綺麗な平面と人とで深度推定の精度がよかったからとか??
近くに映ってるもののほうがよりリアルに立体感を感じるという単純な仕組みでもなさそうなので、いろんなパターンで試して調べようと思います。
本家空間ビデオと比較して
でもMeta QuestのGallelyアプリに入ってるサンプルの空間ビデオ(本物)と比べるとやはり劣ります。こればっかりは実際にHMDで見てもらうしかないんですけどね。
Meta QuestのGalleryアプリの右上のタブから空間ビデオを選択するとサンプルが見れます。
特にこのバンド演奏のボーカルのおっちゃんの飛び出し具合は、VRに慣れてる人でも驚くと思います(2D画像だと伝わらなくて悲しい)。
調べたところ、Appleの空間ビデオは30FPSで撮影されるみたいなので、ヌルヌルだから綺麗に見えるというわけでもなさそうです。まあこちらは深度情報を推定して復元しているわけではないはずなので、綺麗で当たり前ではありますが(Appleだし)。
こちらのなんちゃって空間ビデオの強みとしては、既存の普通の2D動画をあとから立体動画にできるというところです。もっとクオリティが上がって制作コストが下がれば、手軽に立体動画を楽しめるようになるかもしれません。
追記(2025/07/14)
ということでこれを踏まえて本家空間ビデオに変換した記事がこちら。