图像压缩编解码器: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.pyscripts/tools/img_codec_rle.py

  • 解码端src/image/egui_image_qoi.csrc/image/egui_image_rle.c

  • 共享行缓冲src/image/egui_image_decode_utils.c(宽度 = 屏幕宽度)

QOI 编解码器

原理

QOI(Quite OK Image)是一种基于差分和哈希索引的无损图像压缩格式,由 Dominic Szablewski 于 2021 年发布。其设计目标是实现接近 PNG 的压缩率,同时保持极低的编解码复杂度。

QOI 工作在 RGB888/RGBA8888 色彩空间,逐像素顺序处理。解码器维护以下状态:

状态

说明

prev_pixel

上一个已解码像素的 RGBA 值

index[64]

64 槽哈希颜色表,哈希函数:(r*3 + g*5 + b*7 + a*11) & 63

run

当前游程计数(连续相同像素)

每个像素通过以下 6 种操作码之一编码:

操作码

位模式

字节数

含义

QOI_OP_INDEX

00xxxxxx

1

从 64 槽哈希表中查找,0 字节色彩数据

QOI_OP_DIFF

01xxxxxx

1

与前一像素的小差分 dr,dg,db ∈ [-2, +1]

QOI_OP_LUMA

10xxxxxx + 1 字节

2

基于亮度的中等差分,dg ∈ [-32, +31]

QOI_OP_RUN

11xxxxxx

1

与前一像素相同,游程 1–62

QOI_OP_RGB

11111110

4

完整 RGB 值

QOI_OP_RGBA

11111111

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)上操作,不做色彩空间转换:

控制字节

含义

bit7=0

重复模式:后续 blk_size 字节重复 ctrl

bit7=1

字面量模式:后续 (ctrl & 0x7F) × blk_size 字节原样复制

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 块)

判断逻辑:

  1. 从当前位置向前扫描,统计连续相同块数

  2. 若 ≥ 2 个连续相同:发出重复模式

  3. 否则:收集后续不重复的块直到遇到下一段重复(≥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 字节)。编码流程:

  1. RGB565 像素(2 字节)→ 扩展为 RGB888(3 字节)

  2. 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

blk_size=1,重复检测粒度最细

通用建议

  1. 默认选 RLE——它从不膨胀(最差情况每行增加 ceil(行像素/127) 个控制字节,大约 +1%),且解码资源开销最小

  2. 图标/UI 元素优先试 QOI——如果压缩率显著优于 RLE(>20%),就用 QOI

  3. 自然照片慎用 QOI——先看资源报告中的压缩率,如果显示膨胀(正数百分比),切回 RLE 或不压缩

  4. 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

相关文件

文件

说明

scripts/tools/img_codec_qoi.py

QOI Python 编码器

scripts/tools/img_codec_rle.py

RLE Python 编码器

scripts/tools/img2c.py

图像转 C 数组工具(调用上述编码器)

src/image/egui_image_qoi.h/c

QOI C 解码器

src/image/egui_image_rle.h/c

RLE C 解码器

src/image/egui_image_decode_utils.h/c

共享行缓冲与 blend 函数

example/HelloBasic/image/

静态图像对比示例(STD / QOI / RLE)

example/HelloBasic/mp4/

MP4 视频帧对比示例(STD / QOI / RLE)