4. 像素数据结构:RGB与ARGB模型、像素排列方式、像素对齐问题

说到像素数据结构,我得先坦白一件事。我入行头两年,一直觉得这东西太基础,没什么好深究的。直到有一次,我负责的一个嵌入式项目,屏幕显示出来的图片总是偏绿,查了三天硬件,最后发现是代码里把 RGB 和 BGR 搞反了。嗯,从那以后,我再也不敢小看这几个字母的顺序了。

说白了,像素数据就是屏幕上一个点的颜色信息。计算机没法直接画颜色,它得用数字来描述。这个描述方式,就是我们今天要聊的像素数据结构。

4.1 RGB 与 ARGB 模型

RGB 模型,你应该不陌生。红、绿、蓝三个通道,每个通道用一个字节(0-255)表示亮度。三个通道组合起来,就能表示 1600 多万种颜色。这基本覆盖了人眼能识别的所有颜色。

但这里有个细节——颜色深度。常见的 RGB 是 24 位色(8+8+8),但有些老设备或者特殊格式会用 16 位色(5+6+5)。我建议你在做资源解析时,先确认一下颜色深度,否则解析出来的颜色会完全不对。

ARGB 模型呢,就是在 RGB 的基础上加了一个 Alpha 通道。Alpha 表示透明度,0 是完全透明,255 是完全不透明。这个在 UI 开发里太常见了,比如弹窗的遮罩层、渐变的图标、半透明的文字阴影。

关键区别:

  • RGB:24 位,无透明度,适合不透明图片(JPEG、BMP 常见)
  • ARGB:32 位,有透明度,适合需要混合效果的图片(PNG、PSD 常见)

我遇到过一个问题:一个 PNG 图片在 Windows 上显示正常,放到某个 Linux 嵌入式系统上,边缘出现了一圈黑边。查到最后,发现是系统默认把未预乘 Alpha 的图片当成了预乘 Alpha 来处理。这个坑,后面讲 Alpha 预乘时会细说。

4.2 像素排列方式

同样是 RGB 三个通道,它们在内存里的排列顺序却可能不一样。这就像三个人排队,谁站前面,结果完全不同。

排列方式 内存顺序 常见场景
RGB R, G, B JPEG、PNG、大多数图片格式
BGR B, G, R Windows BMP、OpenCV 默认格式
RGBA R, G, B, A PNG、Web 标准
BGRA B, G, R, A DirectX、部分 GPU 纹理

你想想看,如果你用 OpenCV 读了一张 BMP 图片,然后直接把它当成 RGB 数据传给显示驱动,会发生什么?没错,红色和蓝色通道互换,整张图偏蓝偏紫。我曾经在做一个摄像头采集项目时,就因为这个顺序问题,调试了整整一个下午。

我的习惯:在代码里显式标注像素格式,不要依赖默认值。比如用 cv::cvtColor(src, dst, cv::COLOR_BGR2RGB) 这种明确的转换,而不是假设输入是什么格式。

4.3 像素对齐问题

像素对齐,听起来有点抽象。我换个说法:每一行像素数据在内存里的起始地址,是不是 4 的倍数?

为什么要在意这个?因为很多硬件(GPU、DMA 控制器、某些 ARM 处理器)访问内存时,对对齐地址的访问效率更高。不对齐的话,轻则性能下降,重则直接崩溃。

举个例子:一张 3 像素宽的 24 位 RGB 图片,每行数据是 3 × 3 = 9 字节。9 不是 4 的倍数,所以很多图片格式会在每行末尾补上 3 个字节的填充(padding),让每行变成 12 字节。这样每行的起始地址就对齐到 4 字节了。

注意:解析 BMP 文件时,必须考虑行对齐填充。BMP 规定每行数据必须是 4 字节对齐的。如果你直接按宽度 × 3 来读取,读出来的数据会错位,整张图看起来就像被撕裂了一样。

我建议你在处理像素数据时,始终使用 stride(步幅)而不是 width 来计算行偏移。stride 是实际每行占用的字节数,包含了填充。

// 错误做法:直接使用宽度
uint8_t* row = data + y * (width * 3);

// 正确做法:使用 stride
uint8_t* row = data + y * stride;

下面这张图,展示了像素数据从图片文件到屏幕显示的完整流程,以及对齐问题可能出现的环节。

像素数据从文件到屏幕的流程与对齐问题 图片文件 (JPEG/PNG/BMP) 解码器 (解析像素数据) 内存缓冲区 (含 stride 填充) 显示 ⚠ 对齐问题高发区 1. 解码后 stride 计算错误 2. 内存分配未对齐 3. DMA 传输要求 4 字节对齐 ⚠ 排列方式问题 1. RGB vs BGR 混淆 2. Alpha 通道被忽略 3. 颜色深度不匹配 常见花屏现象与根因对照 • 整张图偏蓝/偏红 → RGB/BGR 顺序错误 • 图片出现斜向条纹 → stride 计算错误,行数据错位 • 边缘出现黑边/白边 → Alpha 预乘处理不当 • 颜色断层/色块 → 颜色深度不匹配(如 24 位数据按 16 位解析) • 图片上下颠倒 → 像素行顺序反转(BMP 默认从下往上) • 图片被拉伸/压缩 → 分辨率与显示区域不匹配 排查建议:先确认像素格式,再检查 stride,最后看排列顺序。

4.4 实际项目中的避坑指南

说了这么多理论,我分享几个实际踩过的坑。

  • 坑一:BMP 的 BGR 顺序。我第一次用 Python 的 PIL 库读 BMP 时,直接用了 Image.open(),然后转成 numpy 数组。结果发现红色和蓝色是反的。后来才知道,PIL 默认把 BMP 转成 RGB,但如果你用 cv2.imread(),它保持 BGR 不变。所以,跨库使用时一定要确认像素格式。
  • 坑二:PNG 的 Alpha 通道。有些 PNG 图片的 Alpha 通道是预乘的(premultiplied alpha),有些不是。如果你把预乘的 Alpha 当成非预乘来处理,半透明区域的颜色会偏暗。我建议你在解析 PNG 时,检查一下 IHDR 块里的颜色类型字段。
  • 坑三:硬件对齐要求。我曾经在一个 ARM 平台上做显示驱动,DMA 控制器要求源地址和目标地址都是 4 字节对齐的。如果不对齐,DMA 传输会直接报错。解决办法是在分配缓冲区时,使用 aligned_alloc 或者手动填充到对齐。

一个小技巧:写一个像素格式打印函数,把前几个像素的原始字节打印出来,对照标准颜色值,一眼就能看出顺序对不对。

def dump_pixel(data, x, y, width, bpp):
    """打印指定像素的原始字节"""
    offset = (y * width + x) * bpp
    pixel_bytes = data[offset:offset + bpp]
    hex_str = ' '.join(f'{b:02x}' for b in pixel_bytes)
    print(f'Pixel ({x},{y}): {hex_str}')

嗯,像素数据结构这部分,说白了就是搞清楚三个问题:每个通道占多少位、通道在内存里怎么排、每行数据有没有填充。这三个问题搞清楚了,大部分花屏问题都能找到根因。