トップ 問題解決の記録

GPU SIFT非互換問題

目的

COLMAPでGPU SIFT(特徴点抽出のアクセラレーション)を使用し、高速かつ高品質な特徴点マッチングを実現する。CUDA対応GPUを搭載しているため、GPU処理でパフォーマンスを最大化することを目指した。

問題

COLMAP 3.9.1のGPU SIFT機能を使用すると、全フレームで特徴点(キーポイント)が1つだけしか抽出されない現象が発生。これによりSfM(Structure from Motion、カメラ位置推定)が実質的に機能しなかった。

発見の経緯

CUDA導入後、最初に直面した問題。経緯は以下のとおり:

  1. CUDA導入: パイプラインでGPUアクセラレーションを使うため、COLMAPが依存するCUDA環境(driver + toolkit)を整備。COLMAPのバイナリには cudart64_12.dll などのCUDAランタイムDLLが同梱されている。
  2. DLL解決問題: colmap.exe を直接ターミナルから実行すると STATUS_DLL_NOT_FOUND で起動せず。原因は cudart64_12.dll のPATH解決不足。Pythonスクリプト側で _env_with_colmap() 関数(run-photogrammetry-split.py:99)がCOLMAPのライブラリディレクトリをPATH先頭に追加することで回避。
  3. GPU SIFT問題の発覚: DLL問題を解決してようやくCOLMAPがGPUモードで起動するようになったが、今度はfeature_extractorの出力を確認すると全フレームのキーポイント数が1のみであることが判明。SfMが事実上機能しない状態だった。

つまり、CUDA導入からこの問題の発覚までに「DLLが見つからない」「動いたと思ったら特徴点が1つだけ」という2段階の障害があった。

SIFT特徴点抽出の役割

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を強制する。

MVS(Multi-View Stereo)との関係

GPU SIFTが使えないことの実用上の影響は限定的。その理由は以下のとおり:

実測値による処理時間の内訳

0154_D(229秒動画、77フレーム/セグメント、CPU SIFT)の実測データから各工程の所要時間を分析すると、CPU SIFTの影響の小ささが明確になる:

工程CPU SIFTでの実測所要時間セグメント処理時間に占める割合
feature_extractor(SIFT)49〜94秒約2%
exhaustive_matcher82〜118秒約3-4%
mapper(SfM)59〜341秒約3-10%
patch_match_stereo(MVS)942〜2,178秒(15-36分)約80-85%
stereo_fusion27〜68秒約1-2%
poisson_mesher21〜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自動選択。

コード上の注意点

feature_extractor の呼び出し

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",
           ])

patch_match_stereo のGPU/CPU切替ロジック

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])

これにより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 での別アプローチ

障害復旧用の recover_segments.py では、動作確実性を最優先してより安全側に倒した設定: