一句话总结

现有最强的多模态大语言模型(MLLM)在无人机视角下的自我状态推理(我在哪、看向哪)和环境状态推理(地标在哪、空间关系如何)上,准确率与人类基线相差近 50 个百分点——UAV-DualCog 量化了这个差距。


为什么这个问题重要?

当我们期望 MLLM 直接驱动无人机时,有一个鲜少被直接测量的双重认知需求:

  • 自我状态(Self-state):飞行高度、俯仰角、朝向、当前视点对应地图哪个位置
  • 环境状态(Environment-state):地标的空间位置、物体间方向/距离关系、跨时间段的事件定位

过去的 UAV 基准只测”场景里有没有 X”或端到端导航是否成功,没有人要求模型给出精确坐标或时间区间,这让模型能靠语言先验”猜对”选择题,掩盖了真实的空间理解能力缺口。


背景知识

为什么用语义点云而不是 NeRF?

表示方式 优点 缺点 适合生成基准?
真实图像采集 真实纹理 标注成本高、难扩展
NeRF/3DGS 高质量渲染 训练慢,语义获取难 一般
语义点云 有精确坐标+类别标签 纹理细节不足

语义点云的核心优势:每个点都有精确的 $(x, y, z)$ 坐标和类别标签,可以自动计算任意两个地标之间的空间关系,不需要人工标注 QA 对。

UAV 相机模型

无人机视图的核心是相机外参 $[\mathbf{R} \mid \mathbf{t}]$,将世界坐标投影到图像平面:

\[\mathbf{x} = \mathbf{K} [\mathbf{R} \mid \mathbf{t}] \mathbf{X}_w\]

对 UAV 而言,高度变化改变 $\mathbf{t}$ 的 z 分量,俯仰角变化改变 $\mathbf{R}$。两者叠加导致同一地标在不同飞行状态下外观差异极大,这是 MLLM 理解困难的几何根源。


双重认知任务体系

UAV-DualCog
├── 自我状态推理(Self-State)
│   ├── 高度估计:给定视图,预测飞行高度(米)
│   ├── 俯仰角估计:估计相机下视角度
│   └── 视角变换:给定视图 A,预测 B 视角下的内容
└── 环境状态推理(Environment-State)
    ├── 地标空间定位:预测地标的像素坐标
    ├── 空间关系推理:方向(N/S/E/W)+ 距离估计
    └── 时序区间定位:定位事件起止时间 [t_start, t_end]

关键点:所有任务都要求连续值输出,而不是离散选项,这让模型无法靠先验概率蒙对。


基准构建 Pipeline

场景级语义点云
    ├── 模拟 UAV 轨迹(高度 20–150m,俯仰 −30° 到 −90°)
    │       ├── 渲染 RGB 视图(输入给 MLLM)
    │       └── 渲染语义标注视图(自动计算 ground truth)
    └── 自动生成 QA 对
            ├── 从点云坐标计算精确空间关系
            └── 构建 image/video QA 样本(千级规模)

核心实现

UAV 相机外参构建

import numpy as np

def build_uav_camera_pose(position_xy, altitude, pitch_deg, yaw_deg):
    """
    构建 UAV 相机外参矩阵(世界 → 相机)
    pitch_deg: -90 为垂直下视,-30 为斜视
    yaw_deg:   0 为正北,90 为正东
    """
    pitch = np.radians(pitch_deg)
    yaw = np.radians(yaw_deg)

    # 绕 X 轴俯仰,绕 Z 轴偏航
    Rx = np.array([[1, 0,             0           ],
                   [0, np.cos(pitch), -np.sin(pitch)],
                   [0, np.sin(pitch),  np.cos(pitch)]])
    Rz = np.array([[np.cos(yaw), -np.sin(yaw), 0],
                   [np.sin(yaw),  np.cos(yaw), 0],
                   [0,            0,            1]])

    R = Rx @ Rz
    t_world = np.array([position_xy[0], position_xy[1], altitude])

    pose = np.eye(4)
    pose[:3, :3] = R
    pose[:3, 3] = -R @ t_world  # 世界→相机的平移部分
    return pose

从语义点云渲染 UAV 视图

