AIを用いた単一画像の3次元化(カメラ対応版)
この資料は、AIによる単眼深度推定(1枚の画像から各画素の奥行きを推定する技術)を題材に、2次元のカメラ画像から3次元の点群(ポイントクラウド:色付きの点の集まりで立体形状を表すデータ)を復元する流れを学ぶ演習集である。2つの深度推定モデルを順に試し、結果を見比べることで、モデルの違いが3次元復元の品質にどう影響するかを確かめる。
【目次】
演習1: MiDaS による単眼深度推定
プログラム概要
このプログラムは、AIモデル「MiDaS」を使って、PCカメラの1枚の画像から各画素の奥行き(深度)を推定する演習である。 人間が片目でも遠近感をつかめるように、MiDaSは大量の画像で学習した知識から、画像の見た目だけで手前と奥を予測する。 モデルは初回実行時にインターネットから自動でダウンロードされる。GPU(画像処理向けの演算装置で、AIの計算を高速化する)があれば自動で使い、無ければCPUで動く。 画面には常にカメラ映像と日本語の操作説明が表示され、深度マップ(奥行きを明暗で表した画像)は別ウィンドウに表示される。 sキーを押すと、その時点の画像と推定深度から3次元の点群を生成し、ファイルに保存して立体表示する。 画像が立体データに変わる過程を確かめる。
演習の進め方
演習番号:演習1
テーマ名:MiDaS による単眼深度推定と点群復元
手順:
- 下記のインストールコマンドで必要なライブラリを準備する。
- PCにカメラが接続されていることを確認する。
- 下記のコードを実行(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)する。初回はモデルが自動でダウンロードされる。
- カメラ映像と深度マップの2つのウィンドウが出ることを確認する。
- 奥行きの異なる被写体(手前に手をかざす等)を映し、深度マップの明暗の変化を観察する。
- sキーを押して点群を生成し、別ウィンドウでマウス操作(ドラッグで回転、ホイールで拡大縮小)しながら立体構造を確認する。
ヒント:
- MiDaSの出力は逆深度(視差)で、値が大きいほど手前を表す。深度マップでは明るい部分ほど近い。
- 初回のモデルダウンロードには時間がかかるため、表示が出るまで待つ。
考察ポイント:
- 深度マップの明暗が、実際の被写体の遠近とどの程度一致しているかを確かめる。
- 生成した点群を回転させ、平らな壁や曲面がどのような立体として復元されているかを読み取る。
必要なライブラリのインストール
管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。
そして、以下のコマンドを実行し、関連ライブラリをインストールしたうえで、動作確認を行う。
MiDaSは内部でtimmに依存するため、timmも併せてインストールする。
pip install --no-user timm opencv-python numpy open3d pillow
import torch
import cv2
import numpy as np
import open3d as o3d
from PIL import Image, ImageDraw, ImageFont
import os
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用デバイス: {device}")
# ---- 日本語フォントの取得 ----
def get_japanese_font(size=24):
candidates = [
"/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"C:/Windows/Fonts/meiryo.ttc",
"C:/Windows/Fonts/msgothic.ttc",
"C:/Windows/Fonts/YuGothM.ttc",
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
]
for path in candidates:
if os.path.exists(path):
return ImageFont.truetype(path, size)
return ImageFont.load_default()
jp_font = get_japanese_font(22)
jp_font_small = get_japanese_font(18)
def draw_japanese_text(img_bgr, lines, org=(10, 10), font=jp_font,
color=(255, 255, 255), bg=(0, 0, 0)):
# OpenCV(BGR)画像にPILで日本語テキストを重畳する
img_pil = Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
x, y = org
line_h = font.size + 8
max_w = 0
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
max_w = max(max_w, bbox[2] - bbox[0])
panel = Image.new("RGBA", img_pil.size, (0, 0, 0, 0))
pdraw = ImageDraw.Draw(panel)
pdraw.rectangle(
[x - 6, y - 6, x + max_w + 12, y + line_h * len(lines) + 4],
fill=(bg[0], bg[1], bg[2], 140),
)
img_pil = Image.alpha_composite(img_pil.convert("RGBA"), panel).convert("RGB")
draw = ImageDraw.Draw(img_pil)
for i, line in enumerate(lines):
draw.text((x, y + i * line_h), line, font=font, fill=color[::-1])
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# ---- 学習済みMiDaSモデルをtorch.hubから自動ダウンロード ----
model_type = "MiDaS_small" # カメラ用に軽量版(DPT_Large/DPT_Hybridも選択可)
midas = torch.hub.load("intel-isl/MiDaS", model_type)
midas.to(device)
midas.eval()
midas_transforms = torch.hub.load("intel-isl/MiDaS", "transforms")
if model_type in ("DPT_Large", "DPT_Hybrid"):
transform = midas_transforms.dpt_transform
else:
transform = midas_transforms.small_transform
def estimate_depth(img_rgb):
H, W = img_rgb.shape[:2]
input_batch = transform(img_rgb).to(device)
with torch.no_grad():
prediction = midas(input_batch)
prediction = torch.nn.functional.interpolate(
prediction.unsqueeze(1),
size=(H, W),
mode="bicubic",
align_corners=False,
).squeeze()
return prediction.cpu().numpy()
def make_point_cloud(img_rgb, depth):
# MiDaSの出力は「逆深度(視差)」。値が大きいほど近い。
H, W = img_rgb.shape[:2]
# (1) 視差の外れ値をパーセンタイルでクリップして安定化
disp = depth.astype(np.float32)
lo, hi = np.percentile(disp, 2), np.percentile(disp, 98)
disp = np.clip(disp, lo, hi)
# (2) 視差を 0.1〜1.0 に正規化してから距離へ変換(ゼロ割を回避)
disp_norm = (disp - disp.min()) / (disp.max() - disp.min() + 1e-8)
disp_norm = disp_norm * 0.9 + 0.1 # 0.1〜1.0
z = 1.0 / disp_norm # 近い所ほど z 小、遠い所ほど z 大
# (3) 距離側も外れ値をクリップし、実用的なスケールへ正規化
z = np.clip(z, np.percentile(z, 2), np.percentile(z, 98))
z = (z - z.min()) / (z.max() - z.min() + 1e-8) # 0〜1
z = z * 3.0 + 1.0 # 1.0〜4.0 の奥行き
# (4) ピンホール逆投影で3次元座標へ
fx = fy = W * 0.8
cx, cy = W / 2.0, H / 2.0
xs, ys = np.meshgrid(np.arange(W), np.arange(H))
X = (xs - cx) * z / fx
Y = -(ys - cy) * z / fy
Z = -z
points = np.stack([X, Y, Z], axis=-1).reshape(-1, 3)
colors = img_rgb.reshape(-1, 3).astype(np.float32) / 255.0
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
pcd.colors = o3d.utility.Vector3dVector(colors)
# (5) 重心を原点へ移動(初期視点で中央に収まりやすくする)
pcd = pcd.translate(-pcd.get_center())
return pcd
def show_point_cloud(pcd, window_name):
# ビューを点群にフィットさせて表示(点が消えないよう点サイズも調整)
vis = o3d.visualization.Visualizer()
vis.create_window(window_name=window_name, width=1024, height=768)
vis.add_geometry(pcd)
opt = vis.get_render_option()
opt.point_size = 2.0
opt.background_color = np.array([0.1, 0.1, 0.1])
vis.reset_view_point(True) # 点群全体が収まる初期視点に設定
vis.run()
vis.destroy_window()
def grab_latest_frame(cap, flush=5):
# バッファに残った旧フレームを読み捨てて最新フレームを取得
for _ in range(flush):
cap.grab()
ret, frame = cap.read()
return ret, frame
HELP_LINES = [
"MiDaS 単眼深度推定 デモ",
"[s] キー : 3D点群を生成・保存・表示",
"[d] キー : 深度マップ表示の ON/OFF",
"[Esc] キー : 終了",
]
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
show_depth = True
print("カメラ起動中... 操作方法は画面内に表示されます。")
while True:
ret, frame = grab_latest_frame(cap)
if not ret:
break
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
depth = estimate_depth(img_rgb)
# 別ウィンドウ:深度マップを常時表示
if show_depth:
depth_norm = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8)
depth_vis = (depth_norm * 255).astype(np.uint8)
depth_color = cv2.applyColorMap(depth_vis, cv2.COLORMAP_INFERNO)
depth_color = draw_japanese_text(
depth_color, ["深度マップ(処理結果)"], org=(10, 10),
font=jp_font_small, color=(255, 255, 255))
cv2.imshow("depth (result)", depth_color)
# メインウィンドウ:カメラ画像+日本語操作説明
display = draw_japanese_text(frame.copy(), HELP_LINES, org=(10, 10))
cv2.imshow("camera", display)
key = cv2.waitKey(1) & 0xFF
if key == 27: # Esc
break
elif key == ord('d'):
show_depth = not show_depth
if not show_depth:
cv2.destroyWindow("depth (result)")
elif key == ord('s'): # 点群を生成・保存・別画面表示
pcd = make_point_cloud(img_rgb, depth)
o3d.io.write_point_cloud("pointcloud_midas.ply", pcd)
print("3D点群を保存: pointcloud_midas.ply")
show_point_cloud(pcd, "MiDaS 3D Point Cloud")
cap.release()
cv2.destroyAllWindows()
実行結果例
実行結果動画
演習2: Depth Anything による単眼深度推定
プログラム概要
このプログラムは、より新しいAIモデル「Depth Anything V2」を使って、PCカメラの1枚の画像から奥行きを推定する演習である。 このモデルは多様で大量の画像で学習しているため、屋内外を問わず安定した深度推定ができる。 モデルはHugging Face(学習済みモデルを配布する公開プラットフォーム)から初回実行時に自動ダウンロードされ、GPUがあれば自動で使い、無ければCPUで動作する。 画面には常にカメラ映像と日本語の操作説明が表示され、推定された深度マップは別ウィンドウに表示される。 sキーを押すと、その時点の画像と深度から3次元の点群を生成し、ファイルに保存して立体表示する。 演習1のMiDaSと結果を見比べることで、モデルの違いが3次元復元の品質にどう影響するかを確かめられる。 AIが画像から立体構造を読み取る仕組みへの理解を深める。
演習の進め方
演習番号:演習2
テーマ名:Depth Anything V2 による単眼深度推定と点群復元
手順:
- 下記のインストールコマンドで必要なライブラリを準備する。
- PCにカメラが接続されていることを確認する。
- 下記のコードを実行(メモ帳を用いる場合は a.py のようなファイル名で保存して実行)する。初回はモデルが自動でダウンロードされる。
- 演習1と同じ被写体を映し、深度マップを観察する。
- sキーを押して点群を生成し、立体構造を確認する。
ヒント:
- Depth Anythingの出力は相対深度で、値が大きいほど手前を表す傾向がある。
- 同じ被写体を演習1と本演習の両方で試すと比較しやすい。
考察ポイント:
- 同じ被写体に対して、MiDaSとDepth Anythingの深度マップにどのような差が現れるかを比べる。
- 復元した点群の立体構造が、どちらのモデルでより滑らかか、または正確かを読み取る。
管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。
そして、以下のコマンドを実行し、関連ライブラリをインストールしたうえで、動作確認を行う。
Depth Anything V2はtransformersライブラリ経由で読み込む。
pip install --no-user transformers pillow opencv-python numpy open3d
import torch
import cv2
import numpy as np
import open3d as o3d
from PIL import Image, ImageDraw, ImageFont
import os
from transformers import AutoImageProcessor, AutoModelForDepthEstimation
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用デバイス: {device}")
# ---- 日本語フォントの取得 ----
def get_japanese_font(size=24):
candidates = [
"/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"C:/Windows/Fonts/meiryo.ttc",
"C:/Windows/Fonts/msgothic.ttc",
"C:/Windows/Fonts/YuGothM.ttc",
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
]
for path in candidates:
if os.path.exists(path):
return ImageFont.truetype(path, size)
return ImageFont.load_default()
jp_font = get_japanese_font(22)
jp_font_small = get_japanese_font(18)
def draw_japanese_text(img_bgr, lines, org=(10, 10), font=jp_font,
color=(255, 255, 255), bg=(0, 0, 0)):
# OpenCV(BGR)画像にPILで日本語テキストを重畳する
img_pil = Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
x, y = org
line_h = font.size + 8
max_w = 0
for line in lines:
bbox = draw.textbbox((0, 0), line, font=font)
max_w = max(max_w, bbox[2] - bbox[0])
panel = Image.new("RGBA", img_pil.size, (0, 0, 0, 0))
pdraw = ImageDraw.Draw(panel)
pdraw.rectangle(
[x - 6, y - 6, x + max_w + 12, y + line_h * len(lines) + 4],
fill=(bg[0], bg[1], bg[2], 140),
)
img_pil = Image.alpha_composite(img_pil.convert("RGBA"), panel).convert("RGB")
draw = ImageDraw.Draw(img_pil)
for i, line in enumerate(lines):
draw.text((x, y + i * line_h), line, font=font, fill=color[::-1])
return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
# ---- 学習済みDepth Anything V2モデルをHuggingFaceから自動ダウンロード ----
model_name = "depth-anything/Depth-Anything-V2-Small-hf" # Base/Largeも選択可
processor = AutoImageProcessor.from_pretrained(model_name)
model = AutoModelForDepthEstimation.from_pretrained(model_name)
model.to(device)
model.eval()
def estimate_depth(img_rgb):
H, W = img_rgb.shape[:2]
image = Image.fromarray(img_rgb)
inputs = processor(images=image, return_tensors="pt").to(device)
with torch.no_grad():
outputs = model(**inputs)
predicted_depth = outputs.predicted_depth
prediction = torch.nn.functional.interpolate(
predicted_depth.unsqueeze(1),
size=(H, W),
mode="bicubic",
align_corners=False,
).squeeze()
return prediction.cpu().numpy()
def make_point_cloud(img_rgb, depth):
# Depth Anythingの出力は「相対深度」。値が大きいほど近い傾向。
H, W = img_rgb.shape[:2]
# (1) 外れ値をパーセンタイルでクリップして安定化
d = depth.astype(np.float32)
d = np.clip(d, np.percentile(d, 2), np.percentile(d, 98))
d_norm = (d - d.min()) / (d.max() - d.min() + 1e-8) # 0〜1(大=近い)
# (2) 「近い=z小、遠い=z大」へ反転し、実用スケールへ
z = (1.0 - d_norm) * 3.0 + 1.0 # 1.0〜4.0 の奥行き
# (3) ピンホール逆投影で3次元座標へ
fx = fy = W * 0.8
cx, cy = W / 2.0, H / 2.0
xs, ys = np.meshgrid(np.arange(W), np.arange(H))
X = (xs - cx) * z / fx
Y = -(ys - cy) * z / fy
Z = -z
points = np.stack([X, Y, Z], axis=-1).reshape(-1, 3)
colors = img_rgb.reshape(-1, 3).astype(np.float32) / 255.0
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
pcd.colors = o3d.utility.Vector3dVector(colors)
# (4) 重心を原点へ移動
pcd = pcd.translate(-pcd.get_center())
return pcd
def show_point_cloud(pcd, window_name):
# ビューを点群にフィットさせて表示(点が消えないよう点サイズも調整)
vis = o3d.visualization.Visualizer()
vis.create_window(window_name=window_name, width=1024, height=768)
vis.add_geometry(pcd)
opt = vis.get_render_option()
opt.point_size = 2.0
opt.background_color = np.array([0.1, 0.1, 0.1])
vis.reset_view_point(True) # 点群全体が収まる初期視点に設定
vis.run()
vis.destroy_window()
def grab_latest_frame(cap, flush=5):
# バッファに残った旧フレームを読み捨てて最新フレームを取得
for _ in range(flush):
cap.grab()
ret, frame = cap.read()
return ret, frame
HELP_LINES = [
"Depth Anything 単眼深度推定 デモ",
"[s] キー : 3D点群を生成・保存・表示",
"[d] キー : 深度マップ表示の ON/OFF",
"[Esc] キー : 終了",
]
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
show_depth = True
print("カメラ起動中... 操作方法は画面内に表示されます。")
while True:
ret, frame = grab_latest_frame(cap)
if not ret:
break
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
depth = estimate_depth(img_rgb)
# 別ウィンドウ:深度マップを常時表示
if show_depth:
depth_norm = (depth - depth.min()) / (depth.max() - depth.min() + 1e-8)
depth_vis = (depth_norm * 255).astype(np.uint8)
depth_color = cv2.applyColorMap(depth_vis, cv2.COLORMAP_INFERNO)
depth_color = draw_japanese_text(
depth_color, ["深度マップ(処理結果)"], org=(10, 10),
font=jp_font_small, color=(255, 255, 255))
cv2.imshow("depth (result)", depth_color)
# メインウィンドウ:カメラ画像+日本語操作説明
display = draw_japanese_text(frame.copy(), HELP_LINES, org=(10, 10))
cv2.imshow("camera", display)
key = cv2.waitKey(1) & 0xFF
if key == 27: # Esc
break
elif key == ord('d'):
show_depth = not show_depth
if not show_depth:
cv2.destroyWindow("depth (result)")
elif key == ord('s'): # 点群を生成・保存・別画面表示
pcd = make_point_cloud(img_rgb, depth)
o3d.io.write_point_cloud("pointcloud_anything.ply", pcd)
print("3D点群を保存: pointcloud_anything.ply")
show_point_cloud(pcd, "Depth Anything 3D Point Cloud")
cap.release()
cv2.destroyAllWindows()
実行結果例
実行結果動画