深度画像からのステレオペア画像生成

目次

基本原理と技術概要

ステレオビジョン技術の基礎

ステレオビジョン(Stereo Vision)は、人間の視覚システムを模倣した3次元情報取得技術である。人間の視覚システムでは、左右の眼の位置差(約65mm)により異なる視点から同一対象を観察し、脳が両眼視差を処理して奥行き感を生成する。この生理学的メカニズムを技術的に再現することで、平面画像から立体視画像を生成する。

三角測量の原理と視差計算

ステレオビジョンの核心は三角測量の原理にある。焦点距離をf、ベースライン(カメラ間距離)をB、深度をZとすると、相似三角形の性質により視差d = f × B / Zで求められる。この式により、深度情報を視差に変換し、人間の両眼視差を再現した立体視画像を作成する。

主要技術と論文

技術名:ステレオビジョン(Stereo Vision)
出典:Marr, D., & Poggio, T. (1976). Cooperative computation of stereo disparity. Science, 194(4262), 283-287.

特徴:単一の深度画像から立体視用の右目画像を生成する技術である。オクルージョン処理(隠面処理)と穴埋めアルゴリズムにより、立体視体験を実現する。

アプリケーション例

VR/AR映像制作、3D映画制作、医療画像の立体視、建築設計の可視化、自動運転における距離計測、ロボット視覚システム

体験価値

深度情報が立体視に変換される過程を体験でき、異なる深度表現や視差パラメータによる立体感の変化を比較実験できる。

技術的課題と解決手法

処理上の困難な状況

テクスチャレス領域(模様のない平坦な領域)では視差推定が困難になり、反射面や透明物体では光学的特性により正確な深度取得が困難となる。これらの問題に対しては、周辺画素を用いた補間処理や深度バッファ法(3Dグラフィックスにおける隠面処理技術)による解決が行われる。

関連技術との位置づけ

従来の特徴点マッチングに基づくステレオマッチング手法とは異なり、既存の深度情報を活用する点で効率的である。深層学習を用いた単眼深度推定(MonoDepth)やNeural Radiance Fields(NeRF)などの技術が発展しており、3D復元が可能となっている。これらの手法は大量のデータから学習することで、従来手法では困難だった複雑なシーンの3D理解を実現している。

事前準備

Python, Windsurfをインストールしていない場合の手順(インストール済みの場合は実行不要)。

  1. 管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。
  2. 以下のコマンドをそれぞれ実行する(winget コマンドは1つずつ実行)。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Windsurf をシステム領域にインストール
winget install --scope machine --id Codeium.Windsurf -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul
REM Windsurf のパス設定
set "WINDSURF_PATH=C:\Program Files\Windsurf"
if exist "%WINDSURF_PATH%" (
    echo "%PATH%" | find /i "%WINDSURF_PATH%" >nul
    if errorlevel 1 setx PATH "%PATH%;%WINDSURF_PATH%" /M >nul
)

必要なパッケージのインストール:

管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。


pip install opencv-python numpy

プログラムコード

ソースコード


# ステレオペア画像生成プログラム
#   カラー画像と深度画像からステレオビジョン用画像を生成
#   参考論文: Marr, D., & Poggio, T. (1976). Cooperative computation of stereo disparity. Science, 194(4262), 283-287.
#   GitHub: https://github.com/opencv/opencv
#   特徴: 深度マップから視差計算による立体視画像生成、三角測量の原理を応用
#         オクルージョン処理と穴埋めアルゴリズム

import cv2
import numpy as np
import tkinter as tk
from tkinter import filedialog

# カメラパラメータ定数
FOCAL_LENGTH = 800.0
BASELINE = 65.0
DEPTH_MIN = 500.0
DEPTH_MAX = 5000.0

def generate_confidence_map(depth_image, disparity, max_disparity):
    """深度値の信頼性に基づく信頼度マップを生成"""
    confidence = np.ones_like(depth_image, dtype=np.float32)

    # 深度勾配の計算(エッジ領域の検出)
    depth_float = depth_image.astype(np.float32)
    grad_x = cv2.Sobel(depth_float, cv2.CV_32F, 1, 0, ksize=3)
    grad_y = cv2.Sobel(depth_float, cv2.CV_32F, 0, 1, ksize=3)
    gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2)

    # エッジ領域は信頼度を下げる
    confidence = np.exp(-gradient_magnitude / 50.0)

    # 視差の妥当性チェック
    disparity_confidence = np.clip(1.0 - disparity / max_disparity, 0.1, 1.0)
    confidence *= disparity_confidence

    # 信頼度を0.1-1.0の範囲にクリップ
    confidence = np.clip(confidence, 0.1, 1.0)

    return confidence

