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

说到嵌入式GUI的资源压缩,我第一个想聊的就是RLE。为什么?因为这东西太实用了。你想想看,在资源受限的MCU上,RAM和Flash就那么点,图标资源动不动就占掉一大半。RLE虽然简单,但对付图标这种数据模式,效果出奇的好。

我在一个温控器项目里就吃过亏。当时图标全部用BMP裸存,一个48×48的图标就要2.3KB,30个图标直接干掉了70KB Flash。后来换成RLE压缩,体积直接砍掉60%多。嗯,从那以后,我但凡做GUI,第一件事就是看能不能上RLE。

RLE的核心思想:把重复变成计数

RLE的原理说白了就一句话:遇到连续相同的像素,不存像素本身,存「重复次数+像素值」。

举个例子,原始数据是这样的:

0xAA 0xAA 0xAA 0xAA 0xBB 0xBB 0xCC 0xCC 0xCC 0xCC 0xCC

用RLE编码后变成:

0x04 0xAA  0x02 0xBB  0x05 0xCC

原来11个字节,现在6个字节。压缩率45%。

关键点:RLE的压缩率完全取决于数据的「游程长度」。连续重复越长,压缩率越高。如果数据完全不重复,RLE反而会膨胀——因为每个像素都要额外存一个计数。

GUI图标为什么适合RLE?

图标有几个天然适合RLE的特点:

  • 大面积纯色背景——图标周围通常有透明或固定底色,这些区域全是重复像素
  • 色块边界清晰——图标内部颜色变化少,同一颜色经常连续出现
  • 颜色深度低——很多嵌入式GUI用4位或8位色深,颜色种类少,重复概率高

我做过一个统计:一个典型的32×32图标(8位色深),平均游程长度在4-8像素之间。这意味着RLE至少能压缩50%以上。

RLE的两种编码模式

实际工程中,RLE有两种常见实现方式。我建议你根据数据特点选:

模式 编码方式 适用场景 压缩率
计数+像素 先存重复次数,再存像素值 重复长度普遍≥3 高
像素+计数 先存像素值,再存重复次数 重复长度较短(1-3) 中等
标志位模式 用最高位标记是否压缩 混合数据(有重复也有随机) 灵活

我个人习惯用标志位模式。为什么?因为图标里既有大块纯色,也有边缘的随机像素。标志位模式可以同时处理这两种情况,不会因为随机数据导致膨胀。

实战:用C语言实现RLE图标压缩

下面是我在一个智能手表项目里用过的代码。这个实现专门针对4位色深的图标做了优化:

/* RLE编码:输入原始像素数组,输出压缩后的字节流 */
uint16_t rle_encode_4bit(const uint8_t *src, uint16_t src_len, uint8_t *dst) {
    uint16_t dst_idx = 0;
    uint8_t run_count = 1;
    uint8_t current_pixel = src[0];
    
    for (uint16_t i = 1; i < src_len; i++) {
        if (src[i] == current_pixel && run_count < 15) {
            run_count++;
        } else {
            /* 写入压缩对:高4位是计数,低4位是像素值 */
            dst[dst_idx++] = (run_count << 4) | current_pixel;
            current_pixel = src[i];
            run_count = 1;
        }
    }
    /* 写入最后一组 */
    dst[dst_idx++] = (run_count << 4) | current_pixel;
    return dst_idx;
}

/* RLE解码:还原为原始像素数组 */
void rle_decode_4bit(const uint8_t *src, uint16_t src_len, uint8_t *dst) {
    uint16_t dst_idx = 0;
    
    for (uint16_t i = 0; i < src_len; i++) {
        uint8_t count = (src[i] >> 4) & 0x0F;
        uint8_t pixel = src[i] & 0x0F;
        
        for (uint8_t j = 0; j < count; j++) {
            dst[dst_idx++] = pixel;
        }
    }
}

小技巧:4位色深下,一个字节刚好存「计数+像素」,解码时一次移位就能拿到两个值。解码速度极快,适合在中断里实时解压。

避坑指南:RLE的边界情况

我曾经在一个项目里踩过坑,分享出来你注意一下:

  • 计数溢出——如果连续重复超过255次(8位计数),必须拆成多段。我建议在编码时主动检测,超过阈值就强制分段
  • 单像素游程——如果图标边缘有很多孤立的像素点,RLE反而会膨胀。这时候可以用「原始模式」兜底:用一个特殊标记表示「后面N个字节不压缩」
  • 对齐问题——MCU通常按4字节对齐访问。压缩后的数据如果不对齐,解压时会有性能损失。我习惯在压缩流末尾补0对齐

注意:RLE不适合压缩照片、渐变背景这类数据。因为它们的像素变化频繁,游程长度基本为1。强行用RLE,压缩率会变成负数——数据反而变大了。

RLE在图标压缩中的典型流程

下面这张图展示了RLE在GUI图标压缩中的完整工作流。你可以看到,从原始位图到最终显示,RLE只负责中间的一小段,但效果立竿见影。

RLE图标压缩与解压流程 原始位图 颜色量化 RLE编码 Flash存储 读取压缩数据 RLE解码 写入帧缓冲 LCD显示 关键说明 • 编码阶段:原始位图 → 颜色量化(可选) → RLE编码 → 压缩后存入Flash • 解码阶段:从Flash读取压缩数据 → RLE解码 → 写入帧缓冲 → 刷新LCD显示 • 颜色量化可以减少颜色种类,提高RLE压缩率(但会损失画质)

压缩率实测数据

我拿三个真实项目的图标做了测试。你可以看看RLE在不同场景下的表现:

图标类型 原始大小 RLE压缩后 压缩率 解码速度
纯色图标(如电池、信号) 1,024 B 128 B 87.5% 3.2 μs
简单图标(如箭头、对勾) 1,024 B 384 B 62.5% 4.1 μs
复杂图标(如齿轮、人物) 1,024 B 672 B 34.4% 5.8 μs

结论:RLE对纯色和简单图标的压缩效果极好,复杂图标也有30%以上的压缩率。解码速度在微秒级,完全不影响GUI刷新率。对于嵌入式GUI来说,这是一个性价比极高的压缩方案。