3、无损压缩算法(上):RLE(游程编码)原理与在GUI图标压缩中的应用

说到嵌入式GUI的资源压缩,我第一个想聊的就是RLE。为什么?因为这东西太实用了,而且实现起来极其简单。你想想看,在资源受限的MCU上,你不可能跑个复杂的压缩算法,那会吃掉大量RAM和CPU时间。RLE(Run-Length Encoding,游程编码)恰恰相反——它几乎不消耗额外内存,解压速度还飞快。

我记得刚入行那会儿,接手一个128×64的单色屏项目。图标资源占了Flash的一大半,老板天天催着要省空间。我当时试了好几种压缩方案,最后发现RLE最适合这种场景。说白了,图标里大片的连续相同像素,正是RLE的用武之地。

3.1 RLE的核心思想

RLE的原理简单到令人发指:把连续重复的数据,替换成「重复次数 + 数据值」的对儿。

举个例子,原始数据是:

AAAAABBBCCCCDDDDD

经过RLE编码后变成:

5A3B4C5D

你看,原本17个字符,压缩后只有8个字符。压缩率接近53%。当然,这是理想情况。如果数据本身重复性不高,RLE反而可能让数据变大。这一点我后面会详细说。

核心公式:

压缩率 = (原始大小 - 压缩后大小) / 原始大小 × 100%

当连续重复长度 ≥ 2 时,RLE才有收益。

3.2 RLE的两种常见编码格式

实际工程中,RLE的编码格式有很多变种。我挑两种最常用的来讲,这两种我在项目里都用过。

3.2.1 字节对格式(最常用)

这种格式用两个字节表示一个游程:第一个字节是计数值,第二个字节是数据值。

原始数据 编码后 说明
0xFF 0xFF 0xFF 0xFF 0x04 0xFF 4个0xFF
0x00 0x00 0x00 0x03 0x00 3个0x00
0x12 0x34 0x56 0x01 0x12 0x01 0x34 0x01 0x56 没有重复,反而膨胀了

这种格式的优点是实现简单,解码器只需要循环读取两个字节,然后填充数据即可。缺点是计数值通常限制在255以内(一个字节),如果连续重复超过255次,需要拆分成多个游程。

3.2.2 标志位格式(更灵活)

这种格式用一个标志位来区分「重复数据」和「非重复数据」。我习惯用最高位作为标志:

  • 最高位为1:表示后面是重复数据,低7位是计数值
  • 最高位为0:表示后面是非重复数据,低7位是长度
// 编码示例
原始数据: 0xFF 0xFF 0xFF 0xFF 0x12 0x34 0x56
编码后:   0x84 0xFF 0x03 0x12 0x34 0x56
// 0x84 = 1000 0100b → 重复模式,计数值=4
// 0x03 = 0000 0011b → 非重复模式,长度=3

这种格式的好处是能处理非重复数据,不会像字节对格式那样膨胀。但解码逻辑稍微复杂一点。

我的建议:如果你的图标数据中重复和非重复区域混杂,用标志位格式。如果图标背景色单一(比如大部分是白色背景),用字节对格式更省事。

3.3 RLE在GUI图标压缩中的实战

好,理论说完了,咱们直接上代码。下面是我在一个温控器项目里实际用过的图标压缩代码。

3.3.1 图标数据的特点

嵌入式GUI的图标通常有这些特点:

  • 尺寸小:16×16、24×24、32×32是常见尺寸
  • 颜色少:单色或16色居多
  • 大面积连续色块:背景色往往是大片相同的像素
  • 边缘锐利:图标边缘通常只有1-2个像素的过渡

这些特点决定了RLE非常适合图标压缩。尤其是单色图标,压缩率经常能达到70%以上。

3.3.2 编码器实现(C语言)

/**
 * RLE编码器 - 字节对格式
 * input:  原始数据指针
 * in_len: 原始数据长度
 * output: 编码后数据指针
 * return: 编码后数据长度
 */
uint16_t rle_encode(uint8_t *input, uint16_t in_len, uint8_t *output)
{
    uint16_t out_idx = 0;
    uint16_t i = 0;
    
    while (i < in_len) {
        uint8_t count = 1;
        
        // 统计连续重复的字节数
        while ((i + count < in_len) && 
               (input[i + count] == input[i]) && 
               (count < 255)) {
            count++;
        }
        
        // 写入计数值和数据值
        output[out_idx++] = count;
        output[out_idx++] = input[i];
        
        i += count;
    }
    
    return out_idx;
}

