3365 字
17 分钟
Debayer 是怎么补出颜色的?从一张马赛克走进完整 RGB 图像

一张 RAW 照片里,某个感光位置通常没有完整的红、绿、蓝三个数字。它只测到了其中一种颜色,其余两个通道要靠周围像素估计出来。

这件事叫 Debayer,也常被称为 demosaicing。它把一张单通道的彩色马赛克,重建成每个像素都有 R、G、B 的彩色图像。名字听起来像是在“去掉 Bayer”,实际做的却是一次有约束的插值。

本文讨论最常见的 Bayer CFA 和线性 RAW。示例会刻意简化,以看清算法的骨架;手机与相机中的工业实现还会联合处理噪声、坏点、镜头阴影、色差与锐化。

一个感光位置,为什么只测一种颜色#

硅本身不会像人眼一样,把光自动分成红、绿、蓝。为了得到颜色信息,传感器上方覆盖了一层 CFA(Color Filter Array,彩色滤光阵列):红色滤光片主要让红光通过,绿色滤光片主要让绿光通过,蓝色滤光片主要让蓝光通过。

于是同一个位置的数字可能是红光响应,也可能是绿光或蓝光响应。RAW 文件保存的仍是一张二维数值阵列,颜色身份由这个位置的行列坐标和 CFA Pattern 决定。

最常见的 2×2 排列有四种:

名称第一行第二行
RGGBR GG B
BGGRB GG R
GRBGG RB G
GBRGG BR G

四个名字描述的不是四种算法,而是左上角 2×2 的实际排列。裁掉一行、旋转图像或读错元数据,都可能改变坐标原点与 Pattern 的对应关系。

Bayer 马赛克显微镜

同一片场景经过 CFA 后,每个感光位置只留下一个通道。切换排列,观察左上角的坐标含义怎样改变。

CFA Pattern
观察层
R:16 个G:32 个B:16 个

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

绿色为什么有两份?因为人眼亮度感知以及自然图像的细节信息,与绿色附近的响应关系密切。用一半位置采绿色,可以在颜色采样与空间细节之间取得实用平衡。不过“两份绿色”不代表最后直接复制同一个值:它们仍在不同空间位置上测量。

先拆成三张稀疏通道#

以 RGGB 为例,4×4 RAW 可以写成:

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

把它拆成 R、G、B 三张平面后,每张平面只有对应位置有实测值,其余位置都是空缺:

R 平面 G 平面 B 平面
r · r · · g · g · · · ·
· · · · g · g · · b · b
r · r · · g · g · · · ·
· · · · g · g · · b · b

Debayer 的目标,就是在不改动已知样本的前提下填满这些空缺。输出像素中的某个通道如果来自当前感光位置,它是测量值;另外两个通常是估计值。三个数字放在一起,不等于三个数字都来自同一个物理位置的测量。

下面用代码生成三个稀疏平面:

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
)

这里用 mask 单独记录有效位置,不能把数值 0 当成“空缺”:真实的黑色样本本来就可能是 0。

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

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

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]

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

双线性:不同位置要找不同邻居#

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

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

一个像素怎样补齐三个通道

点击网格中的位置,再选择要恢复的通道。橙色是目标像素,描边位置是这次平均使用的真实样本。

当前步骤
当前位置(3, 3)
已知通道B
估计结果130
G ≈ (100 + 130 + 130 + 160) / 4使用 4 个相邻 G 样本做局部平均。

教学组件使用简单邻域平均。真实双线性实现还会根据中心像素属于 R、G、B 的哪一类,选择十字或对角邻居并处理边界。

例如 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 教学实现。先用边缘复制补齐邻域,避免访问越界:

import numpy as np
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)
20 collapsed lines
) / 4.0
diagonal = (
p(row - 1, column - 1) + p(row - 1, column + 1)
+ 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):
horizontal_change = abs(p(row, column - 1) - p(row, column + 1))
vertical_change = 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 horizontal_change < vertical_change:
return horizontal
if vertical_change < horizontal_change:
return vertical
return (horizontal + vertical) / 2.0

很多算法先恢复 G,再插值色差 RGR-GBGB-G,因为自然图像中的色差通常比三个原始通道更平滑。但“边缘感知”不是万能标签:方向估计本身会被噪声、斜线和重复纹理骗过。

三种 Debayer 算法,同一份 RAW

切换合成测试图,比较速度友好的最近邻、平滑的双线性和简化方向判断。点击结果可把误差热图切到该算法。

测试图
双线性误差热图越亮误差越大
当前算法双线性
测试结构斜线
MAE × 2559.7

边缘感知并不是“永远更锐”。这里它只在恢复绿色时比较水平与垂直梯度;面对斜线和高频纹理,仍会判断错误。

在组件里切换到细条纹。条纹频率接近 CFA 网格时,场景本身已经没有被充分采样;再复杂的插值也无法从缺失数据中唯一还原真相。

四类常见伪影#

