4055 字
20 分钟
Debayer 入门:一个像素怎样找回完整颜色?

你按下快门以后,传感器最先得到的并不是一张完整彩色照片。

它更像一幅规则的颜色马赛克:每个位置只测到红、绿、蓝中的一种。Debayer 要做的,就是利用邻近位置,把另外两个缺失通道估计出来。

这篇文章会沿着同一条路径完整走一遍:先看见现象,再追踪一个像素,接着理解算法,最后进入伪影、排错与工程实现。

传感器测到了什么?CFA 为什么让一个位置只有一个颜色数字。
缺失颜色从哪里来?最近邻、双线性和方向感知究竟参考哪些位置。
怎样判断结果可信?从伪影、Pattern、处理顺序和测试场景验证实现。

本文以线性 RAW 和 RGGB Bayer 为算法主线,同时解释四种 Bayer 相位、RYYB 与富士 X-Trans。组件使用确定性的合成场景,目的是把坐标和插值关系画清楚,不代表手机或相机中的完整工业实现。

先看一次完整旅程#

先按顺序点击四个步骤。对象一直是同一栋小房子,改变的只是它经过了哪一层处理。

成像步骤
1

小房子原本拥有完整颜色

每个位置都有 R、G、B 三个分量,这是我们希望重建的答案。

在“覆盖 CFA”阶段,每个位置前面只有一片滤色片;到了“RAW 采样”,每个位置只留下一个亮度数字。Debayer 再根据周围实测样本,为这个位置估计另外两个通道。

这里最容易混淆的一点是:组件给 RAW 格子染上红、绿、蓝,只是为了标出这个数字属于哪个通道。真实 RAW 通常仍是一张单通道二维数值表,位置的颜色身份由 CFA Pattern 和坐标决定。

CFA 为什么让像素只看一种颜色#

硅感光单元主要测量到达它的光强,本身不会自动输出完整的红、绿、蓝。为了获得颜色,传感器上方覆盖 CFA(Color Filter Array,彩色滤光阵列):红色滤光片主要让红光通过,绿色滤光片主要让绿光通过,蓝色滤光片主要让蓝光通过。

因此,同一个 RAW 数字可能表示红光响应,也可能表示绿色或蓝色响应。它的身份不是写在数字里,而是由行列坐标决定。

Bayer、RYYB 与 X-Trans#

先在显微镜里切换阵列。小框标出最小重复单元:Bayer 和 RYYB 是 2×2,X-Trans 是 6×6。

Bayer 马赛克显微镜

比较 Bayer、RYYB 与富士 X-Trans:每个感光位置只留下一个滤光片响应,左上角描边标出阵列的最小重复单元。

CFA Pattern
观察层

只显示滤光片铺设位置:颜色固定,不表示场景亮度。

R:36G:72B:36

RGGB 左上角框内是 2×2 重复单元: R G / G B绿色样本占一半,是因为人眼视觉与图像亮度细节对绿色附近更敏感。

CFA 类型常见 Pattern最小重复单元单元说明
BayerRGGB2×2R G / G B
BayerBGGR2×2B G / G R
BayerGRBG2×2G R / B G
BayerGBRG2×2G B / R G
RYYBRYYB2×2R Y / Y B
富士 X-TransX-Trans6×620 个 G、8 个 R、8 个 B

Bayer 的四个名字描述的是 2×2 单元在坐标原点处的相位,不是四种 Debayer 算法。RYYB 改变了滤光片的光谱响应,X-Trans 改变了空间采样周期,因此不能直接套用后文的 2×2 Bayer 邻居规则。

绿色为什么有两份?因为人眼亮度感知以及自然图像中的大量空间细节,与绿色附近的响应关系密切。提高绿色采样密度,是颜色采样与空间细节之间的实用折中。但两份绿色仍位于不同空间位置,不能当成同一个测量。

把 RAW 拆开,空缺就出现了#

