深度解读 | 国防科大&清华:基于语言指令的空中目标导航GeoNav,成功率超SOTA模型12%!

基于语言目标的空中导航是具身人工智能领域的一项关键挑战,它要求无人机能够根据文本指令,在城市街区等复杂环境中对目标进行定位。

现有的方法通常改编自室内导航,由于视野有限、物体之间存在语义模糊性,以及缺乏结构化的空间推理能力,这些方法难以进行拓展应用。

©️【深蓝具身智能】

为解决以上问题,国防科大联合清华大学提出的 GeoNav 是一种具备地理空间感知能力的多模态智能体,以实现长距离导航。

图片

图1 | GeoNav工作流程示意图 

GeoNav分三个阶段运行:地标导航、目标搜索和精确定位,这模仿了人类从粗略到精细的空间策略。

为了支持这种推理过程,它会动态构建两种不同类型的空间记忆:

  • 全局但概略的示意性认知地图SCM

它将先前的文本地理知识和具身视觉线索融合成一种自上而下且带有注释的形式,以便能够快速导航至地标区域;

  • 局部但精细的分层场景图HSG

该图表示街区、地标和物体之间的层次空间关系,用于对目标进行明确的定位。

在这种结构化表示的基础之上,GeoNav还采用了一种具备空间感知能力的多模态思维链提示机制,使多模态大语言模型(MLLMs)能够在各个阶段进行高效且可解释的决策。

本文将基于上述核心内容配合代码对该研究成果进行详细梳理。

我们开设此账号,想要向各位对【具身智能】感兴趣的人传递前沿权威的知识讯息外,也想和大家一起见证它到底是泡沫还是又一场热浪?

欢迎关注【深蓝具身智能】👇


图片

方法说明

GeoNav包含三个关键模块:示意性认知地图(SCM)、分层场景图(HSG)和多阶段导航策略(MNS),分别模拟智能体的感知、记忆和决策能力,如图所示:在地标导航和目标搜索阶段构建并使用SCM,在搜索目标时构建HSG,最终用于定位目标。 

接下来,将依次介绍这三个模块的原理及代码实现逻辑。


 示意性认知地图Schematic Cognitive Map(SCM)

图片

图2 | 示意性认知地图构建流程

  • 目的

构建整个城市的语义地图,为无人机导航提供全局的环境先验,避免迂回飞行。

  • 流程说明

1. Geo and Object Data Collection:

Step1:从OpenGIS数据中检索地标的先验地理信息,并将其投影到地标地图(Landmark Layout)上

Step2:使用GroundDino在无人机俯拍的RGB图像中检测物体,得到物体的Bounding Boxes边界框;

Step3:通过Segment Anything优化步骤2中得到的Bounding Boxes边界框,生成Semantic Mask分割掩码;

 2. Spatial Projection: 

将Semantic Mask分割掩码的像素转换到世界坐标系下,像素坐标和世界坐标转化过程如下:

3. LandMark and Object Annotations: 

得到标有物体信息的语义地图,即示意性认知地图SCM;

