0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WindowsでCOLMAPが動かない人へ:WSL2に逃がしてスマホ動画から3DGSを作る

0
Last updated at Posted at 2026-09-24

3DGS作成ガイド
https://qiita.com/kazuyattu/items/a050491ee450670b34ef
pythonのコードはこちら
https://github.com/kazuyattu-stack/3DGS
pythonコード説明
https://qiita.com/kazuyattu/items/6efc5d2ace09d6bc06ac
colmap説明
https://qiita.com/kazuyattu/items/c6226f3dcc6f7abb5016

はじめに

スマホで撮った動画1本から、ブラウザで回せる3Dシーンを作りたい。
そんなときの定番ワークフローが COLMAP → 3D Gaussian Splatting(3DGS) です。

この記事では、Windows環境で次の4工程を、Pythonスクリプト3本で回せるようにした構成を紹介します。

  1. 動画からフレームを切り出す
  2. COLMAPでカメラ姿勢と点群を推定する
  3. 3DGSを学習させる
  4. WebGLビューア用の .splat に変換する

ポイントは、「Windows側のPythonから、WSL2上のCOLMAP/3DGSを呼び出す」 という二重構造です。

パイプライン全体図

COLMAPそのものの仕組みは、別記事「COLMAP入門:3D Gaussian Splattingの前処理を担うSfM/MVSツール」で解説しています。

環境

  • Windows 11 + WSL2(Ubuntu)
  • NVIDIA GPU(CUDA)
  • COLMAP:WSL2上でCUDA対応ビルド
  • 3D Gaussian Splatting:graphdeco-inria/gaussian-splatting をWSL2上のvenvに構築
  • Windows側Python:OpenCV、numpy、plyfile

なぜWSL経由なのか:この環境ではWindowsネイティブ版COLMAPがうまく動かなかったためです。重い処理はすべてWSL2側に寄せ、Windows側のPythonは「司令塔」に徹する構成にしました。

スクリプト構成と実行順

実行順 スクリプト 内容
1 1_colmap_reconstruct.py フレーム抽出(Python)+ COLMAP(WSL)
2 3_train_3dgs.py 3DGSの学習(WSL)
3 2_ply_to_splat.py PLY → .splat 変換(Python)
- render_previews.py など 記事用の画像を作る補助スクリプト(本体には不要)

ファイル名の番号と実行順が一致していません。.splat 変換は学習結果のPLYを使うので、実際は 1 → 3 → 2 の順で実行します。

Step 1:フレーム抽出とCOLMAP(1_colmap_reconstruct.py)

フレーム抽出(純粋なPython)

COLMAPは動画を直接読めないので、OpenCVで静止画に切り出します。
連続フレームは似すぎていて冗長なので、frame_step 枚おきに間引きます。

import cv2

def extract_frames(video_path, images_dir, frame_step):
    cap = cv2.VideoCapture(video_path)
    idx = 0
    ok, frame = cap.read()
    while ok:
        if idx % frame_step == 0:
            cv2.imwrite(f"{images_dir}/frame_{idx:05d}.jpg", frame)
        ok, frame = cap.read()
        idx += 1
    cap.release()

動画ファイルを直接扱うのはこの関数だけです。以降はすべて「画像フォルダ」に対する処理になります。

WindowsパスをWSLパスに変換する

C:\Users\... を、WSLから見える /mnt/c/Users/... に変換します。
自前で文字列置換するより、WSLの wslpath コマンドに任せるのが確実です。

import subprocess

def to_wsl_path(windows_path):
    result = subprocess.run(
        ["wsl", "wslpath", "-a", windows_path],
        capture_output=True, text=True, check=True,
    )
    return result.stdout.strip()

WSL上のCOLMAPを呼び出す

def run_in_wsl(args):
    subprocess.run(["wsl", "--"] + args, check=True)

これで automatic_reconstructor を実行します。

colmap automatic_reconstructor \
  --workspace_path <workspace> \
  --image_path <workspace>/images \
  --data_type video \
  --quality high \
  --single_camera 1 \
  --sparse 1 --dense 1
オプション 意味
--data_type video 連番フレーム向けに sequential matcher(近傍フレームのみ照合)を使う。全ペア照合より高速
--quality low / medium / high / extreme
--single_camera 1 同じスマホで撮っているので内部パラメータを共有
--dense 1ならMVS(歪み補正〜密点群生成)まで実行

automatic_reconstructor は、特徴点抽出 → マッチング → SfM →(dense=1なら)歪み補正 → 深度推定 → 統合までを一括で実行してくれます。

