3. 考虑建筑物高度的三维航迹规划

任务说明

本案例针对城市低空环境,将建筑物高度信息融入三维栅格地图,实现考虑建筑物高度的三维航迹规划。输入为城市区域内的建筑物分布(含位置与高度),输出为从起点到终点、能够避开所有建筑物的三维航迹点序列。核心约束为:航迹在水平方向上不得穿越建筑物占地范围,在垂直方向上不得低于建筑物顶部高度(即航迹点高度需大于所覆盖栅格处的建筑物高度)。

三维栅格地图构建

将城市区域离散化为三维栅格,每个栅格用(x, y, z)索引表示,其中x、y为水平坐标,z为高度层索引。栅格状态分为两类:自由栅格(可通行)与障碍栅格(不可通行)。建筑物占据的水平栅格范围内,所有高度低于或等于建筑物高度的栅格均标记为障碍;高于建筑物顶部的栅格保持自由。航迹规划在三维栅格空间中进行,A*算法搜索从起点栅格到终点栅格的最优路径。

代码实现

import heapq
import math

class Building:
    """建筑物定义:中心坐标、底面半宽、高度"""
    def __init__(self, cx, cy, half_width, height):
        self.cx = cx
        self.cy = cy
        self.half_width = half_width
        self.height = height

    def contains(self, x, y):
        """判断水平坐标(x,y)是否在建筑物占地范围内"""
        return (abs(x - self.cx) <= self.half_width and
                abs(y - self.cy) <= self.half_width)

class GridMap3D:
    """三维栅格地图"""
    def __init__(self, x_min, x_max, y_min, y_max, z_max, resolution):
        self.x_min = x_min
        self.x_max = x_max
        self.y_min = y_min
        self.y_max = y_max
        self.z_max = z_max          # 最大高度层索引(含)
        self.res = resolution       # 栅格边长(米)
        self.x_size = int((x_max - x_min) / resolution) + 1
        self.y_size = int((y_max - y_min) / resolution) + 1
        self.z_size = z_max + 1
        # 障碍标记:True表示该栅格不可通行
        self.obstacle = [[[False for _ in range(self.z_size)]
                          for _ in range(self.y_size)]
                         for _ in range(self.x_size)]

    def world_to_grid(self, wx, wy, wz):
        """世界坐标转栅格索引"""
        gx = int((wx - self.x_min) / self.res)
        gy = int((wy - self.y_min) / self.res)
        gz = int(wz / self.res)
        return gx, gy, gz

    def grid_to_world(self, gx, gy, gz):
        """栅格索引转世界坐标(取栅格中心)"""
        wx = self.x_min + (gx + 0.5) * self.res
        wy = self.y_min + (gy + 0.5) * self.res
        wz = (gz + 0.5) * self.res
        return wx, wy, wz

    def add_buildings(self, buildings):
        """将建筑物写入地图:占地范围内低于或等于楼顶的栅格设为障碍"""
        for b in buildings:
            # 建筑物覆盖的栅格范围
            gx0 = int((b.cx - b.half_width - self.x_min) / self.res)
            gx1 = int((b.cx + b.half_width - self.x_min) / self.res)
            gy0 = int((b.cy - b.half_width - self.y_min) / self.res)
            gy1 = int((b.cy + b.half_width - self.y_min) / self.res)
            gz_top = int(b.height / self.res)
            for gx in range(max(0, gx0), min(self.x_size, gx1 + 1)):
                for gy in range(max(0, gy0), min(self.y_size, gy1 + 1)):
                    for gz in range(0, min(self.z_size, gz_top + 1)):
                        self.obstacle[gx][gy][gz] = True

    def is_free(self, gx, gy, gz):
        """判断栅格是否自由(在地图范围内且非障碍)"""
        if (0 <= gx < self.x_size and 0 <= gy < self.y_size
                and 0 <= gz < self.z_size):
            return not self.obstacle[gx][gy][gz]
        return False

    def get_neighbors(self, gx, gy, gz):
        """获取26邻域中自由且在地图内的邻居栅格"""
        neighbors = []
        for dx in (-1, 0, 1):
            for dy in (-1, 0, 1):
                for dz in (-1, 0, 1):
                    if dx == 0 and dy == 0 and dz == 0:
                        continue
                    nx, ny, nz = gx + dx, gy + dy, gz + dz
                    if self.is_free(nx, ny, nz):
                        neighbors.append((nx, ny, nz))
        return neighbors

def heuristic(a, b):
    """三维欧氏距离启发函数"""
    return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2 + (a[2]-b[2])**2)

