Plane Sweeping Stereo 〜 実装編0(テストコードと入出力)

関東に帰ってきてしまいました...

帰ってきて速攻フリーランスの面談があったのですが,やっぱりなかなか緊張しますね.お腹を下す,,,ほどではなかったものの,前日はそわそわしてました.ただ,仲介してくれている営業さんのフォローや,先方の方にも割とフランクに面談をしていただけたこともあって,うまく行ったかな?と思います.あと数件面談が残ってますが,それが終わって,面談結果をもらって,8月からは社会復帰ですね (`・ω・´)シャキーン

8月までの貴重な時間で,沖縄で勉強したことをまとめておければなと思います.時間が経つと見事に忘れてしまうので...ということで,理論編まで書き残していた Plane Sweeping Stereo の実装編です.

0.リポジトリ

本家の下記リポジトリを少しリファクタリングしてるだけです.
github.com

ちょっとリファクタ&ブログで扱うバージョン
github.com

1.ビルド&実行方法

”ちょっとリファクタバージョン”のビルド方法ですが,自分が Ubuntu 16.04 を使っていることもあって,デフォルトでついてくる CMake, Eigen, CUDA とかの組み合わせがちょっと古くうまくコンパイルできなかったので,部分的に新しい物をダウンロードしてきて使ってます.クローンしたリポジトリに "build.sh" ってファイルが入っていると思うのですが,こいつのパスに適切なもの設定して,あとはビルド&実行で動く...はず!

Git Clone
git clone git@github.com:koichiHik/PlaneSweepLib.git
cd PlaneSweepLib

で,build.shのパスを少し変えてあげて...

ビルド

クローンしたフォルダのままで,

sh build.sh
実行

ビルドしたフォルダのままで,

../build/bin/plane_sweep_stereo_test

実行まですんなり行けば,下記の様なコスト計算過程の画像と視差画像が表示されます.

2.実行結果

コスト計算の過程

コストが低い(=画像が暗い)ほど仮想平面との一致度が高いことを意味してます.カメラに近い仮想平面から遠い仮想平面へ順にコスト計算をしてますが,近くから遠くへ向けて一致度が高い(=画像が暗い)部分が移動していくことがわかります.

youtu.be

視差画像

最終的に算出された視差画像です.今回はコスト計算にSADを使っていることと,Occlusion対策をしてないのであんまり綺麗ではないですが,本家のライブラリにはすべて実装されてまして,実行時に選べます.

f:id:rkoichi2001:20190709154453j:plain
Plane Sweeping Stereo の結果

3.実験データ

実行に使っている実験データですが,git clone したフォルダの配下にある "data" というフォルダにも入っています.ピンホールカメラ用のデータとしては3つほどのデータを提供してくれてあるのですが,どのデータフォルダにも下記のファイルが入っています.

0.K.txt

内部パラメータの "fx, アスペクト比, cx, fy, cy " がこの順番で入っています.

1.image.txt

フォルダの中に画像そのものが入ってますが,そのファイルリストがこのファイルになります.このリストの順番はカメラの外部パラメータリストファイル "model-0-cams.txt" と一致してまして,並び順が何気に重要です.

2.model-0-cams.txt

カメラの外部パラメータリスト.各画像間のホモグラフィー行列を計算する必要があるので,各写真を撮影した時の相対位置関係が必要です.このファイルに記載されている内容はどこかのグローバル座標に対するカメラ行列の R と T になります.つまり,,,,

Xcamx = [R | T] Xglobal

です.このRとTがどっちからどっちの変換かわからなくなって,とってもハマったんですよね...

4.実験コード

中身の解剖は次のエントリからにするとして,実験コードのザクっとした内容です.

Step 1.Extract all necessary file paths

実行時の引数に応じて適当なファイルをロードします.異なるデータセットを試したい場合は,下記みたいな感じで指定してもらえば提供してくれているデータセットにたいしては動きました.

../build/bin/plane_sweep_stereo_test --data_folder_path=./data/pinholeCamera/niederdorf2
Step 2.Load necessary files. (K matrix.)

内部パラメータ・外部パラメータの読み込みです.

Step 3.Parameter settings.

ここがちょっとミソになる部分なんですが,ここで最大奥行き・最小奥行きの設定をしています.本家のコードでは読み込んだ外部パラメータから各カメラの平均距離を計算して,下記のように計算してました.

最大奥行き:各カメラの平均距離 X 100.0
最小奥行き:各カメラの平均距離 X 5.0

この「5.0」とか「100.0」っていうのが結構マジックナンバーに思えたんですが,自分が持ってるステレオカメラ(ZED)の基線長が 12cm で,

計測最大奥行き:12 ~ 13m(≒基線長 x 100)
計測最小奥行き:0.5m ~ 1m(≒基線長 x 5 ~ 10)

ということを考えると,大体その程度のレンジの数字になるのかなと思います.

Step 4.Initialize algorithm

ここでパラメータの設定と,画像の Cuda メモリへの転送をしてます.

Step 5.Run algorithm.

アルゴリズムの実行部分で,次回以降のエントリでここをふかぼっていきます.

Step 6.Visualize final result

可視化...

Step 7.Terminate algorithm.

終了処理...

int main(int argc, char **argv) {
  // 0. System setup.
  google::ParseCommandLineFlags(&argc, &argv, true);
  FLAGS_alsologtostderr = 1;
  FLAGS_stderrthreshold = google::GLOG_INFO;
  google::InitGoogleLogging(argv[0]);

  // 1. Extract all necessary file paths.
  std::vector<std::string> k_mat_file, cammat_file, image_file_paths;
  {
    RaiseAllFilesInDirectory(FLAGS_data_folder_path, k_mat_file,
                             std::vector<std::string>{FLAGS_kmat_file_name});
    CHECK_EQ(k_mat_file.size(), 1) << "Need 1 K.txt file.";
    RaiseAllFilesInDirectory(FLAGS_data_folder_path, cammat_file,
                             std::vector<std::string>{FLAGS_cammat_file_name});
    CHECK_EQ(cammat_file.size(), 1) << "Need 1 cammat file";
    RaiseAllFilesInDirectory(FLAGS_data_folder_path, image_file_paths,
                             std::vector<std::string>{".JPG"});
    CHECK_GT(image_file_paths.size(), 1) << "Need at least 2 image files.";
  }

  // 2. Load necessary files. (K matrix.)
  Eigen::Matrix3d K = LoadKMatrixFromFile(k_mat_file[0]);
  std::map<int, vis::CameraMatrix<double>> cammap =
      LoadCameraMatrixFromFile(cammat_file[0], K);

  // 3. Parameter settings.
  // Specify image idxes to be used.
  std::vector<int> image_idx_to_be_used{0, 1, 2};
  vis::mvs::PlaneSweepStereoParams params;
  params.img_scale = 0.25;
  CalculateMinMaxRange(image_idx_to_be_used, cammap, params.min_z,
                       params.max_z);

  // 4. Initialize algorithm
  vis::mvs::PlaneSweepStereo pss;
  pss.Initialize(params);
  for (auto idx : image_idx_to_be_used) {
    cv::Mat img = cv::imread(image_file_paths[idx]);
    CHECK(!img.empty()) << "The image " << image_file_paths[idx]
                        << " does not exist.";
    pss.AddImage(img, cammap[idx]);
  }

  // 5. Run algorithm.
  int ref_img_idx = image_idx_to_be_used[image_idx_to_be_used.size() / 2];
  pss.Run(ref_img_idx);

  // 6. Visualize final result
  cv::Mat depth_map;
  pss.DrawColoredDepthMap(depth_map);
  cv::imshow("Depth Map", depth_map);
  cv::waitKey(0);

  // 7. Terminate algorithm.
  pss.Finalize();

  return 0;
}


ということで,まずはさわりのとこしかかけませんでしたが,次のエントリで中身に入っていきます.