写真フォルダ → 実寸点群まで― 理論解説と完全自動化パイプライン ―

1. SfM の概要と出力の性質
本文章は執筆途中のものであり、コード類の完全保証はできません。参考程度にお読みください。
SfM(Structure from Motion) は複数枚の写真から特徴点を抽出し、カメラ位置・姿勢推定 + 三角測量で 3D 点群を復元する手法です。
主な OSS / ツール:OpenMVG, COLMAP, OpenSfM, Regard3D など。
- 出力される点群は 相対形状のみ を保持し、絶対スケール(実寸)は不明
- 現実の「1 m」が
1.0
とは限らず、任意スケール で出力される
2. スケール(実寸)付与の基本ロジック
2.1 1 ペアによるスケール復元
- 点群内で現実の距離が分かる 2 点(A,B)を選ぶ
- 点群上の距離
d_sfm = ‖A − B‖
を計算 - 現実距離
d_real
を測定 scale = d_real / d_sfm
を算出- 点群全頂点に
scale
倍を掛ける → 実寸点群 完成
2.2 流れの概念図
┌── 写真群 ──▶ SfM ─▶ 相対点群
│
│(基準マーカーの実距離 d_real)
│
▼
基準点 A,B の座標取得 ──▶ 距離 d_sfm 計算
│
▼
scale = d_real / d_sfm
│
▼
点群全体を scale 倍 ──▶ 実寸点群(scaled.ply)
3. なぜ実寸が必要か?
- BIM/CAD 連携: 柱間距離・壁厚など mm~cm 精度で必要
- 施工・数量計算: 体積・面積が狂うとコスト算出が破綻
- インフラ点検: 亀裂幅・沈下量を実長で追跡
- リバースエンジニアリング: 部品寸法そのものが成果物
スケール未付与点群は “鑑賞物”。道具にするなら必ず実寸化。
4. スケールを失うメカニズム
写真データだけではカメラ‐被写体間の本当の距離が分からず、 たとえ焦点距離・センササイズが分かっても“倍率” は一意に決まらないためです。
解決策:撮影時に基準マーカーを写し、後処理でスケール倍率を掛ける。
5. スケール付与だけを行う最小 Python サンプル
scale_pointcloud.py
import numpy as np
def load_ply_points(ply_path):
with open(ply_path) as f:
lines = f.readlines()
end = [i for i,l in enumerate(lines) if l.strip()=="end_header"][0]
pts = [list(map(float,l.split()[:3])) for l in lines[end+1:] if l.strip()]
return np.array(pts)
def save_ply_points(pts, out_path):
with open(out_path,"w") as f:
f.write("ply\nformat ascii 1.0\n")
f.write(f"element vertex {len(pts)}\n")
f.write("property float x\nproperty float y\nproperty float z\nend_header\n")
for p in pts: f.write(f"{p[0]} {p[1]} {p[2]}\n")
def scale_pointcloud(ply_in, ply_out, idxA, idxB, real_d):
pts = load_ply_points(ply_in)
scale = real_d / np.linalg.norm(pts[idxA] - pts[idxB])
save_ply_points(pts*scale, ply_out)
print(f"→ wrote {ply_out} (scale={scale:.6f})")
# 例: scale_pointcloud("raw.ply","scaled.ply",10,20,1.50)
6. 完全自動化パイプライン構成
段階 | ツール | 実装 | 備考 |
---|---|---|---|
特徴抽出〜密点群 | COLMAP CLI + CUDA | Docker/Bash | GPU ヘッドレス |
スケール付与 | Python + Open3D | 関数化 | 前章ロジック |
制御・監視 | Python | CLI | リトライ & ログ |
7. Docker イメージ
docker/Dockerfile
FROM nvidia/cuda:12.3.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y colmap python3 python3-pip \
&& pip install open3d numpy tqdm
WORKDIR /workspace
ENTRYPOINT ["/bin/bash"]
8. 写真→実寸点群 ワンコマンドスクリプト
run_sfm.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python run_sfm.py <image_dir> <marker_distance_m>
→ scaled.ply を出力
"""
import subprocess, tempfile, shutil, sys, time
from pathlib import Path
import numpy as np, open3d as o3d
IMGDIR, REAL = Path(sys.argv[1]), float(sys.argv[2])
PT = (0,1); RETRY = 3
def sh(cmd,c=None): print(">>"," ".join(map(str,cmd))); subprocess.check_call(cmd,cwd=c)
def colmap(img,work):
db=work/"db.db"; sp=work/"sparse"; de=work/"dense"
sh(["colmap","feature_extractor","--database_path",db,"--image_path",img,"--SiftExtraction.gpu_index","0"])
sh(["colmap","exhaustive_matcher","--database_path",db,"--SiftMatching.gpu_index","0"])
sp.mkdir(); sh(["colmap","mapper","--database_path",db,"--image_path",img,"--output_path",sp])
model=next(sp.iterdir())
sh(["colmap","image_undistorter","--image_path",img,"--input_path",model,"--output_path",de,"--output_type","COLMAP"])
sh(["colmap","patch_match_stereo","--workspace_path",de,"--workspace_format","COLMAP"])
sh(["colmap","stereo_fusion","--workspace_path",de,"--workspace_format","COLMAP",
"--input_type","geometric","--output_path",de/"fused.ply"])
return de/"fused.ply"
def scale(ply_in,ply_out,idxA,idxB,real):
pc=o3d.io.read_point_cloud(str(ply_in)); pts=np.asarray(pc.points)
s=real/np.linalg.norm(pts[idxA]-pts[idxB]); pc.scale(s,center=(0,0,0))
o3d.io.write_point_cloud(str(ply_out),pc); print(f"[OK] {ply_out} scale={s:.6f}")
def main():
work=Path(tempfile.mkdtemp(prefix="sfm_")); print("TMP:",work)
for i in range(1,RETRY+1):
try: fused=colmap(IMGDIR,work); break
except subprocess.CalledProcessError: print("[WARN] retry",i); time.sleep(5)
scale(fused,Path("scaled.ply"),PT[0],PT[1],REAL); shutil.rmtree(work)
if __name__=="__main__":
if len(sys.argv)<3: sys.exit("Usage: run_sfm.py <img_dir> <dist_m>")
main()
9. 実行例(Docker コンテナ)
docker run --gpus all -it \
-v /abs/path/to/images:/imgs \
-v $(pwd):/out colmap-auto \
python3 /workspace/run_sfm.py /imgs 1.50
# → /out/scaled.ply が生成
10. 拡張アイデア & ボトルネック対策
- フォルダ監視:
watchdog
で自動投入/Slack 通知 - マーカー自動検出:色閾値 or AprilTag →
PT
自動決定 - SIFT 抽出は
--SiftExtraction.num_threads=$(nproc)
- 大規模案件は
vocab_tree_matcher
でマッチング高速化 - PatchMatch 画像サイズを GPU VRAM に合わせて 3k~4k
- Docker 共有メモリ不足は
--shm-size 8G
11. まとめ
- 絶対スケールは SfM 処理で必ず失われる—基準マーカー と 一括倍率 で解決
- Python 数行でスケール付与、Docker+COLMAP で完全自動化
- API 化・常駐サービス化で “現場から写真を投げるだけ” 運用へ
写真 → 実寸点群 の自動化は「やるか・やらないか」だけ。
(C) 2025 Bee-Knowledge Design — No-nonsense, copy-ready.