3.3.3 解码器实现(C语言)

/**
 * RLE解码器 - 字节对格式
 * input:  编码后数据指针
 * in_len: 编码后数据长度
 * output: 解码后数据缓冲区
 * return: 解码后数据长度
 */
uint16_t rle_decode(uint8_t *input, uint16_t in_len, uint8_t *output)
{
    uint16_t out_idx = 0;
    uint16_t i = 0;
    
    while (i < in_len) {
        uint8_t count = input[i++];
        uint8_t value = input[i++];
        
        // 填充count个value
        for (uint8_t j = 0; j < count; j++) {
            output[out_idx++] = value;
        }
    }
    
    return out_idx;
}

注意:解码时一定要做边界检查!我曾经在一个项目里因为没检查输出缓冲区大小,导致解码时写穿了内存,整个系统直接挂掉。排查了整整两天才找到问题。

3.4 RLE的局限性

RLE不是万能的。我吃过它的亏,所以得跟你说明白。

场景 RLE表现 原因
单色图标(大面积背景) 优秀,压缩率70-90% 大量连续相同像素
16色图标(色块分明) 良好,压缩率40-60% 颜色切换频繁但仍有连续区域
照片/渐变图 差,可能膨胀 相邻像素几乎都不相同
抖动处理后的图像 极差,必然膨胀 像素值随机分布,无重复

所以,RLE只适合特定类型的图像。在GUI中,它最适合压缩图标、按钮、边框这类图形元素。对于背景图或照片,你得用其他算法,比如LZSS或者哈夫曼编码,这些后面章节会讲到。

3.5 一个完整的图标压缩示例

咱们来看一个实际的32×32单色电池图标。原始数据是128字节(32×32/8),每个bit代表一个像素。

// 原始图标数据(部分)
// 电池图标:中间是电池主体,上下是电极
uint8_t battery_icon[] = {
    0xFF, 0xFF, 0xFF, 0xFF,  // 第1行:全黑(电极)
    0x80, 0x00, 0x00, 0x01,  // 第2行:左右边框
    0x80, 0x00, 0x00, 0x01,  // 第3行:左右边框
    0x80, 0x00, 0x00, 0x01,  // 第4行:左右边框
    0x80, 0x00, 0x00, 0x01,  // 第5行:左右边框
    0xFF, 0xFF, 0xFF, 0xFF,  // 第6行:全黑(电极)
    // ... 省略中间部分
};

// 经过RLE编码后
// 0xFF 0xFF 0xFF 0xFF → 0x04 0xFF
// 0x80 0x00 0x00 0x01 → 0x01 0x80 0x02 0x00 0x01 0x01
// 压缩率计算:
// 原始:128字节
// 压缩后:约52字节
// 压缩率:(128-52)/128 ≈ 59.4%

你看,一个简单的电池图标,压缩率接近60%。如果你的Flash里有100个这样的图标,就能省下7.6KB的空间。在资源紧张的MCU上,这可不是小数目。

优化技巧:如果你发现某些图标的压缩率不理想,可以试试调整图标的绘制方式。比如把渐变改成纯色块,或者减少抖动处理。我曾经把一个压缩率只有20%的图标,通过重新设计,把压缩率提升到了65%。

3.6 性能考量

RLE的解压速度非常快,因为它只需要简单的循环和内存拷贝。在我的测试中,在72MHz的Cortex-M3上,解码一个32×32的图标只需要不到100微秒。这对于GUI刷新来说完全可以接受。

但要注意一点:RLE解码是串行的,你不能随机访问某个像素。如果你需要频繁更新图标的某个局部区域,RLE可能不是最佳选择。这种情况下,可以考虑把图标分成多个小块分别压缩,或者干脆不解压,直接存储原始数据。

嗯,RLE就聊到这儿。它虽然简单,但在嵌入式GUI中确实是个利器。下一节我们会讲另一种无损压缩算法——LZSS,它在处理更复杂的数据时表现更好。

RLE游程编码核心逻辑 原始数据 RLE编码过程 扫描连续重复 → 统计次数 → 生成对儿 编码后数据 RLE解码过程 适用场景 ✓ 单色图标 ✓ 大面积色块 ✓ 简单图形 不适用场景 ✗ 照片/渐变图 ✗ 抖动处理图像 ✗ 随机分布数据 示例 原始: AAAABBBCC 编码: 4A3B2C 压缩率: 55.6%