051、ISP Pipeline架构设计:从RAW到YUV的端到端流水线
051、ISP Pipeline架构设计从RAW到YUV的端到端流水线去年夏天某款旗舰手机在暗光场景下预览画面总是偏紫调试了三天最后发现是ISP pipeline里AWB模块的色温估计被前级DPC模块的坏点校正干扰了。这种“前级挖坑后级填”的破事在ISP架构设计里太常见了。今天就把我从RAW进到YUV出的这条流水线掰开了揉碎了讲清楚。为什么说ISP是影像系统的“心脏起搏器”Sensor吐出来的RAW数据说白了就是一堆被拜耳阵列滤光片“阉割”过的电压值。每个像素只记录了R、G、B中的一种颜色而且带着暗电流噪声、光子散粒噪声、固定模式噪声、坏点……这玩意儿直接显示出来比老式雪花电视还辣眼睛。ISP Pipeline的任务就是把这堆“半成品”变成人眼看着舒服的YUV图像。注意我说的是“人眼看着舒服”不是“物理上最准确”。这两者经常打架比如暗部提亮会放大噪声锐化会带来振铃色彩饱和度高会显得假——这些都是架构设计时需要在各个模块之间反复权衡的。RAW域处理先把底子擦干净黑电平校正BLC——别让暗部飘起来Sensor在完全遮光时像素依然会有微弱的输出这叫黑电平。不同通道的黑电平还不一样G通道通常比R/B通道高那么一两个LSB。如果不校正暗部会偏色而且会影响后续所有依赖绝对亮度的模块。// 这里踩过坑不同增益下的黑电平偏移量不一样// 别写死一个值要查LUT或者实时计算uint16_tblc_correct(uint16_traw,uint8_tchannel,uint16_tgain){// 实际项目中黑电平偏移量随增益线性增长// 我见过有人只校准了1x增益下的值开到4x增益时暗部直接飘成紫色int16_toffsetblack_level_table[channel]gain_offset[channel]*(gain-1);return(rawoffset)?(raw-offset):0;}坏点校正DPC——一颗老鼠屎坏一锅汤Sensor产线不可能保证每个像素都完美总会有几个“死点”或“亮点”。死点永远输出低值亮点永远输出高值。如果不处理这些坏点经过后续的demosaic、锐化后会扩散成一片区域。静态坏点表是出厂时标定的但动态坏点检测更重要。温度变化、长时间曝光都会产生新的坏点。我习惯的做法是先查静态表再用3x3窗口做动态检测——如果中心像素与周围8个像素的差值超过阈值且周围像素方差很小那基本就是坏点。别这样写直接用周围像素的平均值替换。这会在纹理区域引入模糊。应该用中值滤波或者更狠一点用与中心像素同通道的四个对角像素做插值。去噪NR——降噪和细节是死对头RAW域去噪是性价比最高的因为噪声还没被后续处理放大。但这里有个悖论去噪太狠细节就没了去噪太轻后面demosaic会把噪声放大成彩色斑点。双边滤波是经典方案但计算量大。实际工程中我更推荐非局部均值NLM的简化版——用5x5的搜索窗口3x3的相似块做加权平均。权重由像素值差和空间距离共同决定。// 经验值sigma_r像素值域设为噪声标准差的1.5倍// sigma_d空间域设为2.0// 这两个参数调了我两周最后发现跟sensor的ISO是强相关的floatbilateral_weight(floatpixel_diff,floatspatial_dist){floatwrexp(-0.5*(pixel_diff*pixel_diff)/(sigma_r*sigma_r));floatwdexp(-0.5*(spatial_dist*spatial_dist)/(sigma_d*sigma_d));returnwr*wd;}去马赛克Demosaic从拜耳到RGB的魔法这是ISP pipeline里最“玄学”的模块。Bayer阵列每个像素只有一种颜色要猜出另外两种颜色。最简单的双线性插值会带来严重的伪色和锯齿。方向性插值是工程界的标配。先计算水平和垂直方向的梯度沿着梯度小的方向做插值。比如在红色像素位置猜绿色值如果水平梯度小就用左右两个绿色像素的平均值。这里踩过坑梯度计算窗口太小纹理复杂区域容易误判。我用5x5窗口计算梯度并且加入颜色差分的约束——绿色像素在红色和蓝色位置的估计值应该与周围已知的绿色像素保持颜色差分的连续性。更激进的做法是用引导滤波Guided Filter把绿色通道作为引导图对红色和蓝色通道做上采样。效果确实好但计算量翻倍手机平台上得掂量掂量。RGB域处理调色师的舞台白平衡AWB——色温估计不准后面全白搭AWB的核心是估计场景的色温然后对R、G、B通道分别施加增益。灰色世界法假设场景平均色是灰色但遇到大面积单色物体比如蓝天、绿草地就翻车。我现在的方案是先做色温统计把图像分成16x16的块每个块计算R/G和B/G的比值。然后把这些比值映射到色温曲线上找最可能的色温。如果多个色温候选用场景识别结果做加权——比如检测到人脸优先用肤色对应的色温。别这样写直接对整幅图像做灰色世界。遇到日落场景会把暖色调硬拉成白色照片就废了。颜色校正矩阵CCM——从sensor RGB到标准RGB不同sensor的光谱响应曲线不一样同样的RGB值在不同sensor上看起来颜色不同。CCM就是做这个转换的。3x3矩阵9个系数看起来简单调起来要命。矩阵系数会放大噪声尤其是蓝色通道的增益通常大于1会把蓝色噪声放大得很明显。// 经验CCM矩阵的系数和不能偏离1太多// 如果某个通道的增益超过2.0说明sensor的光谱响应有问题// 这时候应该先调sensor的滤光片而不是硬靠矩阵拉floatccm[3][3]{{1.8,-0.5,-0.3},// R通道{-0.2,1.6,-0.4},// G通道{0.1,-0.3,1.2}// B通道};伽马校正Gamma——人眼不是线性传感器人眼对暗部的变化更敏感对亮部的变化不敏感。伽马校正就是把线性光强映射到人眼的感知空间。标准伽马值是2.2但不同场景可以微调——暗光场景用1.8可以提亮暗部但会牺牲对比度。YUV域处理最后的抛光色彩空间转换RGB to YUV——分离亮度和色度YUV的好处是把亮度和色度分开这样可以对Y通道做锐化、对UV通道做降噪互不干扰。转换矩阵是标准的但要注意量化精度——8bit数据用定点数计算时舍入误差会累积。锐化Sharpening——细节和噪声的终极博弈非锐化掩模USM是经典方案先对Y通道做高斯模糊然后用原图减去模糊图得到高频分量再乘以增益加回去。// 这里踩过坑增益不能全局统一// 平坦区域增益要小纹理区域增益要大// 我用了边缘检测器做自适应增益控制floatsharp_gainbase_gain*(1.0-edge_map[x][y]*0.5);// edge_map范围0~1边缘越强增益越大更先进的是CLAHE对比度受限自适应直方图均衡化但计算量大而且处理不好会产生光晕效应。我一般只在医疗影像里用手机平台上太耗电。色度降噪Chroma NR——彩色噪声的克星YUV域的色度噪声表现为彩色斑点非常碍眼。但UV通道的分辨率已经是Y通道的一半4:2:0采样再做降噪容易丢失颜色细节。我的做法是用Y通道的梯度信息引导UV通道的降噪。如果Y通道有边缘UV通道的滤波强度就降低保留颜色边缘。如果Y通道是平坦区域UV通道就狠降噪。实战中的血泪教训模块顺序不能乱DPC必须在BLC之后因为坏点检测依赖正确的黑电平。CCM必须在AWB之后因为白平衡改变了RGB通道的相对强度。Gamma必须在CCM之后因为CCM假设输入是线性光强。参数联动是常态ISO升高时去噪强度、锐化强度、CCM矩阵都需要联动调整。我维护着一张“ISO-参数”映射表每个ISO档位对应一套完整的参数组合。调试工具比算法更重要没有好的可视化工具你根本不知道问题出在哪个模块。我习惯在每个模块的输出端挂一个“监视器”可以实时查看中间结果。哪个模块出了问题一眼就能看出来。硬件加速和软件灵活性的平衡硬件ISP速度快但参数调整不灵活。软件ISP灵活但功耗高。我的做法是把90%的固定处理交给硬件把10%的“智能”处理比如场景识别、自适应参数调整放在软件里。永远留一手产线上的sensor个体差异比你想的大。设计pipeline时每个模块都要留出参数调整的余量别把参数推到极限。否则换一批sensor你的pipeline就崩了。最后说一句ISP架构设计不是把每个模块做到极致而是让所有模块协同工作。一个完美的去噪模块如果破坏了AWB的色温统计那就是灾难。记住系统级优化永远大于模块级优化。