def edge_preserving_inpainting(image, mask, depth_image):
    """エッジ保持を考慮したインペインティング"""
    if len(image.shape) == 3:
        height, width, channels = image.shape
        result = np.zeros_like(image)

        # チャンネルごとにインペインティング実行
        for c in range(channels):
            channel = image[:, :, c]
            result[:, :, c] = cv2.inpaint(channel, mask, 5, cv2.INPAINT_NS)
    else:
        result = cv2.inpaint(image, mask, 5, cv2.INPAINT_NS)

    # エッジ検出による構造保持
    edges = cv2.Canny(depth_image, 50, 150)

    # エッジ近傍では元の画像を優先的に使用
    edge_dilated = cv2.dilate(edges, np.ones((3,3), np.uint8), iterations=1)
    edge_mask = (edge_dilated > 0) & (mask == 0)

    if len(image.shape) == 3:
        edge_mask_3d = np.stack([edge_mask] * 3, axis=2)
        result = np.where(edge_mask_3d, image, result)
    else:
        result = np.where(edge_mask, image, result)

    return result

def uncertainty_based_blur(image, confidence_map):
    """信頼度に基づく適応的ガウシアンブラー"""
    # 信頼度が低い領域にブラーを適用
    blur_strength = (1.0 - confidence_map) * 3.0 + 1.0  # 1.0-4.0の範囲

    # 異なる強度のブラー画像を準備
    blurred_light = cv2.GaussianBlur(image, (5, 5), 1.0)
    blurred_medium = cv2.GaussianBlur(image, (7, 7), 2.0)
    blurred_heavy = cv2.GaussianBlur(image, (9, 9), 3.0)

    # 信頼度に基づく重み付け合成
    if len(image.shape) == 3:
        confidence_3d = np.stack([confidence_map] * 3, axis=2)
        high_conf_mask = confidence_3d > 0.8
        med_conf_mask = (confidence_3d > 0.5) & (confidence_3d <= 0.8)
        low_conf_mask = confidence_3d <= 0.5

        result = np.where(high_conf_mask, image,
                 np.where(med_conf_mask, blurred_light,
                 np.where(low_conf_mask, blurred_medium, blurred_heavy)))
    else:
        high_conf_mask = confidence_map > 0.8
        med_conf_mask = (confidence_map > 0.5) & (confidence_map <= 0.8)
        low_conf_mask = confidence_map <= 0.5

        result = np.where(high_conf_mask, image,
                 np.where(med_conf_mask, blurred_light,
                 np.where(low_conf_mask, blurred_medium, blurred_heavy)))

    return result.astype(np.uint8)

def improved_hole_filling(right_image, depth_image, disparity, max_disparity):
    """穴埋め処理"""

    # 1. 信頼度マップ生成
    confidence = generate_confidence_map(depth_image, disparity, max_disparity)

    # 2. 穴領域の検出
    if len(right_image.shape) == 3:
        mask = np.all(right_image == 0, axis=2).astype(np.uint8)
    else:
        mask = (right_image == 0).astype(np.uint8)

    # 穴がない場合は元の画像を返す
    if np.sum(mask) == 0:
        return right_image

    # 3. マスク領域の軽微な拡張(境界を滑らかに)
    kernel = np.ones((3,3), np.uint8)
    expanded_mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

    # 4. エッジ保持インペインティング
    inpainted = edge_preserving_inpainting(right_image, expanded_mask, depth_image)

    # 5. バイラテラルフィルタによるエッジ保持平滑化
    if len(inpainted.shape) == 3:
        smoothed = cv2.bilateralFilter(inpainted, 9, 75, 75)
    else:
        smoothed = cv2.bilateralFilter(inpainted, 9, 75, 75)

    # 6. 信頼度に基づく適応的ブラー
    final_result = uncertainty_based_blur(smoothed, confidence)

    # 7. 元画像の有効部分を保持
    if len(right_image.shape) == 3:
        valid_mask = ~np.all(right_image == 0, axis=2)
        valid_mask_3d = np.stack([valid_mask] * 3, axis=2)
        final_result = np.where(valid_mask_3d, right_image, final_result)
    else:
        valid_mask = (right_image != 0)
        final_result = np.where(valid_mask, right_image, final_result)

    return final_result

# プログラム開始
print("========================================")
print("ステレオペア画像生成プログラム")
print("========================================")
print("\n【概要】")
print("カラー画像と深度画像から立体視用のステレオペア画像を生成します")
print("\n【操作方法】")
print("1. 深度表現方式を選択(A または B を入力)")
print("2. カラー画像ファイルを選択")
print("3. 深度画像ファイルを選択")
print("4. 処理完了後、画像が表示されます")
print("5. 任意のキーを押すと終了します")
print("\n【注意事項】")
print("- 深度画像はグレースケールまたはカラー画像どちらでも可")
print("- 出力画像は 'stereo_output_improved.jpg' として保存されます")
print("========================================\n")

print("深度表現方式:")
print("  A = 白が遠い/黒が近い")
print("  B = 黒が遠い/白が近い")
depth_mode = input("\n深度表現方式を選択 (A/B): ").upper()

# 画像ファイル選択
root = tk.Tk()
root.withdraw()

print("\nカラー画像を選択してください...")
color_path = filedialog.askopenfilename(title="カラー画像を選択")
if not color_path:
    print("カラー画像が選択されませんでした。終了します。")
    exit()

print("深度画像を選択してください...")
depth_path = filedialog.askopenfilename(title="深度画像を選択")
if not depth_path:
    print("深度画像が選択されませんでした。終了します。")
    exit()

