COLMAPでGPU SIFT(特徴点抽出のアクセラレーション)を使用し、高速かつ高品質な特徴点マッチングを実現する。CUDA対応GPUを搭載しているため、GPU処理でパフォーマンスを最大化することを目指した。
COLMAP 3.9.1のGPU SIFT機能を使用すると、全フレームで特徴点(キーポイント)が1つだけしか抽出されない現象が発生。これによりSfM(Structure from Motion、カメラ位置推定)が実質的に機能しなかった。
CUDA導入後、最初に直面した問題。経緯は以下のとおり:
cudart64_12.dll などのCUDAランタイムDLLが同梱されている。colmap.exe を直接ターミナルから実行すると STATUS_DLL_NOT_FOUND で起動せず。原因は cudart64_12.dll のPATH解決不足。Pythonスクリプト側で _env_with_colmap() 関数(run-photogrammetry-split.py:99)がCOLMAPのライブラリディレクトリをPATH先頭に追加することで回避。つまり、CUDA導入からこの問題の発覚までに「DLLが見つからない」「動いたと思ったら特徴点が1つだけ」という2段階の障害があった。
SIFT(Scale-Invariant Feature Transform)は画像間の対応点を見つけるアルゴリズムで、SfMパイプラインの最初の工程。各フレームから数百〜数千の特徴点(キーポイント+ディスクリプタ)を抽出し、これをもとに画像間のマッチングを行う。特徴点が1つだけではマッチングが成立せず、カメラ位置の推定が不可能になる。
CUDA 13.2ドライバとCOLMAPがコンパイルされたCUDA toolkitのバージョン不互換。COLMAP 3.9.1のリリース時期とマシンにインストールされているCUDAドライバのバージョンが乖離しており、GPU上のSIFTカーネルが正常に動作しなかった。CUDAの後方互換性の問題によりカーネル実行時に異常終了せず、結果として「1特徴点のみ」という一見正常に見える異常出力となる。
以下のコマンドで特徴点数を確認可能:
colmap feature_extractor --database_path segment.db --image_path ./frames --SiftExtraction.use_gpu 1 colmap database_printer --database_path segment.db
database_printer の出力で各画像のキーポイント数を確認できる。正常時は2048〜8192程度、異常時は1のみ。
COLMAPのfeature_extractor呼び出し時に --SiftExtraction.use_gpu 0 を指定し、CPU SIFTに強制切り替え。このオプションは --no-gpu フラグとは独立して常にCPUを強制する。
GPU SIFTが使えないことの実用上の影響は限定的。その理由は以下のとおり:
patch_match_stereo(密対応点探索)が数十分〜数時間と圧倒的に時間を要する。SIFT抽出(数十秒〜数分)がCPUになっても全体に占める割合は小さい。patch_match_stereo はGPU/CUDAで動作し、高速処理を維持。VRAM使用量が3500MBを超えた場合のみCPUに自動フォールバック。0154_D(229秒動画、77フレーム/セグメント、CPU SIFT)の実測データから各工程の所要時間を分析すると、CPU SIFTの影響の小ささが明確になる:
| 工程 | CPU SIFTでの実測所要時間 | セグメント処理時間に占める割合 |
|---|---|---|
| feature_extractor(SIFT) | 49〜94秒 | 約2% |
| exhaustive_matcher | 82〜118秒 | 約3-4% |
| mapper(SfM) | 59〜341秒 | 約3-10% |
| patch_match_stereo(MVS) | 942〜2,178秒(15-36分) | 約80-85% |
| stereo_fusion | 27〜68秒 | 約1-2% |
| poisson_mesher | 21〜80秒 | 約1-2% |
CPU SIFTは全セグメント処理時間の約2% に過ぎない。仮にGPU SIFTが正常動作してこの工程が10倍高速化されたとしても、全体の処理時間は高々 約1.8%の短縮(約3〜8秒/セグメント)にとどまる。一方、patch_match_stereoが全体の80%超を占めるため、こちらでGPUが使えていることこそがパイプラインの実用性を支えている。
恒久対策済み — スクリプト内で SiftExtraction.use_gpu="0" をデフォルト固定。MVSは状況に応じてGPU/CPU自動選択。
run-photogrammetry-split.py L287-299 では常にCPU SIFTを強制する設定になっている:
run_colmap(["feature_extractor",
"--database_path", str(seg_db),
"--image_path", str(seg_fd),
"--ImageReader.camera_model", "SIMPLE_RADIAL",
"--ImageReader.single_camera", "1",
"--SiftExtraction.max_num_features",
str(QualityMaxFeatures.get(quality, 2048)),
"--SiftExtraction.use_gpu", "0",
])
dense_from_model() 関数(L356-380)ではGPUインデックスを動的に切り替えている:
def dense_from_model(sparse_model, image_path, dense_dir, use_gpu):
gpu = "0" if use_gpu else "-1"
if use_gpu:
vram = get_gpu_memory_mb()
if vram >= VRAM_LIMIT_MB:
gpu = "-1"
else:
log(f"VRAM: {vram}MB (limit {VRAM_LIMIT_MB}MB)")
run_colmap(["patch_match_stereo",
"--workspace_path", str(dense_dir),
"--PatchMatchStereo.gpu_index", gpu])
get_gpu_memory_mb() で nvidia-smi から使用中VRAMを取得(memory.used)gpu_index = "-1")gpu_index = "0")で動作これによりSIFT抽出のみCPU、MVSは可能な限りGPUという分割が実現されている。
上記コード内で gpu = "0"(GPU使用)と gpu = "-1"(CPU使用)が生の文字列リテラルとして書かれている。これは --PatchMatchStereo.gpu_index がCOLMAPのCLI仕様として数値の文字列を受け取るためだが、コードを読んだだけでは "0" がGPU、"-1" がCPUを意味することが一意に把握しづらい。改善案として、定数化が考えられる:
# 現在(マジックナンバー) gpu = "0" if use_gpu else "-1" # 改善案(定数化) GPU_INDEX_GPU = "0" GPU_INDEX_CPU = "-1" gpu = GPU_INDEX_GPU if use_gpu else GPU_INDEX_CPU
同様に SiftExtraction.use_gpu も "0" / "1" のマジックナンバーだが、こちらはパラメータ名自体が use_gpu と意味を表しているため、混乱は少ない。
障害復旧用の recover_segments.py では、動作確実性を最優先してより安全側に倒した設定:
--SiftExtraction.use_gpu "0" → CPU強制(本対策と同様)--SiftExtraction.num_threads "4" → CPUスレッド数明示指定--PatchMatchStereo.gpu_index "-1" → MVSもCPU強制--PatchMatchStereo.geom_consistency "false" → photometricモード固定--PatchMatchStereo.max_image_size "2000" → 画像サイズ制限