图像在嵌入式系统中的表示:像素与色彩空间
做嵌入式视觉部署,第一件事就是搞清楚图像在硬件里到底长什么样。你想想看,摄像头传过来的数据,不是一张“图片”,而是一串数字。怎么解读这串数字,直接决定了你后续算法的效率和正确性。
我个人习惯把图像在嵌入式系统中的表示拆成三个层次来看:像素是什么、颜色怎么编码、数据怎么排列。搞懂这三层,你基本就能跟任何一款摄像头芯片“对话”了。
像素——图像的最小单元
说白了,一张数字图像就是一个二维数组。数组里的每个元素就是一个像素。每个像素记录的是该位置的颜色信息。
我在项目中遇到过一个问题:客户说他们摄像头输出的图像“偏绿”。查了半天,发现是像素格式配置错了——他们把BGR数据当RGB解析了。你看,一个像素的“颜色”怎么表示,就是第一个坑。
色彩空间——颜色的“语言”
不同的色彩空间,就是不同的颜色编码方式。嵌入式视觉里最常用的有这几种:
| 色彩空间 | 典型用途 | 数据量(每像素) | 我的经验 |
|---|---|---|---|
| RGB | 显示、桌面应用 | 24 bit | 最直观,但嵌入式里用得少 |
| BGR | OpenCV、很多摄像头 | 24 bit | OpenCV默认格式,容易跟RGB搞混 |
| YUV | 视频编码、ISP输出 | 12~24 bit | 压缩率高,但处理麻烦 |
| 灰度图 | 边缘检测、二值化 | 8 bit | 我最喜欢,省带宽省内存 |
RGB vs BGR——一个经典的坑
嗯,这里要注意。RGB和BGR在数学上完全一样,只是通道顺序不同。但如果你用OpenCV读一张图,默认是BGR;用PIL读,默认是RGB。在嵌入式设备上,摄像头硬件输出的顺序又可能不一样。
我曾经调试一个模型部署项目,推理结果一直不对。折腾了两天,最后发现是预处理时把BGR当RGB送进了模型。模型学的是RGB,你给它BGR,它当然不认识。
YUV——视频领域的“老大哥”
YUV把亮度(Y)和色度(U、V)分开。人眼对亮度敏感,对颜色不敏感,所以色度可以降低采样率。这就是为什么YUV能压缩数据量。
常见的YUV格式有:
- YUV420:U和V的分辨率只有Y的1/4。最常用,H.264/H.265默认输出。
- YUV422:U和V的水平分辨率是Y的一半。
- YUV444:全分辨率,数据量跟RGB一样大。
我在做ISP(图像信号处理器)驱动时,经常要处理YUV到RGB的转换。转换公式本身不复杂,但定点化实现时容易丢精度。我建议你用查表法加速,别在嵌入式MCU上做浮点运算。
灰度图——最“省”的选择
如果你的算法不需要颜色信息(比如边缘检测、轮廓提取、二值化),直接用灰度图。一个像素只占8 bit,是RGB的1/3。在带宽和内存都紧张的嵌入式系统里,这差别很大。
图像分辨率与位深
这两个参数决定了图像的数据量。
- 分辨率:宽×高,比如640×480。分辨率越高,细节越多,但数据量也越大。
- 位深:每个像素用多少bit表示。8 bit最常见,10 bit/12 bit用于高动态范围场景。
数据量计算公式很简单:
图像大小(字节) = 宽 × 高 × 位深 / 8 × 通道数
举个例子:一张640×480的RGB图像(24 bit/像素),大小是:
640 × 480 × 24 / 8 = 921,600 字节 ≈ 0.88 MB
在只有几MB内存的MCU上,这已经不小了。所以嵌入式视觉里经常要权衡分辨率和帧率。
图像数据在内存中的存储格式:HWC vs CHW
这是嵌入式视觉里最容易出问题的地方之一。同样的图像数据,排列方式不同,处理逻辑完全不同。
| 格式 | 含义 | 典型框架 | 内存布局 |
|---|---|---|---|
| HWC | 高度×宽度×通道 | OpenCV、摄像头驱动 | 先存第一行所有像素(RGBRGB...),再存第二行 |
| CHW | 通道×高度×宽度 | PyTorch、Caffe、ONNX | 先存所有R通道,再存所有G通道,再存所有B通道 |
为什么会有这种区别?
- HWC:对像素级操作友好。你要访问某个像素的RGB值,它们在内存里是连续的,一次DMA传输就能拿到。
- CHW:对卷积操作友好。卷积核要同时处理多个通道的同一位置,CHW布局让同一通道的数据连续,缓存命中率高。
我刚开始做模型部署时,没注意这个区别。模型在PC上跑得好好的,部署到嵌入式设备上就出错了。查了半天,发现是输入数据的layout不对——模型期望CHW,我给了HWC。
HWC转CHW的代码示例
这个转换在嵌入式设备上很常见。下面是一个简单的C语言实现:
// HWC转CHW:假设输入是HWC格式的RGB图像
// 输入:hwc_data [H][W][3]
// 输出:chw_data [3][H][W]
void hwc_to_chw(uint8_t* hwc_data, uint8_t* chw_data, int H, int W) {
int total_pixels = H * W;
for (int i = 0; i < total_pixels; i++) {
// HWC中第i个像素的R、G、B值
chw_data[0 * total_pixels + i] = hwc_data[i * 3 + 0]; // R通道
chw_data[1 * total_pixels + i] = hwc_data[i * 3 + 1]; // G通道
chw_data[2 * total_pixels + i] = hwc_data[i * 3 + 2]; // B通道
}
}
嗯,这个实现很直观,但效率不高。在嵌入式设备上,我建议用DMA或者NEON指令来加速。如果数据量大,这个转换本身就会成为瓶颈。
知识结构总览
下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速参考:
总结一下
图像在嵌入式系统中的表示,说白了就是三件事:
- 像素:每个点记录颜色信息,是图像的基本单元。
- 色彩空间:RGB/BGR/YUV/灰度,决定了颜色怎么编码。选对了省事,选错了全是坑。
- 存储格式:HWC还是CHW,决定了数据怎么在内存里排列。搞混了,模型推理结果就是错的。
我个人建议,在开始任何嵌入式视觉项目之前,先花半天时间把这三个问题搞清楚。写一个测试程序,从摄像头读一帧数据,打印前几个像素的值,验证色彩空间和存储格式。这个习惯帮我避免了很多后期调试的麻烦。