def astar_3d(grid, start_w, goal_w):
    """三维A*航迹规划,输入输出均为世界坐标"""
    start = grid.world_to_grid(*start_w)
    goal = grid.world_to_grid(*goal_w)

    # 检查起终点合法性
    if not grid.is_free(*start):
        raise ValueError("起点位于障碍物内或地图外")
    if not grid.is_free(*goal):
        raise ValueError("终点位于障碍物内或地图外")

    open_set = []
    heapq.heappush(open_set, (0.0, start))
    came_from = {}
    g_score = {start: 0.0}
    f_score = {start: heuristic(start, goal)}

    while open_set:
        _, current = heapq.heappop(open_set)
        if current == goal:
            # 回溯路径
            path = []
            while current in came_from:
                path.append(current)
                current = came_from[current]
            path.append(start)
            path.reverse()
            # 转为世界坐标
            return [grid.grid_to_world(*p) for p in path]

        for neighbor in grid.get_neighbors(*current):
            # 移动代价:直线移动为1,对角移动为sqrt(2)或sqrt(3)
            dx = neighbor[0] - current[0]
            dy = neighbor[1] - current[1]
            dz = neighbor[2] - current[2]
            move_cost = math.sqrt(dx*dx + dy*dy + dz*dz)
            tentative_g = g_score[current] + move_cost
            if tentative_g < g_score.get(neighbor, float('inf')):
                came_from[neighbor] = current
                g_score[neighbor] = tentative_g
                f = tentative_g + heuristic(neighbor, goal)
                f_score[neighbor] = f
                heapq.heappush(open_set, (f, neighbor))

    return None  # 无可行路径

# ========== 示例:构建地图并规划 ==========
if __name__ == "__main__":
    # 地图范围:100m x 100m,最大高度50m,栅格分辨率1m
    grid = GridMap3D(0, 100, 0, 100, 50, 1.0)

    # 定义三栋建筑物
    buildings = [
        Building(30, 30, 5, 20),    # 中心(30,30),半宽5m,高20m
        Building(60, 45, 8, 35),    # 中心(60,45),半宽8m,高35m
        Building(40, 70, 6, 15),    # 中心(40,70),半宽6m,高15m
    ]
    grid.add_buildings(buildings)

    # 起点与终点(世界坐标)
    start_w = (5.0, 5.0, 5.0)       # 低空起点
    goal_w = (90.0, 90.0, 30.0)     # 目标点

    path = astar_3d(grid, start_w, goal_w)

    if path:
        print(f"规划成功,航迹点数量:{len(path)}")
        for i, wp in enumerate(path):
            print(f"点{i}: ({wp[0]:.1f}, {wp[1]:.1f}, {wp[2]:.1f})")
    else:
        print("未找到可行航迹")

关键实现说明

  • 障碍判定逻辑:add_buildings方法将建筑物水平占地范围内、高度不超过楼顶的栅格全部标记为障碍。因此航迹在水平方向不会穿越建筑,在垂直方向不会低于楼顶高度。
  • 三维搜索空间:A*算法在三维栅格中搜索,每个节点可向26个邻域方向移动,移动代价按实际欧氏距离计算,保证路径在三维空间中平滑且最短。
  • 起终点合法性检查:若起点或终点落在障碍栅格内,直接抛出异常,避免无效搜索。

运行与验证

运行方式:将代码保存为plan_3d.py,在Python 3环境中执行python plan_3d.py。代码仅依赖Python标准库,无需额外安装包。

验证方法:观察输出航迹点序列,逐点检查以下条件:

  1. 每个航迹点对应的栅格均为自由栅格(即不在任何建筑物内部或下方);
  2. 航迹点从起点(5,5,5)连续变化至终点(90,90,30),相邻点间距不超过栅格对角距离(约1.73m);
  3. 若输出“规划成功”,说明存在可行路径;若输出“未找到可行航迹”,说明当前起终点间无满足避障条件的路径。

判定标准:程序正常输出航迹点序列且无异常抛出,且人工检查各点均满足上述避障条件,即可判定案例实现正确。

三维航迹规划验证示意图(俯视投影 + 高度标注) X: 0m X: 50m X: 100m Y: 100m Y: 50m Y: 0m B1 高20m B2 高35m B3 高15m 起点 (5,5,5) 终点 (90,90,30) z=8 z=12 z=18 z=36 z=32 z=30 图例 建筑物(障碍) 起点 终点 三维航迹 航迹高度需高于 所经位置建筑物顶部 高度约束 • B1顶部: 20m • B2顶部: 35m • B3顶部: 15m 航迹z需>对应位置楼顶