AirSim生成无人机训练数据:从环境搭建到Python后处理全指南

AirSim生成无人机训练数据:从环境搭建到Python后处理全指南 1. 为什么选AirSim生成无人机训练数据仿真数据不是将就是效率的杠杆先说结论如果你做无人机视觉方向训练数据的瓶颈几乎永远不在模型而在数据本身。真实飞行采集数据需要场地审批、天气配合、电池续航管理、传感器标定一套流程走下来一个晴朗周末能采个几千张有效样本已经算高产。而且真实数据的标注成本极其惊人——逐像素抠分割标签、给每张深度图做点云配准外包标注团队遇到无人机航拍场景都头疼因为视角太特殊通用标注工具根本不好用。AirSim这类仿真平台解决的就是这个问题用游戏引擎渲染出逼真的视觉数据同时自动给你生成深度图、分割图、法线图标签零人工成本。前几年大家觉得仿真数据不够真实用了域迁移domain adaptation之后效果依然打折。但现在主流做法是预训练pretrain on synthetic, fine-tune on real——先用大规模仿真数据把模型的基础表征能力练出来再用少量真实数据精调。这个范式在目标检测、语义分割、深度估计任务上都已经被反复验证过尤其中小团队、个人开发者用AirSim生成数据几乎是性价比最高的起点。我在实际项目里的做法是AirSim负责产生量真实飞行负责产生质。一个训练集里80%是仿真数据20%是真实数据混合训练模型在未知场景下的泛化能力比纯真实数据训练提升非常明显。这篇文章我会把从环境搭建、相机配置、数据导出到Python后处理的完整链路讲清楚踩过的坑也一并列出照着走一遍你就能拥有一个自动化的无人机训练数据生产线。2. 环境搭建与配置先把无人机在UE4里飞起来2.1 版本选型UE4还是UE5AirSim哪个版本更稳AirSim目前对UE4的支持最成熟UE5虽然也能跑但插件编译的坑多一些。做数据生成项目稳定压倒一切我的建议是UE 4.27 AirSim 1.8.1这套组合社区资料最多遇到问题基本都能搜到解决方案。安装步骤其实很简单但有个顺序问题很多人搞反先装Visual Studio 2019勾选C开发组件再装Unreal Engine 4.27。UE通过Epic Games Launcher安装时记得在启动器里把引擎源码选项一起装上不然后面编译AirSim插件会缺模块。克隆AirSim仓库执行build.cmd。这一步会把AirSim的插件编译好输出到Unreal\Plugins目录。新建一个空的C工程把AirSim插件目录复制到工程的Plugins文件夹。用Visual Studio打开工程编译通过后启动。如果你是纯做数据生成不打算改AirSim源码其实还有一条捷径直接在Marketplace里找支持AirSim的现成环境比如Blocks环境省去自己搭建场景的时间。但自制场景的灵活度高很多特别是你需要特定地形、光照、障碍物分布来模拟真实飞行环境时自建环境是值得的。2.2 settings.json的核心配置相机、光照、天气参数AirSim的行为完全由settings.json控制它位于Documents\AirSim\目录下。这个文件是数据采集的核心所有相机参数、图像类型、保存设置都在这里定义。我贴一份我常用的配置模板{ SettingsVersion: 1.2, SimMode: Multirotor, ClockSpeed: 0.8, CameraDefaults: { CaptureSettings: [ { ImageType: 0, Width: 1920, Height: 1080, FOV_Degrees: 90, AutoExposureSpeed: 10, MotionBlurAmount: 0 }, { ImageType: 1, Width: 1920, Height: 1080, FOV_Degrees: 90 }, { ImageType: 2, Width: 1920, Height: 1080, FOV_Degrees: 90 } ] }, Camera: { X: 0, Y: 0, Z: -20, Pitch: -15, Roll: 0, Yaw: 0, CaptureSettings: [ { ImageType: 0, PublishToRos: 0 }, { ImageType: 1, PublishToRos: 0 }, { ImageType: 2, PublishToRos: 0 } ] }, Recording: { RecordOnMove: true, RecordInterval: 0.05, Cameras: [ { CameraName: 0, ImageType: 0 }, { CameraName: 0, ImageType: 1 }, { CameraName: 0, ImageType: 2 } ] }, SubWindows: [ {WindowID: 0, CameraID: 0, ImageType: 0, Visible: true}, {WindowID: 1, CameraID: 0, ImageType: 1, Visible: true}, {WindowID: 2, CameraID: 0, ImageType: 2, Visible: true} ] }几个关键注释ImageType0对应场景相机RGB1对应深度相机depth2对应分割相机segmentation。ClockSpeed控制仿真时钟速度设为大于1可以加速数据采集但对性能要求高如果场景复杂建议保持1.0。RecordInterval相邻两帧的最小间隔单位秒。无人机飞行速度固定时这个值决定了采集的数据密度。CameraDefaults和Camera里的CaptureSettings如果都写了后者的优先级更高。我习惯在CameraDefaults里统一定义所有相机的分辨率避免单独设置时漏掉某一个。2.3 首次起飞测试确认三大模态都能正常输出环境配置好之后最简单的验证方式是用AirSim自带的Python API直接控制无人机并保存图像。我写了一个极简测试脚本import airsim import os client airsim.MultirotorClient() client.confirmConnection() client.enableApiControl(True) client.armDisarm(True) client.takeoffAsync().join() client.moveToZAsync(-10, 3).join() # 起飞到10米高度 # 请求当前帧的三种图像 responses client.simGetImages([ airsim.ImageRequest(0, airsim.ImageType.Scene), airsim.ImageRequest(0, airsim.ImageType.DepthPerspective), airsim.ImageRequest(0, airsim.ImageType.Segmentation) ]) print(fScene: {responses[0].image_data_uint8.shape}) print(fDepth: {responses[1].image_data_float.shape}) print(fSegmentation: {responses[2].image_data_uint8.shape})如果这个脚本能正常跑通说明你的AirSim环境是完整的。最常见的失败情况是Segmentation图全黑或全灰这个问题我在第5节专门讲。首次验证建议直接在UE编辑器的PIE模式Play In Editor里跑方便实时调试相机视角。3. 三类图像数据的输出原理从引擎渲染到像素矩阵3.1 RGB图不仅是截图还涉及曝光和色调映射RGB图走的是UE4的场景渲染管线所以游戏引擎里所有影响画面表现的设置都会影响输出后处理体积Post Process Volume、自动曝光、泛光、环境光照、雾效。AirSim默认开启自动曝光这意味着同一场景下相机朝向不同图片亮度会自适应变化。对训练语义分割模型来说这其实是好事——增加亮度多样性等于天然的数据增强。但如果你做的是对亮度敏感的视觉里程计或光流估计建议把自动曝光关掉使用固定曝光参数CaptureSettings: [ { ImageType: 0, Width: 1920, Height: 1080, FOV_Degrees: 90, AutoExposureMethod: 1, AutoExposureBias: 0, AutoExposureSpeed: 0, MotionBlurAmount: 0 } ]把AutoExposureSpeed设为0会使自动曝光锁定在当前测光值相当于固定曝光。这个细节在真实项目中极其重要——你在仿真里跑通的模型部署到真实无人机上时相机曝光参数往往也是固定的训练数据曝光不停变化反而会引入偏差。RGB图像通过image_data_uint8返回是一个H x W x 3的uint8数组取值范围0-255。直接用Python把这个数组转成PNG存储即可。这里有个容易踩的坑AirSim返回的RGB数组是直接以RGB顺序排列的但很多图像处理库比如cv2默认的通道顺序是BGR。如果你直接用cv2.imwrite保存会导致红蓝通道互换整个图像色调完全跑偏。正确做法是保存前加一步通道转换import numpy as np import cv2 rgb_image np.frombuffer(responses[0].image_data_uint8, dtypenp.uint8) rgb_image rgb_image.reshape(responses[0].height, responses[0].width, 3) bgr_image cv2.cvtColor(rgb_image, cv2.COLOR_RGB2BGR) cv2.imwrite(rgb.png, bgr_image)3.2 深度图透视深度还是平面深度别选错了AirSim提供三种深度图像类型这也是新手最容易混淆的地方DepthPerspective透视深度图每个像素值代表该像素对应的3D点到相机中心的距离单位是米。它和真实深度传感器的输出比如双目深度、ToF语义一致。DepthPlanar平面深度图每个像素值代表该点到相机平面的垂直距离也就是z值。DepthVis可视化深度图把深度值映射到0-255的灰度区间只适合人眼观察不适合训练。实际训练中99%的情况应该选择DepthPerspective。因为神经网络回归深度时学的是某种几何投影规则下的度量关系透视深度和光度一致性约束更匹配。深度图像返回的是image_data_float32位浮点数组。注意AirSim还有个小坑它把无效深度距离太远无法估计的点设为很大的值比如1e9直接用会污染loss。存储深度图建议保存为16位PNG用毫米单位既省空间又保留精度depth_img np.frombuffer(responses[1].image_data_float, dtypenp.float32) depth_img depth_img.reshape(responses[1].height, responses[1].width) # 把无效深度设为0便于后续处理 depth_img[depth_img 10000] 0 # 保存为16位PNG单位毫米 depth_mm (depth_img * 1000).astype(np.uint16) cv2.imwrite(depth.png, depth_mm)16位PNG在Python和C生态都很友好读取时除以1000就得到米为单位的深度值。这里不建议用npy格式存储因为后续用halcon或OpenCV做点云重建时通用图像格式会更方便。3.3 分割图ID映射决定了你后续能不能省下标注人工分割相机是AirSim最独特的输出——UE4引擎为每个物体自动分配一个稳定的ID分割图里每个像素就是该物体所属实例的ID。这个ID不是随机的它由物体名称的哈希决定或者你也可以手动指定。默认情况下AirSim用MeshNaming的Hash值生成物体ID。问题在于如果你加载了同一场景的不同版本比如改了场景里的物体名称ID就会变化你保存的标签映射就失效了。所以正式采集前一定要先手动固定ID。AirSim提供了运行时修改分割ID的API# 给场景中名为Building_12的对象指定ID为23 client.simSetSegmentationObjectID(Building_12, 23)更高效的做法是直接改配置文件SegmentationSettings: { InitMethod: 1, StrictChecking: false, MeshNamingMethod: 1 }其中MeshNamingMethod为1表示按物体的完整名称匹配为0表示按网格名称匹配。实际项目中我推荐在正式采集前写一个脚本遍历场景中所有对象把ID统一导出成一个JSON映射文件# 获取场景中所有可分割对象名称 names client.simListSceneObjects(.*) seg_mapping {} for idx, name in enumerate(names, start1): client.simSetSegmentationObjectID(name, idx) seg_mapping[name] idx import json with open(seg_mapping.json, w) as f: json.dump(seg_mapping, f, indent4)这样每个物体ID都是确定的1、2、3...序列后续做语义分割时直接按类别合并即可。比如你要的类别是建筑植被道路就把对应物体名的ID映射到类别索引。分割图返回的是image_data_uint8但注意它的通道含义当物体ID小于256时三个通道的值相同同一个灰度值如果ID超过255三个通道分别存储ID的低、中、高字节。很多人在这一步踩坑直接把三通道分割图当作RGB图像存了训练时才发现标签全是乱的。稳妥做法是只取第一个通道seg_img np.frombuffer(responses[2].image_data_uint8, dtypenp.uint8) seg_img seg_img.reshape(responses[2].height, responses[2].width, 3) # 取第一个通道作为有效分割IDID 256时三个通道相同 seg_label seg_img[:, :, 0]4. 采集流程与Python后处理链路从原始数据到干净的数据集4.1 设计一条符合真实飞行习惯的航迹数据生成的质量很大程度取决于采集时的飞行路径设计。很多人的做法是让无人机悬停在几个固定位置转圈拍一圈这样生成的数据视角单一模型一换场景就废。我在项目中的经验是航迹设计要模拟真实巡检任务的运动模式。航线式飞行无人机沿一条直线匀速飞行适合生成航拍拼接、语义分割数据。环绕式飞行围绕目标物体做圆周运动适合三维重建、目标检测的多视角数据。高度分层同一区域分别在20米、50米、100米高度飞行覆盖不同的地面采样距离增强尺度多样性。云台角度变化相机俯仰角从-45度逐渐过渡到-15度模拟真实云台转动。我把这些逻辑直接写成Python脚本控制无人机而不是手动操作import airsim import numpy as np import time client airsim.MultirotorClient() client.confirmConnection() client.enableApiControl(True) # 起飞并上升到指定高度 client.takeoffAsync().join() client.moveToZAsync(-30, 5).join() # 高度30米 # 航线飞行沿X方向飞行每隔0.1秒采集一帧 waypoints [] for i in range(50): x -100 i * 4 y 0 z -30 waypoints.append(airsim.Vector3r(x, y, z)) for wp in waypoints: client.moveToPositionAsync(wp.x_val, wp.y_val, wp.z_val, 5).join() time.sleep(0.1) # 等待到达 client.armDisarm(False)这里moveToPositionAsync的最后一个参数是速度m/s速度越快帧间位移越大采集到的数据跳跃感越强。实际操作中如果用的是4K相机建议速度控制在5m/s左右每帧画面重叠率足够高。4.2 多模态数据同步与文件名组织说到多模态数据离线的同步问题往往比在线采集更让人头疼。AirSim的simGetImages虽然在一次调用里同时返回RGB、深度、分割三种图但它们是在同一时刻、同一相机位姿下渲染的天然对齐。你需要做的就是保证帧间命名一致让RGB、深度、分割图能通过文件名正确关联。我的数据集目录结构长这样dataset/ ├── train/ │ ├── rgb/ │ │ ├── 000000.png │ │ ├── 000001.png │ │ └── ... │ ├── depth/ │ │ ├── 000000.png │ │ ├── 000001.png │ │ └── ... │ └── seg/ │ ├── 000000.png │ ├── 000001.png │ └── ... ├── val/ └── test/这种组织方式几乎是所有主流语义分割框架如MMSegmentation、detectron2默认支持的格式后续训练可以直接接入。采集脚本里用同一帧号命名三种图像frame_id 0 while True: # 一次API调用同时取三种图像 responses client.simGetImages([ airsim.ImageRequest(0, airsim.ImageType.Scene), airsim.ImageRequest(0, airsim.ImageType.DepthPerspective), airsim.ImageRequest(0, airsim.ImageType.Segmentation) ]) # 保存三种图像帧号统一 save_rgb(responses[0], ftrain/rgb/{frame_id:06d}.png) save_depth(responses[1], ftrain/depth/{frame_id:06d}.png) save_seg(responses[2], ftrain/seg/{frame_id:06d}.png) frame_id 1 time.sleep(0.05) # 20FPS4.3 深度图转点云用相机内参把像素投影回三维空间深度图单独用的时候可以做单目深度估计但很多下游任务如三维重建、点云语义分割需要的是点云。AirSim输出的深度图没有内参信息但内参是已知的——你配置了宽度、高度和FOV就可以反算出相机内参矩阵def depth_to_pointcloud(depth_img, width, height, fov_degrees): import numpy as np scale 1000.0 # 深度图单位换算假设存的是毫米 depth depth_img.astype(np.float32) / scale # 由FOV计算焦距 f (width / 2) / np.tan(np.radians(fov_degrees / 2)) cx width / 2 cy height / 2 # 生成像素坐标网格 ys, xs np.mgrid[0:height, 0:width] # 转换到相机坐标系 points_z depth points_x (xs - cx) * points_z / f points_y (ys - cy) * points_z / f # 组合成 N*3 的点云数组 points np.stack((points_x, points_y, points_z), axis-1).reshape(-1, 3) # 过滤无效点 points points[np.isfinite(points).all(axis1)] points points[points[:, 2] 0] return points注意这里的坐标系是相机坐标系X向右Y向下Z向前深度方向。很多工业软件如halcon做点云处理时对坐标系定义很敏感导入前需要确认约定是否一致。如果做无人机航拍视角的几何任务通常还需要把相机坐标系转换到以起飞点为原点的世界坐标系这就要用到无人机姿态数据相机在世界坐标系的旋转和平移。AirSim的simGetVehiclePose()可以直接拿到无人机在世界坐标系下的位姿结合相机在机体坐标系的安装偏移就是你配置Camera节点里的X、Y、Z和Pitch、Roll、Yaw就能完成从像素到世界坐标系的完整投影。4.4 读取RGB值做颜色统计数据质量校验的快速手段拿到一批RGB图之后第一件事不是直接丢给模型训练而是先做一轮数据质量校验。最朴素也最有效的方法是直接统计像素值的分布发现异常图像。我在项目中写了一个快速校验脚本读取一批图片的rgb值画直方图、算均值方差重点检查三件事是否有过曝或欠曝图像均值接近255说明过曝信息大多丢失了均值接近0说明欠曝细节在暗部。RGB三通道分布是否合理如果三通道的直方图形状差异非常大可能是渲染bug或者通道顺序搞错了。相邻帧的差异是否过小如果两个连续帧的像素值几乎一模一样说明无人机在悬停赶紧调整航迹避免产生大量冗余数据。import cv2 import numpy as np import glob files sorted(glob.glob(train/rgb/*.png)) r_means, g_means, b_means [], [], [] for f in files: bgr cv2.imread(f) b, g, r cv2.split(bgr) r_means.append(r.mean()) g_means.append(g.mean()) b_means.append(b.mean()) print(fR通道均值范围{min(r_means):.1f} ~ {max(r_means):.1f}) print(fG通道均值范围{min(g_means):.1f} ~ {max(g_means):.1f}) print(fB通道均值范围{min(b_means):.1f} ~ {max(b_means):.1f})如果某一段数据的RGB均值突然发生剧烈跳变多半是航迹经过了阴影区或者自动曝光在切换。这不一定需要剔除但如果你的训练任务对光照一致性要求高可以做一个光照分桶——把图像按亮度均值分成几个桶训练时按桶采样确保每个批次光照分布均匀。5. 踩坑记从报错到数据异常的完整排查链路5.1 No frames received相机没有真正启动这个报错在调用simGetImages时特别常见尤其是刚换场景或者改了settings.json之后。报错no frames received 无法获取深度和rgb本质是图像请求到达了渲染线程但渲染线程还没有准备好这一帧。可能是相机参数无效、场景没有加载完成、或者resource的带宽不足。我的排查顺序先在UE编辑器里打开该关卡确认场景能正常渲染如果场景本身都是黑的问题出在场景加载不在AirSim。确认settings.json里CameraDefaults的CaptureSettings正确特别是ImageType范围0-2和Width/Height是否合法。调低分辨率试试——1920x1080在低端显卡上如果同时请求三通道图像很容易出现渲染超时。我也遇到过因为显卡内存不足导致的一直No frames received降到1280x720就好了。最后再检查是否有多个进程同时连接AirSim。AirSim的客户端模型是单连接的如果有旧的Python脚本没有释放连接新脚本会一直拿不到数据。ClockSpeed设得过高也会导致这个报错——渲染速度跟不上逻辑速度图像帧被跳过。我之前把ClockSpeed调到2.0结果采集的数据只有预期的一半帧数就是这个原因。5.2 深度图全是黑的或花的深度图用16位PNG保存后直接用看图软件打开往往是一团黑或者花屏新手很容易误以为数据坏了。其实这是显示问题不是数据问题。16位PNG的值域是0-65535而AirSim返回的深度值在几十米的范围内换算成毫米也就是几千到几万看图软件无法自动缩放到合适的显示范围所以呈现黑色。验证方法很简单用Python读取深度图打印一下像素值分布depth cv2.imread(depth.png, cv2.IMREAD_UNCHANGED) print(depth.min(), depth.max(), depth.mean())如果最大值在几百到几万之间说明数据是好的直接除以1000就能得到米单位深度。另外还有一种深度图全是花的情况呈现密集的噪声条纹这种多半是深度图保存时用了有损压缩格式比如JPG深度值被破坏。深度图只能保存为PNG不能保存为JPG。5.3 分割图全灰或全黑你的场景没设置好分割图输出全灰或全黑是最容易让人困惑的故障。全黑说明分割ID为0只有无ID的对象才会是这个值全灰也很常见说明所有物体都拿到了同一个ID。真实原因是UE4场景中很多物体是由多个Mesh构件组成的AirSim按Mesh而不是按Actor来分配分割ID。你看到的一栋建筑在UE里是一个Actor但它内部可能由几十个独立的Mesh组成每个Mesh默认拥有不同的ID所以分割图上同一栋建筑被分成了五颜六色的碎片。反过来如果场景里的物体共享同一个Mesh资源比如复制了100棵树它们的初始ID完全相同分割图就是全灰的。解决办法是在采集脚本里先遍历场景对象按规则合并ID像前面第3节写的那样为每个物体名称设置稳定的ID。设置完记得再取一张分割图验证打印一下图像里有多少个不同的值seg cv2.imread(seg.png, cv2.IMREAD_UNCHANGED) unique_ids np.unique(seg) print(f分割图中出现的唯一ID数{len(unique_ids)})如果ID数远小于场景中的物体数就要检查是不是很多物体共用了一个ID。5.4 图像通道顺序肉眼看似正常的陷阱最后提一个最隐蔽的坑AirSim的RGB图返回的是标准的RGB顺序但很多人习惯用cv2显示和保存图像cv2的imshow和imwrite默认是BGR顺序。如果你直接把image_data_uint8reshape后扔给cv2.imwrite出来的图像红蓝通道颠倒。更麻烦的是如果图片上恰好有红色和蓝色物体你一眼能发现但如果是绿色植被、灰色道路的航拍图误用BGR保存后颜色偏差其实很难用肉眼察觉。等模型训练完到评测阶段你才会发现分割精度、检测精度莫名其妙比预期低但就是找不到原因——特征形状都对颜色响应不对模型的可判别力被削弱了。我的建议是在采集脚本里统一加一行cv2.cvtColor(img, cv2.COLOR_RGB2BGR)让保存到磁盘的文件符合绝大多数CV工具链的习惯。同时写一个检查函数采样几个已知颜色的像素打印RGB值来验证# 验证通道顺序图像中心点的RGB值 r, g, b img[height//2, width//2] print(fCenter pixel - R:{r}, G:{g}, B:{b})6. 进阶配置与扩展让数据生成管线再提升一个档次6.1 光照和时间晴天、阴天、黄昏的数据怎么来无人机在一天中不同时段飞行光照差异极大。AirSim的场景可以通过修改天气参数来模拟不同的光照条件。聚合一下可以在采集脚本里动态调整时间# 设置仿真时间为下午3点 client.simSetTimeOfDay(True, 2024-06-01 15:00:00)配合多个时间点拍摄同一场景就能产生同一地点、不同时段的数据这对训练鲁棒的地面分割、阴影检测非常有用。另一种做法是直接修改UE环境的光照方向、强度和颜色。但通过simSetTimeOfDay动态调整更灵活不用重启场景。我常用的是把一天分成几个典型时段时段光照特点典型应用日出/日落长阴影、低照度、色温偏暖光照鲁棒性训练正午高照度、阴影最短常规分割、检测阴天无阴影、对比度低弱纹理场景测试夜间低照度、依赖人工光源夜间巡检数据扩充6.2 用配置文件管理多套数据生成方案实际项目做多了以后你会发现每次数据采集都要重新设定航迹、相机参数、分割ID映射非常繁琐。后来我把整套流程做成了配置驱动# collection_config.yaml scene: map: CityEnvironment time: 2024-06-01 12:00:00 camera: width: 1920 height: 1080 fov: 90 pitch: -30 drone: speed: 5 altitude: 40 pattern: grid output: format: png depth_unit: mm save_seg_id: truePython脚本读取YAML配置后自动完成所有设置。这样换场景换参数时只需要改配置文件不需要改代码。同配置下跑出的数据也具有可复现性——你可以在不同时间生成完全一致的数据集这在消融实验里很有用。6.3 分割ID到类别标签做语义分割的最后一公里拿到分割图之后你大概率还需要把物体的实例ID映射成语义类别ID。比如把Building_A和Building_B都归为建筑这一类类别ID1所有植被归为植物类别ID2。最直接的方式是在采集时就用类别ID覆盖物体IDCLASS_MAPPING { Building: 1, Tree: 2, Road: 3, Vehicle: 4, Person: 5, Background: 0 # 默认 } for name, obj_id in seg_mapping.items(): for class_key, class_id in CLASS_MAPPING.items(): if class_key.lower() in name.lower(): client.simSetSegmentationObjectID(name, class_id) break这样输出分割图本身就已经是语义标签图后续训练连映射转换都不需要直接喂给模型。工程上这是最省事的做法。但要注意很多场景物体的名字不是标准的语义类别名称比如SM_Factory_Wall_001这种。所以稳妥做法是先simListSceneObjects导出所有名字人工检查一遍再写映射规则。这个过程看似繁琐但只做一次之后所有采集都能复用。7. 数据质量检查与交付给训练管线的最后一道保险数据采集完成不等于数据可用。我在正式训练前总会过一遍完整的数据质量检查包括文件数量、图像可读性、深度值的有效性、分割图的类别覆盖度。这里放一个最终的统计脚本import cv2 import numpy as np import glob from collections import Counter rgb_files sorted(glob.glob(dataset/train/rgb/*.png)) depth_files sorted(glob.glob(dataset/train/depth/*.png)) seg_files sorted(glob.glob(dataset/train/seg/*.png)) # 1. 文件数量一致性 assert len(rgb_files) len(depth_files) len(seg_files), 文件数量不一致 # 2. 图像可读性检查 invalid [] for f in rgb_files: img cv2.imread(f) if img is None: invalid.append(f) # 3. 深度值统计 for f in depth_files: depth cv2.imread(f, cv2.IMREAD_UNCHANGED) valid_ratio (depth 0).mean() if valid_ratio 0.8: print(f警告{f} 的有效深度比例只有 {valid_ratio:.2f}) # 4. 分割类别统计 class_counter Counter() for f in seg_files: seg cv2.imread(f, cv2.IMREAD_UNCHANGED) ids, counts np.unique(seg, return_countsTrue) for obj_id, count in zip(ids, counts): class_counter[obj_id] count print(分割图各类别出现的帧数) for obj_id, count in sorted(class_counter.items()): print(f ID {obj_id}: {count} 帧)这几步检查虽然零技术含量但几乎每次都能揪出问题——我遇到过最离谱的一次是采集了2000帧结果深度图有800帧是全零的原因是一个物体挡住了大部分画面深度无效区域被置零了。最后根据我个人的实际体会AirSim生成数据最值得投入精力的环节不是环境搭建而是数据多样性和标签语义的设计。环境搭好了、脚本跑通了这些都是一次性的工作但数据多样性决定了模型上线的表现上限。多花时间设计航迹、光照变化、场景布局远比多采几千张同一个角度转圈圈的图更有价值。