【Rust + OpenCV】視差マッピング

# 前提
最近業務で初めてのコンピュータビジョン分野を経験させて頂ける機会がありまして、Side by Side方式で撮影した映像を3Dモードにして立体に見せるという技術に触れています。私の専門はWebでして、趣味でゲームを作ることしかやっておらず、画像認識は本当に初めての経験です。

そのため、私自身が学習中であることをご理解いただいた上で、Rustで画像認識をしてみたいという方に読んで頂けると嬉しいです。

以下の項目で構成していますので、コードだけ見たい方は「実装」項目にお進みください。
- 視差マッピングを作るにあたっての基礎概念
- 使用する2枚の画像
- 完成した視差マッピング
- 実装
- 終わりに

# 視差マッピングを作るにあたっての基礎概念
視差マッピングに触れる前に前提知識をまとめておきます。

## Side by Side方式とは
> サイドバイサイド方式の3D映像とは、左右に分かれた2つの映像を使って立体的な視覚効果を生み出す方法です。この技術は、右目用と左目用に少し異なる視点から見た映像を並べて表示し、それぞれの目に対応する映像を見せることで奥行きや立体感を感じさせます。

当たり前かもしれませんが、人間の左目と右目で位置が異なるため、左目だけで見たときの景色と右目だけで見たときの景色は異なります。その両方の映像を重ね合わせることで立体に見えるという人体の理(ことわり)をコンピュータの技術で再現したと言えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/5ab436bc-c5b9-4cb0-821b-cde49a8de3cd.png)

[3Dに不可欠なサイドバイサイド方式の立体映像について](https://mdisplay-tech.com/news/post-2227/)

## 視差マッピング
視差マッピングは英語で表すと Parallax Mapping と言い、Adobeの公式サイトでは視差マッピングのことをパララックスマッピングと表現しています。

> パララックスマッピングは、コンピュータ生成グラフィックスのテクスチャサーフェスに奥行きとディティールを追加する手法です。パララックスマッピングは、3Dモデルにポリゴンを追加することなく、サーフェスディティールの錯覚を生み出すための一般的なツールです。この技術を使用すると、モデルを異なる角度から見たときに、テクスチャの凹凸やへこみなどがわかるようにすることができます。

[パララックスマッピングとは](https://www.adobe.com/jp/products/substance3d/discover/what-is-parallax-mapping.html?msockid=0d686fd85104633a1af37b55507e6247)

## エピポーラ幾何とは
> エピポーラ幾何 (Epipolar Geometry)とは，ステレオビジョンにおける2カメラ間での幾何のことである．同一物体・シーンを，異なる2箇所・(2方向) から同一シーンを撮影した際に，カメラ中心2つとシーン中の１点の間で必ず発生する「幾何的拘束( =エピポーラ拘束)」 が存在する．この拘束をうまく利用すると，ステレオ3次元復元のための幾何が便利に操作できるエピポーラ幾何が活用できる．

[エピポーラ幾何 (Epipolar Geometry)](https://cvml-expertguide.net/terms/epipolar-geometry)

名称の通り「幾何学」のため主に行列を扱った数式で構成される分野です。私は数式を説明することが苦手なため、下記を参考にされると良いかと思います。ご興味のある方だけ詳しく調べていただければ嬉しいです。

他参考：
・技術ブログ
[【コンピュータビジョン】ネコと学ぶエピポーラ幾何](https://qiita.com/ykoga/items/14300e8cdf5aa7bd8d31)
[エピポーラ幾何](https://qiita.com/Thought_Nibbler/items/9cb7c2637000eecc1a30)
[エピポーラ幾何とカメラパラメータの推定](https://qiita.com/ground0state/items/70ec4a6ffe337b800af3)

・スライド
[画像メディア工学特論(8)](https://vislab.jp/hiura/lec/ime/2010/8.pdf)
[画像情報処理　カメラの幾何学](https://vislab.jp/hiura/lec/iip/geometry2015.pdf)

※兵庫県立大学　大学院工学研究科　電子情報工学専攻　視覚メディア工学研究室さんのスライドが比較的明快なので、こちらを読むといいかもしれません。

・サイト
[兵庫県立大学　大学院工学研究科　電子情報工学専攻　視覚メディア工学研究室](https://vislab.jp/)

# 使用する2枚の画像
視差マッピングを作成するに際して、次の2枚の画像を使用しました。
※出典先は失念してしまいました。申し訳ない。。。確か海外の研究室のデータセットからお借りした経緯です。

### 左
![right.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/069a3de9-9552-4f7d-ab92-790a6ff4840c.png)

### 右
![left.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/08206c52-c498-40f0-80db-f8f90d9755f4.png)

# 完成した視差マッピング
![disparity_map.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/0d28e4b5-9bb2-427e-b4bc-52f6f11b2229.jpeg)


ここまで付き合わせてきて申し訳ないですが、完成度はかなり低いです。
ですが、画像に写る対象物の明るさは十分であり、テクスチャも多いため、
完成度を向上させるためには定数定義したパラメータの調整で実現できるかと思います。

# 実装
```rust
use opencv::{
    calib3d::{
        StereoMatcher,
        StereoSGBM
    },
    core::{
        absdiff,
        min_max_loc,
        normalize,
        Mat,
        Ptr,
        Rect,
        Vector,
        Size,
        BORDER_DEFAULT,
        CV_8U,
        NORM_MINMAX
    },
    imgcodecs,
    imgproc,
    prelude::*,
    ximgproc::create_disparity_wls_filter,
};

// SGBM (Semi-Global Block Matching) パラメータ設定
const MIN_DISPARITY: i32 = 0;                          // 最小視差
const NUM_DISPARITIES: i32 = 256;                      // 視差の範囲
const BLOCK_SIZE: i32 = 5;                             // ブロックサイズ
const P1: i32 = 16 * BLOCK_SIZE * BLOCK_SIZE;          // P1 (平滑化パラメータ)
const P2: i32 = 64 * BLOCK_SIZE * BLOCK_SIZE;          // P2 (平滑化パラメータ)
const DISP_12_MAX_DIFF: i32 = 1;                       // 視差の最大差
const PRE_FILTER_CAP: i32 = 31;                        // 前処理キャッピング
const UNIQUENESS_RATIO: i32 = 5;                       // 一意性比率
const SPECKLE_WINDOW_SIZE: i32 = 25;                   // スペックル検出ウィンドウサイズ
const SPECKLE_RANGE: i32 = 4;                          // スペックルの範囲
const MODE: i32 = opencv::calib3d::StereoSGBM_MODE_HH; // モード設定

// WLSフィルタのパラメータ
const LAMBDA: f64 = 500.0;                             // WLSフィルタのスムージングパラメータ
const FILTER_ALPHA: f64 = 1.0;                         // フィルタのスケール
const FILTER_BETA: f64 = 0.0;                          // フィルタのバイアス

// ブラー処理のパラメータ
const K_SIZE_HEIGHT: i32 = 3;                          // カーネル高さ
const K_SIZE_WIDTH: i32 = 3;                           // カーネル幅
const SIGMA_X: f64 = 0.0;                              // X方向のガウスぼかしの標準偏差
const SIGMA_Y: f64 = 0.0;                              // Y方向のガウスぼかしの標準偏差

// エッジ検出のパラメータ
const THRESHOLD_1: f64 = 100.0;                        // Cannyエッジ検出の下限閾値（弱いエッジを抑制）
const THRESHOLD_2: f64 = 200.0;                        // Cannyエッジ検出の上限閾値（強いエッジを保持）
const APERTURE_SIZE: i32 = 3;                          // Sobelフィルタのカーネルサイズ（3×3）

fn main() -> opencv::Result<()> {
    // 入力画像の読み込み
    let left = imgcodecs::imread("./img/left.png", imgcodecs::IMREAD_GRAYSCALE)?;
    let right = imgcodecs::imread("./img/right.png", imgcodecs::IMREAD_GRAYSCALE)?;

    // StereoSGBMの初期化
    let mut sgbm = StereoSGBM::create(
        MIN_DISPARITY,
        NUM_DISPARITIES,
        BLOCK_SIZE,
        P1,
        P2,
        DISP_12_MAX_DIFF,
        PRE_FILTER_CAP,
        UNIQUENESS_RATIO,
        SPECKLE_WINDOW_SIZE,
        SPECKLE_RANGE,
        MODE,
    )?;

    // 視差マップの生成
    let mut disparity_sgbm = Mat::default();            // 左画像からの視差マップ
    let mut disparity_right = Mat::default();           // 右画像からの視差マップ
    sgbm.compute(&left, &right, &mut disparity_sgbm)?;  // 左から右の視差計算
    sgbm.compute(&right, &left, &mut disparity_right)?; // 右から左の視差計算

    // WLSフィルタの適用（視差の滑らかな補正）
    let sgbm_ptr: Ptr<StereoMatcher> = Ptr::from(sgbm);
    let mut wls_filter = create_disparity_wls_filter(sgbm_ptr)?;
    wls_filter.set_lambda(LAMBDA)?;

    let mut filtered_disparity = Mat::default();        // フィルタリングされた視差
    wls_filter.filter(
        &disparity_sgbm,
        &left,
        &mut filtered_disparity,
        &disparity_right,
        Rect::default(),
        &right,
    )?;

    // 浮動小数点型に変換
    let mut disparity_float = Mat::default();
    filtered_disparity.convert_to(&mut disparity_float, opencv::core::CV_32F, FILTER_ALPHA, FILTER_BETA)?;

    // 視差マップのぼかし処理（ガウシアンぼかし）
    let mut disparity_blurred = Mat::default();
    imgproc::gaussian_blur(&disparity_float, &mut disparity_blurred, Size::new(K_SIZE_WIDTH, K_SIZE_HEIGHT), SIGMA_X, SIGMA_Y, BORDER_DEFAULT)?;

    // ゼロ行列作成
    let size = disparity_blurred.size()?; // 視差画像のサイズ取得
    let width = size.width;
    let height = size.height;
    
    // ゼロ行列（背景用）
    let mat_zeros = Mat::zeros(height, width, disparity_blurred.typ())?;
    
    // 視差マップとゼロ行列の差分を計算
    let disparity_blurred_clone = disparity_blurred.clone();
    absdiff(&disparity_blurred_clone, &mat_zeros, &mut disparity_blurred)?;

    // 視差範囲の確認
    let mut min_val = 0.0;
    let mut max_val = 0.0;
    min_max_loc(&disparity_blurred, Some(&mut min_val), Some(&mut max_val), None, None, &Mat::default())?;
    println!("Disparity range: min = {}, max = {}", min_val, max_val);

    // 正規化（視差画像を0~255の範囲にスケーリング）
    let mut disparity_visual = Mat::default();
    normalize(
        &disparity_blurred,
        &mut disparity_visual,
        min_val,
        max_val,
        NORM_MINMAX,
        CV_8U,
        &Mat::default(),
    )?;

    // Cannyエッジ検出（視差画像のエッジを抽出）
    let mut edges = Mat::default();
    imgproc::canny(&disparity_visual, &mut edges, THRESHOLD_1, THRESHOLD_2, APERTURE_SIZE, false)?;

    imgcodecs::imwrite("./out/disparity_map.jpg", &disparity_visual, &Vector::new())?;

    Ok(())
}
```

[【GitHub】Disparity Mapping with OpenCV Stereo SGBM](https://github.com/Ometeor-Zheero-OMZ/disparity_mapping_StereoSGBM)

# 終わりに
私自身がコンピュータビジョンという分野に足を踏み入れたのが今週の月曜日からのため、
知識は相当低いです。そのため、概念の解説に関してはほとんどが他者様の引用であり、コードに関してはOpenCVクレートのドキュメントを見て、ChatGPTにパラメータ調整を手伝ってもらった次第です。ここまで読んでくださりありがとうございました。

また、Rustには Bevy という発展中のゲームエンジンが存在し、視差マッピングに関するサンプルコードがございますので、こちらも合わせて確認すると良いかと思います。

[3D Rendering / Parallax Mapping](https://bevyengine.org/examples/3d-rendering/parallax-mapping/)