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

写真→実寸点群:理論と完全自動化パイプライン

1. SfM の概要と出力の性質

本文章は執筆途中のものであり、コード類の完全保証はできません。参考程度にお読みください。
SfM(Structure from Motion)
は複数枚の写真から特徴点を抽出し、カメラ位置・姿勢推定 + 三角測量で 3D 点群を復元する手法です。 主な OSS / ツール:OpenMVG, COLMAP, OpenSfM, Regard3D など。

  • 出力される点群は 相対形状のみ を保持し、絶対スケール(実寸)は不明
  • 現実の「1 m」が 1.0 とは限らず、任意スケール で出力される

2. スケール(実寸)付与の基本ロジック

2.1 1 ペアによるスケール復元

  1. 点群内で現実の距離が分かる 2 点(A,B)を選ぶ
  2. 点群上の距離 d_sfm = ‖A − B‖ を計算
  3. 現実距離 d_real を測定
  4. scale = d_real / d_sfm を算出
  5. 点群全頂点に 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 + CUDADocker/BashGPU ヘッドレス
スケール付与Python + Open3D関数化前章ロジック
制御・監視PythonCLIリトライ & ログ

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.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA