简介:这份资源面向无人机自主飞行算法学习者与竞赛参与者,聚焦复杂仿真环境下穿越多个移动障碍物并抵达终点的完整实现方案。项目基于UE4与AirSim搭建仿真场景,采用霍夫圆检测识别圆形障碍物,结合卡尔曼滤波与运动模态识别预测障碍物运动轨迹,并通过PID控制完成轨迹优化,是学习感知、预测与控制全链路协同的典型实践案例。压缩包共2个文件,包含1个Python主程序与1个Markdown说明文档,整体约7KB,体量轻便,便于快速阅读与二次开发。该方案曾获上海交通大学电子信息与电气工程学院主办的“智在飞翔”无人飞行器智能感知技术竞赛三等奖第一名,目前已有2454人学习下载。读者可从中获取赛题级算法思路、仿真环境配置要点与代码组织方式,适合具备一定Python与自动控制基础、希望深入理解移动障碍物避障策略的开发者参考。
1. 复杂环境下的无人机自主飞行,AirSim 能帮你验证到什么程度
城市楼宇间的 GPS 拒止、树林里的动态障碍、室内走廊的狭窄转弯——这些场景在真机上试一次,炸机成本少则几千,多则整个项目停摆。AirSim 仿真平台的价值就在这里:它把 Unreal Engine 的视觉渲染和物理引擎接在一起,让你在电脑里先把自主飞行算法跑通,再决定要不要上真机。这个标题指向的是一套完整的工程链路:环境搭建、传感器配置、飞控接口对接、路径规划算法验证、复杂场景注入。适合已经写过 Python 或 C++、对无人机有基本认知、但不想拿真机试错的开发者。读完你应该能判断:自己的算法在仿真里能验证到什么颗粒度,哪些坑必须提前知道。
2. AirSim 环境搭建与无人机模型配置:从零到能起飞
2.1 为什么选 AirSim 而不是纯数学模型或游戏引擎
做无人机自主飞行验证,常见路线有三条:纯 MATLAB/Simulink 数学模型、Gazebo + PX4 SITL、AirSim + PX4。纯数学模型的优势是轻量、可解析,但传感器数据是理想化的,视觉感知算法根本没法验证。Gazebo 生态成熟,但渲染质量差,做视觉 SLAM 或目标检测时,图像域和真实场景差距太大,模型迁移到真机容易翻车。AirSim 的核心差异在于它构建在 Unreal Engine 上,视觉逼真度接近游戏级别,同时提供了 Python/C++ API 和 ROS 接口,能直接拿来做无人机视觉感知、路径规划、避障的闭环验证。
选 AirSim 的另一个理由是它的 API 设计。你不需要懂 Unreal 的蓝图系统,用 Python 就能控制无人机的位置、速度、姿态,还能直接读取深度图、RGB 图、激光雷达点云。对于做无人机路径规划和视觉感知的团队,这意味着算法迭代周期从「改代码→烧固件→试飞→炸机→重来」压缩到「改代码→仿真跑→看结果」。
但 AirSim 不是没有代价。它的硬件门槛比 Gazebo 高,官方推荐 16GB 内存起步,实际跑复杂场景建议 32GB 加独立显卡。如果你的机器只有集成显卡,建议先降低渲染质量或改用 Blocks 环境。
2.2 环境搭建的最小可行步骤
以下步骤基于 Windows 10/11 + Visual Studio 2022 + Python 3.8+ 的常见组合。Linux 下流程类似,但 Unreal 的编译工具链换成 clang。
第一步,安装 Unreal Engine。AirSim 官方推荐 UE 4.27 或 UE 5.0+,但注意 UE 5.0 之后的 API 有变动,部分社区插件可能不兼容。如果你只是跑预编译的 Blocks 环境,可以跳过 UE 安装,直接下载 AirSim 的预编译二进制包。
第二步,获取 AirSim 源码并编译。
git clone https://github.com/microsoft/AirSim.git cd AirSim # Windows 下用 build.cmd,Linux 下用 ./build.sh ./build.sh编译完成后,你会得到AirSimNH或Blocks可执行文件。Blocks是一个轻量场景,适合快速验证飞控逻辑;AirSimNH是更复杂的城市环境,适合测试避障和路径规划。
第三步,配置settings.json。这个文件决定了无人机用哪种飞控、传感器怎么开、仿真频率多少。
{ "SettingsVersion": 1.2, "SimMode": "Multirotor", "ClockSpeed": 1.0, "Vehicles": { "Drone1": { "VehicleType": "SimpleFlight", "AutoCreate": true, "Sensors": { "LidarSensor1": { "SensorType": 6, "Enabled": true, "NumberOfChannels": 16, "Range": 100 }, "CameraFront": { "SensorType": 0, "Enabled": true, "CaptureSettings": [ { "ImageType": 0, "Width": 640, "Height": 480, "FOV_Degrees": 90 } ] } } } } }SimMode设为Multirotor表示多旋翼模式。VehicleType用SimpleFlight是 AirSim 内置的简化飞控,适合快速验证;如果要接 PX4,改成PX4Multirotor并启动 PX4 SITL。ClockSpeed控制仿真时间流速,1.0 是实时,设成 0.5 可以慢放,方便调试高频控制逻辑。激光雷达的NumberOfChannels和Range按你的避障算法需求调,16 线 100 米是常见配置。
第四步,启动仿真并连接 Python API。
import airsim import time # 连接仿真环境 client = airsim.MultirotorClient() client.confirmConnection() # 解锁电机并起飞 client.enableApiControl(True) client.armDisarm(True) client.takeoffAsync().join() # 飞到指定位置:北向 10 米,东向 5 米,高度 -8 米(NED 坐标系) client.moveToPositionAsync(10, 5, -8, 5).join() # 读取前向摄像头的 RGB 图像 responses = client.simGetImages([ airsim.ImageRequest("CameraFront", airsim.ImageType.Scene, False, False) ]) print(f"图像尺寸: {responses[0].width}x{responses[0].height}") # 降落 client.landAsync().join() client.armDisarm(False) client.enableApiControl(False)这段代码做了四件事:连接仿真、起飞、飞到目标点、读图。moveToPositionAsync的第四个参数是速度(m/s),AirSim 用的是 NED 坐标系,所以高度是负值。simGetImages返回的是原始字节流,实际做视觉算法时需要转成 numpy 数组。
提示:如果
confirmConnection()报超时,先检查仿真程序是否已经启动,再确认settings.json里的ApiServerPort是否被防火墙拦截。
3. 复杂环境注入与传感器数据获取:让仿真场景真正「复杂」起来
3.1 复杂环境的三个维度:几何、动态、感知退化
「复杂环境」这个词在论文里经常被滥用,落到工程上,我一般把它拆成三个维度。几何复杂度指障碍物密度和通道宽度,比如城市楼宇间的峡谷、树林里的枝干交错。动态复杂度指移动障碍物的数量和运动不确定性,比如其他无人机、行人、车辆。感知退化指传感器在特定条件下的性能下降,比如低光照、烟雾、纹理缺失的墙面。
AirSim 默认的 Blocks 环境几何简单、无动态障碍、光照理想,直接跑路径规划算法会得到过于乐观的结果。你需要主动注入复杂度。常见做法有三种:换用 AirSimNH 或自定义 Unreal 场景、用 API 动态生成障碍物、在传感器数据上加噪声。
3.2 用 Python API 动态生成障碍物和移动目标
AirSim 提供了simSpawnObject和simSetObjectPose接口,可以在运行时往场景里加物体。下面这段代码在无人机前方生成一排圆柱体作为障碍,并让其中一个横向移动。
import airsim import time client = airsim.MultirotorClient() client.confirmConnection() # 在场景中生成静态障碍物 for i in range(5): pose = airsim.Pose( airsim.Vector3r(15 + i * 3, 0, -5), airsim.to_quaternion(0, 0, 0) ) client.simSpawnObject( f"Obstacle_{i}", "Cylinder", pose, airsim.Vector3r(1, 1, 5), 1.0, False ) # 生成一个移动障碍物 client.simSpawnObject( "MovingTarget", "Cylinder", airsim.Pose(airsim.Vector3r(20, -5, -5), airsim.to_quaternion(0, 0, 0)), airsim.Vector3r(1, 1, 3), 1.0, False ) # 让移动障碍物横向往复运动 for step in range(100): y = -5 + 5 * (1 if (step // 10) % 2 == 0 else -1) client.simSetObjectPose( "MovingTarget", airsim.Pose(airsim.Vector3r(20, y, -5), airsim.to_quaternion(0, 0, 0)) ) time.sleep(0.1)simSpawnObject的第一个参数是物体名,第二个是资产名(AirSim 内置了 Cylinder、Sphere 等基础几何体),第三个是位姿,第四个是缩放,第五个是质量,第六个是否可碰撞。simSetObjectPose每调用一次就更新一次位置,配合循环就能模拟移动障碍。
注意:动态障碍物的更新频率受限于 Python 循环速度,实际测试中大约 10-20Hz。如果你的避障算法需要更高频率的动态目标,建议用 C++ API 或直接在 Unreal 里写蓝图。
3.3 传感器数据获取与噪声注入
做视觉感知的读者最关心的是图像质量。AirSim 的simGetImages支持多种 ImageType:Scene(RGB)、DepthPlanar(深度)、Segmentation(语义分割)。下面是一个获取深度图并转成点云的例子。
import airsim import numpy as np import cv2 client = airsim.MultirotorClient() client.confirmConnection() # 同时获取 RGB 和深度图 responses = client.simGetImages([ airsim.ImageRequest("CameraFront", airsim.ImageType.Scene, False, False), airsim.ImageRequest("CameraFront", airsim.ImageType.DepthPlanar, True) ]) # RGB 转 numpy rgb = np.frombuffer(responses[0].image_data_uint8, dtype=np.uint8) rgb = rgb.reshape(responses[0].height, responses[0].width, 3) # 深度图转 numpy(float32,单位米) depth = np.array(responses[1].image_data_float, dtype=np.float32) depth = depth.reshape(responses[1].height, responses[1].width) # 给深度图加高斯噪声,模拟真实传感器退化 noise = np.random.normal(0, 0.05, depth.shape) depth_noisy = depth + noise # 保存可视化结果 cv2.imwrite("rgb.png", rgb) cv2.imwrite("depth.png", (depth_noisy / depth_noisy.max() * 255).astype(np.uint8))ImageRequest的第三个参数pixels_as_float设为 True 时返回浮点数据,深度图必须用这个模式。DepthPlanar返回的是平面深度,单位是米;如果需要射线距离,用DepthPerspective。噪声注入是感知算法验证的关键一步,真实深度相机在远距离和低反射表面上的噪声远不止高斯噪声,但高斯噪声至少能让你发现算法对噪声的敏感度。
4. 自主飞行算法在 AirSim 中的验证:路径规划与避障的落地细节
4.1 从 A* 到 RRT:在仿真里对比路径规划算法
路径规划算法在论文里比的是路径长度和计算时间,但在 AirSim 里,你还要关心轨迹是否可跟踪、是否满足无人机动力学约束。A* 在栅格地图上跑得快,但生成的折线路径需要后处理才能让无人机飞得平滑。RRT 适合高维空间,但随机性导致每次结果不一致,调试时容易怀疑人生。
我一般会先在 AirSim 里用 A* 生成粗路径,再用样条曲线平滑,最后交给飞控跟踪。下面是一个简化的 A* 实现,在 2D 栅格上规划,然后映射到 AirSim 的 NED 坐标。
import airsim import numpy as np import heapq def astar(grid, start, goal): """A* 路径规划,grid 中 0 可通行,1 障碍""" rows, cols = grid.shape open_set = [(0, start)] came_from = {} g_score = {start: 0} while open_set: _, current = heapq.heappop(open_set) if current == goal: path = [] while current in came_from: path.append(current) current = came_from[current] return path[::-1] for dx, dy in [(-1,0),(1,0),(0,-1),(0,1),(-1,-1),(-1,1),(1,-1),(1,1)]: neighbor = (current[0]+dx, current[1]+dy) if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): continue if grid[neighbor] == 1: continue tentative_g = g_score[current] + np.hypot(dx, dy) if neighbor not in g_score or tentative_g < g_score[neighbor]: came_from[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + np.hypot(neighbor[0]-goal[0], neighbor[1]-goal[1]) heapq.heappush(open_set, (f, neighbor)) return [] # 构建 20x20 栅格,中间放一堵墙 grid = np.zeros((20, 20)) grid[5:15, 10] = 1 path = astar(grid, (2, 2), (18, 18)) print(f"路径长度: {len(path)}") # 连接 AirSim 并跟踪路径 client = airsim.MultirotorClient() client.confirmConnection() client.enableApiControl(True) client.armDisarm(True) client.takeoffAsync().join() for (r, c) in path: # 栅格坐标转 NED:每格 2 米,起点偏移 x = (r - 10) * 2 y = (c - 10) * 2 client.moveToPositionAsync(x, y, -5, 3).join() client.landAsync().join()这段代码的关键在坐标映射。栅格的行列对应 NED 的北向和东向,每格代表 2 米,起点偏移让路径居中。moveToPositionAsync的第四个参数是速度,设成 3 m/s 比较接近真实多旋翼的巡航速度。实际测试中,A* 的折线路径会让无人机在拐角处急停急转,建议在路径点之间插入样条插值。
4.2 避障验证:用激光雷达数据做反应式避障
路径规划是全局的,避障是局部的。AirSim 的激光雷达返回点云,你可以用简单的势场法或 VFH 做反应式避障。下面是一个基于最近障碍距离的减速逻辑。
import airsim import numpy as np client = airsim.MultirotorClient() client.confirmConnection() client.enableApiControl(True) client.armDisarm(True) client.takeoffAsync().join() # 向前飞,同时监测前方障碍 for step in range(200): # 获取激光雷达数据 lidar_data = client.getLidarData("LidarSensor1") if len(lidar_data.point_cloud) < 3: client.moveByVelocityAsync(2, 0, 0, 0.1).join() continue points = np.array(lidar_data.point_cloud, dtype=np.float32) points = points.reshape(-1, 3) # 只关心前方 60 度锥角内的点 forward = points[(points[:, 0] > 0) & (np.abs(points[:, 1]) < points[:, 0] * 0.57)] if len(forward) > 0: min_dist = np.min(forward[:, 0]) if min_dist < 3.0: # 障碍太近,减速并侧移 client.moveByVelocityAsync(0.5, 1.5, 0, 0.1).join() else: client.moveByVelocityAsync(2, 0, 0, 0.1).join() else: client.moveByVelocityAsync(2, 0, 0, 0.1).join() client.landAsync().join()getLidarData返回的点云是扁平数组,每三个数一个点。points[:, 0]是前向距离,points[:, 1]是侧向距离。锥角判断用abs(y) < x * tan(30°)近似。这个逻辑很粗糙,但能让你快速验证避障回路的延迟和稳定性。实际项目中,我会用 Octomap 或 ESDF 做更精细的障碍表示。
提示:激光雷达点云在 AirSim 里默认是传感器坐标系,如果你需要世界坐标系,用
client.simGetVehiclePose()做变换。
5. 避坑与排查:AirSim 仿真中那些让人抓狂的瞬间
5.1 无人机起飞后原地打转或直接坠地
现象:调用takeoffAsync()后,无人机螺旋桨转但不起飞,或者升到一半突然掉高度。
原因:最常见的是settings.json里SimpleFlight的参数不匹配。AirSim 的简化飞控对质量、推力系数敏感,如果你改了无人机模型但没改飞控参数,推力不够就会坠地。另一个原因是仿真频率和 API 调用频率不匹配,ClockSpeed设得太高导致物理步进不稳定。
解决:先用默认的SimpleFlight配置跑通,确认能稳定悬停后再改参数。如果必须自定义模型,在settings.json里显式设置ThrustCoefficient和Mass,并逐步调整。另外把ClockSpeed设回 1.0,不要为了跑快而牺牲稳定性。
5.2 图像获取返回全黑或全白
现象:simGetImages返回的 RGB 图像全是 0 或 255,深度图全是无穷大。
原因:相机没有正确附着在无人机上,或者CaptureSettings里的ImageType和实际请求的不匹配。AirSim 的相机需要在settings.json里显式声明,并且AutoCreate为 true 时才会随无人机创建。另一个常见原因是仿真程序没有获得显卡渲染权限,比如在远程桌面或虚拟机里跑。
解决:检查settings.json的Sensors段,确认相机名称和simGetImages里的名称一致。如果是远程桌面,改用本地控制台或确保 GPU 直通。深度图全无穷大通常是因为场景里没有物体在相机范围内,把无人机飞到有障碍的地方再试。
5.3 API 调用超时或连接断开
现象:Python 脚本跑几分钟后报RpcException或Timeout,仿真程序还在运行但 API 无响应。
原因:AirSim 的 RPC 服务在高频调用下会积压消息,尤其是simGetImages和getLidarData同时高频调用时。另外,如果 Python 脚本异常退出没有释放连接,下次连接可能被拒绝。
解决:在脚本里加try/except确保异常时调用enableApiControl(False)和armDisarm(False)。降低传感器读取频率,图像和激光雷达不要在同一帧里都读。如果连接被拒绝,重启仿真程序。
5.4 路径规划结果在仿真里飞得歪歪扭扭
现象:A* 或 RRT 生成的路径在数学上最短,但无人机跟踪时频繁过冲、震荡。
原因:路径点太稀疏,飞控在两点之间做直线插值,到了路径点才转向,导致急停急转。另外 AirSim 的moveToPositionAsync默认用的是位置控制,没有速度前馈,跟踪折线路径时滞后明显。
解决:在路径点之间做样条插值,生成密集的航点序列。或者改用moveByVelocityAsync做速度控制,配合路径跟踪算法(如纯追踪)。我一般会在 A* 路径上每隔 0.5 米插一个点,再用三次样条平滑。
5.5 动态障碍物穿模或不受重力
现象:用simSpawnObject生成的物体悬浮在空中,或者移动时穿过其他物体。
原因:AirSim 的simSpawnObject默认不启用物理模拟,物体是运动学控制的。如果你需要物体受重力或碰撞响应,需要在 Unreal 里设置物理属性,或者用simSetObjectPose手动模拟运动。
解决:对于动态障碍物,手动用simSetObjectPose更新位置是最可控的方式。如果需要物理交互,在 Unreal 编辑器里给物体加Simulate Physics,但注意这会增加计算开销。
6. 用 AirSim 做硬件在环与算法迁移的进阶技巧
仿真验证到最后,总要面对「仿真里跑通了,真机上行不行」的问题。AirSim 支持硬件在环(HITL),可以把真实的飞控板接进来,仿真环境提供传感器数据,飞控输出电机指令。这条路能大幅缩小仿真和真机的差距,但配置复杂度也上一个台阶。
我自己的习惯是先在SimpleFlight下把算法逻辑跑通,确认路径规划和避障的决策没问题,再切到 PX4 SITL 验证飞控接口,最后才上 HITL。每一步的验证重点不同:SimpleFlight看算法逻辑,PX4 SITL 看消息接口和时序,HITL 看真实传感器的噪声和延迟。
如果你要做视觉感知的迁移,AirSim 的图像域和真实相机仍有差距。我的做法是在 AirSim 里生成大量带标注的数据,训练一个基线模型,然后在真实数据上做微调。AirSim 的Segmentation图像类型可以直接生成语义分割标签,省去人工标注。但注意,仿真里的纹理和光照分布和真实场景不同,直接拿仿真数据训练、真机测试,mAP 通常会掉 20-30 个百分点。域随机化是缓解这个问题的常见手段:在 AirSim 里随机化光照、纹理、物体位置,让模型见到更多变化。
还有一个容易被忽略的点:AirSim 的仿真时间步长和真实飞控的时钟不同步。在 HITL 模式下,如果仿真步长设得太大,飞控的积分会累积误差,导致姿态控制发散。我一般会把ClockSpeed设成 0.8-0.9,让仿真稍慢于实时,给飞控留出计算余量。
最后说一个验证技巧:在 AirSim 里跑算法时,同时记录仿真状态和算法内部状态,用时间戳对齐。这样当无人机在某个弯道翻车时,你能回放看到是感知延迟、规划错误还是控制饱和。这个「黑匣子」习惯帮我省了无数次瞎猜的时间。希望帮到你。
本文还有配套的精品资源,点击获取