COLMAPのスパース点群

3DGSの学習に密点群(fused.ply)自体は不要です。ただし、--dense 1 の途中で作られる 歪み補正済みデータセット(dense/ 以下) を次のステップで学習データとして使います。--dense 0 にすると dense/ が作られないので注意してください。

Step 2:3DGSの学習(3_train_3dgs.py)

パス変換はStep 1と同じ to_wsl_path() を再利用します。そのうえで、WSL上のvenvのPythonで公式の train.py を実行します。

~/gaussian-splatting/venv/bin/python train.py \
  -s <colmap_workspace>/dense \
  -m <model_output_dir> \
  --iterations 30000 \
  --eval

-s には、COLMAPが作った dense/(images/ と sparse/0/*.bin)をそのまま渡します。

train.py は自作ではなく、公式リポジトリのコードです。PyTorch + CUDAで各ガウシアンの位置・スケール・回転・不透明度・球面調和関数を最適化し、point_cloud/iteration_N/point_cloud.ply として保存します。

Step 3:PLYを.splatに変換(2_ply_to_splat.py)

3DGSの出力PLYは球面調和関数の係数まで含むため重く、WebGLビューアでは扱いにくいです。
そこで、1ガウシアンあたり 32バイト固定長 の .splat 形式に詰め直します。COLMAPも3DGSも呼ばない、numpyだけの処理です。

要素 変換 型 サイズ
position そのまま float32 × 3 12 byte
scale exp() float32 × 3 12 byte
color + opacity SH 0次項からRGB、opacityは sigmoid()、0–255に量子化 uint8 × 4 4 byte
rotation クォータニオンを正規化して0–255に量子化 uint8 × 4 4 byte
import numpy as np
from plyfile import PlyData

SH_C0 = 0.28209479177387814

def ply_to_splat(ply_path, out_path):
    v = PlyData.read(ply_path)["vertex"]

    pos = np.stack([v["x"], v["y"], v["z"]], axis=1).astype(np.float32)
    scales = np.exp(np.stack(
        [v["scale_0"], v["scale_1"], v["scale_2"]], axis=1)).astype(np.float32)

    f_dc = np.stack([v["f_dc_0"], v["f_dc_1"], v["f_dc_2"]], axis=1)
    rgb = 0.5 + SH_C0 * f_dc
    opacity = 1 / (1 + np.exp(-v["opacity"]))
    color = (np.clip(np.column_stack([rgb, opacity]), 0, 1) * 255).astype(np.uint8)

    rot = np.stack([v["rot_0"], v["rot_1"], v["rot_2"], v["rot_3"]], axis=1)
    rot /= np.linalg.norm(rot, axis=1, keepdims=True)
    rot = np.clip(rot * 128 + 128, 0, 255).astype(np.uint8)

    # 大きくて不透明なガウシアンを先に並べる
    order = np.argsort(-(scales.prod(axis=1) * opacity))

    with open(out_path, "wb") as f:
        for i in order:
            f.write(pos[i].tobytes())
            f.write(scales[i].tobytes())
            f.write(color[i].tobytes())
            f.write(rot[i].tobytes())

最後のソートは、WebGLビューア側の描画順の都合で見た目が破綻するのを防ぐためのものです。

補助スクリプト(記事用)

以下はパイプライン本体には関係なく、記事用の画像を作るためだけのスクリプトです。

  • render_previews.py:Open3Dでスパース点群をオフスクリーンレンダリングし、PNGで保存する
  • crop_preview.py:レンダリング画像の余白をピクセル値から自動検出してクロップする(PIL / numpy)
  • make_pipeline_diagram.py:matplotlibでパイプライン全体図を描画する

役割分担まとめ

Pythonが担当 COLMAP / 3DGS(WSL上の外部コマンド)が担当
動画からのフレーム抽出(OpenCV) 特徴点抽出・マッチング・SfM(COLMAP)
Windows ⇔ WSL のパス変換 歪み補正・密なステレオ復元(COLMAP)
subprocessでの外部コマンド呼び出し ガウシアンの学習(公式 train.py、PyTorch + CUDA)
PLY → .splat 変換(numpy)

Python側は「データの出し入れと指揮」、重い計算はCUDA対応のWSL側、ときれいに分けられるのがこの構成の良いところです。

おわりに

Windowsネイティブで環境構築に詰まった場合でも、WSL2に重い処理を寄せて subprocess.run(["wsl", ...]) で呼べば、Windows側の使い勝手を保ったままパイプラインを組めます。
同じように3DGSを試したい方の参考になれば幸いです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?