以 RGGB 为例,采集结束时所有样本仍在同一张 RAW 矩阵中。点击按钮,让同一栋小房子的采样值按颜色身份展开成 R、G、B 三张平面。

通道分层

先看平面关系:原图经过 CFA 采样成为 RAW

  1. 1平面采样
  2. 23D 拆成三层
原始 RGB 场景每格都有 R、G、B
同一栋小房子
采样后的 RGGB RAW12 × 12 · 每格只有一个值
R 平面36 个实测值
G 平面72 个实测值
B 平面36 个实测值
当前:平面采样点击后,RAW 会先倾斜,再自动拆成三层。
CFA 把小房子采成一张单通道 RAW右侧平面上的颜色和亮度,仍能对应左侧房顶、墙面、天空与草地。
空格 ≠ 数值 0

Debayer 的任务可以重新表述为:保留已知样本,为三张稀疏平面填满空缺,再把它们合成完整 RGB。

三个数字最后放在同一个输出像素里,并不意味着三个数字都由这个物理位置直接测得。通常只有一个通道来自实测,另外两个是估计。

下面用 mask 明确记录哪些位置有效:

import numpy as np
def split_rggb(raw):
"""把 RGGB RAW 拆成数值平面与有效位置 mask。"""
rows, columns = raw.shape
red_mask = np.zeros((rows, columns), dtype=bool)
green_mask = np.zeros_like(red_mask)
blue_mask = np.zeros_like(red_mask)
red_mask[0::2, 0::2] = True
green_mask[0::2, 1::2] = True
green_mask[1::2, 0::2] = True
blue_mask[1::2, 1::2] = True
return (
np.where(red_mask, raw, 0.0),
np.where(green_mask, raw, 0.0),
np.where(blue_mask, raw, 0.0),
(red_mask, green_mask, blue_mask),
1 collapsed line
)

不能用数值 0 代表“这里没有样本”,因为真实黑色本来就可能测到 0。数据值与有效位置必须分开表达。

只追踪一个像素#

整张图的 Debayer,就是同一套局部动作在每个位置重复发生。

点击任意格子,再用“下一步”逐步显示来源。目标位置始终是橙色实线;红、绿、蓝虚线分别表示对应通道的参考样本。

逐像素演示

点击任意一格,看它怎样补齐颜色

插值算法
目标R 来源G 来源B 来源
1 步,共 4 步
步骤 1 / 4

这个位置实测的是 B

RAW[5, 5] 只有一个数字,另外两个通道还不存在。

最近邻:把最近的答案搬过来#

最近邻找到距离当前位置最近的同色样本,直接复制。它几乎不需要运算,适合快速预览和理解数据结构。

def nearest_value(raw, pattern, row, column, target_channel):
if pattern[row % 2][column % 2] == target_channel:
return raw[row, column]
for radius in (1, 2):
for dr in range(-radius, radius + 1):
for dc in range(-radius, radius + 1):
rr = np.clip(row + dr, 0, raw.shape[0] - 1)
cc = np.clip(column + dc, 0, raw.shape[1] - 1)
if pattern[rr % 2][cc % 2] == target_channel:
return raw[rr, cc]

它的问题也很直接:一份样本会被铺到多个输出位置,边缘出现色块和锯齿;三个通道如果向不同方向复制,还会在轮廓旁形成彩色镶边。最近邻是有用的基线,不是高质量成像的终点。

双线性:参考形状会随位置改变#

双线性不是“把周围八个数字全部平均”。邻居的形状由中心位置和目标通道共同决定:

  • 在 R 位置,G 通常来自上、下、左、右四个 G,B 来自四个对角 B;
  • 在 B 位置,情况与 R 对称;
  • 在 G 位置,R 和 B 分别来自水平或垂直的两个样本,方向取决于这个 G 位于哪一类行列相位。

例如 RGGB 中一个内部 R 位置:

