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本で回せるようにした構成を紹介します。
- 動画からフレームを切り出す
- COLMAPでカメラ姿勢と点群を推定する
- 3DGSを学習させる
- 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なら)歪み補正 → 深度推定 → 統合までを一括で実行してくれます。
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を試したい方の参考になれば幸いです。
参考
- COLMAP 公式: https://colmap.github.io/
- COLMAP GitHub: https://github.com/colmap/colmap
- 3D Gaussian Splatting 公式実装: https://github.com/graphdeco-inria/gaussian-splatting