Debayer 结果中经常见到以下现象:

  1. False color(假色):原本中性的高对比细节出现彩色斑点或边缘。各通道在不同位置采样,插值把空间变化误认为颜色变化。
  2. Zipper(拉链纹):斜边或竖边旁出现交替明暗的小齿。方向判断或逐行插值在边界两侧反复切换。
  3. Moiré(摩尔纹):织物、屏幕、建筑栅格出现大尺度波纹。场景频率与 CFA/像素采样产生混叠,它不只是“插值不够聪明”。
  4. Maze artifact(迷宫纹):细节区出现迷宫状结构,常与迭代式方向选择或颜色差插值判断错误有关。

镜头色差、运动、传感器串扰和过强锐化也会制造相似外观。判断问题时,最好同时查看线性 RAW、不同 Debayer 算法结果以及未锐化版本,而不是只盯最终 JPEG。

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

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

Bayer Pattern 排错器

传感器固定输出 RGGB。你可以故意用另一种 Pattern 解读,或模拟裁掉一行/一列却没更新起始坐标。

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

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

典型现象包括:

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

最可靠的排查方法不是凭画面“看起来偏蓝”猜测,而是用一块已知均匀色区域与 2×2 坐标打印结果,确认左上四个样本的物理身份。

Debayer 前,RAW 还需要准备什么#

插值会把一个坏样本传播给周围多个输出像素。因此有些校正通常应在 Debayer 前或与它联合完成:

  • 黑电平校正:先减去电子偏置;否则偏置也会参与通道比较与插值。
  • 坏点修复:孤立的极亮或极暗点会污染邻居。
  • 饱和标记:已经饱和的样本不是可靠的颜色比例,不能当普通值平均。
  • 镜头阴影校正:若增益随位置变化,要明确它与插值的顺序和精度。
  • Bayer 域白平衡:可以在插值前按 CFA 位置乘对应增益;下一篇会比较它与 RGB 域白平衡。

Debayer 应在线性域工作。先做 Gamma 或强烈 Tone Curve,会改变平均值的物理意义,让插值后的亮度与颜色关系失真。

边界、数据类型与裁剪#

图像最外一圈没有完整邻域,常见策略有三种:复制边缘、镜像填充,或只输出内部有效区域。它们没有无条件的唯一答案,但同一管线必须保持一致;某些“彩色边框”其实只是边界索引越界或负索引绕回了数组尾部。

还要避免在无符号整数中直接计算差值和平均。uint16(100) - uint16(200) 可能下溢成很大的正数。更安全的做法是减黑电平后转成 float32,保留暂时小于 0 或大于 1 的值,等管线明确要求时再裁剪。

linear = raw.astype(np.float32) - black_level
linear /= white_level - black_level
# 坏点、镜头阴影、Bayer 域 WB ……
rgb = debayer_bilinear_rggb(linear)
# 不要仅为“看起来合法”就在每个中间步骤 clip。

Debayer 不是普通的图片放大#

普通 resize 的输入像素已经拥有完整 RGB,它只需要估计新的空间位置。Debayer 的三个通道缺失位置各不相同,并且缺失模式与 CFA 坐标绑定。把 RAW 当灰度图放大,再复制成三通道,只会得到更大的灰度马赛克;把马赛克先染成红绿蓝再缩放,也会把滤光片图案当成场景纹理。

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

实际项目怎样选算法#

如果目标是教学、缩略图或极低延迟预览,最近邻与双线性足够透明。实时视频管线常使用轻量方向性方法,在质量、吞吐和功耗之间折中。高质量 RAW 转换器还会使用更复杂的颜色差、频域、迭代或学习型方法,并结合噪声模型。

选择时不要只比较一张风景照。至少准备:斜边、细条纹、彩色边界、低照度噪声、高光饱和和真实织物;同时比较误差、伪影类型、执行时间与内存占用。复杂算法可能在干净测试图上更锐,却在高 ISO 噪声里产生更显眼的彩色纹理。

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

  1. CFA Pattern、坐标原点、裁切、旋转和通道顺序是否一致;
  2. 黑电平、坏点和饱和样本是否在插值前得到正确处理;
  3. 已知样本有没有被算法意外改写;
  4. 边界策略是否明确,是否出现负索引或跨行访问;
  5. 中间计算是否使用足够精度的浮点类型;
  6. 是否在 Gamma 之前的线性域工作;
  7. 是否用合成结构和真实 RAW 同时验证,而不只看“照片似乎正常”。

当这七项都说得清楚,Debayer 就不再是一颗神秘的黑盒:它是从稀疏测量出发、带着采样限制做出的空间与颜色估计。

Debayer 是怎么补出颜色的?从一张马赛克走进完整 RGB 图像
https://cloudsir.top/posts/how-debayer-reconstructs-color/
作者
CloudSir
发布于
2026-08-24
许可协议
CC BY-NC-SA 4.0
第一次使用相机:从开机到拍出清楚照片
CCM 是怎么校正颜色的?从 3×3 矩阵看懂 RGB 通道混合