G^i,j=Gi1,j+Gi+1,j+Gi,j1+Gi,j+14\hat G_{i,j}=\frac{G_{i-1,j}+G_{i+1,j}+G_{i,j-1}+G_{i,j+1}}{4}B^i,j=Bi1,j1+Bi1,j+1+Bi+1,j1+Bi+1,j+14\hat B_{i,j}=\frac{B_{i-1,j-1}+B_{i-1,j+1}+B_{i+1,j-1}+B_{i+1,j+1}}{4}

下面是一份刻意写得明确的 RGGB 教学实现。边缘先采用复制填充,避免访问越界:

def debayer_bilinear_rggb(raw):
"""教学版 RGGB 双线性 Debayer;输入应为浮点线性 RAW。"""
padded = np.pad(raw, 1, mode="edge")
height, width = raw.shape
rgb = np.empty((height, width, 3), dtype=np.float32)
def p(row, column):
return padded[row + 1, column + 1]
for row in range(height):
for column in range(width):
row_even = row % 2 == 0
column_even = column % 2 == 0
cross = (
p(row - 1, column) + p(row + 1, column)
+ p(row, column - 1) + p(row, column + 1)
) / 4.0
diagonal = (
p(row - 1, column - 1) + p(row - 1, column + 1)
17 collapsed lines
+ p(row + 1, column - 1) + p(row + 1, column + 1)
) / 4.0
if row_even and column_even: # R 位置
rgb[row, column] = (p(row, column), cross, diagonal)
elif not row_even and not column_even: # B 位置
rgb[row, column] = (diagonal, cross, p(row, column))
elif row_even: # R 行上的 G
red = (p(row, column - 1) + p(row, column + 1)) / 2.0
blue = (p(row - 1, column) + p(row + 1, column)) / 2.0
rgb[row, column] = (red, p(row, column), blue)
else: # B 行上的 G
red = (p(row - 1, column) + p(row + 1, column)) / 2.0
blue = (p(row, column - 1) + p(row, column + 1)) / 2.0
rgb[row, column] = (red, p(row, column), blue)
return rgb

循环版本清楚但不快。生产实现通常会按四类坐标向量化、使用卷积核,或调用经过 SIMD/GPU 优化的库。先验证坐标语义,再做性能优化,比一开始写紧凑切片更容易排错。

为什么平滑插值会越过边缘#

双线性的隐含假设是局部颜色变化足够平滑,但黑色栏杆与明亮天空之间并不平滑。跨边界平均会把两个物体混在一起;三个通道的采样位置不同,混合量也不同,中性边缘就可能冒出红、青或紫色。

一种轻量改进,是比较水平与垂直方向的变化:

dh=xi,j1xi,j+1,dv=xi1,jxi+1,jd_h=|x_{i,j-1}-x_{i,j+1}|,\qquad d_v=|x_{i-1,j}-x_{i+1,j}|

dh<dvd_h<d_v,水平方向更平滑,就优先沿水平插值;反之沿垂直插值;相等时再平均两个方向。

def interpolate_green_edge_aware(p, row, column):
dh = abs(p(row, column - 1) - p(row, column + 1))
dv = abs(p(row - 1, column) - p(row + 1, column))
horizontal = (p(row, column - 1) + p(row, column + 1)) / 2.0
vertical = (p(row - 1, column) + p(row + 1, column)) / 2.0
if dh < dv:
return horizontal
if dv < dh:
return vertical
return (horizontal + vertical) / 2.0

很多算法会先恢复 G,再插值色差 RGR-GBGB-G,因为自然图像中的色差通常比三个原始通道更平滑。但“方向感知”不是万能标签:噪声、斜线和重复纹理都可能让方向判断出错。

回到整张图比较算法#

像素公式正确,不代表整张图一定好看。切换算法,再把场景换成高频斜纹,观察轮廓、颜色与误差如何变化。

整图实验

原图、RAW 和结果保持在同一个位置

