Bayer 阵列、RAW10 打包与双线性去马赛克

Bayer 图像、打包后的 RAW 数据流和 RGB 图像分别对应相机处理流程中的不同阶段。本文将介绍 Bayer 彩色滤光片阵列如何把感光单元映射为颜色采样、MIPI RAW10 如何保存这些采样值,以及如何用简单的双线性算法重建 RGB 图像。

Bayer CFA 与传感器感光单元

Bayer pattern 描述的是图像传感器感光单元上方实际排列的彩色滤光片阵列(Color Filter Array,CFA)。每个感光单元只测量经过一种颜色滤光片后的光强,而不是直接获得完整的 RGB 三通道数据。

标准 Bayer 阵列有四种 2x2 排列:

1
2
3
4
RGGB    GRBG    GBRG    BGGR

R G G R G B B G
G B B G R G G R

该排列会在整个有效传感器区域内重复。例如,GRBG 展开后为:

1
2
3
4
G R G R G R ...
B G B G B G ...
G R G R G R ...
B G B G B G ...

标准 Bayer CFA 中绿色采样数量是红色或蓝色的两倍:

1
R : G : B = 1 : 2 : 1

绿色对人眼感知的亮度和空间细节贡献较大,因此提高绿色采样密度可以在图像质量和传感器复杂度之间取得良好平衡。两组绿色采样并不是两个独立的颜色通道,使用的也不是不同种类的绿色滤光片。

对于 RGGB 阵列,有时会根据位置进一步写成:

1
2
R   Gr
Gb B
  • Gr 表示与红色采样位于同一行的绿色。
  • Gb 表示与蓝色采样位于同一行的绿色。

这种写法可以明确相邻关系:Gr 水平方向的相邻点是红色,Gb 水平方向的相邻点是蓝色。

感光单元、RAW 采样与 RGB 像素

假设传感器的有效区域为 100x100,并采用 1:1 读取:

  • 每个有效感光单元都会被读取;
  • 不进行像素合并、裁剪或缩放;
  • 传感器输出 100x100 个 Bayer 采样值;
  • 去马赛克后得到 100x100 个 RGB 像素。

整个过程中,空间尺寸保持不变:

flowchart TD A["100 x 100 个感光单元"] --> B["100 x 100 个单色 Bayer 采样"] B --> C["100 x 100 个重建后的 RGB 像素"]

对于输出位置 (x, y),其中一个颜色分量由对应的感光单元直接测得,另外两个颜色分量则根据周围的采样值估算。

因此,重复出现的 2x2 Bayer 块不是一个超级像素,它只是描述 CFA 的排列方式。在 1:1 处理流程中,每个感光单元在空间位置上仍然对应一个输出 RGB 像素。

Bayer 顺序与 RAW 打包相互独立

Bayer pattern 回答的是:

坐标 (x, y) 处的采样值代表哪一种颜色?

RAW 打包方式回答的是:

每个采样值的比特在内存或传输数据中如何排列?

两者是相互独立的属性。数据可以是 GRBG + packed RAW10,也可以是 RGGB + unpacked RAW12,或者其他组合。

必须根据传感器、接收端或文件格式文档同时确认这两个属性。正确的 RAW10 解包程序无法弥补 Bayer pattern 选择错误的问题;同样,正确选择 Bayer pattern 也无法修复错误的字节解析。

MIPI RAW10 打包格式

RAW10 为每个感光单元保存一个 10 bit 采样值。在常见的 MIPI CSI-2 排列中,4 个像素占用 5 个字节:

1
2
3
4
5
6
7
8
9
B0 = P0[9:2]
B1 = P1[9:2]
B2 = P2[9:2]
B3 = P3[9:2]

B4[1:0] = P0[1:0]
B4[3:2] = P1[1:0]
B4[5:4] = P2[1:0]
B4[7:6] = P3[1:0]

可以按以下方式还原采样值:

1
2
3
4
P0 = (B0 << 2) | ((B4 >> 0) & 0x03)
P1 = (B1 << 2) | ((B4 >> 2) & 0x03)
P2 = (B2 << 2) | ((B4 >> 4) & 0x03)
P3 = (B3 << 2) | ((B4 >> 6) & 0x03)

下面的示例逐行解包,每次处理完整的 4 像素组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

bool unpack_mipi_raw10(
const uint8_t *src,
size_t src_stride,
uint16_t *dst,
size_t dst_stride,
size_t width,
size_t height)
{
if (src == NULL || dst == NULL || width % 4 != 0) {
return false;
}

const size_t packed_row_bytes = (width / 4) * 5;
if (src_stride < packed_row_bytes || dst_stride < width) {
return false;
}

for (size_t y = 0; y < height; ++y) {
const uint8_t *src_row = src + y * src_stride;
uint16_t *dst_row = dst + y * dst_stride;

for (size_t x = 0; x < width; x += 4) {
const uint8_t *group = src_row + (x / 4) * 5;
const uint8_t low = group[4];

dst_row[x + 0] =
((uint16_t)group[0] << 2) | ((low >> 0) & 0x03);
dst_row[x + 1] =
((uint16_t)group[1] << 2) | ((low >> 2) & 0x03);
dst_row[x + 2] =
((uint16_t)group[2] << 2) | ((low >> 4) & 0x03);
dst_row[x + 3] =
((uint16_t)group[3] << 2) | ((low >> 6) & 0x03);
}
}

return true;
}

