简介:这份资源面向无人机自主飞行与智能感知方向的学习者与竞赛选手,聚焦复杂仿真环境下穿越多个移动障碍物并抵达终点的完整实现。项目基于UE4与AirSim搭建仿真场景,采用霍夫圆检测识别圆形障碍物,结合卡尔曼滤波与运动模态识别预测障碍物运动趋势,并通过PID控制完成轨迹优化,是理解感知—预测—控制闭环的典型实践案例。压缩包共2个文件,包含1个Python主程序与1个Markdown说明文档,整体约7KB,体量轻便,便于快速阅读与二次开发。该方案曾获上海交通大学电子信息与电气工程学院主办的“智在飞翔”无人飞行器智能感知技术竞赛三等奖第一名,目前已有2454人学习下载。读者可从中获取赛题级算法思路、仿真环境下的代码组织方式与调参排错参考,适合具备一定Python与自动控制基础、希望深入无人机自主导航的读者研读。
1. 复杂环境无人机自主飞行:AirSim 仿真平台到底能帮你验证什么
炸机是每个飞控工程师绕不开的痛。真机在树林里撞一次,轻则断桨重则烧电调,更别提复杂环境下的自主避障、动态路径重规划这些高风险科目——真机试错成本高到离谱。AirSim 仿真平台就是来解决这个问题的:它基于虚幻引擎构建,提供物理精度可调的无人机动力学模型和视觉渲染,让你在虚拟环境里把自主飞行算法跑通再上真机。这个方案适合做无人机路径规划、视觉感知、飞控算法验证的从业者和学生,尤其是需要快速迭代但不想反复修飞机的团队。复杂环境指的是有障碍物、有动态目标、有光照变化的场景,自主飞行则要求无人机不依赖遥控器,靠机载算法完成感知-决策-控制闭环。
2. AirSim 仿真环境搭建与无人机模型配置
2.1 为什么选 AirSim 而不是 Gazebo 或 Webots
机器人仿真平台选择这件事,我踩过不少坑。Gazebo 的物理引擎对多旋翼的气动建模偏简化,做路径规划验证够用,但一旦涉及视觉感知和复杂光照,渲染质量就拖后腿。Webots 上手快,可它的场景规模和材质系统撑不起“复杂环境”这四个字。AirSim 的优势在于虚幻引擎的渲染管线——你可以直接导入建筑模型、植被资产、甚至用无人机航拍工地数据公开数据集里的纹理做场景贴图,视觉感知算法的验证结果更接近真机表现。
另一个关键点是 AirSim 的 API 设计。它提供 Python 和 C++ 接口,能直接拿到 IMU、GPS、气压计、双目相机、深度图、激光雷达的仿真数据,控制指令支持位置、速度、姿态多种模式。这意味着你可以把真机飞控代码里的感知模块和决策模块原封不动搬进来,只替换底层通信层。我一般会建议团队先用 AirSim 跑通算法逻辑,再迁移到硬件在环仿真平台做半实物验证,最后上真机。
2.2 从零搭建 AirSim 环境的完整步骤
先确认硬件底线:显卡 GTX 1060 起步,推荐 RTX 3060 以上,内存 16GB 打底。虚幻引擎对显卡的消耗主要在渲染线程,复杂场景下显存占用可能超过 6GB。
第一步,装 Epic Games Launcher,下载虚幻引擎 4.27 版本。别追新版本,AirSim 对 UE5 的支持在社区版里仍有兼容性问题,4.27 是经过验证的稳定选择。
第二步,获取 AirSim 插件。常见做法是从 GitHub 克隆 AirSim 仓库,然后运行build.cmd编译插件。编译过程会拉取 Eigen、rpclib 等依赖,网络环境不好的话容易卡住,建议提前配好镜像源。
# 克隆 AirSim 仓库 git clone https://github.com/microsoft/AirSim.git cd AirSim # 运行编译脚本,Windows 下用 build.cmd ./build.sh # 编译完成后,插件位于 Unreal/Plugins/AirSim 目录编译脚本会同时生成 Python 客户端包,位于PythonClient目录下。用pip install -e .安装,后续在 Python 脚本里import airsim就能调用。
第三步,创建虚幻项目并导入插件。打开 UE4.27 编辑器,新建一个 Blank 项目,把编译好的AirSim插件文件夹复制到项目根目录的Plugins下。重启编辑器,在 World Settings 里把 GameMode 改为 AirSimGameMode。这时按 Play,你应该能看到默认的 Landscape 场景和一台四旋翼无人机。
第四步,配置settings.json。这个文件放在Documents/AirSim目录下,控制无人机的数量、传感器类型、仿真参数。下面是一个带双目相机和激光雷达的配置示例:
{ "SettingsVersion": 1.2, "SimMode": "Multirotor", "Vehicles": { "Drone1": { "VehicleType": "SimpleFlight", "AutoCreate": true, "Cameras": { "front_left": { "CaptureSettings": [ { "ImageType": 0, "Width": 640, "Height": 480, "FOV_Degrees": 90 } ], "X": 0.2, "Y": -0.1, "Z": -0.1, "Pitch": 0, "Roll": 0, "Yaw": 0 }, "front_right": { "CaptureSettings": [ { "ImageType": 0, "Width": 640, "Height": 480, "FOV_Degrees": 90 } ], "X": 0.2, "Y": 0.1, "Z": -0.1, "Pitch": 0, "Roll": 0, "Yaw": 0 }, "depth": { "CaptureSettings": [ { "ImageType": 1, "Width": 320, "Height": 240, "FOV_Degrees": 90 } ], "X": 0.2, "Y": 0, "Z": -0.1, "Pitch": 0, "Roll": 0, "Yaw": 0 } }, "Lidar": { "NumberOfChannels": 16, "RotationsPerSecond": 10, "PointsPerSecond": 100000, "X": 0, "Y": 0, "Z": -0.2, "DrawDebugPoints": true } } } }ImageType: 0表示场景彩色图,ImageType: 1表示深度图。X/Y/Z是相机相对机体中心的安装位置,单位米。Pitch/Roll/Yaw是安装角度,单位度。激光雷达的NumberOfChannels决定线束,16 线在路径规划里够用,32 线以上适合做高精度建图。
2.3 用 Python API 控制无人机起飞与获取传感器数据
环境跑通后,先用最小脚本验证通信链路。下面这段代码让无人机起飞到 5 米高度,悬停 3 秒,然后降落,同时保存一张前视相机图像:
import airsim import cv2 import numpy as np import time # 连接仿真器,默认端口 41451 client = airsim.MultirotorClient() client.confirmConnection() # 解锁电机并起飞 client.enableApiControl(True) client.armDisarm(True) client.takeoffAsync().join() # 飞到 5 米高度,悬停 3 秒 client.moveToZAsync(-5, 2).join() time.sleep(3) # 获取前视相机图像 responses = client.simGetImages([ airsim.ImageRequest("front_left", airsim.ImageType.Scene, False, False) ]) response = responses[0] # 将字节流转为 numpy 数组并保存 img_1d = np.frombuffer(response.image_data_uint8, dtype=np.uint8) img_rgb = img_1d.reshape(response.height, response.width, 3) cv2.imwrite("front_view.png", img_rgb) # 获取 IMU 数据 imu_data = client.getImuData() print(f"加速度: {imu_data.linear_acceleration}") print(f"角速度: {imu_data.angular_velocity}") # 降落并锁定 client.landAsync().join() client.armDisarm(False) client.enableApiControl(False)confirmConnection()会阻塞直到仿真器响应,如果卡在这里说明 settings.json 没被正确加载或端口被占用。moveToZAsync(-5, 2)里的-5是 NED 坐标系下的高度,负值表示向上,2是速度 m/s。simGetImages返回的是原始字节流,需要按height × width × 3的维度 reshape 才能还原成图像。IMU 数据默认 100Hz 更新,做滤波算法验证时可以直接用这个频率。
3. 复杂环境场景搭建与自主飞行算法接入
3.1 在虚幻引擎里构建障碍物与动态目标场景
默认的 Landscape 场景太干净,验证不了避障算法。我一般会往场景里丢三类东西:静态障碍物、动态障碍物、光照变化。
静态障碍物用 UE 编辑器直接拖拽 Mesh 就行。建筑模型可以从 UE Marketplace 找免费资源,树木植被用 Foliage 工具批量刷。关键是障碍物的碰撞体要设置正确——AirSim 的激光雷达和深度相机依赖 UE 的碰撞检测,如果 Mesh 没有碰撞体,传感器数据里就不会出现这个障碍物。
动态障碍物有两种做法。简单的是用 AirSim 的simSetObjectPoseAPI 在 Python 脚本里周期性更新障碍物位置,适合做匀速直线运动的移动目标。复杂一点的是在 UE 里写 Actor 蓝图,让障碍物按预设轨迹运动,这样不占用 Python 线程,实时性更好。
光照变化通过调整 Directional Light 的强度和角度实现。做视觉感知验证时,我会把光照从正午强光调到黄昏弱光,观察检测算法的召回率变化。这一步在真机上很难复现,仿真里就是拖个滑块的事。
3.2 用 AirSim API 实现基于激光雷达的局部路径规划
下面这段代码实现一个简化版的反应式避障:无人机朝目标点飞行,激光雷达检测到前方 3 米内有障碍物时,向侧面偏移。
import airsim import numpy as np import time client = airsim.MultirotorClient() client.confirmConnection() client.enableApiControl(True) client.armDisarm(True) client.takeoffAsync().join() # 目标点(NED 坐标系) target = np.array([50, 30, -8]) # 当前期望位置 current_setpoint = np.array([0, 0, -5]) client.moveToPositionAsync(*current_setpoint, 3).join() for step in range(500): # 获取激光雷达数据 lidar_data = client.getLidarData() if len(lidar_data.point_cloud) < 3: time.sleep(0.05) continue # 将点云转为 numpy 数组,每三个数一个点 points = np.array(lidar_data.point_cloud, dtype=np.float32) points = points.reshape(-1, 3) # 只关心前方 120 度扇面内的点 forward_mask = points[:, 0] > 0 forward_points = points[forward_mask] # 计算前方最小距离 if len(forward_points) > 0: distances = np.linalg.norm(forward_points, axis=1) min_dist = np.min(distances) else: min_dist = float('inf') # 计算朝向目标的方向 direction = target - current_setpoint direction = direction / np.linalg.norm(direction) # 如果前方有障碍物,向侧面偏移 if min_dist < 3.0: # 简单策略:向右偏转 lateral = np.array([0, 1, 0]) direction = direction + 0.8 * lateral direction = direction / np.linalg.norm(direction) # 更新期望位置,步长 0.5 米 current_setpoint = current_setpoint + direction * 0.5 client.moveToPositionAsync(*current_setpoint, 2).join() time.sleep(0.05) client.landAsync().join() client.armDisarm(False) client.enableApiControl(False)getLidarData()返回的point_cloud是扁平数组,每三个浮点数构成一个点的x/y/z坐标,坐标系是机体坐标系。forward_mask筛选出机体前方(x 轴正方向)的点,实际使用时可以根据激光雷达安装角度调整。min_dist < 3.0是避障触发阈值,这个值要结合飞行速度和制动距离来设——速度 2m/s 时,3 米大约留出 1.5 秒反应时间。侧向偏移系数0.8决定了避障的激进程度,太大容易震荡,太小避不开。
3.3 视觉感知模块的接入方式
如果你要做无人机视觉感知,比如目标检测或语义分割,AirSim 的simGetImages可以同时拿多路相机的图像。我一般会开一个独立线程专门拉图像,主线程跑控制逻辑,避免图像传输阻塞控制指令。
import threading import airsim import numpy as np import cv2 client = airsim.MultirotorClient() client.confirmConnection() frame_buffer = {} lock = threading.Lock() def capture_thread(): while True: responses = client.simGetImages([ airsim.ImageRequest("front_left", airsim.ImageType.Scene, False, False), airsim.ImageRequest("depth", airsim.ImageType.DepthVis, True, False) ]) with lock: for i, resp in enumerate(responses): if resp.image_data_uint8: img = np.frombuffer(resp.image_data_uint8, dtype=np.uint8) img = img.reshape(resp.height, resp.width, 3) frame_buffer[i] = img time.sleep(0.03) # 约 30fps t = threading.Thread(target=capture_thread, daemon=True) t.start()ImageType.DepthVis返回的是可视化深度图,像素值经过归一化,适合直接喂给神经网络。如果需要原始深度值,用ImageType.DepthPlanar配合pixels_as_float=True,拿到的是浮点数组,单位米。注意simGetImages是同步调用,在 30fps 下单次耗时约 5-10ms,放在主线程里会拖慢控制循环。
4. AirSim 仿真避坑与常见问题排查
4.1 仿真器启动后无人机不受控或直接坠地
现象:按 Play 后无人机螺旋桨不转,或者刚起飞就翻扣在地上。原因通常是 settings.json 格式错误或路径不对。AirSim 读取的是Documents/AirSim/settings.json,Windows 下是C:\Users\你的用户名\Documents\AirSim\settings.json。如果文件里有 JSON 语法错误(比如多余的逗号),AirSim 会静默使用默认配置,而默认配置可能不包含你的无人机。解决方法是把 settings.json 内容贴到 JSON 校验工具里检查,确认Vehicles字段下有正确的无人机定义。
4.2 激光雷达点云为空或点数远低于预期
现象:getLidarData()返回的point_cloud长度小于 3,或者点数只有几百个。原因有两个:一是场景里的障碍物没有碰撞体,激光雷达打上去没有回波;二是PointsPerSecond设得太低。检查 UE 编辑器里每个 Mesh 的 Collision Presets,确保设置为BlockAll或至少BlockVisibility。另外PointsPerSecond在 16 线雷达下建议不低于 50000,否则点云稀疏到无法做避障。
4.3 Python API 调用超时或连接被拒绝
现象:client.confirmConnection()卡住不返回,或者抛出ConnectionRefusedError。常见原因是仿真器还没完全启动就调用了 API,或者端口 41451 被其他进程占用。解决方法是先启动仿真器,等场景完全加载后再运行 Python 脚本。如果端口冲突,在 settings.json 里加"ApiServerPort": 41452换一个端口,Python 端用airsim.MultirotorClient(port=41452)连接。
4.4 仿真帧率过低导致控制指令延迟
现象:无人机响应迟钝,moveToPositionAsync要好几秒才执行。原因是虚幻引擎渲染帧率低于 20fps,AirSim 的控制循环跟着变慢。降低渲染负载的方法:把settings.json里的"ViewMode"设为"NoDisplay",关掉实时渲染窗口;降低相机分辨率,640×480 足够做感知验证;减少场景里的动态光源和粒子效果。如果显卡实在吃力,把"ClockSpeed"设为 1.0 以下,让仿真时间慢于真实时间,给算法留出计算余量。
4.5 真机代码迁移到仿真后行为不一致
现象:真机上飞得好好的 PID 参数,在 AirSim 里震荡或响应过慢。原因是 AirSim 的SimpleFlight模型和真机飞控的动力学参数有差异,电机响应时间、桨叶效率、机体惯量都不一样。解决方法是不要直接搬 PID 参数,先在仿真里重新整定。把SimpleFlight换成PX4或ArduPilot固件的 SITL 模式,动力学模型会更接近真机。AirSim 支持通过VehicleType: "PX4"接入 PX4 SITL,这时候仿真里的参数可以直接迁移到 Pixhawk 飞控上。
5. 用仿真数据做路径规划算法验证的进阶技巧
仿真跑通之后,怎么判断你的路径规划算法是真的能打,而不是在特定场景里过拟合?我一般会做三件事:批量场景生成、指标量化、边界测试。
批量场景生成靠脚本改 settings.json 和 UE 场景参数。比如写一个 Python 脚本,循环修改障碍物的位置和数量,每次启动仿真跑一遍算法,记录成功率和路径长度。AirSim 支持命令行参数-settings指定配置文件,配合-windowed和-ResX/-ResY控制分辨率,可以做到无人值守批量跑。
指标量化至少看四个数:任务成功率、平均路径长度、平均飞行时间、最小障碍物距离。成功率低于 90% 的算法不要上真机。最小障碍物距离如果长期低于 0.5 米,说明避障策略太激进,真机上一阵风就炸了。
边界测试专门挑极端场景:窄缝通道、动态障碍物突然横穿、光照剧烈变化、传感器噪声注入。AirSim 的simSetWind可以加风扰,simSetTimeOfDay可以快速切换光照。我习惯在算法交付前跑一轮“魔鬼测试”,把能想到的极端情况都过一遍,翻车了总比真机翻车强。
还有一个容易被忽略的点:仿真里的感知数据是“干净”的,没有真实相机的运动模糊、卷帘快门畸变、镜头污渍。如果你的算法严重依赖图像质量,建议在 AirSim 里给图像加高斯噪声和运动模糊,或者直接用无人机识别数据集里的真实图像做域适应训练,再把模型放进仿真里测。仿真和真机之间的域间隙(domain gap)是客观存在的,别指望在仿真里跑满分就能直接上真机拿满分。
我自己的习惯是:仿真里验证算法逻辑和边界条件,真机上只做参数微调和最终验收。每次看到有人跳过仿真直接真机试错,我都替他们的飞机心疼。希望帮到你。
本文还有配套的精品资源,点击获取