测试场景
Debayer 算法
原始场景Ground truth
RAW 采样每格一个通道
重建结果双线性
现在应该看哪里

过渡更平滑,也更容易跨过边缘

平均邻居能消除块状感,但会把边界两侧混在一起,因此清晰轮廓可能变软。

平均绝对误差8.0/ 255,越低越好

这三个教学算法展示了质量与复杂度之间的基本取舍:

方法核心动作常见优点常见问题
最近邻复制最近同色样本极快、易实现色块、锯齿、颜色错位
双线性平均规则邻域平滑、高效、易硬件化边缘模糊、伪色、拉链纹
方向感知优先沿平滑方向插值更尊重部分边缘会误判斜线、纹理与噪声

高质量 RAW 转换器还可能使用 Malvar–He–Cutler、AHD、LMMSE、残差插值、频域方法或学习型模型,并与噪声模型联合优化。算法越复杂,并不意味着所有场景都必然更好。

看懂四类常见伪影#

1. False color(假色)#

原本中性的高对比细节出现彩色斑点或彩边。各通道在不同位置采样,插值把空间变化误判成了颜色变化。

2. Zipper(拉链纹)#

斜边或竖边旁出现交替明暗的小齿。方向判断或逐行插值在边界两侧反复切换时尤其明显。

3. Moiré(摩尔纹)#

织物、屏幕或建筑栅格出现大尺度波纹。场景频率与像素/CFA 网格发生混叠,它不只是“插值还不够聪明”。当原始采样已经丢失信息,再复杂的算法也无法保证唯一恢复。

4. Maze artifact(迷宫纹)#

细节区域出现迷宫状结构,常与迭代式方向选择或色差插值判断错误有关。

镜头色差、运动、传感器串扰、降噪和过强锐化也会制造相似外观。排查时应同时查看线性 RAW、不同 Debayer 结果和未锐化版本,而不是只盯最终 JPEG。

Pattern 错了,程序为什么不报错#

一张 4000×3000 的数组,无论按 RGGB 还是 BGGR 解读,尺寸和数据类型都完全合法。算法会认真地把真实红样本当成蓝样本,再把错误结果插值得很平滑。

Bayer Pattern 排错器

小房子保持原来的像素造型,但把采样网格扩展到 48×48。你可以故意用另一种 Pattern 解读,或模拟裁掉一行/一列却没更新起始坐标。

参考场景
当前解读
程序认为的 Pattern
起始坐标
传感器元数据RGGB
当前解释BGGR
误差 × 25562.6

红蓝位置互换:常见表现是整体冷暖颠倒。

常见错误包括:

  • RGGB 与 BGGR 混淆,红蓝大面积互换;
  • GRBG 与 GBRG 混淆,红蓝位置同样会错;
  • 裁掉一行或一列后仍使用旧 Pattern,CFA 相位整体平移;
  • 图像旋转或翻转后,Pattern 元数据没有同步变换;
  • 内存里的 BGR 输出又被当成 RGB,额外制造一次红蓝互换。

最可靠的排查方法不是根据“画面看起来偏蓝”猜测,而是使用已知均匀色区域,打印左上 2×2 的坐标与数值,确认四个位置的物理身份。

Debayer 在 ISP 中并不孤立#

插值会把一个坏样本传播到周围多个输出像素。因此 RAW 在 Debayer 前通常还要完成或考虑以下步骤:

  1. 黑电平校正:先减去电子偏置,否则偏置也会参与通道比较与插值;
  2. 坏点修复:孤立的极亮或极暗点会污染邻域;
  3. 饱和标记:饱和样本不再包含可靠颜色比例,不能当普通值平均;
  4. 镜头阴影校正:明确空间增益与插值的顺序及精度;
  5. Bayer 域白平衡:可以在插值前按 CFA 位置应用通道增益;
  6. 噪声处理:实际系统可能在 Bayer 域先降噪,也可能与 Debayer 联合完成。

一个典型的简化顺序是:

线性 RAW → 黑电平 → 坏点/饱和处理 → 镜头阴影与 Bayer 域增益 → Debayer → RGB 域颜色处理

具体产品会因硬件、噪声模型和画质策略调整顺序,但每一次调整都必须说明处理域与数值范围。

边界、精度与线性域#

图像边界没有完整邻域#

最外一圈缺少部分邻居,常见策略包括复制边缘、镜像填充和只输出内部有效区域。没有无条件唯一的答案,但同一管线必须一致。某些“彩色边框”其实来自负索引绕回数组尾部或跨行访问。

不要用无符号整数直接算差值#

uint16(100) - uint16(200) 可能下溢成很大的正数。更安全的方式是减黑电平后转成 float32,保留暂时小于 0 或大于 1 的中间结果,到明确的输出阶段再裁剪。

linear = raw.astype(np.float32) - black_level
linear /= white_level - black_level
# 坏点、镜头阴影、Bayer 域白平衡……
rgb = debayer_bilinear_rggb(linear)
# 不要为了“看起来合法”就在每一步都 clip。

Debayer 应在线性域工作#

先做 Gamma 或强 Tone Curve,会改变平均值原本的物理意义。插值通常应在线性光强关系仍然成立时完成,再进入显示编码和风格处理。

Debayer 不是普通图片放大#

普通 resize 的输入像素已经拥有完整 RGB,只需要估计新的空间位置。Debayer 的三个通道缺失位置各不相同,而且缺失模式绑定 CFA 坐标。

把 RAW 当灰度图放大再复制成三通道,只会得到更大的灰度马赛克;把马赛克先染成红绿蓝再缩放,则会把滤色片排列误当成场景纹理。

更准确的理解是:Debayer 利用同一场景的空间相关性,从交错的通道测量中重建三个共址颜色平面。 它给出合理估计,不是找回传感器从未测到的秘密数据。

实际项目怎样选择和验证#

教学、缩略图或极低延迟预览可以使用最近邻与双线性。实时视频管线常采用轻量方向性或固定核方法,在质量、吞吐、延迟与功耗之间折中。高质量 RAW 转换器则可能使用更复杂的颜色差、迭代或学习方法。

不要只拿一张风景照比较算法。至少准备:

  • 水平、垂直与斜边;
  • 高频细条纹和织物;
  • 饱和红蓝边界与中性高对比边缘;
  • 低照度噪声与孤立坏点;
  • 高光饱和区域;
  • 已知 Ground Truth 的合成图与真实 RAW。

同时记录平均误差、边缘伪影、执行时间、内存占用和极端输入表现。复杂算法可能在干净测试图上更锐,却在高 ISO 噪声中制造更显眼的彩色纹理。

最后用这份清单检查实现:

  1. CFA Pattern、坐标原点、裁切、旋转和通道顺序是否一致;
  2. 黑电平、坏点和饱和样本是否在插值前得到正确处理;
  3. 已知样本有没有被算法意外改写;
  4. 边界策略是否明确,是否存在负索引或跨行访问;
  5. 中间计算是否使用足够精度;
  6. 是否在线性域工作;
  7. 是否同时用合成结构和真实 RAW 验证;
  8. 质量提升是否值得额外的延迟、功耗和内存。

确认你真正理解了什么#

理解检查 1 / 3

一个 RGGB RAW 位置实际测到了几个颜色通道?

当你能明确区分“实测值”和“估计值”,能指出公式参考的实际坐标,并知道采样丢失与插值错误不是同一件事,Debayer 就不再是一颗神秘黑盒。它是一套围绕 CFA 坐标、局部空间关系和工程约束做出的颜色重建决策。

Debayer 入门:一个像素怎样找回完整颜色?
https://cloudsir.top/posts/how-debayer-reconstructs-color-v2/
作者
CloudSir
发布于
2026-08-27
许可协议
CC BY-NC-SA 4.0
Ghostty 配置