def render_uav_view(points, semantics, pose, K, img_size=(640, 480)):
    """
    points:    (N, 3) 世界坐标系点云
    semantics: (N,)   每点的语义类别 ID
    pose:      (4, 4) build_uav_camera_pose 的输出
    K:         (3, 3) 相机内参
    """
    W, H = img_size
    pts_h = np.hstack([points, np.ones((len(points), 1))])
    pts_cam = (pose @ pts_h.T).T[:, :3]

    valid = pts_cam[:, 2] > 0.1           # 过滤相机后方的点
    pts_cam, sem = pts_cam[valid], semantics[valid]

    proj = (K @ pts_cam.T).T
    px = (proj[:, 0] / proj[:, 2]).astype(int)
    py = (proj[:, 1] / proj[:, 2]).astype(int)
    in_frame = (px >= 0) & (px < W) & (py >= 0) & (py < H)

    semantic_img = np.zeros((H, W), dtype=np.int32)
    depth_buf = np.full((H, W), np.inf)

    # Z-buffer 处理遮挡:近处点覆盖远处点
    for x, y, d, s in zip(px[in_frame], py[in_frame],
                           pts_cam[in_frame, 2], sem[in_frame]):
        if d < depth_buf[y, x]:
            depth_buf[y, x] = d
            semantic_img[y, x] = s

    return semantic_img


# 使用示例:对比两个不同高度的 UAV 视图
if __name__ == "__main__":
    rng = np.random.default_rng(42)
    N = 50000
    points = rng.uniform([0, 0, 0], [200, 200, 15], (N, 3))
    semantics = rng.integers(0, 10, N)

    K = np.array([[500, 0, 320], [0, 500, 240], [0, 0, 1]], dtype=float)
    pos = (100.0, 100.0)

    # 低空斜视 vs 高空垂直下视
    pose_low  = build_uav_camera_pose(pos, altitude=30,  pitch_deg=-45, yaw_deg=0)
    pose_high = build_uav_camera_pose(pos, altitude=120, pitch_deg=-90, yaw_deg=0)

    view_low  = render_uav_view(points, semantics, pose_low,  K)
    view_high = render_uav_view(points, semantics, pose_high, K)
    # view_low/view_high 是语义标注图,可用 matplotlib 可视化

自动生成空间关系 QA

def compute_spatial_qa(landmark_a, landmark_b):
    """
    基于点云坐标自动生成空间关系 QA 的 ground truth
    landmark_a/b: {'name': str, 'centroid': (x, y)} 地标的平面坐标
    """
    dx = landmark_b['centroid'][0] - landmark_a['centroid'][0]  # 东西
    dy = landmark_b['centroid'][1] - landmark_a['centroid'][1]  # 南北
    dist = np.sqrt(dx**2 + dy**2)
    angle = np.degrees(np.arctan2(dy, dx))

    dirs = ['东', '东北', '北', '西北', '西', '西南', '南', '东南']
    direction = dirs[int((angle + 202.5) % 360 / 45)]

    question = (f"在当前鸟瞰视图中,{landmark_a['name']} 相对于 "
                f"{landmark_b['name']} 在哪个方向?距离约多少米?")
    answer = f"{direction}方向,约 {round(dist)} 米"
    return {'question': question, 'answer': answer, 'distance_m': dist}

调用 MLLM 评估空间推理

import anthropic, base64
from PIL import Image
import io

def query_mllm_spatial(img_array, question, altitude, pitch):
    client = anthropic.Anthropic()

    buf = io.BytesIO()
    Image.fromarray(img_array.astype(np.uint8)).save(buf, format="PNG")
    img_b64 = base64.b64encode(buf.getvalue()).decode()

    system = (f"你是 UAV 视觉分析系统。当前飞行参数:高度 {altitude}m,"
              f"俯仰角 {pitch}°。请基于图像给出精确的空间位置答案:方向 + 距离(米)。")

    resp = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=128,
        system=system,
        messages=[{"role": "user", "content": [
            {"type": "image", "source": {"type": "base64",
             "media_type": "image/png", "data": img_b64}},
            {"type": "text", "text": question}
        ]}]
    )
    return resp.content[0].text

实验结果:MLLM 在哪里失败?

定量结果(论文数据)