为了保持示例清晰,这段代码要求图像宽度能够被 4 整除。实际解码器必须按照数据源的具体规则处理不完整像素组、行填充、行头和 stride。并非所有被称为“RAW10”的文件都是连续紧密排列的 MIPI CSI-2 payload。

Unpacked RAW10 通常会使用 16 bit 容器保存每个采样值,但字节序和有效位对齐方式由具体格式决定。10 个有效 bit 可能位于容器的低位,也可能位于高位。

根据坐标确定 Bayer 颜色

解包后,每个数组元素的取值范围为 0~1023。下一步是确定每个坐标对应的颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typedef enum {
BAYER_RGGB,
BAYER_GRBG,
BAYER_GBRG,
BAYER_BGGR
} BayerPattern;

typedef enum {
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE
} BayerColor;

static inline BayerColor bayer_color(
BayerPattern pattern,
int x,
int y)
{
const int xe = x & 1;
const int ye = y & 1;

switch (pattern) {
case BAYER_RGGB:
return ye == 0
? (xe == 0 ? COLOR_RED : COLOR_GREEN)
: (xe == 0 ? COLOR_GREEN : COLOR_BLUE);

case BAYER_GRBG:
return ye == 0
? (xe == 0 ? COLOR_GREEN : COLOR_RED)
: (xe == 0 ? COLOR_BLUE : COLOR_GREEN);

case BAYER_GBRG:
return ye == 0
? (xe == 0 ? COLOR_GREEN : COLOR_BLUE)
: (xe == 0 ? COLOR_RED : COLOR_GREEN);

case BAYER_BGGR:
default:
return ye == 0
? (xe == 0 ? COLOR_BLUE : COLOR_GREEN)
: (xe == 0 ? COLOR_GREEN : COLOR_RED);
}
}

Bayer pattern 改变时,RAW10 解包代码不需要修改。只有坐标到颜色的映射以及后续去马赛克逻辑与 Bayer pattern 有关。

支持四种排列的双线性去马赛克

双线性去马赛克通过相邻采样值的平均值估算缺失的颜色分量:

  • 在红色位置,绿色取上下左右四个采样的平均值,蓝色取四个对角采样的平均值;
  • 在蓝色位置,绿色取上下左右四个采样的平均值,红色取四个对角采样的平均值;
  • 在绿色位置,根据当前绿色属于 Gr 还是 Gb,从水平或垂直方向分别估算红色和蓝色。

图像最外圈需要单独处理,因为部分相邻坐标会超出图像范围。下面这个教学示例采用镜像坐标处理边界。向内镜像一个位置还能保持 Bayer pattern 的奇偶关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
static inline int mirror_coordinate(int value, int limit)
{
if (value < 0) {
return -value;
}
if (value >= limit) {
return 2 * limit - value - 2;
}
return value;
}

static inline uint16_t raw_sample(
const uint16_t *raw,
int width,
int height,
int x,
int y)
{
x = mirror_coordinate(x, width);
y = mirror_coordinate(y, height);
return raw[y * width + x];
}

static inline uint8_t raw10_to_u8(uint32_t value)
{
return (uint8_t)((value * 255U + 511U) / 1023U);
}

