Single-image 6DoF Visual Positioning System on top of COLMAP / hloc.
1 枚の画像からカメラ姿勢を推定する、CLI / Python / ROS2 兼用の VPS
Each frame: a single query image (left) and the camera pose recovered against a COLMAP map (right, green = localized camera).
South-Building dataset, 118 db / 10 query images, DISK + LightGlue + NetVLAD.
| Success | Median rot. err | Median trans. err | |
|---|---|---|---|
| south-building 118 db / 10 query, single capture day |
10 / 10 | 0.066° | 0.034 % of scene |
| Cambridge ShopFacade 231 db / 103 query, multi-day, w/ pedestrians |
103 / 103 | 0.61° | 0.23 % of scene |
| Cambridge Old Hospital 895 db / 182 query, multi-day, larger building |
182 / 182 | 1.11° | 1.37 % of scene |
COLMAP で構築した SfM 地図に対して、1 枚の query 画像から 6DoF カメラ姿勢を推定する Visual Positioning System (VPS) 実装です。 内部では hloc (Hierarchical-Localization) の Retrieval / Matching パイプラインを薄くラップしつつ、CLI と クリーンな Python API、そして ROS2 ノードを同梱しています。
flowchart LR
Q([query image]) --> R[NetVLAD<br/>top-K retrieval]
R --> M[DISK / SuperPoint<br/>+ LightGlue matching]
M --> C[2D-3D 対応構築]
C --> P[PnP + RANSAC<br/>pycolmap → OpenCV fallback]
P --> O([6DoF pose<br/>R, t, qvec])
style Q fill:#fef3c7,stroke:#f59e0b,color:#000
style O fill:#d1fae5,stroke:#10b981,color:#000
style P fill:#dbeafe,stroke:#3b82f6,color:#000
- ハイライト
- Quick Start
- インストール
- 使い方 (CLI)
- Python API
- レイテンシの傾向
- 公開データセット検証 (south-building)
- 公開データセット検証 (Cambridge ShopFacade)
- 公開データセット検証 (Cambridge Old Hospital)
- ROS2 統合
- 制約 / 注意
- 開発
- ライセンス
| CLI ファースト | visual-map-localizer build-map と ... localize だけで完結 |
| Python API | VisualMapLocalizer().localize(path or np.ndarray) で常駐サーバ向けに使い回し可 |
| ROS2 ノード同梱 | /camera/image_raw → /vps_pose (PoseWithCovarianceStamped) |
| GPU / CPU 両対応 | CUDA があれば実用的な速度、CPU でも動く |
| JSON 出力 | pose・inlier 数・reprojection error・retrieval Top-K |
| PnP 二段構え | pycolmap が第 1 選択 / OpenCV solvePnPRansac が fallback |
| Apache-2.0 構成 | DISK + LightGlue + NetVLAD なら third-party submodule 不要 |
| 軽量 install | lazy import で torch / hloc 抜きでも import / unit-test が通る |
south-building (COLMAP 公式の 128 枚デモデータセット) で end-to-end が動くまでを 1 つのブロックに。 1 枚を held-out query にして残り 127 枚で build-map することで、ちゃんと "未知画像を localize する" 流れになります。
# 0) インストール (deep extras + hloc は localize / build-map に必要)
pip install -e ".[deep]"
pip install git+https://github.com/cvg/Hierarchical-Localization.git@master
# 1) データ取得
mkdir -p /tmp/vml-public && cd /tmp/vml-public
curl -L -o south-building.zip \
https://github.com/colmap/colmap/releases/download/3.11.1/south-building.zip
unzip -q south-building.zip
# 2) 1 枚を held-out query に分離 (= 残り 127 枚を DB として使う)
mkdir -p /tmp/vml-public/db_images
cp /tmp/vml-public/south-building/images/*.JPG /tmp/vml-public/db_images/
mv /tmp/vml-public/db_images/P1180141.JPG /tmp/vml-public/query.JPG
# 3) マップ構築 (DISK + LightGlue, retrieval-based pairs)
visual-map-localizer build-map \
--images /tmp/vml-public/db_images \
--output /tmp/vml-public/map \
--local-feature disk --matcher disk+lightglue \
--num-covisible-pairs 20
# 4) held-out query を localize (south-building の intrinsics をそのまま指定)
visual-map-localizer localize \
--map /tmp/vml-public/map \
--query /tmp/vml-public/query.JPG \
--camera-model SIMPLE_RADIAL \
--camera-params 2559.68,1536,1152,-0.0204997 \
--image-size 3072x2304TL;DR: 出力 JSON に
success: trueと 1000 を超えるinliers、reproj_error < 3pxが出ていれば成功です。 より厳密な精度評価 (10 query × Sim(3) 整列 + reference SfM 比較) は 公開データセット検証 を参照。
torch / hloc を入れずに済むので CI / ROS bridge / 解析スクリプトに最適。
visual_map_localizer は PEP 562 lazy import で深層学習依存を遅延ロード
するため、Retrieval / Matching を呼ばない限りこの構成でも動きます。
pip install -e .# 1) PyTorch を CUDA に合わせて (例: CUDA 12.1)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
# 2) 本パッケージ + deep extras
pip install -e ".[deep]"
# 3) hloc (PyPI 未公開なので git から)
pip install git+https://github.com/cvg/Hierarchical-Localization.git@master
# 4) 動作確認
python -c "import pycolmap; print(pycolmap.__version__)"
python -c "from hloc import extract_features; print(list(extract_features.confs)[:5])"# 既定 (SuperPoint + LightGlue) — SuperGluePretrainedNetwork submodule が必要
visual-map-localizer build-map \
--images ./images \
--output ./map
# Apache-2.0 構成 (third-party submodule 不要、商用利用可)
visual-map-localizer build-map \
--images ./images \
--output ./map \
--local-feature disk \
--matcher disk+lightglue補足: 既定の SuperPoint は hloc が
third_party/SuperGluePretrainedNetworkサブモジュールから読み込みます。pip 経由で hloc を入れた場合はサブモジュールが 含まれないため、上記の DISK + LightGlue 構成のほうが確実に動きます。
主なオプション:
| Option | 既定 | 説明 |
|---|---|---|
--local-feature |
superpoint_aachen |
hloc の local feature config 名 |
--global-descriptor |
netvlad |
hloc の global descriptor config 名 |
--matcher |
superpoint+lightglue |
hloc の matcher config 名 |
--num-covisible-pairs |
None (exhaustive) |
retrieval-based pairs (大規模向け) |
--overwrite |
off | 中間ファイルを再生成 |
出力ディレクトリ構造:
map/
├── sfm/ # COLMAP sparse model
├── features.h5 # SuperPoint / DISK
├── global_descriptors.h5 # NetVLAD
├── pairs-sfm.txt
├── matches-sfm.h5
├── db_images.txt
└── map_meta.json
visual-map-localizer localize \
--map ./map \
--query query.jpglocalize 側は
map_meta.jsonを読んで build-map 時と同じlocal-feature/global-descriptor/matcherを自動採用します。 明示指定したい場合のみ CLI オプションで上書きしてください。
主なオプション:
| Option | 既定 | 説明 |
|---|---|---|
--top-k |
10 | retrieval 候補数 |
--ransac-max-error |
12.0 | RANSAC reprojection threshold (px) |
--min-inliers |
12 | これ未満なら success=false |
--matcher |
superpoint+lightglue |
localize 時の matcher |
--camera-model |
(推定) | 例: PINHOLE (params も必須) |
--camera-params |
(推定) | カンマ区切り (例: fx,fy,cx,cy) |
--image-size |
(画像から取得) | --camera-model 指定時に必要 |
--output |
stdout | JSON を書き出すパス |
成功時の JSON:
{
"success": true,
"pose": {
"R": [[0.999, 0.034, 0.012], [-0.034, 0.999, 0.005], [-0.012, -0.005, 1.000]],
"t": [1.234, -0.567, 5.890],
"qvec": [0.9999, 0.017, 0.006, 0.001]
},
"inliers": 128,
"reproj_error": 0.91,
"num_matches": 612,
"retrieval": ["db/0001.jpg", "db/0007.jpg", "db/0042.jpg"],
"query": "/abs/path/query.jpg",
"timing": {"total": 1.42, "pnp": 0.03, "matching": 0.21}
}失敗時:
{
"success": false,
"error": "PnP failed",
"retrieval": ["db/0001.jpg", "..."],
"num_matches": 4,
"query": "/abs/path/query.jpg",
"timing": {"total": 0.97}
}CLI の終了コード:
| Code | 意味 |
|---|---|
0 |
成功 |
2 |
ローカライズ失敗 (画像 / マップは正常に読めた) |
1 |
引数 / IO エラー |
visual-map-localizer inspect --map ./mapfrom visual_map_localizer import VisualMapLocalizer
from visual_map_localizer.config import LocalizeConfig
localizer = VisualMapLocalizer(
"./map",
config=LocalizeConfig(top_k=15, ransac_max_error_px=8.0),
)
result = localizer.localize("query.jpg")
print(result.success, result.inliers)
print(result.to_json())import numpy as np
from PIL import Image
import pycolmap
from visual_map_localizer import VisualMapLocalizer
from visual_map_localizer.config import LocalizeConfig
localizer = VisualMapLocalizer("./map", config=LocalizeConfig(top_k=10))
# どこかのストリーム / cv_bridge / カメラ SDK から:
rgb = np.asarray(Image.open("query.jpg").convert("RGB")) # H×W×3 uint8 (RGB)
camera = pycolmap.Camera(
model="PINHOLE", width=rgb.shape[1], height=rgb.shape[0],
params=[fx, fy, cx, cy],
)
result = localizer.localize(rgb, camera=camera, name="frame_0123.png")
print(result.inliers, result.pose["t"])localize() は Path / str / np.ndarray のいずれも受け取ります。
ndarray の場合は camera 必須(EXIF が無いため)。
name は省略可能で、内部キャッシュキーになります。
from visual_map_localizer.mapping import build_map
from visual_map_localizer.config import MappingConfig
build_map(
image_dir="./images",
output_dir="./map",
config=MappingConfig(num_covisible_pairs=20),
)絶対値はハードウェアと画像サイズに大きく依存するので数値は出しません。
代わりに 設計上の傾向 を整理しておきます (詳細プロファイルは
scripts/profile_localize.py で測れます):
localizeを 毎回 subprocess で起動するよりも、VisualMapLocalizerインスタンスを使い回す ほうが圧倒的に速い (warm-up の NetVLAD / DISK / LightGlue 初期化を 1 回で済ませられるため)。- ndarray を渡す経路は path を渡す経路よりも僅かに遅い (PNG エンコードのコスト)。 ROS の 1280×720 クラスではこの差はほぼ無視できる。
- query の解像度を半分にすると DISK / LightGlue が概ね線形に高速化する。
COLMAP 公式の south-building データセット (Schönberger 氏配布、reference SfM 同梱) で
end-to-end 検証した結果です。128 枚を 118 db / 10 query に分割し、118 枚で
build-map → 10 枚を順次 localize → 同梱 reference SfM との pose 誤差を
Sim(3) 整列で評価:
| 指標 | 値 |
|---|---|
| 成功率 | 10 / 10 |
| inlier 数 | 2616〜5229 |
| reprojection error | 1.25〜2.99 px |
| 回転誤差 | median 0.066°, mean 0.074°, max 0.174° |
| 並進誤差 | median 0.0034, max 0.0046 (シーン全幅 10.81、つまり 0.03〜0.04 %) |
| 自前 SfM と reference SfM の整列残差 | mean 0.0035 (= 0.03 % of scene scale) |
つまり、localizer が出す pose は SfM 自身の数値ノイズと同オーダー で reference SfM と一致しています。
再現手順 (クリックで展開)
# 1) データ取得
mkdir -p /tmp/vml-public && cd /tmp/vml-public
curl -L -o south-building.zip \
https://github.com/colmap/colmap/releases/download/3.11.1/south-building.zip
unzip -q south-building.zip
# 2) 118 / 10 に分割 (seed 固定)
python3 - <<'PY'
import random, shutil
from pathlib import Path
src = Path('/tmp/vml-public/south-building/images')
imgs = sorted(p.name for p in src.iterdir() if p.suffix.lower() == '.jpg')
qs = sorted(random.Random(42).sample(imgs, 10))
db = [n for n in imgs if n not in qs]
Path('/tmp/vml-public/db_images').mkdir(exist_ok=True)
Path('/tmp/vml-public/query_images').mkdir(exist_ok=True)
for n in db: shutil.copy(src/n, Path('/tmp/vml-public/db_images')/n)
for n in qs: shutil.copy(src/n, Path('/tmp/vml-public/query_images')/n)
PY
# 3) build-map (118 imgs, retrieval-based pairs)
visual-map-localizer build-map \
--images /tmp/vml-public/db_images \
--output /tmp/vml-public/map \
--local-feature disk --matcher disk+lightglue \
--num-covisible-pairs 20
# 4) 10 queries を localize
mkdir -p /tmp/vml-public/results
for q in /tmp/vml-public/query_images/*.JPG; do
visual-map-localizer localize \
--map /tmp/vml-public/map --query "$q" \
--camera-model SIMPLE_RADIAL \
--camera-params 2559.68,1536,1152,-0.0204997 \
--image-size 3072x2304 \
--output /tmp/vml-public/results/$(basename "$q" .JPG).json
done
# 5) reference SfM と比較
python3 scripts/evaluate_south_building.py \
--dataset /tmp/vml-public/south-building \
--map-dir /tmp/vml-public/map \
--results-dir /tmp/vml-public/resultssouth-building は単一日のキャプチャなので "条件が一定で簡単" な部類です。 そこでもう 1 段難しい標準ベンチマーク Cambridge Landmarks ShopFacade (屋外の通り、撮影日が異なる、人や車の写り込みあり) でも検証しました。 公式 train/test split (231 db / 103 test) をそのまま使い、 DISK + LightGlue + NetVLAD で 1 度だけ実行した結果:
整列方法: 自前 SfM 座標系から NVM 座標系への Sim(3) を求める際、 自前 SfM 側で稀に大きく誤推定される train 画像 (ShopFacade で 2 枚 / 231) が Least-Squares 整列を引っ張る現象があったため、
evaluate_cambridge.pyでは IRLS robust Sim(3) を既定にしました (整列残差が中央値の 5 倍を超える train 画像を 反復で除外)。--no-robust-sim3で従来の LS 挙動に戻せます。
| 指標 | 値 |
|---|---|
| 成功率 | 103 / 103 |
| inlier 数 (median) | ≈ 5000 |
| reprojection error | 〜5 px |
| 回転誤差 | median 0.61°, mean 0.71°, max 2.88° |
| 並進誤差 | median 0.096 m, max 2.03 m (シーン全幅 42.7 m、つまり 0.23 % / 4.76 %) |
| Sim(3) 整列に使った train 画像 | 229 / 231 (IRLS で 2 枚を SfM 整列の outlier として除外) |
south-building (median 0.066° / 0.034%) より 1 桁悪化していますが、 照度変化・歩行者・車両・経年変化を含む屋外データに対して 全 103 枚 が成功し、 median 並進誤差がシーン全幅の 0.23 % に収まっているのは、 このパイプラインが "easy デモ" 専用ではなく VPS ベンチの本流で動くことを示しています (SfM ベース手法 Active Search の 0.12 m / 0.4° と同オーダー)。
再現手順 (クリックで展開)
# 1) データ取得 (~1.4 GB)
mkdir -p /tmp/vml-public/cambridge && cd /tmp/vml-public/cambridge
curl -L -o ShopFacade.zip \
"https://api.repository.cam.ac.uk/server/api/core/bitstreams/4e5c67dd-9497-4a1d-add4-fd0e00bcb8cb/content"
unzip -q ShopFacade.zip
# 2) train/test を flat な symlink に並べ、intrinsics.json を書き出す
python3 scripts/evaluate_cambridge.py prepare \
--scene /tmp/vml-public/cambridge/ShopFacade \
--work /tmp/vml-public/cambridge/work
# 3) 231 train 画像で build-map
visual-map-localizer build-map \
--images /tmp/vml-public/cambridge/work/train_images \
--output /tmp/vml-public/cambridge/work/map \
--local-feature disk --matcher disk+lightglue \
--num-covisible-pairs 20
# 4) 103 test 画像を一気に localize (in-process API)
python3 scripts/evaluate_cambridge.py localize-all \
--scene /tmp/vml-public/cambridge/ShopFacade \
--work /tmp/vml-public/cambridge/work
# 5) Sim(3) 整列 + 回転 / 並進誤差を集計
python3 scripts/evaluate_cambridge.py score \
--scene /tmp/vml-public/cambridge/ShopFacade \
--work /tmp/vml-public/cambridge/workShopFacade はキャンパス内通りの 1 ファサードでしたが、もう 1 段スケールが大きい
Cambridge Old Hospital (病院ファサード、895 train / 182 test、シーン全幅 ~62 m)
でも同じパイプラインで end-to-end 検証しました。evaluate_cambridge.py は scene-agnostic
なので --scene を差し替えるだけです。
| 指標 | 値 |
|---|---|
| 成功率 | 182 / 182 |
| 回転誤差 | median 1.11°, mean 1.19°, max 3.21° |
| 並進誤差 | median 0.85 m, max 2.40 m (シーン全幅 62.30 m、つまり 1.37 % / 3.85 %) |
| Sim(3) 整列に使った train 画像 | 882 / 895 (IRLS で 13 枚を SfM 整列の outlier として除外) |
ShopFacade (median 0.23 %) より約 6 倍悪化していますが、これは想定内です: シーンが約 1.5 倍広く、撮影距離も伸び、視点間の overlap も小さくなるため、 SfM ベース手法の絶対誤差は素直にスケールします。それでも 全 182 枚 が成功し、 median 並進誤差がシーン全幅の 1.4 % 程度に収まっているので、 PoseNet 系 (~5 % @ Old Hospital) よりは明らかに良く、 Active Search / DSAC* といった専用手法 (~0.3 %) には劣る、という "hloc 派生の SfM パイプラインらしい" 立ち位置に着地しています。
再現手順 (クリックで展開)
# 1) データ取得 (~5 GB)
mkdir -p /tmp/vml-public/cambridge && cd /tmp/vml-public/cambridge
curl -L -C - -o OldHospital.zip \
"https://api.repository.cam.ac.uk/server/api/core/bitstreams/ae577bfb-bdce-488c-8ce6-3765eabe420e/content"
unzip -q OldHospital.zip
# 2)〜5) ShopFacade と同じ流れ。--scene と --work だけ OldHospital 用に差し替え。
python3 scripts/evaluate_cambridge.py prepare \
--scene /tmp/vml-public/cambridge/OldHospital \
--work /tmp/vml-public/cambridge/work_oh
visual-map-localizer build-map \
--images /tmp/vml-public/cambridge/work_oh/train_images \
--output /tmp/vml-public/cambridge/work_oh/map \
--local-feature disk --matcher disk+lightglue \
--num-covisible-pairs 20
python3 scripts/evaluate_cambridge.py localize-all \
--scene /tmp/vml-public/cambridge/OldHospital \
--work /tmp/vml-public/cambridge/work_oh
python3 scripts/evaluate_cambridge.py score \
--scene /tmp/vml-public/cambridge/OldHospital \
--work /tmp/vml-public/cambridge/work_ohROS2 (Jazzy 想定) ノードは ros2/visual_map_localizer_ros/ 配下にあります。
# (build-map で作ったマップに対して)
ros2 launch visual_map_localizer_ros vps.launch.py \
map_dir:=/abs/path/to/map \
publish_tf:=true| sub | /camera/image_raw (sensor_msgs/Image) + /camera/camera_info (CameraInfo) |
| pub | /vps_pose (geometry_msgs/PoseWithCovarianceStamped) ※ ROS 慣例 world-from-camera |
| TF | frame_id → child_frame_id (publish_tf:=true) |
| drop policy | in-flight 中の画像は drop (1Hz 級の絶対姿勢源として利用) |
| NumPy 2.x 互換 | cv_bridge を使わず手書き変換でビルド済み |
詳細は ros2/visual_map_localizer_ros/README.md を参照。
詳細は docs/limitations.md 参照。
- 屋外の 強い照度変化 / 季節変化 には弱い (NetVLAD + SuperPoint の限界)
--camera-modelを指定しない場合は EXIF / 60° FoV からの推定にフォールバック- SfM が成立する程度の画像 overlap が必要 (経験則: 隣接で 60% 以上)
- SuperPoint / SuperGlue は研究用ライセンス。商用は DISK + LightGlue を推奨
docs/architecture.md— モジュール構成 / データフローdocs/ros2_integration.md— 設計ノート (実装はros2/)- 大規模地図対応 (sharding / Faiss ANN) はアーキテクチャドキュメントに記載
pip install -e ".[dev]"
pytest -q
ruff check visual_map_localizer teststests/test_pnp.pyは pycolmap / OpenCV があれば実行されます。tests/test_imports.pyは重い依存なしで通る import スモークテストです。tests/test_array_path.pyは ndarray 入力のキャッシュキー不変性を検証します。
CI ではこれを Python 3.10 / 3.11 / 3.12 で実行しています (.github/workflows/ci.yml)。
examples/build_map_example.pyexamples/localize_example.pyscripts/evaluate_south_building.py— south-building 用の Sim(3) 整列 + pose 誤差評価scripts/evaluate_cambridge.py— Cambridge Landmarks (NVM) 用の 3-stage 評価 (prepare/localize-all/score)scripts/render_cambridge_results.py— README 埋め込み用の trajectory + error histogram 図 ([demo]extras 必須)scripts/profile_localize.py— 常駐プロセスのレイテンシ計測
Apache-2.0 (本リポジトリ)。
内部で利用するモデル重み (SuperPoint / SuperGlue / NetVLAD) は 各々のライセンスに従う必要 があります。 完全に商用フレンドリーな構成にしたい場合は、本 README で示す DISK + LightGlue + NetVLAD の組み合わせをご利用ください。

