一个面向点云地点识别(Place Recognition)的 UGP 风格 Benchmark 骨架,目标是:
- 提供
cross-dataset与cross-distance两套协议 - 统一
manifest -> descriptors -> evaluator评测链路 - 支持 Oxford + MulRan 两个数据集 adapter
- 接入 MinkLoc3D / TransLoc3D / CASSPR 三个 baseline 的“只推理”接口
在点云地点识别(Place Recognition)领域,过去几年涌现出大量方法并取得了显著进展。然而,由于不同工作在数据划分、正样本定义、描述子提取流程、距离度量与评估指标上的设置差异,现有结果往往缺乏可比性,限制了该方向的系统性发展。为此,我们构建了 PlaceForge,一个面向点云地点识别的统一评测基准与协议框架。当前版本支持 Oxford 与 MulRan 两个数据集适配器,提供 cross-dataset 与 cross-distance 两类协议,统一描述子评估接口,并接入 MinkLoc3D、TransLoc3D 与 CASSPR 的“仅推理”评测流程。
PlaceForge 通过统一 manifest、position-based positives 构建规则、以及标准化 evaluator(Recall@1、Recall@1%、可选 mAP)实现可复现与公平比较。基于该框架,我们可系统分析关键技术因素(如距离度量、正样本半径、密度分桶设定)对性能与鲁棒性的影响,并结合失败案例切片(稀疏场景、别名匹配、动态干扰)定位现有方法的薄弱环节。我们期望 PlaceForge 成为点云地点识别研究中的基础设施,促进统一报告、可靠复现与后续方法创新。
当前状态:
- evaluator / positives / protocol runner 可运行
- Oxford / MulRan adapter 为最小可用版本(读取 pose + 生成 manifest)
- MinkLoc3D wrapper 为最小可跑版(依赖 baseline 环境)
- TransLoc3D wrapper 为最小可跑推理实现(依赖 baseline 环境与配置)
- CASSPR wrapper 为最小可跑推理实现(依赖 baseline 环境与配置)
本项目用于标准化点云地点识别评测流程:
- 使用 adapter 将数据集转换为统一 manifest
- 使用 baseline wrapper 读取 manifest 导出 descriptors
- 使用统一 evaluator 计算
recall@1、recall@1%(可选 mAP) - 使用 protocol YAML 复现实验配置并保存
runs/<protocol>/metrics.json
.
├── bench/
│ ├── baselines/
│ │ ├── base.py # baseline 抽象接口(ExtractionRequest / Result)
│ │ ├── minkloc3d_wrapper.py # MinkLoc3D 最小可跑推理 wrapper
│ │ ├── transloc3d_wrapper.py # TransLoc3D 最小可跑推理 wrapper
│ │ └── casspr_wrapper.py # CASSPR 最小可跑推理 wrapper
│ ├── datasets/
│ │ ├── common.py # pose/scan 通用解析工具
│ │ ├── oxford.py # Oxford adapter -> manifest_db/q.json
│ │ └── mulran.py # MulRan adapter -> manifest_db/q.json
│ ├── evaluator.py # 检索评测主逻辑(cosine/l2, topk, recall/mAP)
│ ├── metrics.py # 指标函数(recall@k, AP, mAP, FPR@95Recall)
│ ├── positives.py # 按位置半径生成 positives
│ ├── types.py # Sample / Database / Query dataclass
│ ├── manifest.py # manifest 读写辅助与分桶辅助
│ ├── io.py # JSON/manifest I/O
│ └── local_config.py # 单文件本地路径与运行配置(推荐修改)
├── protocols/
│ ├── cross_dataset.yaml # 跨数据集协议模板
│ └── cross_distance.yaml # 跨距离(分桶)协议模板
├── scripts/
│ ├── prepare_oxford.py # 生成 Oxford manifests
│ ├── prepare_mulran.py # 生成 MulRan manifests
│ ├── prepare_data.py # 通用 prepare 入口
│ ├── build_positives.py # 从 q/db 坐标生成 positives.json
│ ├── extract_desc.py # baseline descriptor 导出总入口
│ ├── eval_from_npy.py # 读取 npy/positives 直接评测
│ ├── run_eval.py # eval_from_npy 别名入口
│ ├── run_protocol.py # 协议执行入口
│ ├── run_from_config.py # 读取 bench/local_config.py 的一键入口
│ ├── run_all_baselines.sh # 一键跑多个 baseline 的提特征+评测
│ ├── analyze_failures.py # 失败案例统计 + 文本卡片 PNG
│ └── toy_example.py # toy 数据快速验证 evaluator
├── assets/
│ ├── failures/ # 失败案例输出目录
│ └── plot_failure_cards.py # failure 分析辅助调用脚本
├── tests/
│ ├── test_evaluator.py
│ ├── test_evaluator_cosine.py
│ ├── test_evaluator_l2.py
│ ├── test_positives.py
│ ├── test_pose_parsing.py
│ └── test_baseline_wrapper_utils.py
├── requirements.txt
└── README.md
- Python 3.10+
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtrequirements.txt(最小依赖):
numpyPyYAMLmatplotlib(失败案例文本卡片绘图)
bench/types.py 中定义:
Sample:id,path,pos, 可选timestamp,npoints,bucket,dynamic_ratioDatabase:name + samplesQuery:name + samples
Oxford / MulRan adapter 统一输出 list[Sample.to_dict()] 的 JSON manifest。
入口:scripts/prepare_oxford.py
关键参数:
--data_root--sequences--pose_file_relpath(默认poses.csv,支持.csv/.txt/.npy)--pose_format/--timestamp_col/--x_col/--y_col/--z_col(可配置 pose 列映射)--scan_relpath(默认pointclouds)--scan_ext(默认.bin .pcd .ply)--db_stride,--q_stride,--split_seed
输出:
data/oxford/manifest_db.jsondata/oxford/manifest_q.json
入口:scripts/prepare_mulran.py
关键参数:
--data_root--sequences--pose_file_relpath(默认poses.csv,常见可设为global_pose.csv)--pose_format(支持csv/txt/npy)与列/索引参数(timestamp_col/txt_*_index/npy_*_index)--scan_relpath(默认Ouster)--scan_ext(默认.bin .pcd .ply)--db_stride,--q_stride,--split_seed
输出:
data/mulran/manifest_db.jsondata/mulran/manifest_q.json
若解压后为:
data/mulran_raw/
KAIST01/
Ouster/
*.bin
global_pose.csv
可执行:
python3 scripts/prepare_mulran.py \
--data_root data/mulran_raw \
--sequences KAIST01 \
--pose_file_relpath global_pose.csv \
--scan_relpath Ouster \
--pose_format csv \
--timestamp_col timestamp --x_col x --y_col y --z_col z \
--db_stride 10 --q_stride 10实现:bench/evaluator.py + bench/metrics.py
db_desc:(N_db, D)float32q_desc:(N_q, D)float32db_pos:(N_db, 2|3)float64q_pos:(N_q, 2|3)float64positives:list[list[int]]- 可选:
q_ids,db_ids
- 必选:
recall@1,recall@1%1% = max(1, floor(0.01 * N_db))
- 可选:
mAP per_querytopk 结果(用于 failure 分析)
cosine(默认,向量归一化)l2
Top-k 使用矩阵乘法 + argpartition 快速检索。
实现:bench/positives.py
接口:
build_positives(q_pos, db_pos, radius_m, use_dim='xy'|'xyz', q_yaw=None, db_yaw=None, max_yaw_diff_deg=None) -> list[list[int]]
CLI:scripts/build_positives.py
python3 scripts/build_positives.py \
--q_pos q_pos.npy \
--db_pos db_pos.npy \
--radius_m 25 \
--use_xy \
--out positives.json若需要加 yaw 约束:
python3 scripts/build_positives.py \
--q_pos q_pos.npy \
--db_pos db_pos.npy \
--q_yaw q_yaw.npy \
--db_yaw db_yaw.npy \
--max_yaw_diff_deg 45 \
--yaw_unit deg \
--radius_m 25 \
--use_xy \
--out positives.json默认半径建议:25m(可按协议修改)。
可选角度约束(yaw):当提供 q_yaw/db_yaw 且设置 max_yaw_diff_deg 后,positives 会同时满足距离与角度阈值。
定义:
train_domain/test_domain- prepare 配置(按 domain)
- positives 半径、xy/xyz,以及可选 yaw 角度约束(
max_yaw_diff_deg) - metric(cosine/l2)、topk
- descriptor 路径
定义:
- 单域
domain bucketing.field(默认npoints)train_buckets=[dense, medium]test_buckets=[sparse]- 检索设置(metric、radius、topk)
Oxford / MulRan adapter 现在都支持:
pose_format:auto/csv/txt/npy- CSV 列映射:
timestamp_col,x_col,y_col,z_col - TXT 索引映射:
txt_timestamp_index,txt_x_index,txt_y_index,txt_z_index(可配txt_delimiter) - NPY 索引映射:
npy_timestamp_index,npy_x_index,npy_y_index,npy_z_index
常见 MulRan pose 来源(global_pose.csv / odom.txt / poses.npy)都可通过上述参数对齐。
入口:scripts/run_protocol.py
行为:
- 读取 YAML
- 若 manifest 不存在,调用对应 adapter prepare
- 若 descriptors 存在,执行 evaluator
- 输出
runs/<protocol_name>/metrics.json(包含分阶段运行耗时runtime_ms)
当数据路径/descriptor 缺失时,会将跳过状态与错误信息写入 metrics.json。
入口:scripts/extract_desc.py
统一命令:
python3 scripts/extract_desc.py \
--baseline minkloc3d \
--repo_root ../MinkLoc3D \
--ckpt ../MinkLoc3D/weights/minkloc3d_baseline.pth \
--manifest data/oxford/manifest_db.json \
--out runs/descriptors/oxford_db_desc.npy统一输出:
desc.npy(float32,(N, D))ids.json(与 manifest 顺序一致)
支持:
--batch_size--device {auto,cpu,cuda}--fp16(默认关闭)
状态说明:
minkloc3d: 最小可跑实现transloc3d: 最小可跑实现(动态导入 baseline 仓库)casspr: 最小可跑实现(动态导入 baseline 仓库)
如果你希望“只改一个文件就能跑”,请直接编辑:
bench/local_config.py
然后执行:
python3 scripts/run_from_config.py --baselines minkloc3d或一次跑多个 baseline:
python3 scripts/run_from_config.py --baselines minkloc3d,transloc3d,casspr这个入口会自动:
- 按
local_config.py里的prepare配置生成 manifest - 按
baselines配置导出 descriptors - 按
evaluation.protocol运行协议评测并输出runs/<run_prefix>_<baseline>/metrics.json
本仓库里的很多路径是占位路径,你需要替换成自己的本地路径(支持相对路径)。
如果你采用单文件模式,优先修改 bench/local_config.py 即可;
不采用单文件模式时,再分别改命令行参数和 protocol YAML。
你至少要替换这些参数:
--data_root--sequences--pose_file_relpath--scan_relpath
示例(Oxford):
python3 scripts/prepare_oxford.py \
--data_root data/oxford_raw \
--sequences 2015-08-14-14-54-57 \
--pose_file_relpath poses.csv \
--scan_relpath pointclouds示例(MulRan):
python3 scripts/prepare_mulran.py \
--data_root data/mulran_raw \
--sequences KAIST01 \
--pose_file_relpath global_pose.csv \
--scan_relpath Ouster你必须替换:
--repo_root(baseline 仓库目录)--ckpt(checkpoint)--config/--model_config(TransLoc3D、CASSPR 常需要)
示例:
python3 scripts/extract_desc.py \
--baseline transloc3d \
--repo_root ../TransLoc3D \
--ckpt <your_transloc_ckpt> \
--config ../TransLoc3D/configs/transloc3d_baseline_cfg.py \
--manifest data/mulran/manifest_db.json \
--out runs/descriptors/mulran_db_desc.npy你需要检查并替换 protocols/cross_dataset.yaml、protocols/cross_distance.yaml 中这些字段:
prepare.*.data_rootprepare.*.sequencesprepare.*.pose_file_relpathprepare.*.scan_relpathpaths.db_manifest/paths.q_manifestdescriptors.db/descriptors.q
如果你用 scripts/run_all_baselines.sh,脚本会自动生成一个临时 protocol 并覆盖 descriptors.db/q,但其余字段(尤其 prepare.*)仍要与你的数据目录匹配。
支持 csv/txt/npy 三种格式。
csv:必须能映射到timestamp,x,y,z(列名可通过参数改)- 例:
timestamp,x,y,z1000,1.0,2.0,0.0
- 例:
txt:每行是数值,默认索引是timestamp=0, x=1, y=2, z=3(可通过--txt_*_index改)- 例:
1000 1.0 2.0 0.0
- 例:
npy:二维数组[N, C],默认索引是timestamp=0, x=1, y=2, z=3(可通过--npy_*_index改)
wrapper 当前支持:
.npy:(N,3)或(N,4)(前 3 列为 xyz)- 二进制
.bin:float64 xyz(长度可被 3 整除)- 或
float32 xyz/xyzi(长度可被 3 或 4 整除)
manifest_db.json 与 manifest_q.json 都是 list[dict],每条样本至少需要:
id:strpath:str(点云文件路径)pos:list[float],长度 2 或 3
可选字段:timestamp, npoints, bucket, dynamic_ratio。
最小示例:
[
{"id": "mulran:KAIST01:db:00000000", "path": "data/mulran_raw/KAIST01/Ouster/123.bin", "pos": [10.0, 20.0, 0.0], "timestamp": 123.0}
]db_desc.npy:float32,形状(N_db, D)q_desc.npy:float32,形状(N_q, D)- 行数必须与对应 manifest 样本数一致
positives.json 为 list[list[int]],长度必须等于 query 数量。
示例:
[[0,1,2],[5],[3,7]]results.json / metrics.json 至少包含:
recall@1recall@1%per_query(每个 query 的 topk 索引/分数/距离)
可选:mAP。
git clone https://github.com/jac99/MinkLoc3D.git ../MinkLoc3D
git clone https://github.com/slothfulxtx/TransLoc3D.git ../TransLoc3D
git clone https://github.com/Yan-Xia/CASSPR.git ../CASSPRpython3 scripts/prepare_oxford.py \
--data_root data/oxford_raw \
--sequences 2015-08-14-14-54-57 \
--pose_file_relpath poses.csv \
--scan_relpath pointclouds \
--db_stride 10 --q_stride 10
python3 scripts/prepare_mulran.py \
--data_root data/mulran_raw \
--sequences KAIST01 \
--pose_file_relpath global_pose.csv \
--scan_relpath Ouster \
--pose_format csv \
--timestamp_col timestamp --x_col x --y_col y --z_col z \
--db_stride 10 --q_stride 10python3 scripts/extract_desc.py \
--baseline minkloc3d \
--repo_root ../MinkLoc3D \
--ckpt ../MinkLoc3D/weights/minkloc3d_baseline.pth \
--manifest data/mulran/manifest_db.json \
--out runs/descriptors/mulran_db_desc.npy
python3 scripts/extract_desc.py \
--baseline minkloc3d \
--repo_root ../MinkLoc3D \
--ckpt ../MinkLoc3D/weights/minkloc3d_baseline.pth \
--manifest data/mulran/manifest_q.json \
--out runs/descriptors/mulran_q_desc.npypython3 scripts/run_protocol.py --protocol protocols/cross_dataset.yaml输出:runs/cross_dataset/metrics.json
bash scripts/run_all_baselines.sh \
--baselines minkloc3d,transloc3d,casspr \
--ckpt_minkloc3d ../MinkLoc3D/weights/minkloc3d_baseline.pth \
--ckpt_transloc3d <your_transloc_ckpt> \
--ckpt_casspr <your_casspr_ckpt> \
--trans_config ../TransLoc3D/configs/transloc3d_baseline_cfg.py \
--casspr_config ../CASSPR/config/config_oxford.txt \
--casspr_model_config ../CASSPR/config/model_config_oxford.txt输出:
- descriptors:
runs/descriptors/<baseline>_{db,q}_desc.npy - metrics:
runs/cross_dataset_<baseline>/metrics.json
runs/<run_name>/
└── metrics.json
metricnum_db,num_query,num_valid_queryempty_positive_ratioone_percent_krecall@1,recall@1%mAP(可选)runtime_ms(prepare/load_inputs/bucket_filter/evaluate/total,按协议类型出现)per_query(topk 详细结果)
入口:scripts/analyze_failures.py
python3 scripts/analyze_failures.py \
--results runs/cross_dataset/metrics.json \
--manifest_q data/mulran/manifest_q.json \
--manifest_db data/mulran/manifest_db.json \
--positives positives.json \
--out_dir assets/failures \
--topk 5输出:
assets/failures/summary.csvassets/failures/summary.jsonassets/failures/fail_qXXXXXX.png
失败切片:
sparsealiasing_proxydynamic_proxy(若 manifest 有dynamic_ratio)
python3 scripts/toy_example.py你也可以用 CLI 版本:
python3 - <<'PY'
import json
import numpy as np
from pathlib import Path
out = Path('toy')
out.mkdir(exist_ok=True)
N_db, N_q, D = 100, 10, 16
rng = np.random.default_rng(0)
db_desc = rng.normal(size=(N_db, D)).astype(np.float32)
q_desc = db_desc[:N_q].copy()
db_pos = rng.normal(size=(N_db, 3)).astype(np.float64)
q_pos = db_pos[:N_q].copy()
positives = [[i] for i in range(N_q)]
np.save(out / 'db_desc.npy', db_desc)
np.save(out / 'q_desc.npy', q_desc)
np.save(out / 'db_pos.npy', db_pos)
np.save(out / 'q_pos.npy', q_pos)
(out / 'positives.json').write_text(json.dumps(positives), encoding='utf-8')
PY
python3 scripts/eval_from_npy.py \
--db_desc toy/db_desc.npy \
--q_desc toy/q_desc.npy \
--db_pos toy/db_pos.npy \
--q_pos toy/q_pos.npy \
--positives toy/positives.json \
--out toy/results.json-
缺少 pose 文件
- 报错:
pose file not found - 处理:检查
--pose_file_relpath是否相对每个 sequence 根目录正确。
- 报错:
-
点云路径不匹配
- 报错:
scan directory not found/No scan files found - 处理:检查
--scan_relpath与--scan_ext。
- 报错:
-
显存不足
- 处理:减小
extract_desc.py --batch_size,必要时使用--device cpu。
- 处理:减小
-
评测速度慢
- 处理:增大 evaluator 的
--batch_size,或增大db/q stride减少样本。
- 处理:增大 evaluator 的
-
TransLoc3D / CASSPR 运行报错
- 处理:先确认 baseline 依赖(Torch+MinkowskiEngine+对应 CUDA)与
--config/--model_config是否和 checkpoint 匹配。
- 处理:先确认 baseline 依赖(Torch+MinkowskiEngine+对应 CUDA)与