bool demosaic_bilinear(
const uint16_t *raw,
uint8_t *rgb,
int width,
int height,
BayerPattern pattern)
{
if (raw == NULL || rgb == NULL || width < 2 || height < 2) {
return false;
}

for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
uint32_t red = 0;
uint32_t green = 0;
uint32_t blue = 0;
const uint32_t center =
raw_sample(raw, width, height, x, y);

switch (bayer_color(pattern, x, y)) {
case COLOR_RED:
red = center;
green = (
raw_sample(raw, width, height, x - 1, y) +
raw_sample(raw, width, height, x + 1, y) +
raw_sample(raw, width, height, x, y - 1) +
raw_sample(raw, width, height, x, y + 1)) / 4;
blue = (
raw_sample(raw, width, height, x - 1, y - 1) +
raw_sample(raw, width, height, x + 1, y - 1) +
raw_sample(raw, width, height, x - 1, y + 1) +
raw_sample(raw, width, height, x + 1, y + 1)) / 4;
break;

case COLOR_BLUE:
blue = center;
green = (
raw_sample(raw, width, height, x - 1, y) +
raw_sample(raw, width, height, x + 1, y) +
raw_sample(raw, width, height, x, y - 1) +
raw_sample(raw, width, height, x, y + 1)) / 4;
red = (
raw_sample(raw, width, height, x - 1, y - 1) +
raw_sample(raw, width, height, x + 1, y - 1) +
raw_sample(raw, width, height, x - 1, y + 1) +
raw_sample(raw, width, height, x + 1, y + 1)) / 4;
break;

case COLOR_GREEN:
green = center;

if (bayer_color(pattern, x ^ 1, y) == COLOR_RED) {
red = (
raw_sample(raw, width, height, x - 1, y) +
raw_sample(raw, width, height, x + 1, y)) / 2;
blue = (
raw_sample(raw, width, height, x, y - 1) +
raw_sample(raw, width, height, x, y + 1)) / 2;
} else {
red = (
raw_sample(raw, width, height, x, y - 1) +
raw_sample(raw, width, height, x, y + 1)) / 2;
blue = (
raw_sample(raw, width, height, x - 1, y) +
raw_sample(raw, width, height, x + 1, y)) / 2;
}
break;
}

const int index = (y * width + x) * 3;
rgb[index + 0] = raw10_to_u8(red);
rgb[index + 1] = raw10_to_u8(green);
rgb[index + 2] = raw10_to_u8(blue);
}
}

return true;
}

该函数输出 RGB 交错排列的数据,并支持 RGGB、GRBG、GBRG 和 BGGR。它的目标是清晰展示基本原理,并不是生产级算法。更先进的去马赛克算法会利用梯度和色差,以减少边缘附近的拉链伪影、伪色和细节损失。

去马赛克只是图像处理流程的一步

把 10 bit 采样值直接转换为 8 bit 适合用来演示数据格式,但实际相机通常还会执行更多处理:

flowchart TD A["打包 RAW"] --> B["解包"] B --> C["黑电平校正"] C --> D["坏点与镜头阴影校正"] D --> E["白平衡"] E --> F["去马赛克"] F --> G["颜色校正"] G --> H["色调映射 / Gamma"] H --> I["RGB 输出"]

如果不进行黑电平校正和白平衡,输出图像可能存在明显偏色。如果不进行 Gamma 或色调映射,线性传感器数据在普通显示器上通常会显得很暗。

由于传感器或模拟读出电路的差异,两个绿色位置也可能存在轻微不一致,因此部分处理流程会分别校准 GrGb。某些算法可能选择其中一个绿色子平面作为参考,但直接丢弃另一个子平面会损失一半绿色采样,并不具备普遍的去马赛克优势。

写入与识别 PNG、BMP 文件

文件扩展名不能完全证明文件的真实格式,应当检查文件签名:

1
2
PNG: 89 50 4E 47 0D 0A 1A 0A
BMP: 42 4D

在 PowerShell 7 中:

1
Get-Content -AsByteStream -TotalCount 16 your_image.png | Format-Hex

在 Windows PowerShell 5.1 中:

1
Get-Content -Encoding Byte -TotalCount 16 your_image.png | Format-Hex

PNG 使用无损的 DEFLATE 压缩。噪声较多的图像通常难以压缩,因此 PNG 文件大小接近 BMP 并不意味着它没有经过压缩。

常见的 Windows BMP 由以下部分组成:

1
2
3
4
14 字节 BITMAPFILEHEADER
DIB header,通常为 40 字节 BITMAPINFOHEADER
可选的调色板或通道掩码
像素数组

对于未压缩的 24 bit BMP,每行按照 4 字节对齐:

1
2
3
row_size = ((width * 24 + 31) / 32) * 4
pixel_size = row_size * abs(height)
file_size = pixel_offset + pixel_size

传统 24 bit BMP 的像素字节按照 B、G、R 顺序保存。高度为正数时,各行从下到上保存;高度为负数则表示从上到下保存。

对于 1920x1080 图像,每行数据本身已经满足 4 字节对齐:

1
1920 * 1080 * 3 = 6,220,800 字节

即大约 6.22 MB 或 5.93 MiB,再加上很小的文件头。

总结

理解相机数据时,需要把处理流程中的不同层次区分开:

flowchart TD A["物理感光单元与 Bayer CFA"] --> B["单色传感器测量值"] B --> C["RAW10 或其他 RAW 打包格式"] C --> D["解包后的 Bayer 采样数组"] D --> E["去马赛克与颜色处理"] E --> F["RGB 图像"] F --> G["PNG、BMP 或其他图像文件"]

最重要的几个概念是:

  • 感光单元(photosite)是物理上的感光位置;
  • Bayer 采样值是一个感光单元产生的数字测量结果;
  • RGB 像素是在对应空间位置重建出的三通道数据;
  • 2x2 Bayer 块是重复的 CFA 排列,而不是一个超级像素;
  • RAW 打包方式决定字节布局,Bayer 顺序决定颜色解释。