4、色彩空间与像素格式:RGB565、RGB888、YUV422、灰度图格式详解、色彩空间转换原理、查表法加速转换
做嵌入式视觉,说白了就是跟像素打交道。你拿到的原始数据长什么样,直接决定了你后续的处理难度和速度。我见过不少新手,一上来就调算法,结果发现颜色不对、内存爆了,最后回头一看——像素格式搞错了。
这一章,咱们就把几种常见的像素格式掰开揉碎讲清楚。包括它们怎么存的、怎么用的,以及怎么在裸机环境下高效转换。
4.1 RGB565:省内存的“小钢炮”
RGB565 是嵌入式领域的常客。为什么?因为它只用 16 位(2 字节)就能表示一个彩色像素。
它的分配方式是这样的:
- 红色(R):5 位,取值范围 0-31
- 绿色(G):6 位,取值范围 0-63
- 蓝色(B):5 位,取值范围 0-31
你可能会问:为什么绿色多一位?因为人眼对绿色最敏感。多给绿色一点精度,视觉上更舒服。这是当年做显示标准的人琢磨出来的。
我在项目里用 STM32 驱动一块 320x240 的 TFT 屏时,就用 RGB565。一张全彩图只需要 320×240×2 = 153600 字节,也就是 150KB 左右。换成 RGB888 的话,直接翻倍到 300KB。对于片内 RAM 只有 64KB 的 MCU 来说,这差别就是能不能跑的区别。
存储格式(小端模式):
// 一个像素的 16 位布局
// 高字节 [R4 R3 R2 R1 R0 G5 G4 G3]
// 低字节 [G2 G1 G0 B4 B3 B2 B1 B0]
// 打包成 uint16_t
uint16_t rgb565_pixel = ((r & 0x1F) << 11) |
((g & 0x3F) << 5) |
(b & 0x1F);
4.2 RGB888:真彩色,但吃内存
RGB888 就是每个颜色分量各占 8 位,一共 24 位(3 字节)。它能表示 1600 万种颜色,也就是我们常说的“真彩色”。
在裸机环境下,RGB888 通常用于:
- 需要高色彩精度的图像处理(比如颜色识别、肤色检测)
- 与 PC 端算法对接时的中间格式
- 摄像头传感器直接输出的原始格式(很多 OV 系列传感器支持)
存储方式一般有两种:
- 打包模式:连续存放 R、G、B,每个像素 3 字节
- 平面模式:所有 R 放一块,所有 G 放一块,所有 B 放一块
我个人更推荐打包模式,因为访问一个像素时只需要一次地址计算,缓存命中率也高。
// RGB888 打包模式,像素 (x,y) 的偏移
uint32_t offset = (y * width + x) * 3;
uint8_t r = buffer[offset];
uint8_t g = buffer[offset + 1];
uint8_t b = buffer[offset + 2];
4.3 YUV422:视频领域的“老大哥”
YUV 格式在视频领域用得最多。Y 是亮度,UV 是色度。人眼对亮度变化敏感,对颜色变化不敏感。所以 UV 分量可以降低采样率,节省带宽。
YUV422 的意思是:每 2 个像素共享一组 UV。具体来说:
- 像素 0:Y0, U0
- 像素 1:Y1, V0
- 像素 2:Y2, U1
- 像素 3:Y3, V1
所以 2 个像素一共占 4 字节,平均每个像素 2 字节——跟 RGB565 一样省内存,但色彩还原度更好。
我记得有一次调试 USB 摄像头,驱动返回的就是 YUYV 格式(一种 YUV422 的变体)。我一开始没注意,直接当 RGB 处理,结果画面全是紫绿色的。后来才反应过来——得先做 YUV 到 RGB 的转换。
YUV422 的存储排列(YUYV 顺序):
// 内存布局:Y0 U0 Y1 V0 Y2 U2 Y3 V2 ...
// 每 4 字节代表 2 个像素
uint8_t y0 = buffer[0];
uint8_t u0 = buffer[1];
uint8_t y1 = buffer[2];
uint8_t v0 = buffer[3];
4.4 灰度图:最简单的格式
灰度图每个像素只有一个值,范围 0-255。0 是纯黑,255 是纯白。
它有多简单?简单到你可以直接当一维数组处理。320x240 的灰度图只需要 76.8KB。
在嵌入式视觉里,很多算法第一步就是把彩色图转成灰度图。因为:
- 数据量减少 2/3(RGB888 转灰度)
- 后续处理(边缘检测、二值化)只关心亮度,不关心颜色
- 内存占用小,适合裸机环境
灰度转换的标准公式:
// 标准 ITU-R BT.601 亮度公式
uint8_t gray = (uint8_t)(0.299f * r + 0.587f * g + 0.114f * b);
但浮点运算在 MCU 上太慢了。我一般用整数近似:
// 整数近似,误差在 1% 以内
uint8_t gray = (r * 77 + g * 150 + b * 29) >> 8;
4.5 色彩空间转换原理
色彩空间转换,本质上就是坐标变换。RGB 和 YUV 之间是线性关系,可以用矩阵乘法表示。
RGB 转 YUV(BT.601 标准):
Y = 0.299*R + 0.587*G + 0.114*B
U = -0.147*R - 0.289*G + 0.436*B + 128
V = 0.615*R - 0.515*G - 0.100*B + 128
YUV 转 RGB:
R = Y + 1.140*V
G = Y - 0.395*U - 0.581*V
B = Y + 2.032*U
你想想看,如果每个像素都做一次浮点乘加,320x240 的图像就是 76800 次运算。在几十兆赫兹的 MCU 上,这能跑死。
所以,裸机环境下我们几乎不用浮点。全部转成定点数:
// 定点数版本,系数放大 1024 倍
int y = (r * 306 + g * 601 + b * 117) >> 10;
int u = ((b * 446 - r * 150 - g * 296) >> 10) + 128;
int v = ((r * 630 - g * 527 - b * 102) >> 10) + 128;
4.6 查表法:让转换飞起来
查表法(LUT,Look-Up Table)是嵌入式优化的核心手段。它的思想很简单:把计算结果提前算好存起来,运行时直接查,省去计算时间。
对于 RGB 转灰度,输入是 3 个 8 位值,组合起来有 256^3 = 1600 万种可能。全查表不现实。但我们可以拆开:
- 预计算 r * 77 的所有可能值(256 个)
- 预计算 g * 150 的所有可能值(256 个)
- 预计算 b * 29 的所有可能值(256 个)
这样只需要 3 个 256 元素的数组,总共 768 字节。运行时:
// 预计算表
static uint16_t lut_r[256], lut_g[256], lut_b[256];
void lut_init(void) {
for (int i = 0; i < 256; i++) {
lut_r[i] = i * 77;
lut_g[i] = i * 150;
lut_b[i] = i * 29;
}
}
// 查表转换
uint8_t fast_gray(uint8_t r, uint8_t g, uint8_t b) {
return (lut_r[r] + lut_g[g] + lut_b[b]) >> 8;
}
这个优化效果有多明显?我在一个 72MHz 的 STM32F4 上测试过:
- 浮点版本:每像素约 1.2μs
- 定点版本:每像素约 0.4μs
- 查表版本:每像素约 0.15μs
查表法比浮点快了 8 倍。对于 640x480 的图像,总时间从 368ms 降到了 46ms。这就是能不能做到实时处理的区别。
4.7 实战建议:怎么选格式?
根据我多年的经验,给你几个选择建议:
| 场景 | 推荐格式 | 原因 |
|---|---|---|
| 显示图片(TFT 屏) | RGB565 | 省内存,屏驱直接支持 |
| 颜色识别(如找红色球) | RGB888 | 色彩精度高,阈值好调 |
| 摄像头视频流 | YUV422 | 带宽小,兼容性好 |
| 边缘检测/二值化 | 灰度图 | 数据量最小,处理最快 |
嗯,这一章的内容就这些。记住一句话:选对格式,优化查表,你的裸机视觉程序就成功了一半。下一章咱们聊聊图像预处理——滤波和边缘检测,那才是真正考验算法功底的地方。