class LandmarkNavMap(Map):    def plot(        self,        map_type,        query_engine=None,        current_pos=None    ):        import numpy as np        # 颜色配置(RGBA格式)        layer_colors = {            'current view area': (0.5, 0, 0.5, 0.3),   # 紫色        }
        # 合并地图数据        map_layers = np.concatenate([            np.expand_dims(self.to_array()[1],0),        ])         # 创建画布        fig, ax = plt.subplots(figsize=(10, 10))        fig = plt.figure(figsize=(10, 10), facecolor='white')        ax = fig.add_subplot(111)        # 绘制当前视野区域        if map_type != 'landmark':            for idx, (layer_name, rgba) in enumerate(layer_colors.items()):                self._draw_layer(ax, map_layers[idx], rgba)
        # 绘制landmark和semantic图层        self._merge_color_layers(ax, map_type)        # 添加landmark标注        if map_type != 'w/o annotation':            self._annotate_landmarks(ax)
        # === 新增轨迹绘制逻辑 ===        # 绘制方向箭头        if map_type != 'w/o annotation':            arrow_length = 15  # 箭头长度(像素)            if len(self.trajectory) == 1:                pose = self.trajectory[0]                col, row = self.to_row_col(pose.xy)[::-1]                dx = arrow_length * np.cos(pose.yaw)                dy = - arrow_length * np.sin(pose.yaw)                ax.arrow(                        x=col, y=row,                        dx=dy, dy=dx,  # 注意坐标轴方向转换                        width=3,                         head_width=8,                        head_length=10,                        fc='lime',  # 箭头填充色                        ec='darkgreen',  # 箭头边缘色                        alpha=0.8)  # 半透明箭头            elif len(self.trajectory) >1:                # 转换轨迹坐标                trajectory_points = [                    self.to_row_col(pose.xy)[::-1]  # 转换为(col, row)格式                    for pose in self.trajectory                ]
                # 绘制轨迹线                x_coords, y_coords = zip(*trajectory_points)                ax.plot(x_coords, y_coords, color = 'black', linestyle='-', linewidth=3.5, alpha=0.7)  # 白色虚线轨迹                 prev_pose = trajectory_points[-2]                pose = trajectory_points[-1]                delta_x = pose[0] - prev_pose[0]                delta_y = pose[1] - prev_pose[1]                theta = np.arctan2(delta_y, delta_x)  # 箭头角度是上一位置到当前位置的角度                col, row = pose
                # 计算箭头方向                dx = arrow_length * np.cos(theta)                dy = arrow_length * np.sin(theta)
                ax.arrow(                    x=col, y=row,                    dx=dx, dy=dy,  # 注意坐标轴方向转换                    width=3,                     head_width=8,                    head_length=10,                    fc='lime',  # 箭头填充色                    ec='darkgreen',  # 箭头边缘色                    alpha=0.4)  # 半透明箭头
        # 创建图例        if map_type != 'landmark':            self._annotate_objects(ax, query_engine=query_engine, current_pos=current_pos)            legend_elements = self._create_legend_elements()            ax.legend(handles=legend_elements,                     loc='upper center',                    bbox_to_anchor=(0.5, -0.05),                    ncol=3)         # 生成输出图像        plot_img = self._render_output(fig, map_type)        plt.close('all')
        return plot_img
  • 代码说明:这部分代码描述了示意性认知地图(SCM)绘制构建逻辑。

 分层场景图Hierarchical Scene Graph(HSG)

图片

图3 | 构建和查询分层场景图HSG的一个例程

  • 说明

如图所示,分层场景图HSG是一个分层次的拓扑图形结构,节点Node有3种类型:城市街区(Block)、地标(LandMark)、物体(Object)。

边Edge表示节点和节点的空间关系,例如:Block和LandMark是包含(Contains)关系,LandMark和Object,Object和Object有方位(On the left of,On the right of,Next to等)关系。

  • 目的

分层场景图HSG能提供更加精确的空间信息,保证无人机即使在一堆相似物体中也能精确识别到目标物体。

  • 流程说明

1. 每个节点必须有唯一的ID

2. 每个节点的必备属性:

    - 对象类型:取值为[车辆, 道路, 建筑物, 停车场, 绿地等]中的一个

    - 边界框:边界框坐标,格式为[xmin, ymin, xmax, ymax]

3. 可选属性:

    - 颜色:取值为[白色, 黑色, 红色, 灰色, 蓝色, 绿色, 棕色, 银色]中的一个。 

构建节点的要求

1. 仅使用以下具有特定含义的关系标签:

(1)拓扑关系:

- “contains”(包含):表示一个物体完全在另一个物体内部。

- “adjacent_to”(相邻):表示物体彼此紧邻。

- “near_corner”(靠近角落):表示一个物体靠近另一个物体的角落。

  (2)方向关系(从航拍视角的绝对方向): 

- 主要方向:“north_of”(在……北方)、“south_of”(在……南方)、“east_of”(在……东方)、“west_of”(在……西方)。

- 对角线方向:“northeast_of”(在……东北方)、“northwest_of”(在……西北方)、“southeast_of”(在……东南方)、“southwest_of”(在……西南方)。 

2. 绝对方向关系优先于相对术语:

- “behind”(在……后面)转换为“north_of”(在……北方)。

- “next to”(在……旁边)转换为“adjacent_to”(相邻)。

- “in”(在……里面)转换为“contains”(包含)。  

构建节点的要求

class LLMController:    def build_scene_graph(self, subgraph, gsm):        # 过滤node_data.get("bbox", [])==[]的节点        subgraph["nodes"] = [node for node in subgraph["nodes"] if node.get("bbox", [])]        id_mapping = {}        bboxes = [node_data.get("bbox", []) for node_data in subgraph["nodes"]]
        # pos, class, confidence, timestamp        poses = gsm.bbox_to_global_pos(bboxes)        for pos, node in zip(poses, subgraph["nodes"]):            # 提取节点的所有属性            node_attrs = {                'obj_class': node['object_type'],                'confidence': 1.0,                'timestamp': self.timestep            }
            # 添加可选属性            if 'color' in node:                node_attrs['color'] = node['color']
            # 添加其他可能的属性            for key, value in node.items():                if key not in ['id', 'object_type', 'bbox']:                    node_attrs[key] = value
            # 创建节点并添加到场景图中            global_id = self.scene_graph.add_object_node_with_attrs(pos, node_attrs)
            if global_id is not None:                id_mapping[node['id']] = global_id                 parent_geo, relation_type = self._find_parent_geo(pos)                if parent_geo:                    # 添加基于空间关系的边                    self.scene_graph.add_edge(                        parent_geo.id,                         global_id,                         relation_type                    )
                    # 如果不是 "contains" 关系,还可以添加更详细的空间描述                    if relation_type != "contains" and relation_type != "adjacent_to":                        spatial_rel = self._describe_spatial_relation(                            parent_geo,                             self.scene_graph.nodes[global_id]                        )                        self.scene_graph.add_edge(                            parent_geo.id,                            global_id,                            spatial_rel                        )         # 处理边        for edge in subgraph.get("edges", []):            source = id_mapping.get(edge["source"])            target = id_mapping.get(edge["target"])            if source is None or target is None:                 continue
            # 提取边的属性            relationship = edge["relationship"]            edge_attrs = {}
            # 添加其他可能的属性            for key, value in edge.items():                if key not in ['source', 'target', 'relationship']:                    edge_attrs[key] = value
            # 添加边到场景图中            self.scene_graph.add_edge(source, target, relationship, **edge_attrs)
        self.query_engine = QueryEngine(self.scene_graph) # 用
  • 代码说明:这部分代码描述了场景图HSG的构建逻辑。

3. 定义基于LLM的目标检索模块

Step1:给定一个指令后,在分层场景图HSG中检索提及的地标节点;

Step2:根据目标与地标节点之间的关系,过滤掉错误边分支;

Step3:得到子图分支后,通过目标与其他物体的关系来定位目标;

Step4:如果查询失败,自动调整查询条件(例如,放宽关系类型),并递归地扩展搜索范围,以确保检索到最相关的结果。

将导航命令转换为一系列查询操作。可用的操作如下:

- get_geonode_by_name(name_pattern):根据名称查找地理节点。

- get_child_nodes(parent, relation_type):获取与父节点具有指定关系的子节点。

  可用的关系类型有:“包含”、“相邻”、“靠近角落”、“在……北方”、“在……南方”、“在……东方”、“在……西方”、“在……东北方”、“在……西北方”、“在……东南方”、“在……西南方”。

- `filter_by_class(obj_class)`:按类别过滤对象节点。类别可以是[“车辆”、“道路”、“建筑物”、“停车场”、“绿地” 等] 中的一个。

- `filter_by_attribute(key, value)`:按属性过滤对象节点。

注意事项:

(1) “在……前面” 的描述,在北朝上的地图中,通常对应 “在……北方” 。

(2) “在……后面” 的描述,在北朝上的地图中,通常对应 “在……南方” 。

(3) 对于 “停在道路上”,通常使用道路对象的 “包含” 关系。

(4) 当描述多个对象的相对位置时,需要找到连接这些对象的关系链。

(5) 当多个操作链表示相对关系时,要确保操作链连贯一致。

示例指令:“这是一辆停在戴维路(Davey Road)上的白色汽车。有一辆灰色汽车停在它前面,朝向反方向。”

返回的操作链:

[

{"方法": "get_geonode_by_name",

"参数": ["戴维路(Davey Road)"]},

{"方法": "get_child_nodes",

"关键字参数": {"关系类型": "包含"}},

{"方法": "filter_by_class",

"参数": ["车辆"]},

{"方法": "filter_by_attribute",

"参数": ["颜色", "白色"]}

]

当前指令:{指令} 

检索目标时用到的提示模板

class LLMController:    def query_scene_graph(self, instruction: str, debug=False):        """使用增强版的robust_subgraph_query查询场景图"""        from scenegraphnav.prompt.geonav import QUERY_OPERATION_CHAIN_PROMPT        import json
        # 使用LLM生成操作链        prompt = QUERY_OPERATION_CHAIN_PROMPT.format(instruction=instruction)        client = OpenAI(            api_key= os.environ.get("OPENAI_API_KEY"),            base_url= os.environ.get("OPENAI_BASE_URL"),        )
        response = client.chat.completions.create(            model="gpt-4-turbo",            response_format={"type": "json_object"},            messages=[                {"role": "system", "content": "You are a professional query planner."},                {"role": "user", "content": prompt}            ]        )
        try:            operation_chain = json.loads(response.choices[0].message.content)            if debug:                print(f"生成的操作链: {json.dumps(operation_chain, indent=2, ensure_ascii=False)}")
            # 使用robust_subgraph_query执行查询            results = self.query_engine.robust_subgraph_query(                operation_chain, fallback=True, min_results=1, debug=debug            )
            return results        except Exception as e:            print(f"查询场景图时发生错误: {str(e)}")            return []
  • 代码说明:这部分代码描述了对场景图HSG的查询逻辑

  4. 增量更新分层场景图HSG

def rgb_entropy(rgb_img):    hsv_img = color.rgb2hsv(rgb_img)    entropy = 0    for i in range(3):        channer = (hsv_img[..., i]*255).astype(np.uint8)        hist = cv2.calcHist([channer], [0], None, [256], [0, 256])        hist = hist / hist.sum() + 1e-10        entropy += -np.sum(hist * np.log2(hist))        return entropy
def structural_similarity(current, previous):    """计算结构相似性指标"""    from skimage.metrics import structural_similarity as ssim    return ssim(current, previous, multichannel=True, win_size=3) class ExplorationAnalyzer:    def __init__(self, threshold=0.08):        self.buffer = []        self.threshold = threshold        self.history_entropy = []        self.grid_size = 20  # 将地图划分为20x20网格
    def _grid_analysis(self, rgb_map):        """网格级信息变化检测"""        height, width = rgb_map.shape[:2]        cell_h = height // self.grid_size        cell_w = width // self.grid_size
        grid_entropy = np.zeros((self.grid_size, self.grid_size))        for i in range(self.grid_size):            for j in range(self.grid_size):                cell = rgb_map[i*cell_h:(i+1)*cell_h, j*cell_w:(j+1)*cell_w]                grid_entropy[i,j] = rgb_entropy(cell)
        return grid_entropy.std()  # 返回网格熵值的标准差     def should_continue(self, current_map, previous_map):        # 信息熵差异        current_e = rgb_entropy(current_map)        previous_e = rgb_entropy(previous_map)        delta_e = abs(current_e - previous_e)        print(f"Delta Entropy: {delta_e}")
        # # 结构相似性        # ssim_score = structural_similarity(current_map, previous_map)
        # 空间分布变化        spatial_variation = self._grid_analysis(current_map - previous_map)
        # 综合决策(可调节权重)        decision_score = delta_e #+ 0.3*(1-ssim_score) + 0.2*spatial_variation
        self.history_entropy.append(decision_score)        return decision_score > self.threshold
  • 代码说明:这部分代码描述了相似性度量的代码逻辑,通过和上一帧进行比较,进而判断是否完成目标搜索任务。

 多阶段导航策略(MNS)

  • 说明

整个导航过程分为3个阶段:导航、搜索和定位:

导航和搜索阶段采用VLA范式,结合示意性认知地图SCM得到actions;

定位阶段查询分层场景图HSG,确定最终目标,然后控制无人机到达目标位置。

  • 流程说明

针对阶段的切换,设计了一种静态任务规划期,其工作原理:

(1)导航阶段

(2)搜索阶段

(3)定位阶段

任务规划器中,采用了CoT的策略来设计各阶段的提示指令:

图片

图4 | GeoNav中MLLM的多模态推理

(a)地标导航提示(Prompting for landmark navigation)

  • 子目标描述(Subgoal Description):导航至圣约翰学院图书馆。

  • 推理依据(Rationale):告知当前不在任何地标处,圣约翰学院图书馆在东边94.1米 ,并要求答案给出思考和行动两部分,逐步给出位置和方向推理过程。

  • 答案(Answer):包含思考过程和行动结果两部分

这是典型的思维链(Chain of Thought, CoT)应用,通过逐步推理明确行动方向 。

(b)目标搜索提示(Prompting for target search)

  • 子目标描述(Subgoal Description):在图书馆附近区域搜索一栋建筑。

  • 推理依据(Rationale):提示当前在城市地标附近,应依据新颖性和目标吸引力搜索区域,并要求答案给出思考和行动两部分,基于地图逐步推理并确定移动方向。

  • 答案(Answer):包含思考过程和行动结果两部分。

利用CoT引导模型基于环境信息完成行动规划。

(c)目标定位提示(Prompting the MLLM for target localization)

  • 子目标描述(Subgoal Description):定位具体的目标建筑。

  • 推理依据(Rationale):将导航指令转换为一系列查询操作,列出可用操作,要求返回操作链。

  • 答案(Answer):包含操作链步骤

通过CoT将复杂定位任务拆解为有序步骤,实现目标定位。 ​​​​​​​

def check_subgoals(self, task):    if task['strategy'] == 'Navigate':        self.switch_time = self.controller.timestep        if self.controller.timestep > 6: #防止陷入NotFound            self.strategy_distances['Navigate'] = self.controller.pose.xy.dist_to(self.episode.target_position.xy)            return True        # 导航阶段,判断是否抵达lamdmark,是就进入下一阶段        return all(lm.position.xy.dist_to(self.controller.pose.xy) < 40.0 for lm in self.landmark_nav_map.landmark_map.landmarks)    elif task['strategy'] == 'Search':        if self.controller.timestep > (self.switch_time + 6): #防止陷入search around            self.strategy_distances['Search'] = self.controller.pose.xy.dist_to(self.episode.target_position.xy)            return True        # evaluate whether the information has changed        sem_map = self.landmark_nav_map.get_semantic_map()
        if not hasattr(self, 'exploration_analyzer'):            self.exploration_analyzer = ExplorationAnalyzer(threshold=1e-8)        # 与上一帧比较        if self.exploration_analyzer.should_continue(sem_map, self.previous_sem_map):            self.previous_sem_map = sem_map.copy()            return False    elif task['strategy'] == 'Locate':        # 利用知识推断目标名称,飞到对应位置        # 假如没找到目标,还要继续        return True    return False
def run(self):    # Step 5: check the subgoals    if self.check_subgoals(current_task):        self.subtask_status = "completed"        self.current_task_index += 1        # 检查是否所有任务完成        if self.current_task_index >= len(self.plan['sub_goals']):            print("All sub-tasks completed!")            self.strategy_distances['Locate'] = self.controller.pose.xy.dist_to(self.episode.target_position.xy)            if self.controller.reached_target(self.controller.pose, self.target):                print("Target reached.")                Success = True            pos_log.append(self.controller.pose)            self.results["steps"].append({            "time_step": self.controller.timestep,            "pose": (self.controller.pose.x, self.controller.pose.y, self.controller.pose.z, self.controller.pose.yaw),            "distance_to_target": self.controller.pose.xy.dist_to(self.target.xy),            "plan": current_task,            "observation_suggestion":self.observation,            "action_suggestion": self.action,        })            break  # 提前退出循环
  • 代码说明:这部分代码描述了导航策略切换的逻辑。

图片

实验

  • 评估指标:

(1)NE是导航误差,智能体最终位置与目标真实位置的欧氏距离;

(2)SR是导航成功率,智能体导航到的最终位置在目标位置的20米范围内,视为导航成功,计算导航成功的百分比;

(3)OSR 指智能体导航过程中是否出现距离目标位置小于阈值20米的情况,计算出现这种情况占总导航次数的百分比;

(4)SPL是成功率乘以到目标位置的最短路径长度与实际路径长度的比率。

图片

图5 | 与当前SOTAs相比的整体空中导航性能

  • 说明:

如上表所示,将GeoNav与当前最先进的方法在CityNav数据集上进行比较,可以发现,GeoNav远超其他方法。

(1)在简单难度任务中,GeoNav的SR指标比第二名的MGP高出12.53%,OSR指标高出49.47%;

(2)在中等难度任务中,GeoNav的SR指标比第二名的MGP高出6.25%,OSR指标高出20.83%;

(3)在困难难度任务中,GeoNav的SR指标比第二名的MGP高出 8.34%,OSR指标高出12.5%。

(4)在SPL指标上,GeoNav也表现出色,说明GeoNav很好的平衡了导航成功率和路径规划效率。

图片

图6 | 在CityNav验证中不同组件的消减情况

说明:

如上表所示,是GeoNav在CityNav验证集上进行不同组件组合的消融实验

(1)仅使用SCM组件时,导航误差精度NE最大,且成功率都很低;

(2)使用SCM+HSG的组合,导航误差精度减少18.8米,SR、OSR和SPL分别提高2.28%、27.78%和3.03%;

(3)使用SCM+MNS的组合,效果和SCM+HSG的组合相当,但在SR和SPL上明显落后于完整的组合;

(4)使用HSG+MNS的组合,相较于完整的组合,SR大幅下降了17.64%。

综上所述,各模块组件对提升整体性能都是至关重要的。

图片

结论

GeoNav框架在语言目标空中导航任务中取得一定成果,但导航成功率和效率仍有提升空间。未来可优化多模态思维链提示调度框架,联合构建和推理两种空间表示以提高模型性能。

论文题目:GeoNav: Empowering MLLMs with Explicit Geospatial Reasoning Abilities for Language-Goal Aerial Navigation

论文作者:Haotian Xu, Yue Hu, Chen Gao, Zhengqiu Zhu, Yong Zhao, Yong Li, Quanjun Yin

论文链接:https://arxiv.org/pdf/2504.09587

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值