我电脑里存着不少从手机、工业相机导出的RAW文件。有件事特别有意思同一个传感器拍同一张灰卡只是代码里把Bayer排列从RGGB认成了GRBG画面就可能一个偏绿一个偏紫边缘还会冒出整排彩色条纹。所以说RAW图像处理的第一步从来不是调颜色而是先把藏在马赛克里的RGB信息按正确顺序取出来。很多人拿到RAW的第一个困惑是为什么这么暗、这么灰第二个困惑是为什么不同软件解出来的颜色不一样。这两个问题的答案都指向同一个底层结构——Bayer滤镜。手机摄像头和工业相机虽然价格差了几十倍、成像链路也完全不同但只要涉及彩色成像就得先过Bayer这一关。这篇文章我就把这套东西拆开从传感器原理讲到去马赛克代码再到手机ISP和工业相机的流程差异把RGGB、GRBG这些格式彻底讲透。1. 为什么传感器天生只能看见灰阶Bayer滤镜的物理真相1.1 一个像元只能记一个数CMOS图像传感器的基本单位是光电二极管它做的事非常简单接收光子把光子数量转换成电压再经过模数转换变成一个整数。这个整数只说明这个像素点接收到了多少光完全不包含颜色信息。打个比方传感器就像一个只认人数不认衣服颜色的检票员红衣服和蓝衣服在它眼里只有来了几个人的区别。要想输出彩色图像必须在硬件层面做颜色分离让不同的像素点只响应特定波长的光。工业上最常见的做法是在每个像元上方覆盖一层微型滤光片只允许红光、绿光或蓝光通过。这个滤光片阵列就是Bayer阵列。这里要澄清一个常见误解滤镜挡住了一部分光确实会损失灵敏度但它换来的是每个像素只负责一种颜色的确定性。后续通过去马赛克算法利用相邻像素推算出每个位置的RGB三通道值最终还原出一张完整彩图。1.2 2×2阵列RGGB为什么是两绿一红一蓝如果你把一个Bayer传感器的像素逐一放大看会发现它并不是均匀排列成RGBRGB而是以2×2为一个基本单元不断重复。在这个2×2单元里绿色占了两个位置红色和蓝色各占一个位置。为什么绿色要占一半因为人眼的视锥细胞对绿色最敏感而且人类对画面细节的感知主要来自亮度信息绿色对亮度贡献最大。Bayer从1960年代提出这个排列时就参照了人眼特性让采样最多的通道对应人眼最敏感的波段。这样在同样的传感器像素数量下感知分辨率最高伪彩也最不明显。RGGB这个名字描述的是2×2单元左上角开始逐行读取的颜色顺序第一行是R、G第二行是G、B。它是最常见的排列但不是唯一一种。传感器的物理设计、读出方向、光学镜头的安装方向都会影响最终哪个颜色落在哪个位置这就导出了后面要讲的GRBG、GBRG、BGGR。1.3 手机与工业相机的传感器差异像元尺寸背后的取舍手机传感器和工业相机传感器都用Bayer但设计取向很不一样。手机传感器像元普遍在0.7μm到1.0μm左右单位面积能捕捉的光子少动态范围和信噪比天然受限。为了弥补这一点很多高像素手机传感器用的是Quad Bayer结构也就是把2×2同色像素合并成一个大像素低光时四合一输出牺牲分辨率换灵敏度。工业相机则普遍不玩四合一这一套。它的像元通常做到2.4μm以上大一点的工业面阵相机能达到10μm。像元大满阱容量就大能记录的光子数量多动态范围和高光细节明显更好。工业相机的核心诉求是可重复性、低噪声和精确的曝光控制它不需要讨好手机屏幕上的消费者而是要把最原始的传感器数据交给算法所以彩色工业相机多数仍然使用标准Bayer阵列。正是这种原始属性让工业相机的RAW处理和手机摄影的RAW处理走上了完全不同的技术路线。2. RGGB/GRBG/GBRG/BGGR四种排列的差别和一排错就翻车的现象2.1 四种排列到底是哪四种Bayer四种排列的区别说白了就是2×2单元里R、G、B三个通道的摆放位置不同。用表格看最清楚Pattern第一行第二行RGGBR GG BGRBGG RB GGBRGG BR GBGGRB GG RRGGB是最常见的GRBG在部分CMOS里也很常见GBRG和BGGR相对少一些但同样存在于大量工业相机和少数手机传感器中。关键不在于哪种更好而在于你的软件必须知道当前数据到底是哪一种。这里有个容易忽略的细节同一个传感器物理焊接在电路板上时如果安装方向旋转了或者镜头光轴相对于读出方向发生了镜像翻转CFA Pattern就可能从RGGB变成BGGR从GRBG变成GBRG。换句话说Pattern不完全是传感器die的物理属性而是物理排列 × 读出方向 × 安装方向综合作用的结果。所以很多相机SDK里会专门提供一个字段让用户配置CFA Pattern而不是永远写死。2.2 解析顺序错乱后画面会变成什么样把RGGB数据按GRBG去解析最直接的后果是颜色通道错位。RGGB的第一行第二个像素是G但GRBG认为它是R第二行第一个像素本来是GGRBG认为是B。结果就是几乎所有像素都被套用了错误的颜色角色。在没有去马赛克、直接单像素显示的情况下图像会呈现明显的棋盘状彩色噪点。跑了去马赛克之后错位会通过插值扩散图像不会变成黑白而是整幅偏绿、偏紫或偏红边缘出现一像素宽的彩色条纹。我曾经遇到过一台工业相机现场日志里明明写的BayerRG8按照RGGB去处理灰卡却总是偏紫。最后发现相机坐标系里RGGB的定义是从最左上角像素开始而驱动软件在裁剪ROI之后没有同步更新Pattern元数据导致整帧数据整体偏移了一列。这种情况单独看像素数据根本发现不了只能靠拍灰卡做验证。2.3 一块灰卡快速确认当前Sensor的真实CFA Pattern如果你拿到的Sensor文档不完整或者无从确认Pattern最实用的办法是直接做一次灰卡测试。准备一个中性灰的色卡或者一张白得均匀的A4纸放在均匀光源下拍一帧RAW。然后分别用RGGB、GRBG、GBRG、BGGR四种Pattern去解析。正确的Pattern得到的画面应该接近中性灰没有明显色偏灰卡边缘平滑错误的Pattern会留下可辨识的偏色和边缘伪彩。这个方法我用了很多年比对着datasheet猜可靠得多。注意有些相机在预览模式、视频模式、静态拍照模式下输出的Pattern可能不一样尤其是手机摄像头。如果你要对接底层数据必须以实际输出的那一帧为准不能只看SDK里的一句描述。3. RAW数据解码实操黑电平、去马赛克与白平衡的一条龙代码3.1 RAW文件不等于照片位深、黑电平与线性数据RAW数据本质上是一个二维数组每个元素代表一个像素的曝光量。这个数组有几个关键参数决定你后面怎么处理位深、黑电平、白电平、以及对每个通道可能独立生效的增益。位深最常见的是10bit、12bit、14bit。10bit数据有很多传感器会用16bit容器保存也可能是按10bit打包成字节流存储解码时需要注意对齐。黑电平更关键传感器完全遮光时的输出并不一定是0可能是64、128甚至更高。这段基准值不扣掉图像暗部就会蒙上一层灰雾对比度上不去颜色也会偏。一个简化的RAW解码流程是这样的读取原始像素数据按位深转成uint16数组。减去黑电平。根据CFA Pattern分离R、G、B三通道。做去马赛克得到完整RGB图。做白平衡、颜色校正矩阵然后做tone mapping和Gamma。少一步画面都不正常。很多人抱怨为什么RAW文件这么暗一大半原因就是根本没做Gamma校正线性数据直接显示当然暗。3.2 用PythonNumpy实现RGGB双线性去马赛克这里给一个教学演示级别的双线性去马赛克实现。它不追求最高画质但逻辑足够清晰适合理解核心流程。import numpy as np from scipy.ndimage import uniform_filter def demosaic_bilinear(cfa, patternRGGB): 双线性去马赛克教学实现 cfa: (H, W) uint16, 已减黑电平的Bayer原始数据 pattern: RGGB / GRBG / GBRG / BGGR 返回: (H, W, 3) float32, 通道顺序为RGB H, W cfa.shape R np.zeros((H, W), dtypenp.float32) G np.zeros((H, W), dtypenp.float32) B np.zeros((H, W), dtypenp.float32) # 根据pattern确定2x2单元内每个位置的颜色 # 这里用字符串索引比如RGGB - pos00R, pos01G, pos10G, pos11B table { RGGB: [(R,G), (G,B)], GRBG: [(G,R), (B,G)], GBRG: [(G,B), (R,G)], BGGR: [(B,G), (G,R)], } if pattern not in table: raise ValueError(unknown pattern) mat table[pattern] for r in range(H): row_mat mat[r % 2] for c in range(W): color row_mat[c % 2] if color R: R[r, c] cfa[r, c] elif color G: G[r, c] cfa[r, c] else: B[r, c] cfa[r, c] # 双线性插值用3x3均值滤波填补缺失位置 kernel_size 3 R_filled uniform_filter(R, sizekernel_size, modemirror) G_filled uniform_filter(G, sizekernel_size, modemirror) B_filled uniform_filter(B, sizekernel_size, modemirror) rgb np.stack([R_filled, G_filled, B_filled], axis2) return rgb这个实现的缺点是会把所有像素都做平均包括R通道自己已有的像素也会被再次平均导致画质偏软、边缘发虚。真实产品里不会这么写而是分别处理原始像素和待插值位置或者直接用边缘导向算法。但作为理解RAW解析的核心流程这段代码足够说明问题了。3.3 白平衡和颜色校正矩阵RAW为什么普遍偏暗偏灰去马赛克之后得到的RGB图像颜色大概率还是不对的。因为传感器的红绿蓝滤光片透射率并不是理想状态不同通道对相同光源的响应差异很大。白平衡的任务就是让一块中性灰在画面里显示为中性灰。最简单的方法是灰卡白平衡选画面里一块已知是灰色的区域求这个区域R、G、B三通道的平均值然后用绿色作为基准计算红色和蓝色的增益系数# 假设 gray_region 是画面中灰卡区域的像素集合 R_mean gray_region[:, :, 0].mean() G_mean gray_region[:, :, 1].mean() B_mean gray_region[:, :, 2].mean() R_gain G_mean / R_mean B_gain G_mean / B_mean white_balanced rgb.copy() white_balanced[:, :, 0] * R_gain white_balanced[:, :, 2] * B_gain白平衡做完颜色方向对了但饱和度往往还是不够。因为Bayer滤光片之间存在光谱串扰蓝色像素里可能混进了一部分红光。这时需要上颜色校正矩阵CCM。CCM是一个3×3矩阵不同传感器标定结果不同通常由传感器厂商或相机厂商提供。做完去马赛克、白平衡、CCM之后原本又暗又灰的线性RAW数据才有了接近人眼感知的样子。再用sRGB Gamma曲线压缩动态范围画面就会恢复正常的亮度层次。3.4 顺带说一句为什么佳能RAW人像预设这种词是个伪命题很多人会在网上搜佳能RAW人像预设这多少有点误解了RAW的本质。RAW文件本身是一份线性、未完成的数字负片它没有最终色彩这个概念。所谓的人像预设在Lightroom或者Capture One里其实是针对解出来的RGB图像做的调色组合而不是直接作用在RAW的Bayer数组上。当然了RAW文件里会记录白平衡、色彩风格等参考元数据软件拿到这些元数据后会自动做初步调色所以同一张RAW在不同软件里打开可能呈现完全不同的初始颜色。这是RAW解析流程的一部分不是玄学。理解这一层你才不会在RAW调色时被原片颜色带着跑。4. 手机ISP与工业相机SDK同样叫RAW路径完全不同4.1 手机的RAW已经是被加工过的计算RAW手机上的RAW通常以DNG格式保存但千万别以为这就是传感器直接吐出来的裸数据。现代手机拍照时系统会自动做多帧合成、HDR、降噪甚至部分去马赛克和色调映射。你拿到的DNG可能已经是多帧对齐、去噪、增益调整之后的结果。以典型计算摄影流程为例相机连续拍摄多帧短曝光和长曝光在GPU或NPU上做对齐和合并生成一张动态范围更高的中间帧再通过ISP流水线输出给用户。这里的DNG如果是从合并后的中间帧导出的它虽然还是线性RAW结构但噪声水平和原始单帧已经完全不同。所以手机开发如果要做图像算法不要假设Camera2 API返回的RAW等于硬件原始数据。它可能带有厂商的计算摄影痕迹。想要相对原始的传感器数据需要在拍照模式下关闭HDR、夜景、AI增强这些计算功能。4.2 工业相机的RAW才是真正把ISP交给你工业相机走的是另一条路。以GigE Vision或USB3 Vision接口为例相机输出的PixelFormat通常直接就是BayerRG8、BayerGR10、Mono12这类格式。SDK把原始字节流交到你手里不帮你做去马赛克不帮你调白平衡甚至不帮你做坏点校正。这意味着你用工业相机做视觉项目时ISP工作全都要自己做。好处是可控性极强可以精确控制曝光时间、增益、黑电平可以按自己的需求设计噪声抑制算法。坏处是门槛高很多人第一次从手机转过来接上工业相机发现图像灰暗偏色以为相机是坏的其实只是还没有写ISP流水线。我接触过的视觉项目里最稳的做法是直接用相机SDK把Bayer数据拉出来自己维护一套去马赛克、黑电平、平场校正、白平衡的处理管线。整套管线一旦标定好放到生产环境里可以稳定跑很久不会像手机ISP那样随场景自动调整。4.3 手机RAW和工业相机RAW的关键差异维度手机摄像头工业相机RAW内容DNG可能含多帧合成、降噪传感器原始Bayer数据通常无ISP处理位深10bit或12bit常见8bit到14bit都有去马赛克由SoC ISP完成用户可选接口有限由主机端软件完成曝光/同步无法精确外触发滚动快门为主支持硬件触发、全局快门、精确同步增益控制自动调节为主手动锁定增益可逐帧控制适用场景摄影、移动端视觉缺陷检测、测量、科学成像、自动化如果你的项目是做高精度尺寸测量或缺陷检测我建议优先选工业相机并且拿走它的Bayer原始数据自己做ISP因为手机相机内置的自动白平衡、自动增益很可能会在不同场景下给同一物体不同的颜色和亮度响应这对一致性要求高的视觉项目是致命的。4.4 同样是调白平衡手机自动完成工业相机需要你写算法手机上的自动白平衡是ISP里3A模块的一部分它会对当前场景做统计估计色温然后自动调整通道增益。这个过程是为人眼觉得好看服务的。工业相机场景里自动白平衡不是不能用但很多精密视觉项目会直接锁死固定增益靠固定的灰卡标定来保证批次一致性。比如在生产线上检测产品表面颜色如果相机每次自动调整白平衡同一批次产品可能因为环境光波动出现误判这是需要极度避免的。5. 实战避坑从错误CFA到去马赛克伪彩的完整排查链路5.1 现象一灰卡拍出来像彩虹布怎么排查这是我在群里见到最多的问题对着均匀灯光拍灰卡解析出来的画面却像一块染了色的花布边缘还有一堆彩色噪点。排查顺序建议是这样的先不要跑复杂的去马赛克算法直接按最朴素的每个像素显示自己的颜色方式把RAW渲染出来。如果看到棋盘状色块说明数据本身是Bayer格式问题出在后续解析。分别用四种CFA Pattern去试每试一种就截一小块灰卡区域检查颜色是否中性且均匀。如果四种Pattern都不是纯中性检查是否忘了减黑电平。黑电平没减干净时灰卡的暗部会发绿或发紫。如果边缘伪彩依然存在但整体颜色正常大概率不是Pattern问题是去马赛克算法太粗糙。这套流程哪怕对完全陌生的Sensor一般十分钟内能定位问题。5.2 现象二RAW文件暗得没细节未必是曝光问题RAW直出的画面偏暗有三个主要原因。第一是没有做Gamma线性的曝光值和显示器需要的sRGB非线性曲线不匹配暗部看起来就很黑。第二是减黑电平没做好导致暗部对比度被压缩。第三才是真正的曝光不足。你如果在搜索引擎里搜RAW相关的问题有时候会搜到磁盘工具或者文件系统格式化为RAW之类的词。那是存储介质无法识别文件系统时的叫法跟图像RAW文件完全是两回事。图像RAW只是未处理的传感器数据的意思不是原始格式的磁盘。判断到底是哪种原因最直接的方法是看直方图。RAW线性数据的直方图如果高光处还有很大空白说明曝光不足如果没有空白且暗部堆积严重多半是Gamma和黑电平的问题。前者靠加曝光或者补光后者靠处理流程修正千万不要一上来就狂拉曝光。5.3 现象三镜头边缘发暗平场校正怎么救Bayer解析完成后你还会经常看到画面四周比中心暗这就是镜头渐晕。光学镜头边缘进光量本身小于中心加上Bayer滤光片对不同角度的入射光响应不一致边缘还会出现轻微色偏。解决做法是平场校正。找一块均匀照明的白色平板拍一帧平均下来作为平场参考图再把实际拍摄图减去黑电平后除以平场参考图就能把渐晕和部分像素响应差异抹平。在工业相机里这个功能有时候叫Flat Field CorrectionSDK会提供标定接口。手机ISP里也有类似处理叫Lens Shading Correction只不过会通过增益矩阵在硬件层面自动补偿用户很少感知到。我自己的建议是如果做定量分析比如表面缺陷检测平场校正是必须做的。如果不做边缘区域的灰度和颜色会跟中心区域出现系统性偏差算法阈值再准也没用。5.4 现象四边缘拉链伪影与摩尔纹去马赛克不是万能的。双线性插值在黑白相接的高对比边缘会出现一种叫zipper effect的现象看起来像拉链一样沿着边缘一排明暗交替的锯齿。更麻烦的是伪彩黑色边缘附近出现红绿蓝三色的噪声点。缓解手段包括改用边缘导向插值、Malvar-He-Cutler算法或者在Bayer域做边缘方向的低通滤波。OpenCV里有cv2.demosaicing可以选更高级的算法但工业相机厂商更建议用硬件ISP芯片或者经过严格标定的软件处理流程。摩尔纹是另一个坑。手机传感器前面通常有光学低通滤镜来消除摩尔纹但工业相机为了锐度很少装OLPF。拍摄高密度纹理比如布料、丝网、电路板的密集走线就特别容易出现彩色摩尔纹。解决办法往往是调整拍摄角度、稍微虚化一点点或者在算法层面对特定空间频率做抑制。如果你也在集成一款陌生Sensor我的建议是先不要急着写完整的ISP流水线而是先把最笨的CFA灰卡验证做掉拍一张灰卡四种Pattern逐个跑肉眼确认哪个没有伪彩然后再做黑电平、平场、白平衡。别小看这十分钟我至少见过五个人拿着正确的Sensor却因为CFA Pattern设错把整个晚上的调试时间全部耗在偏色调参上。先把RAW这层地基砸实后面的颜色才有的谈。