root.destroy()

# 画像読み込み
print("\n画像を読み込み中...")
color_image = cv2.imread(color_path)
depth_image_raw = cv2.imread(depth_path)

if color_image is None:
    print("カラー画像の読み込みに失敗しました。")
    exit()
if depth_image_raw is None:
    print("深度画像の読み込みに失敗しました。")
    exit()

# 深度画像の明度抽出
if len(depth_image_raw.shape) == 3:
    depth_image = cv2.cvtColor(depth_image_raw, cv2.COLOR_BGR2GRAY)
else:
    depth_image = depth_image_raw

# メイン処理
target_height, target_width = color_image.shape[:2]
src_h, src_w = depth_image.shape[:2]

print(f"カラー画像サイズ: {target_width}x{target_height}")
print(f"深度画像サイズ: {src_w}x{src_h}")

# サイズ調整(切り抜き→パディング)
if src_h > target_height or src_w > target_width:
    start_y = max(0, (src_h - target_height) // 2)
    start_x = max(0, (src_w - target_width) // 2)
    end_y = min(src_h, start_y + target_height)
    end_x = min(src_w, start_x + target_width)
    depth_image = depth_image[start_y:end_y, start_x:end_x]

current_h, current_w = depth_image.shape[:2]
if current_h < target_height or current_w < target_width:
    pad_top = (target_height - current_h) // 2
    pad_bottom = target_height - current_h - pad_top
    pad_left = (target_width - current_w) // 2
    pad_right = target_width - current_w - pad_left

    depth_image = cv2.copyMakeBorder(
        depth_image, pad_top, pad_bottom, pad_left, pad_right,
        cv2.BORDER_REPLICATE
    )

# 深度値正規化と視差計算
print("\n視差を計算中...")
if depth_mode == 'A':
    # A: 白が遠い(255→DEPTH_MAX)、黒が近い(0→DEPTH_MIN)
    depth_values = DEPTH_MIN + (depth_image / 255.0) * (DEPTH_MAX - DEPTH_MIN)
else:
    # B: 黒が遠い(0→DEPTH_MAX)、白が近い(255→DEPTH_MIN)
    depth_values = DEPTH_MAX - (depth_image / 255.0) * (DEPTH_MAX - DEPTH_MIN)

depth_safe = np.clip(depth_values, DEPTH_MIN, DEPTH_MAX)
disparity = (FOCAL_LENGTH * BASELINE) / depth_safe
max_disparity = target_width * 0.15  # ステレオビジョンの一般的な範囲
disparity = np.clip(disparity, 0, max_disparity).astype(np.float32)

# 右画像生成
height, width = color_image.shape[:2]
right_image = np.zeros_like(color_image)
depth_buffer = np.full((height, width), np.inf)

print("右画像を生成中...")
for y in range(height):
    if y % 100 == 0:
        print(f"  進捗: {y}/{height} 行")
    for x in range(width):
        disp = disparity[y, x]
        new_x = int(x - disp)

        if 0 <= new_x < width:
            current_depth = depth_safe[y, x]

            if current_depth < depth_buffer[y, new_x]:
                right_image[y, new_x] = color_image[y, x]
                depth_buffer[y, new_x] = current_depth

print("\n穴埋め処理を実行中...")
right_image = improved_hole_filling(right_image, depth_image, disparity, max_disparity)

# 結果出力
stereo_pair = np.hstack([color_image, right_image])

output_filename = 'stereo_output_improved.jpg'
cv2.imwrite(output_filename, stereo_pair)
print(f"\n処理完了!")
print(f"ステレオペア画像を '{output_filename}' として保存しました")
print("左側:元画像、右側:右目用画像")
print("\n任意のキーを押すと終了します...")

cv2.imshow('ステレオペア画像', stereo_pair)
cv2.waitKey(0)
cv2.destroyAllWindows()

使用方法

  1. プログラムを実行する
  2. 深度表現方式を選択する(A:黒が近い/白が遠い、B:白が近い/黒が遠い)
  3. カラー画像ファイルを選択する
  4. 深度画像ファイルを選択する
  5. 生成されたステレオペア画像が表示される
  6. 結果はstereo_output.jpgとして保存される

実験・探求のアイデア

パラメータ実験

FOCAL_LENGTH値を変更(400、800、1200)して立体感の変化を比較する。BASELINE値を変更(30、65、100)して視差効果の違いを観察する。DEPTH_MIN/DEPTH_MAX値を調整して深度範囲による影響を確認する。

深度表現実験

同じ画像でモードAとモードBを比較し、深度解釈の違いを体験する。異なる種類の深度画像(人物、風景、建物)で立体視効果を比較する。

視差効果の探求

生成された右画像の視差分布を分析し、距離による視差の変化パターンを発見する。穴埋めアルゴリズムの効果を確認するため、穴埋め前後の画像を比較する。

実験アイデア

深度画像の品質が立体視効果に与える影響を検証する。異なる焦点距離設定での立体視の自然さを主観評価する。オクルージョン領域の処理方法による視覚的違いを観察する。