一张 RAW 照片里,某个感光位置通常没有完整的红、绿、蓝三个数字。它只测到了其中一种颜色,其余两个通道要靠周围像素估计出来。
这件事叫 Debayer,也常被称为 demosaicing。它把一张单通道的彩色马赛克,重建成每个像素都有 R、G、B 的彩色图像。名字听起来像是在“去掉 Bayer”,实际做的却是一次有约束的插值。
本文讨论最常见的 Bayer CFA 和线性 RAW。示例会刻意简化,以看清算法的骨架;手机与相机中的工业实现还会联合处理噪声、坏点、镜头阴影、色差与锐化。
一个感光位置,为什么只测一种颜色
硅本身不会像人眼一样,把光自动分成红、绿、蓝。为了得到颜色信息,传感器上方覆盖了一层 CFA(Color Filter Array,彩色滤光阵列):红色滤光片主要让红光通过,绿色滤光片主要让绿光通过,蓝色滤光片主要让蓝光通过。
于是同一个位置的数字可能是红光响应,也可能是绿光或蓝光响应。RAW 文件保存的仍是一张二维数值阵列,颜色身份由这个位置的行列坐标和 CFA Pattern 决定。
最常见的 2×2 排列有四种:
| 名称 | 第一行 | 第二行 |
|---|---|---|
| RGGB | R G | G B |
| BGGR | B G | G R |
| GRBG | G R | B G |
| GBRG | G B | R G |
四个名字描述的不是四种算法,而是左上角 2×2 的实际排列。裁掉一行、旋转图像或读错元数据,都可能改变坐标原点与 Pattern 的对应关系。
Bayer 马赛克显微镜
同一片场景经过 CFA 后,每个感光位置只留下一个通道。切换排列,观察左上角的坐标含义怎样改变。
RGGB 的左上 2×2 是 R G / G B。绿色样本占一半,是因为人眼视觉与图像亮度细节对绿色附近更敏感。
绿色为什么有两份?因为人眼亮度感知以及自然图像的细节信息,与绿色附近的响应关系密切。用一半位置采绿色,可以在颜色采样与空间细节之间取得实用平衡。不过“两份绿色”不代表最后直接复制同一个值:它们仍在不同空间位置上测量。
先拆成三张稀疏通道
以 RGGB 为例,4×4 RAW 可以写成:
R G R GG B G BR G R GG B G B把它拆成 R、G、B 三张平面后,每张平面只有对应位置有实测值,其余位置都是空缺:
R 平面 G 平面 B 平面r · r · · g · g · · · ·· · · · g · g · · b · br · r · · g · g · · · ·· · · · g · g · · b · bDebayer 的目标,就是在不改动已知样本的前提下填满这些空缺。输出像素中的某个通道如果来自当前感光位置,它是测量值;另外两个通常是估计值。三个数字放在一起,不等于三个数字都来自同一个物理位置的测量。
下面用代码生成三个稀疏平面:
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 位于哪一行。
一个像素怎样补齐三个通道
点击网格中的位置,再选择要恢复的通道。橙色是目标像素,描边位置是这次平均使用的真实样本。
G ≈ (100 + 130 + 130 + 160) / 4使用 4 个相邻 G 样本做局部平均。教学组件使用简单邻域平均。真实双线性实现还会根据中心像素属于 R、G、B 的哪一类,选择十字或对角邻居并处理边界。
例如 RGGB 中一个内部 R 位置的绿色与蓝色估计是:
下面是一份刻意写得明确的 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 优化的库。先验证坐标语义,再做性能优化,会比一开始写紧凑切片更容易排错。
为什么平滑插值会越过边缘
双线性的隐含假设是:局部颜色变化足够平滑。但黑色栏杆与白色天空之间并不平滑。若中心落在边界附近,左右平均可能把两个物体混在一起;三个通道的采样位置不同,混合量也不同,于是中性边缘会冒出红、青或紫色。
一个轻量的改进,是先比较水平和垂直方向的变化:
若 ,说明水平方向更平滑,就优先沿水平插值;反之沿垂直插值。相等时再平均两个方向。
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,再插值色差 与 ,因为自然图像中的色差通常比三个原始通道更平滑。但“边缘感知”不是万能标签:方向估计本身会被噪声、斜线和重复纹理骗过。
三种 Debayer 算法,同一份 RAW
切换合成测试图,比较速度友好的最近邻、平滑的双线性和简化方向判断。点击结果可把误差热图切到该算法。
边缘感知并不是“永远更锐”。这里它只在恢复绿色时比较水平与垂直梯度;面对斜线和高频纹理,仍会判断错误。
在组件里切换到细条纹。条纹频率接近 CFA 网格时,场景本身已经没有被充分采样;再复杂的插值也无法从缺失数据中唯一还原真相。
四类常见伪影
Debayer 结果中经常见到以下现象:
- False color(假色):原本中性的高对比细节出现彩色斑点或边缘。各通道在不同位置采样,插值把空间变化误认为颜色变化。
- Zipper(拉链纹):斜边或竖边旁出现交替明暗的小齿。方向判断或逐行插值在边界两侧反复切换。
- Moiré(摩尔纹):织物、屏幕、建筑栅格出现大尺度波纹。场景频率与 CFA/像素采样产生混叠,它不只是“插值不够聪明”。
- Maze artifact(迷宫纹):细节区出现迷宫状结构,常与迭代式方向选择或颜色差插值判断错误有关。
镜头色差、运动、传感器串扰和过强锐化也会制造相似外观。判断问题时,最好同时查看线性 RAW、不同 Debayer 算法结果以及未锐化版本,而不是只盯最终 JPEG。
Pattern 错了,程序为什么不报错
一张 4000×3000 的数组,无论按 RGGB 还是 BGGR 解读,尺寸与数据类型都完全合法。算法会认真地把真实红样本当成蓝样本,再把错误结果插值得很平滑。
Bayer Pattern 排错器
传感器固定输出 RGGB。你可以故意用另一种 Pattern 解读,或模拟裁掉一行/一列却没更新起始坐标。
红蓝位置互换:常见表现是整体冷暖颠倒。
典型现象包括:
- 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_levellinear /= white_level - black_level
# 坏点、镜头阴影、Bayer 域 WB ……rgb = debayer_bilinear_rggb(linear)
# 不要仅为“看起来合法”就在每个中间步骤 clip。Debayer 不是普通的图片放大
普通 resize 的输入像素已经拥有完整 RGB,它只需要估计新的空间位置。Debayer 的三个通道缺失位置各不相同,并且缺失模式与 CFA 坐标绑定。把 RAW 当灰度图放大,再复制成三通道,只会得到更大的灰度马赛克;把马赛克先染成红绿蓝再缩放,也会把滤光片图案当成场景纹理。
Debayer 更准确的理解是:利用同一场景的空间相关性,从交错的通道样本重建三个共址颜色平面。 插值结果是合理估计,不是找回传感器从未测到的秘密数据。
实际项目怎样选算法
如果目标是教学、缩略图或极低延迟预览,最近邻与双线性足够透明。实时视频管线常使用轻量方向性方法,在质量、吞吐和功耗之间折中。高质量 RAW 转换器还会使用更复杂的颜色差、频域、迭代或学习型方法,并结合噪声模型。
选择时不要只比较一张风景照。至少准备:斜边、细条纹、彩色边界、低照度噪声、高光饱和和真实织物;同时比较误差、伪影类型、执行时间与内存占用。复杂算法可能在干净测试图上更锐,却在高 ISO 噪声里产生更显眼的彩色纹理。
最后用这份清单检查实现:
- CFA Pattern、坐标原点、裁切、旋转和通道顺序是否一致;
- 黑电平、坏点和饱和样本是否在插值前得到正确处理;
- 已知样本有没有被算法意外改写;
- 边界策略是否明确,是否出现负索引或跨行访问;
- 中间计算是否使用足够精度的浮点类型;
- 是否在 Gamma 之前的线性域工作;
- 是否用合成结构和真实 RAW 同时验证,而不只看“照片似乎正常”。
当这七项都说得清楚,Debayer 就不再是一颗神秘的黑盒:它是从稀疏测量出发、带着采样限制做出的空间与颜色估计。