AirSim 相机加速实战:JPEG 可调质量压缩实现 20× 帧率提升
在使用 Colosseum(AirSim 的继任仿真平台)做仿真时,相机图像通常通过 RPC(如 simGetImages)从 Unreal 端传到 Python 客户端。默认要么返回原始 RGB 像素,要么用 PNG 压缩。原始数据体积大(例如 1920×1080×3 ≈ 6MB/帧),PNG 虽能压缩但仍有明显体积和编码开销,在 WSL 或 远程连接 场景下容易成为瓶颈,帧率上不去。本文在 Colosseum + Unreal Engine 5.7.2 环境下增加了 可调质量的 JPEG 压缩 支持,在保证画质可接受的前提下减小传输体积、提高帧率。下面记录需求背景、设计思路和具体实现,方便日后维护。
一、为什么需要 JPEG 压缩?
1.1 问题场景
- RPC 传输体积大:每帧图像若以原始 RGB 或 PNG 形式通过 msgpack-RPC 传输,在 1080p 下数据量可达数 MB,网络或进程间拷贝成本高。
- 帧率受限:传输时间占主导时,相机帧率上不去,影响实时控制、算法测试或数据采集。
- WSL / 远程:
- WSL:AirSim 跑在 Windows,Python 跑在 WSL,图像要跨 WSL 边界,带宽和延迟都敏感。
- 远程:客户端与仿真不在同一台机器时,网络带宽往往是瓶颈。
1.2 方案选择
引入 JPEG 压缩,并增加一个 质量参数(1–100),在「体积」和「画质」之间可调:
- 质量 85–90:视觉上接近无损,体积通常比 PNG 小一个数量级。
- 质量 70–80:适合对画质要求不极致的场景,进一步减小体积、提高帧率。
同时保留原有行为:不压缩(raw) 和 PNG,通过同一套参数区分,避免破坏现有 API。
二、设计:compress_quality 语义
在原有 ImageRequest 的 compress(是否压缩)基础上,增加 compress_quality,用一个整数同时表示「是否压缩」和「压缩格式」:
compress_quality |
含义 |
|---|---|
| 0 | 不压缩,返回原始 RGB(BGR 顺序,每像素 3 字节) |
| -1 | 使用 PNG 压缩(与原先 compress=true 时行为一致) |
| 1–100 | 使用 JPEG 压缩,数值为 JPEG 质量(1 最差、100 最好) |
这样客户端只需在构造 ImageRequest 时传入一个参数(例如 85 表示 JPEG 质量 85),无需额外布尔或枚举。
三、整体数据流
从「客户端发起请求」到「拿到压缩后的图像」的流程可以概括为:
- 客户端(如 Python):构造
ImageRequest(..., compress_quality=85),通过 RPC 发给 AirSim 服务端。 - AirLib:解析请求,把
compress_quality填入内部ImageCaptureBase::ImageRequest,并传给 Unreal 插件。 - Unreal 插件:
- 用 Scene Capture 把相机渲染到 RenderTarget,读回像素(
FColor数组)。 - 根据
compress_quality分支:> 0→ 调用 JPEG 压缩(UE 的IImageWrapper),输出 JPEG 字节流;== -1或 原有compress == true→ PNG 压缩;- 否则 → Raw,按行拷贝 BGR。
- 用 Scene Capture 把相机渲染到 RenderTarget,读回像素(
- 压缩结果(或 raw 数据)填回
ImageResponse,经 RPC 返回客户端。 - 客户端:收到的
image_data_uint8在 JPEG 模式下已是 JPEG 文件字节流,用 OpenCV 或 PIL 解码即可得到 numpy 图像。
下面按「层」说明各处的具体修改。
四、修改点详解
4.1 AirLib:请求与响应结构(C++)
文件:AirLib/include/common/ImageCaptureBase.hpp
- 在
ImageCaptureBase::ImageRequest中增加字段:int compress_quality = 0; // 0=raw, -1=PNG, 1-100=JPEG quality
- 构造函数中增加参数
compress_quality_val,并在初始化列表里赋值。
这样所有使用 ImageRequest 的 C++ 代码都能带上「压缩质量」信息。
4.2 RPC 适配层(msgpack 序列化)
文件:AirLib/include/api/RpcLibAdaptorsBase.hpp
- 在 RPC 用的
ImageRequest适配结构体中增加:int compress_quality = 0;
- 在 MSGPACK_DEFINE_ARRAY 中在末尾追加
compress_quality(重要:若已有客户端只发 4 个字段,末尾加一个 int 可保持兼容,老客户端发 0 即 raw)。 - 在
ImageRequest的「从 AirLib 转 RPC」和「从 RPC 转 AirLib」的构造函数 /to()方法中,增加对compress_quality的读写。
这样 Python 或 C++ 客户端传入的 compress_quality 能正确在服务端被解析。
4.3 Unreal:渲染参数与三路分支
文件:
Unreal/Plugins/AirSim/Source/RenderRequest.hUnreal/Plugins/AirSim/Source/RenderRequest.cppRenderParams:增加成员
int compress_quality;,构造函数增加参数compress_quality_val = 0。UnrealImageCapture.cpp:在构造
RenderParams时传入requests[i].compress_quality,这样渲染管线里每一帧请求都带上「要 JPEG / PNG / raw」的信息。
在 RenderRequest.cpp 的 getScreenshot 中,从 RenderTarget 读回像素后(已按 stride 处理成 src_bmp),对非 float 图像做分支:
1 | if (params[i]->compress_quality > 0) { |
这样 JPEG / PNG / raw 三路清晰分离,且与 compress_quality 约定一致。
4.4 Unreal:JPEG 压缩实现(AirBlueprintLib)
文件:
Unreal/Plugins/AirSim/Source/AirBlueprintLib.hUnreal/Plugins/AirSim/Source/AirBlueprintLib.cpp在头文件中声明:
static void CompressImageArrayJPEG(int32 width, int32 height, const TArray<FColor>& src, TArray<uint8>& dest, int32 quality = 90);
在实现中:
- Unreal 的
FColor是 BGRA,而 UE 的IImageWrapper::SetRaw常用 RGBA,因此先对src做 R/B 互换(或按引擎文档要求的格式)得到 RGBA。 - 用 ImageWrapper 模块(
FModuleManager::LoadModuleChecked<IImageWrapperModule>("ImageWrapper"))创建 JPEG 类型的IImageWrapper。 - 调用
SetRaw(..., width, height, ERGBFormat::RGBA, 8)填入原始像素。 - 调用
GetCompressed(quality)得到 JPEG 字节流,写入dest。
- Unreal 的
注意:在 Unreal Engine 5.7(本文在 UE 5.7.2 下验证)中,应使用 GetCompressed(quality) 传入质量,不要依赖已废弃的 SetQuality 等接口,否则可能编译不过或行为异常。
4.5 Python 客户端
文件:PythonClient/airsim/types.py
- 在
ImageRequest中增加:- 类属性:
compress_quality = 0 # 0=raw, -1=PNG, 1-100=JPEG quality attribute_order中追加('compress_quality', int)(顺序须与 C++ 端 MSGPACK 一致)。
- 类属性:
__init__增加参数compress_quality=0,并赋值给self.compress_quality。
这样 Python 侧构造请求时即可指定 JPEG 质量,例如:
1 | req = airsim.ImageRequest("0", airsim.ImageType.Scene, False, False, 85) |
五、使用示例(Python)
5.1 请求 JPEG 并解码
若脚本放在 Colosseum 的 PythonClient/multirotor/ 等子目录下,需先设置路径并 import setup_path,再 import airsim,否则会找不到 airsim 模块:
1 | import os |
5.2 不同格式对比
(以下代码需在 import setup_path 与 import airsim 之后使用。)
1 | # Raw(不压缩) |
- Raw:
image_data_uint8为height * width * 3的 BGR 像素,可直接img = np.frombuffer(..., dtype=np.uint8).reshape(h, w, 3)。 - PNG/JPEG:
image_data_uint8为编码后的字节流,需用cv2.imdecode解码。
六、修改文件汇总
| 层级 | 文件 | 修改内容 |
|---|---|---|
| AirLib | AirLib/include/common/ImageCaptureBase.hpp |
ImageRequest 增加 compress_quality 及构造函数参数 |
| AirLib | AirLib/include/api/RpcLibAdaptorsBase.hpp |
RPC 用 ImageRequest 增加 compress_quality,MSGPACK 与转换逻辑 |
| Unreal | Unreal/Plugins/AirSim/Source/RenderRequest.h |
RenderParams 增加 compress_quality |
| Unreal | Unreal/Plugins/AirSim/Source/RenderRequest.cpp |
根据 compress_quality 分支:JPEG / PNG / raw |
| Unreal | Unreal/Plugins/AirSim/Source/AirBlueprintLib.h |
声明 CompressImageArrayJPEG |
| Unreal | Unreal/Plugins/AirSim/Source/AirBlueprintLib.cpp |
实现 CompressImageArrayJPEG(R/B 互换 + ImageWrapper JPEG + GetCompressed(quality)) |
| Unreal | Unreal/Plugins/AirSim/Source/UnrealImageCapture.cpp |
构造 RenderParams 时传入 requests[i].compress_quality |
| Python | PythonClient/airsim/types.py |
ImageRequest 增加 compress_quality 与 attribute_order、构造函数 |
七、注意事项
- MSGPACK 顺序:Python 的
attribute_order与 C++ 的MSGPACK_DEFINE_ARRAY字段顺序必须一致,且compress_quality放在末尾,便于兼容未传该字段的旧客户端(可视为 0)。 - UE 5.7.x(含 5.7.2):使用
IImageWrapper::GetCompressed(quality)传质量,不要使用已废弃的SetQuality。 - Stride:渲染目标可能存在 stride(每行字节数大于
width*4),在压缩前已按行拷贝到紧凑的src_bmp,避免 JPEG/PNG 压缩读到多余 padding。 - 解码端:JPEG 返回的是完整 JPEG 文件二进制,客户端用
cv2.imdecode或 PIL 解码即可,无需再区分 RGB/BGR(OpenCV 的imdecode默认 BGR,与 OpenCV 其他接口一致)。 - Python 路径:从
PythonClient子目录(如multirotor/)运行脚本时,需在import airsim前先import setup_path(并视情况用sys.path.insert把 PythonClient 根目录加入路径),否则会报错。
八、小结
通过在 AirSim 中增加 compress_quality 参数,并在 Unreal 渲染管线中根据该参数在 JPEG / PNG / raw 三路中选择一路输出,我们实现了可调质量的 JPEG 压缩:在 WSL、远程或高分辨率场景下,能显著减小 RPC 传输体积、提高相机帧率,同时保持与原有 raw/PNG 行为的兼容。实现上主要涉及 AirLib 请求结构、RPC 适配、Unreal 渲染参数与压缩分支、以及 Python 客户端的字段与序列化顺序;JPEG 编码本身复用 UE 的 IImageWrapper,无需自实现 DCT/熵编码。
代码位置:本文所述 JPEG 压缩修改基于 Colosseum + Unreal Engine 5.7.2,已合入笔者的 Colosseum 分支,可在 goodisok/Colosseum 的 feature/jpeg-geomag-px4 分支中查看或拉取。