图像压缩编解码器:QOI 与 RLE¶
EmbeddedGUI 提供两种轻量级图像压缩编解码器,用于减少嵌入式系统的 ROM 占用:QOI(Quite OK Image)和 RLE(Run-Length Encoding)。两者均为无损压缩,解码端无需额外内存分配,适配 PFB 逐行渲染架构。
编解码架构概览¶
编码(PC / 构建时) 解码(MCU / 运行时)
┌─────────────────────────┐ ┌──────────────────────────┐
│ PNG/JPEG 原始图 (RGB888) │ │ PFB Tile Iteration │
│ ↓ │ │ ↓ │
│ Pillow 解码为 raw 像素 │ │ egui_image_xxx_draw() │
│ ↓ │ │ ↓ │
│ RGB565/RGB32 量化 │ ──C数组→ │ 逐行解压到共享行缓冲 │
│ ↓ │ │ ↓ │
│ QOI/RLE 逐行压缩 │ │ blend_row → PFB buffer │
└─────────────────────────┘ └──────────────────────────┘
两种编解码器共享相同的资源生成管线和解码基础设施:
编码端:
scripts/tools/img_codec_qoi.py、scripts/tools/img_codec_rle.py解码端:
src/image/egui_image_qoi.c、src/image/egui_image_rle.c共享行缓冲:
src/image/egui_image_decode_utils.c(宽度 = 屏幕宽度)
QOI 编解码器¶
原理¶
QOI(Quite OK Image)是一种基于差分和哈希索引的无损图像压缩格式,由 Dominic Szablewski 于 2021 年发布。其设计目标是实现接近 PNG 的压缩率,同时保持极低的编解码复杂度。
QOI 工作在 RGB888/RGBA8888 色彩空间,逐像素顺序处理。解码器维护以下状态:
状态 |
说明 |
|---|---|
|
上一个已解码像素的 RGBA 值 |
|
64 槽哈希颜色表,哈希函数: |
|
当前游程计数(连续相同像素) |
每个像素通过以下 6 种操作码之一编码:
操作码 |
位模式 |
字节数 |
含义 |
|---|---|---|---|
|
|
1 |
从 64 槽哈希表中查找,0 字节色彩数据 |
|
|
1 |
与前一像素的小差分 dr,dg,db ∈ [-2, +1] |
|
|
2 |
基于亮度的中等差分,dg ∈ [-32, +31] |
|
|
1 |
与前一像素相同,游程 1–62 |
|
|
4 |
完整 RGB 值 |
|
|
5 |
完整 RGBA 值 |
编码优先级:RUN → INDEX → DIFF → LUMA → RGB/RGBA。解码器按照操作码前缀位即可确定类型,无需回溯。
EGUI 中的适配¶
由于 EGUI 目标平台使用 RGB565 而非 RGB888,QOI 编解码在编码端和解码端做了色彩空间转换:
编码端(Python):将 RGB565 像素扩展为 RGB888 后再进行 QOI 编码。对于含 Alpha 通道的图像,Alpha 作为 RGBA 的第 4 通道编入 QOI 流
解码端(C):QOI 解码为 RGB888 后立即转换为 RGB565 写入行缓冲
这意味着存在 两次量化损失(RGB888→RGB565→RGB888→RGB565),但对于 16 位显示设备来说,单个颜色分量最大误差为 ±1 LSB,视觉上不可感知。
解码资源开销¶
资源 |
占用 |
|---|---|
解码状态 |
~280 字节(64×4 字节哈希表 + 控制变量) |
额外 ROM |
约 1.2 KB 代码 |
行缓冲 |
共享,屏幕宽度 × 4 字节 |
RLE 编解码器¶
原理¶
RLE(Run-Length Encoding)是最经典的无损压缩算法之一。其核心思想是将连续相同的数据块替换为”计数 + 数据”的紧凑表示。
EGUI 的 RLE 实现采用 LVGL 兼容 的控制字节格式,直接在 原始像素格式(RGB565/RGB32/GRAY8)上操作,不做色彩空间转换:
控制字节 |
含义 |
|---|---|
|
重复模式:后续 |
|
字面量模式:后续 |
blk_size 由像素格式决定:RGB565 = 2 字节,RGB32 = 4 字节,GRAY8 = 1 字节。每个控制字节最多编码 127 个像素块。
编码约束:RLE 编码 逐行 进行,游程不跨越行边界。这是为了适配 PFB 逐行解码——解码器每次调用恰好解压一行像素,如果游程跨行会导致解码流失同步。
Alpha 通道独立压缩为单独的 RLE 流(blk_size=1),解码时与像素数据分别处理。
编码策略¶
输入行: [A A A B C C D D D D D E]
控制字节: 解析为:
0x03 A 重复 A × 3
0x82 B C 字面量: B, C(2 块,bit7=1 → 0x80|2=0x82)
0x05 D 重复 D × 5
0x81 E 字面量: E(1 块)
判断逻辑:
从当前位置向前扫描,统计连续相同块数
若 ≥ 2 个连续相同:发出重复模式
否则:收集后续不重复的块直到遇到下一段重复(≥2),发出字面量模式
解码资源开销¶
资源 |
占用 |
|---|---|
解码状态 |
~16 字节(流位置 + 行号) |
额外 ROM |
约 0.8 KB 代码 |
行缓冲 |
共享,屏幕宽度 × 4 字节 |
压缩率对比与分析¶
以下数据来自 example/HelloBasic/image/ 的实际资源生成报告:
图像 |
特征 |
原始大小 |
QOI |
RLE |
|---|---|---|---|---|
test.png (80×80) |
自然照片,无透明 |
12,800 B |
13,781 B (+8%) |
10,884 B (-15%) |
star.png (80×80) |
图标/图形,含透明+大面积纯色 |
19,200 B |
2,307 B (-88%) |
3,374 B (-82%) |
为什么 QOI 对自然照片反而膨胀?¶
QOI 在 RGB888 色彩空间工作,而原始数据是 RGB565(每像素 2 字节)。编码流程:
RGB565 像素(2 字节)→ 扩展为 RGB888(3 字节)
QOI 编码 RGB888 →压缩后每像素平均 > 2 字节
对于自然照片,相邻像素差异大、颜色丰富,QOI 的差分和哈希索引命中率低。此时 QOI 编码大部分像素需要使用 QOI_OP_RGB(4 字节/像素)或 QOI_OP_LUMA(2 字节),比原始 RGB565 的 2 字节/像素还多,导致 压缩后反而变大。
而 RLE 直接在 RGB565 上操作(blk_size=2),相邻像素即使只有少量重复,也能通过重复模式节省空间。自然照片中天空、地面等区域仍然有局部重复,因此 RLE 略有压缩效果(-15%)。
为什么 QOI 对图标可以压缩 88%?¶
star.png 是一个带透明通道的星形图标,具有以下特征:
大面积透明区域:alpha=0 的像素占多数,颜色统一
纯色填充:星形内部颜色变化极少
边缘锐利:颜色跳变集中在少量像素
这些特征让 QOI 的各种操作码都能高效工作:
透明区域 →
QOI_OP_RUN(1 字节编码最多 62 个相同像素)纯色区域 →
QOI_OP_INDEX(1 字节,哈希命中)颜色跳变 →
QOI_OP_DIFF/QOI_OP_LUMA(1-2 字节差分)
RLE 也表现优秀(-82%),主要受益于大面积连续相同像素的重复模式。但 QOI 在此场景下比 RLE 更优,因为它能在颜色变化时用差分编码,而 RLE 遇到不同像素必须切换为字面量模式(额外开销 1 字节控制字节 + 原始数据)。
适用场景选择指南¶
场景 |
推荐编解码器 |
原因 |
|---|---|---|
图标和 UI 图形 |
QOI |
大面积纯色 + 透明,差分和哈希效率极高 |
含大面积透明的图片 |
QOI 或 RLE |
两者都能高效压缩连续相同像素 |
自然照片(无透明) |
RLE |
QOI 可能膨胀;RLE 至少略有压缩 |
自然照片(含透明) |
QOI |
透明通道内嵌流中,整体还是有良好的压缩 |
解码 RAM 极度受限 |
RLE |
解码状态仅 16 字节 vs QOI 的 280 字节 |
MP4 视频帧 |
看内容而定 |
视频帧通常是自然场景,RLE 更安全 |
灰度图像 |
RLE |
|
通用建议¶
默认选 RLE——它从不膨胀(最差情况每行增加 ceil(行像素/127) 个控制字节,大约 +1%),且解码资源开销最小
图标/UI 元素优先试 QOI——如果压缩率显著优于 RLE(>20%),就用 QOI
自然照片慎用 QOI——先看资源报告中的压缩率,如果显示膨胀(正数百分比),切回 RLE 或不压缩
ROM 极度紧张时两者都试——比较
app_egui_resource_generate_report.md中的报告数字
启用方法¶
在应用的 app_egui_config.h 中添加:
#define EGUI_CONFIG_FUNCTION_IMAGE_CODEC_QOI 1 /* 启用 QOI */
#define EGUI_CONFIG_FUNCTION_IMAGE_CODEC_RLE 1 /* 启用 RLE */
在 app_resource_config.json 的图像条目中指定 compress 字段:
{
"img": [
{"file": "icon.png", "name": "icon_qoi", "compress": "qoi", "...": "..."},
{"file": "photo.png", "name": "photo_rle", "compress": "rle", "...": "..."},
{"file": "raw.png", "name": "raw_std", "compress": "none", "...": "..."}
]
}
运行 make resource_refresh APP=<AppName> 即可生成压缩后的 C 资源文件。
技术限制¶
限制 |
说明 |
|---|---|
不支持 resize 渲染 |
压缩图像无法在渲染时缩放,需预生成目标尺寸 |
不支持随机访问 get_point |
必须从头顺序解码,不适合需要单点采样的场景 |
PFB 多 tile 重复解码 |
当图像跨越多列 PFB tile 时,后续列需 reset 重新解码(性能代价,但正确性无影响) |
单实例静态状态 |
QOI 和 RLE 各有一个全局解码状态,同时渲染多张同类型压缩图时会频繁 reset |
相关文件¶
文件 |
说明 |
|---|---|
|
QOI Python 编码器 |
|
RLE Python 编码器 |
|
图像转 C 数组工具(调用上述编码器) |
|
QOI C 解码器 |
|
RLE C 解码器 |
|
共享行缓冲与 blend 函数 |
|
静态图像对比示例(STD / QOI / RLE) |
|
MP4 视频帧对比示例(STD / QOI / RLE) |