转载自公众号: 敢敢AUTOHUB
0. 简介
如果把“定位”理解为一个纯几何问题,那么最常见的输入应该是图像、激光雷达扫描、GPS 或 IMU;但在真实交互场景里,人类给机器提供线索时,往往首先使用的是语言。例如乘客不会说“我的全局坐标是某某”,而更可能说“我站在灰色马路上,东边是人行道,南边有一片黑色植被”。VLM-Loc 关注的核心问题,是机器能否只凭一张已有的 3D 点云地图和一段自然语言,把位置推出来。它试图建立的是“语言描述”和“城市点云地图”之间的可解释推理链,而不是简单做一个黑盒坐标回归器。
从任务定义上看,这项工作属于文本到点云定位,英文常写作 Text-to-Point-Cloud Localization,简称 T2P。论文将问题形式化为:给定一个局部点云地图和一段描述目标位置周围环境的文本,模型需要输出地面平面上的二维位置,也就是一个 2-DoF 的平面坐标。这里的难点不在“识别物体”本身,而在“利用物体之间的空间关系还原位置”。 一句“在 road 上、sidewalk 的东侧、vegetation 的南侧”,本质上同时包含了语义、相对方位和局部拓扑。传统视觉感知模型擅长看见物体,但并不天然擅长把这类关系描述组织成严谨的空间推理过程,这也是 VLM-Loc 切入的关键原因。
这篇论文的现实意义并不抽象。论文作者在引言中提到,城市环境中的 GNSS 容易受到多路径效应和大气延迟影响,尤其在高楼林立的区域,粗定位虽然能给出一个范围,但很难精确到真正的上车点或会合点。这个时候,语言会成为一种自然、低成本、与人类习惯一致的补充信号。
VLM-Loc 的价值不在替代地图或传感器,而在于把“人类可说出的空间描述”转成“机器可计算的定位依据”。 从人机交互、自动驾驶接驳,到机器人户外导航,这类能力都很有潜力。
论文主页:https://arxiv.org/abs/2603.09826
Github仓库:https://github.com/MCG-NKU/nku-3d-vision
图 1:论文与仓库 README 中给出的主框架图。上半部分强调“用语言描述位置”的应用场景,下半部分对比了传统 Text2Loc 式方法与 VLM-Loc 的结构差异。
在 VLM-Loc 之前,文本到点云定位已经有一条相对清晰的技术路线。Text2Pos、Text2Loc、MNCL、CMMLoc 等方法大体遵循“先建立文本和点云对象的对应,再做位置回归”的范式,它们的核心思路是把语言和局部点云映射到共享特征空间,然后通过检索、匹配或回归得到坐标。这类方法并非无效,事实上它们在较小子图和较简化环境里已经能取得不错结果;但论文指出两个根本限制。第一,过去常用子图太小,容易高估模型能力。第二,这些方法更偏“特征对齐”,没有显式建模可靠锚点与空间推理链条。
VLM-Loc 的核心思路,是先把点云改写成 VLM 更擅长处理的表示,再让模型沿着“识别锚点、建立对应、执行空间推理、输出坐标”的链条完成定位。 论文提出的核心判断非常鲜明:视觉语言模型之所以可能胜任这件事,并不是因为它比传统方法参数更多,而是因为它原本就被训练去处理“图像中的实体、关系和描述”这类结构化语义问题。换言之,VLM-Loc 不是把 VLM 当作万能黑箱,而是在认真设计“点云地图怎样改写成 VLM 能读懂的输入”。
1. 输入表示
VLM-Loc 的第一步,不是直接输入原始点云,而是把局部地图转换成两种互补表示:BEV 图像和场景图。 BEV,也就是 bird’s-eye-view 俯视图,本质上是把三维点投影到地面平面上,再栅格化成一张二维图像。这样做的意义很直接,因为大多数视觉语言模型主要在 RGB 图像上预训练, BEV 相当于把“城市几何结构”翻译成“VLM 熟悉的视觉布局”。 道路、建筑、植被、杆体等对象在俯视图里会形成稳定的空间排布,模型更容易理解“谁在谁的东边、谁包围了谁、目标点大致落在什么区域”。
仅有 BEV 还不够,因为视觉布局虽然保留了几何结构,却没有明确告诉模型“这个区域到底是什么类别、中心坐标在哪里、与文本中的某个短语是否对应”。因此论文进一步构造了 scene graph。这里的“图”并不是复杂到必须显式建立所有边关系的图神经网络输入,而是一个对象节点集合,每个节点至少包含 node_id、label 和 pixel_center。论文特别说明,由于 BEV 上的像素坐标本身已经隐式编码了相对空间关系,实际实现里可以省去显式边集。 这是一个很有工程味的设计:保留足够强的结构化信息,但不引入过重的图建模负担。 公开代码中的 create_scene_graph 也能印证这一点,节点字段非常克制,只保留了后续 grounding 和定位真正需要的内容。
def create_scene_graph(centers_info):
raw_nodes = []
node_id = 0
for it in centers_info:
lbl = str(it.get("label", "")).lower()
pc = it.get("pixel_center")
wc = it.get("world_center")
raw_nodes.append({
"node_id": node_id,
"label": lbl,
"pixel_center": [int(pc[0]), int(pc[1])],
"world_center": [float(wc[0]), float(wc[1])],
})
node_id += 1
return {"nodes": raw_nodes}
图 2:论文方法总览。离线阶段把点云地图转换成 BEV 图像和场景图;训练时把图像、场景图、系统提示词以及用户文本一起送入 VLM,由模型自回归输出节点对齐结果和最终位置。
从论文方法和开源代码可以看到,BEV 与 scene graph 的分工非常明确:前者负责整体布局,后者负责“命名”和“坐标化”。 二者组合之后,模型既不会只看到抽象标签,也不会只看到无标签的纹理块,而是在统一上下文里同时拥有“看整体”和“读结构”的能力。这说明 VLM-Loc 不是把点云简单转成图片,而是把几何、语义和可解释推理接口同时准备好。
如果继续往底层看,仓库里负责世界坐标到 BEV 像素坐标投影的函数也很值得一起保留,因为后面的训练标签、推理输出和评测都共享这套坐标定义。这个函数体现了两件事:点先被分箱到像素网格,y 轴再翻转成更符合 BEV 直觉的方向。
def project_points_to_pixels(xy, x_min, y_min, x_scale, y_scale, img_size_minus_1):
W = int(img_size_minus_1) + 1
H = W
bev_range_x = W / float(x_scale)
bev_range_y = H / float(y_scale)
x = xy[:, 0]
y = xy[:, 1]
eps = 1e-9
i = np.floor(((x - x_min) * W / bev_range_x) - eps).astype(np.int64)
j0 = np.floor(((y - y_min) * H / bev_range_y) - eps).astype(np.int64)
np.clip(i, 0, W - 1, out=i)
np.clip(j0, 0, H - 1, out=j0)
j = (H - 1) - j0
return i.astype(np.int32), j.astype(np.int32), (j * W + i).astype(np.int64)
2. 节点分配
如果说 BEV 和 scene graph 解决的是“如何让 VLM 看懂地图”,那么VLM-Loc 最关键的创新则是 Partial Node Assignment,简称 PNA。 这个设计之所以重要,是因为真实语言描述经常会提到一些局部地图中并不完全可见、甚至根本不在当前截取范围内的对象。举例来说,乘客说“我在路上,东边是人行道,南边有植被”,但实际送入模型的只是一个 50 米见方的子图,其中某片植被可能只切进来一点边缘,或者那条人行道在当前裁剪窗口里根本不完整。如果模型强迫每一个文本对象都必须在图里找到对应节点,错误匹配会像连锁反应一样把最终坐标拉偏。
PNA 的思想,是先判断“这个对象值不值得拿来做定位依据”,而不是上来就硬配。 论文给出的判定逻辑是:对文本中提到的每个对象,在 scene graph 中寻找同类节点,再根据它在当前地图中的可见部分与查询位置可见区域中的几何关系,判断该对象是否可 grounding。若可见性与位置一致,则标记为 grounded=true 并绑定到具体 node_id;若当前地图不足以支持可靠对齐,则直接输出 grounded=false 和 matched_node=null。这一层筛选的作用,是让模型知道“哪些锚点可信,哪些锚点应当忽略”。 公开实现 ground_pose_to_image_scene_graph_v2 的核心部分如下,可以直接看出它先按语义过滤,再做距离比较,最后按阈值决定是否 grounding。
cand_nodes = label2nodes.get(label, [])
if not cand_nodes:
grounded_info.append(matched)
continue
dists = [
float(np.hypot(*(nd["world_center"] - pose_obj_center_world)))
for nd in cand_nodes
]
best_i = int(np.argmin(dists))
best_nd = cand_nodes[best_i]
best_dist = dists[best_i]
thr = 15.0 if label in STUFF_CLASSES else 5.0
if label == "road":
thr = 50.0
if best_dist <= thr:
matched["grounded"] = True
matched["matched_node"] = {
"node_id": best_nd["node_id"],
"label": best_nd["label"],
"pixel_center": best_nd["pixel_center"],
"world_center": best_nd["world_center"].tolist(),
"distance_m": best_dist,
"threshold_m": thr,
}
图 3:PNA 的示意图。论文通过可见区域中心之间的距离阈值来判断一个文本对象是否应当参与 grounding,这比强制全量匹配更符合真实地图裁剪下的观测条件。
这也是 VLM-Loc 在可解释性上优于传统回归方法的原因:它先给出对象级判断,再给出最终坐标。 研究者和工程人员可以直接检查:模型到底依据了哪些文字线索,哪些线索因为不可见而被舍弃,错误究竟发生在节点匹配阶段还是空间推理阶段。论文的实验也说明,正确节点分配数量越多,最终定位误差越低。 这意味着 PNA 不是可有可无的辅助模块,而是影响整个定位链条稳定性的核心环节。
3. 输出格式
在位置预测阶段,VLM-Loc 没有额外挂一个孤立的数值回归头,而是把对齐结果和位置坐标统一放在自回归输出中完成。 也就是说,模型会先输出文本对象到 scene graph 节点的对应情况,再继续输出目标位置在 BEV 图像中的二维像素坐标 point_2d。这一设计有两个直接好处。第一,推理链条前后统一。第二,工程实现更简单,因为结果天然就是结构化 JSON。
从仓库中的 system_prompt.txt 可以非常直观地看到这种设计。作者把坐标系约定、方向规则、任务目标和输出格式全写在系统提示中,让模型明确按照像素坐标工作,并严格返回一个 JSON 对象。这也是它能把定位过程收束成稳定生成任务的重要原因。
In BEV pixel coordinates:
- Up = North = y decreases
- Down = South = y increases
- Left = West = x decreases
- Right = East = x increases
* All reasoning and distance computation MUST be in pixel coordinates.
* len(assignments) MUST equal the number of mentioned objects.
* Output exactly ONE JSON object in the schema below.
下面这段代码不是论文里的伪代码,而是公开实现中的关键接口,足以说明 VLM-Loc 的输出并非自由文本,而是受约束的“结构化推理结果”。
{
"assignments": [
{"object_label": "parking", "grounded": true, "matched_node": 0},
{"object_label": "terrain", "grounded": true, "matched_node": 8},
{"object_label": "road", "grounded": true, "matched_node": 4},
{"object_label": "vegetation", "grounded": true, "matched_node": 11}
],
"point_2d": [45, 135]
}
这一输出格式的价值,在于它把定位从“不可审计的数值预测”改造成“可分析的中间推理 + 最终结果”。 如果模型输出错了,研究者可以立即区分究竟是 assignments 阶段出了偏差,还是节点已经对齐但最终几何推理仍然出错。对严肃系统来说,这是从“模型能用”走向“模型可维护”的重要一步。
4. 样本组织
如果只读论文,读者容易把 VLM-Loc 理解成一种很抽象的多模态框架;但打开仓库后会发现,它的实现路径其实相当具体。data/dataset_generation_semantics_cityloc-k.py 会把每个局部地图样本整理成一条对话式训练数据,用户消息中包含 scene graph 和文本查询,助手消息则是目标 JSON。也就是说,作者实际上把定位任务改写成了一个“看图并输出结构化答案”的监督微调任务。 这种做法完全契合当前 VLM 的训练方式,也解释了为什么他们能直接基于 Qwen3-VL 之类的通用模型做适配。
下面这段代码展示了训练标签的核心构造方式。可以看到,脚本会先把六条对象描述拼成一句查询文本,再把对齐结果和目标像素坐标打包成 assistant_message。这意味着模型训练时学到的不是单点回归,而是“先理解语言中的对象与方向,再输出可解释结果”的生成模式。
assign_pairs.append({
"object_label": hint.object_label,
"grounded": ground_info["grounded"],
"matched_node": ground_info["matched_node"]['node_id'] if ground_info["matched_node"] else None
})
phrases.append(f"{hint.direction} of a {hint.object_color_text} {hint.object_label}")
assistant_message_obj = {
"assignments": assign_pairs,
"point_2d": pose_pixel_coor,
}
assistant_message = json.dumps(assistant_message_obj, ensure_ascii=False)
scene_graph_message = json.dumps(scene_graph, ensure_ascii=False)
如果把这一段和上面的 JSON 标签拼起来看,样本真正写入训练集前还会再包成 messages + images 的多模态对话格式。这个细节很重要,因为它说明仓库并没有额外发明一套专用数据接口,而是直接复用了 ms-swift 的标准输入约定。
user_content = f"<image> {user_message}"
assistant_content = assistant_message
return {
"messages": [
{"role": "user", "content": scene_graph_message},
{"role": "user", "content": user_content},
{"role": "assistant", "content": assistant_content},
],
"images": [image_path],
}
这里还暴露出一个很值得注意的实现细节:当前公开脚本里的训练文本并不是完全自由的自然语言,而是由模板自动拼接出来的。 这个信息来自仓库代码本身,而不是论文正文的口头概括。它说明 VLM-Loc 虽然处理的是语言定位问题,但当前公开训练分布仍然带有较强的模板化特征。 换句话说,论文验证的是“VLM 能否在受控语言描述下学会空间 grounding”,而不是已经完全解决了开放域自由表达的语言定位问题。
模板拼接的方式在代码里也写得很直白,下面这一段适合直接放在文章里,因为它能把“语言查询是怎么生成的”说清楚。
phrases = []
for i, hint in enumerate(poses[pid].descriptions):
ground_info = pose_ground_pairs[i]
assign_pairs.append({
"object_label": hint.object_label,
"grounded": ground_info["grounded"],
"matched_node": ground_info["matched_node"]['node_id'] if ground_info["matched_node"] else None
})
phrases.append(f"{hint.direction} of a {hint.object_color_text} {hint.object_label}")
natural_clause = _join_phrases_with_and(phrases)
query_description = f"The target location is {natural_clause}."
5. 训练与推理
公开仓库的 README 写明,VLM-Loc 构建在 ms-swift 之上,并采用 LoRA 方式对底座 VLM 做参数高效微调。train.sh 默认调用 swift sft,基座模型为 Qwen3-VL-8B-Instruct,并把 system_prompt.txt 直接作为系统提示词传入训练过程。与很多论文只给高层概念不同,这套实现给出的工程信号非常明确:作者不是另起炉灶训练一个专用网络,而是把 T2P 问题转换成标准的多模态指令微调任务。
swift sft
--system ./system_prompt.txt
--model /disk/deepdata/localization/persional/kang/Qwen3-VL-8B-Instruct
--dataset dataset_items/CityLoc-K/vlmloc_training_data.json
--val_dataset dataset_items/CityLoc-K/vlmloc_val_data.json
--train_type lora
--torch_dtype bfloat16
--num_train_epochs 5
--learning_rate 1e-4
--lora_rank 8
--lora_alpha 16
--target_modules all-linear
这段脚本还有两点值得深挖。**第一,仓库脚本给出了更具体的训练超参数,例如 lora_rank=8、lora_alpha=16、learning_rate=1e-4。第二,论文配置与公开默认脚本并不必然一一对应。**对复现者而言,最稳妥的办法是优先参考仓库发布的脚本、数据项说明和已提供的 adapter 权重。
推理脚本同样很直接。test.sh 默认调用 swift infer,输入测试集 JSON,并通过 --adapters 加载训练出的 LoRA 权重。对于复现实验流程的人来说,这段命令比论文里抽象的“inference stage”描述更有操作意义。
swift infer
--model /disk/deepdata/localization/persional/kang/Qwen3-VL-8B-Instruct
--adapters checkpoints/output/v0-20251101-125202-qwen3_8b/checkpoint-3600
--infer_backend pt
--val_dataset dataset_items/CityLoc-K/vlmloc_testing_data.json
--max_new_tokens 2048
--system ./system_prompt.txt
推理结果的后处理也值得单独放一段代码,因为仓库并不是直接假设模型一定会输出合法 JSON,而是先做一次容错解析,再从结果里抽取 point_2d。 这部分代码虽短,但很能反映实际工程里对生成式输出的处理方式。
def safe_json_loads(x):
if x is None:
return None
if isinstance(x, dict):
return x
if isinstance(x, str):
x = x.strip()
if not x:
return None
try:
return json.loads(x)
except json.JSONDecodeError:
return None
return None
def extract_point(obj):
if not obj or "point_2d" not in obj:
return None
p = obj["point_2d"]
if not isinstance(p, (list, tuple)) or len(p) != 2:
return None
return float(p[0]), float(p[1])
评测部分同样很有启发。recall.py没有使用复杂的三维姿态误差,而是直接在 BEV 像素平面上计算距离,再按 50m / 224px 换算成米。 也就是说,METERS_PER_PX ≈ 0.223。这种做法把模型训练、推理和评测统一到同一个空间里。 对于工程实现来说,这种约定非常清晰,既避免了多坐标系误差堆叠,也方便直接做可视化分析。
PX_PER_50M = 224.0
METERS_PER_PX = 50.0 / PX_PER_50M
dpx = dist_px(pred_pt, gt_pt)
dm = dpx * METERS_PER_PX
if dm <= 5: hits_5 += 1
if dm <= 10: hits_10 += 1
if dm <= 15: hits_15 += 1
此外,数据生成脚本里的阈值设计也揭示了 PNA 的工程逻辑。论文正文写到,“object” 类默认阈值为 5 米,“stuff” 类默认为 15 米;而公开代码中 ground_pose_to_image_scene_graph_v2还额外对 road 设成了 50 米阈值。 这说明作者在具体实现时已经意识到,道路这类大尺度连续区域的几何中心并不适合用和小物体相同的严格标准衡量。这个差异虽然不起眼,却非常能体现一件事: VLM-Loc 并不是一个完全依赖大模型“悟性”的方法,它在底层仍然有一套细致的几何归一化和规则设计。
6. 数据来源
*VLM-Loc 的另一个重要贡献,是提出了 CityLoc 基准。**论文指出,过去常用的 KITTI360Pose 虽然是城市级点云定位数据,但用于精定位的局部子图通常较小、对象相对简单,这会在无形中降低任务难度。为了更系统地评估语言定位,作者构建了 CityLoc,并分成 CityLoc-K 和 CityLoc-C 两部分。前者基于 KITTI-360 的车载 LiDAR 点云,后者基于 CityRefer 所依托的城市级摄影测量点云与 SensatUrban 场景,用来检验跨模态、跨域泛化能力。这个双源设计很关键,因为它避免了模型只在单一采集方式和单一语义分布上“练熟手感”。
从外部资料看,KITTI-360 官方网站说明该数据集覆盖德国 Karlsruhe 郊区 73.7 公里行驶距离,包含超过 320k 图像、100k 激光扫描,并提供一致的 3D 语义与实例标注;CityRefer 的官方 README 和论文摘要则说明,该数据集为城市级点云视觉 grounding 提供了 35k 条自然语言描述和 5k 条与 OpenStreetMap 对齐的地标标签,并强调其描述由人工校验。这些材料解释了为什么 VLM-Loc 选择 KITTI-360 做车载点云底座、再用 CityRefer/SensatUrban 检验跨域泛化。
如果按照仓库 README 自己准备数据,相关命令也比较明确。CityLoc-K 和 CityLoc-C 的核心区别不在训练接口,而在前面的数据准备路径不同。对复现文章的读者来说,保留这段命令比只说“作者构建了数据集”更实用。
# CityLoc-K
python datapreparation/kitti360pose/prepare_cityloc-k.py
python data/dataset_generation_semantics_cityloc-k.py
# CityLoc-C
python datapreparation/kitti360pose/prepare_cityloc-c.py
python data/dataset_generation_semantics_cityloc-c.py
| 数据来源 | 主要特点 | 在 VLM-Loc 中的作用 |
|---|---|---|
| KITTI-360 | 车载采集、长距离城市道路、带 3D 语义与实例标注 | 构建 CityLoc-K,验证模型设计与主实验结果 |
| CityRefer / SensatUrban | 城市级摄影测量点云、自然语言描述和地标信息更丰富 | 构建 CityLoc-C,测试跨域泛化与不同点云模态下的适应能力 |
7. 复现流程
如果把论文和仓库脚本拼到一起,**VLM-Loc 的实现流程其实非常清楚:准备数据、训练、推理、评测。**换句话说,这项工作虽然概念上涉及 BEV、scene graph、PNA 和自回归定位,但工程入口并不复杂。
# 1. 生成或下载数据
python data/dataset_generation_semantics_cityloc-k.py
# 2. 训练
bash train.sh
# 3. 推理
bash test.sh
# 4. 评测
python recall.py --jsonl <result>.jsonl
这条流程线索很重要,因为它把论文里的各个模块压缩成了一条具体的复现路径。理解这条路径之后,再回头看 BEV、scene graph 和 PNA 的设计,就会发现它们不是彼此孤立的概念,而是在数据、提示词、模型输出和评测脚本中前后贯通的。
8. 小结
如果要用一句话概括 VLM-Loc,它把“语言定位”从旧范式中的特征匹配问题,推进成了一个带有显式锚点选择、结构化对象 grounding 和可解释坐标生成的空间推理问题。它最聪明的地方不是盲目追求更复杂的网络,而是把 3D 点云转成了 BEV 与 scene graph 两种对 VLM 友好的表示,再用 PNA 解决“部分可见、部分可对齐”难题。读完这篇工作,至少应当把握三层核心意思:第一,语言确实可以成为城市定位的有效输入;第二,VLM 的价值在于推理链条,而不只是感知能力;第三,真正让系统可靠落地的,往往不是一个巨大的模型,而是输入表达、节点约束、监督格式和评测协议这些被认真打磨过的细节。
351