资源压缩基础:图像压缩原理与嵌入式取舍

做嵌入式GUI,说白了就是跟资源较劲。屏幕越来越大,效果要求越来越高,但Flash和RAM就那么点。图像压缩,就是咱们手里的第一把刀。

我刚开始做嵌入式GUI那会儿,总觉得压缩是上层应用的事。直到有一次,一个简单的开机动画就把256MB的Flash撑爆了……嗯,从那以后我再也不敢小看压缩这回事了。

有损 vs 无损:本质区别在哪?

先搞清楚一个核心问题:压缩到底在干什么?

无损压缩,就是压缩完解压出来,跟原来一模一样。一个像素都不差。有损压缩呢?它会丢掉一些人眼不太敏感的信息,换来更高的压缩比。

核心区别一句话:无损压缩靠的是数据冗余,有损压缩靠的是视觉冗余。

举个例子。一张纯蓝色的图,每个像素都是(0,0,255)。无损压缩会记录成"连续1000个蓝色像素",而不是存1000遍。有损压缩呢?它可能会把相邻几个差不多的蓝色合并成一个,反正人眼看不出区别。

我在项目里遇到过这种情况:用JPEG压缩一张带文字的按钮图,结果文字边缘全是鬼影。为什么?因为JPEG对有高频细节的区域(比如文字边缘)压缩效果很差。这时候就得换无损格式。

特性 无损压缩 有损压缩
还原度 100%还原 有信息丢失
压缩比 一般2:1~5:1 可达10:1~50:1
适用场景 图标、文字、UI控件 照片、渐变背景、纹理
典型格式 PNG、BMP(RLE) JPEG、WebP(有损模式)

PNG、JPEG、BMP:嵌入式里的三选一

这三种格式,做嵌入式GUI的几乎天天见。但怎么选,很多人其实没想清楚。

BMP:最老实,也最占地方

BMP就是不压缩,或者最多做个简单的RLE(游程编码)。每个像素原样存着。好处是解码快,几乎不消耗CPU。坏处是……太占空间了。

一个800×480的32位BMP,算一下:800×480×4 = 1,536,000字节,差不多1.5MB。放几张图,Flash就没了。

注意:BMP的RLE压缩只支持8位及以下的索引色。真彩色BMP的RLE压缩效果很差,基本等于没压。

我个人习惯,BMP只用在两种场景:一是Flash特别大、CPU特别弱的平台;二是做临时帧缓冲,直接操作显存。

PNG:无损之王,但解码有代价

PNG用的是Deflate算法,无损压缩。对UI图标、小控件来说,压缩比很不错。但问题在于解码。

PNG解码需要做几件事:

  • 解压Deflate数据流
  • 重建滤镜(每行像素的差值还原)
  • 如果是索引色,还要查调色板

这些操作在MCU上跑,挺吃力的。我记得有一次在Cortex-M4上解码一张320×240的PNG,花了将近200ms。用户按个按钮,等200ms才看到变化,体验很差。

小技巧:如果非要用PNG,可以预解码成RGB565或索引色格式存到外部Flash。运行时直接读像素数组,不用再解压。

JPEG:压缩率高,但质量要小心

JPEG的压缩率确实诱人。一张照片级的背景图,BMP要几MB,JPEG可能就几十KB。但JPEG有两个坑:

  • 块效应:压缩率太高时,8×8的块边界会很明显
  • 不支持透明:JPEG没有Alpha通道

我做开机画面时用过JPEG。效果还行,但要注意质量因子。质量因子低于70时,文字和线条边缘就开始糊了。UI上用的JPEG,我建议质量因子不低于85。

格式 压缩类型 解码速度 存储效率 透明支持 嵌入式推荐度
BMP 无/简单RLE 极快 极低 支持 ★★☆☆☆
PNG 无损(Deflate) 较慢 中等 支持 ★★★★☆
JPEG 有损(DCT) 中等 高 不支持 ★★★☆☆

调色板与索引色:嵌入式GUI的杀手锏

说完了格式,咱们聊聊一个特别适合嵌入式的东西——索引色。

你想想看,一张图标可能只有16种颜色。用32位真彩色存,每个像素4字节。但如果用索引色呢?每个像素只存一个字节的索引值,然后查调色板得到真实颜色。

这就是调色板(Palette)和索引色(Indexed Color)的核心思想。

调色板怎么工作?

调色板就是一个颜色查找表。比如4位索引色:

// 调色板:16种颜色,每种用RGB565表示
uint16_t palette[16] = {
    0x0000, // 索引0: 黑色
    0xFFFF, // 索引1: 白色
    0xF800, // 索引2: 红色
    0x07E0, // 索引3: 绿色
    0x001F, // 索引4: 蓝色
    // ... 其他12种颜色
};

// 像素数据:每个字节存一个索引值
uint8_t pixel_data[] = {
    0x01, 0x02, 0x03, 0x04,  // 第一行像素
    0x04, 0x03, 0x02, 0x01,  // 第二行像素
    // ...
};

显示的时候,每个像素先读索引值,再到调色板里查颜色,然后送显存。就这么简单。

位深度与存储节省

索引色支持不同的位深度:

位深度 颜色数 每像素字节数 适用场景
1位 2色 1/8字节 黑白图标、文字
2位 4色 1/4字节 简单图形
4位 16色 1/2字节 图标、小控件
8位 256色 1字节 复杂UI、渐变

对比一下:一张32×32的图标,用32位真彩色要4096字节。用4位索引色只要512字节(像素数据)+ 32字节(调色板)。省了将近8倍。

关键点:索引色的存储节省是立竿见影的。但前提是——你的图像颜色数不能超过调色板容量。否则就得做颜色量化,把相近的颜色合并。

颜色量化:怎么把256色变成16色?

颜色量化是个算法活。常用的有:

  • 均匀量化:把RGB空间均匀切块,每个块取平均值。简单,但效果一般。
  • 中位切分法:每次选颜色分布最分散的维度,切一刀。递归直到得到目标颜色数。效果不错。
  • 八叉树量化:用八叉树结构合并颜色节点。速度快,效果好,我比较推荐。

我曾经在项目里用八叉树量化,把一张256色的照片压到16色。人眼几乎看不出区别,但存储从原来的80KB降到了10KB。值不值?太值了。

实用建议:做UI设计时,尽量控制每个控件的颜色数。图标不超过16色,按钮不超过32色。这样可以直接用索引色,省掉量化的麻烦。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个决策流程图:

嵌入式图像压缩决策流程 原始图像资源 有损压缩 无损压缩 JPEG / WebP(有损) 适合:照片、渐变背景 PNG / BMP(RLE) 适合:图标、文字、控件 索引色(调色板) 颜色量化(中位切分/八叉树) 最终:存储节省 4~8 倍

从原始图像出发,先判断能不能接受有损。能接受,走JPEG路线;不能接受,走PNG或索引色路线。索引色是嵌入式特有的优化手段,配合颜色量化,效果往往比直接用PNG还好。