# 图像压缩编解码器: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 色彩空间**,逐像素顺序处理。解码器维护以下状态: | 状态 | 说明 | |------|------| | `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` 中添加: ```c #define EGUI_CONFIG_FUNCTION_IMAGE_CODEC_QOI 1 /* 启用 QOI */ #define EGUI_CONFIG_FUNCTION_IMAGE_CODEC_RLE 1 /* 启用 RLE */ ``` 在 `app_resource_config.json` 的图像条目中指定 `compress` 字段: ```json { "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=` 即可生成压缩后的 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) |