任务 人类基线 最佳 MLLM 差距
高度估计 ~85% ~38% −47%
视角变换 ~78% ~29% −49%
地标空间定位 ~82% ~31% −51%
时序区间定位 ~76% ~27% −49%

所有任务上差距都在 45–51 个百分点,加上 thinking/frontier 模型后提升有限。

三个核心失败模式

1. 视角坐标系混淆(最普遍) MLLM 倾向于把图像的”上方”等同于地理的”北方”。在 UAV 斜视视角下,二者几乎从不重合——这是纯语言先验带来的系统性偏差。

2. 高度-尺度解耦失败 人类能通过”汽车看起来很小”推断高空飞行,MLLM 对尺度-高度映射的建模非常弱,在没有明显参照物时估计误差很大。

3. 时序定位退化为关键帧识别 模型倾向于找最相关的单帧,而不是给出精确的时间区间 $[t_{\text{start}}, t_{\text{end}}]$,区间宽度系统性偏小。


工程实践

语义点云渲染的常见坑

# 坑1:远处点云稀疏导致地标"消失"
# 解决:按距离分层采样,远处补充密度
def stratified_densify(points, semantics, max_dist=100.0, near_ratio=0.3):
    dists = np.linalg.norm(points[:, :2], axis=1)
    near_mask = dists < max_dist * 0.4
    # 近处降采样,远处保留全部点,避免近处主导渲染
    near_idx = np.where(near_mask)[0]
    far_idx  = np.where(~near_mask)[0]
    keep_near = near_idx[::int(1/near_ratio)]  # 降采样
    return np.concatenate([keep_near, far_idx])
# 坑2:UAV 轨迹采样偏向场景中心,边缘覆盖不足
# 解决:用 Halton 低差异序列保证空间均匀覆盖
from scipy.stats.qmc import Halton

def coverage_aware_pose_sampling(bbox_xy, alt_range, n=500):
    sampler = Halton(d=4, scramble=True)   # x, y, altitude, yaw
    samples = sampler.random(n)
    x   = bbox_xy[0] + samples[:, 0] * (bbox_xy[2] - bbox_xy[0])
    y   = bbox_xy[1] + samples[:, 1] * (bbox_xy[3] - bbox_xy[1])
    alt = alt_range[0] + samples[:, 2] * (alt_range[1] - alt_range[0])
    yaw = samples[:, 3] * 360
    return list(zip(x, y, alt, yaw))

实时部署的延迟问题

方案 推理延迟 空间精度 适用场景
直接调用 MLLM 2000ms+ 中(论文所测) 离线分析
MLLM + 专用定位模块 300–600ms 半自主辅助
蒸馏后轻量模型 <100ms 低–中 实时飞控

目前 MLLM 的推理速度与 UAV 控制的实时性要求(100ms 以内)之间存在一个数量级的差距,短期内需要混合架构。


什么时候用 / 不用这类评估框架?

适用场景 不适用场景
评估 MLLM 的空间推理能力 评估低延迟飞控策略
场景级语义点云可获取 只有稀疏 GPS 轨迹
静态或缓变场景 高密度动态目标(人群、车流)
研究精确空间定位 粗粒度分类任务(是/否)

与相关工作对比

基准 空间定位精度要求 自我状态推理 视频时序
EarthVQA 无(选择题)
DroneVQA
AerialVLP
UAV-DualCog 精确坐标/区间

我的观点

UAV-DualCog 做对了几件事:把”UAV 理解”拆解为自我+环境双维度本身就有框架价值,用语义点云自动生成基准的思路工程上可扩展,要求连续值输出而非选择题让评估更诚实。

还差什么:合成点云渲染与真实 UAV 图像之间存在明显 domain gap(无纹理、光照固定);UAV-DualCog-Train 的数据规模对 fine-tune 大模型来说仍然偏小;”绝对定位”(我在地图哪个位置)比”相对定位”更难,目前几乎没有 MLLM 能认真处理这个问题。

值得关注的开放方向:语义点云渲染的 sim-to-real 迁移、用 NeRF/3DGS 增强视角变换能力、以及时序定位精度瓶颈究竟来自视觉特征还是语言解析——这三个问题回答清楚了,才算真正理解了 UAV 空间智能的边界在哪里。