013、Bayer 格式与去马赛克
发布时间:2026/10/28 0:09:31 锦皓数字建站

013、Bayer 格式与去马赛克开篇先讲个真事。去年调一块Sensor驱动预览画面在暗光下满屏“水彩纹”尤其是黑色文字边缘红蓝绿噪点跟下雨似的。我看了一眼dump出来的raw图像素值本身很干净问题出在后面的去马赛克——ISP里那个demosaic模块没配好。当时我的第一反应是“这Sensor的Bayer排布是不是搞错了”查驱动里regRGGB没错但用标准双线性插值去解边缘就是稀碎。后来换了保边界的插值权重瞬间就清净了。从那天起我才算真正意识到Bayer格式不只是“排列顺序”那么简单它决定了后面整条图像流水线的行为。Bayer 是个啥说白了就是残缺但聪明的颜色采样人眼有RGB三种视锥细胞相机Sensor想模仿一个像素就得同时感知三种颜色。但物理上做不到啊要么每个像素叠三层像Foveon贵又少见要么就搞个滤色阵列让每个像素只放行一种颜色。Bayer就是最经典的2x2网格两个绿色、一个红色、一个蓝色如图脑补一下。为啥绿色俩因为人眼对绿色最敏感亮度细节主要靠绿色通道撑起来。所以你别嫌Bayer“残缺”这正好是拿空间的色度分辨率换亮度分辨率是“把钱花在刀刃上”的典范。Bayer排列有一堆变体RGGB、BGGR、GBRG、GRBG。这里有个坑——很多新手拿到Sensor第一件事就是对着datasheet查排列然后写死代码。但同一个Sensor在不同驱动版本、不同输出模式下可能默认排布都不一样。更坑的是有些ISP的demosaic模块吃的是“线性排列”的raw有些吃“镜像翻转”后的raw你配置错了画面直接变“万花筒”。我排障过最离谱的一次是板子上Sensor装反了物理上翻转了180度但驱动没改mirror/flag结果RGB通道全部错位——红色变成蓝色人物肤色发绿像外星人。所以要养成习惯每次拿到新板子先输一张纯红纯绿纯蓝的画面看三个通道的响应用这个原始数据反推Bayer排列别信datasheet。去马赛克到底在干嘛瞎猜每个像素缺的两门课拿RGGB排列举例位置(0,0)是R它缺G和B位置(0,1)是G它缺R和B。去马赛克就是把每个像素缺的两种颜色值猜出来补成完整的RGB三元组。你可能会想“那还不简单把周围的像素直接平均一下就行呗。”对这就是最朴素的“双线性插值”。但对一个像素来说它的R邻居远B邻居更远平均出来的颜色会“糊”掉尤其在高频边缘和细纹理区域会出现严重的假色false color和拉链效应zipper artifacts。你看那些廉价摄像头拍的深色条纹衬衫边缘一跳一跳的多半就是这个原因。我实际调ISP的时候demosaic算法都是从简单到复杂走一遍。双线性是最快的但画质最差。后来用“基于梯度检测”的插值——先算水平和垂直方向的梯度哪个方向变化小就往哪个方向插值。比如对于缺失G的红色像素比较左林右邻的G值差和上邻下邻的G值差选小的那个方向做加权平均。这个算法在开源库和很多老ISP里很常见效果比双线性强一截但遇到斜线还是会有伪色。再高级一点什么Hamilton-Adams、LMMSE、AHD异质性检测这些经典算法基本都是围绕“怎么用色差/色比的先验”来猜得更准。现在SOC集成的3A调优里demosaic往往还配合自适应降噪、边缘增强一起混着做不是独立模块了。代码走一遍简单双线性实现的坑为了讲清楚Bayer的原始数据长啥样我给个模拟读取的示意代码。这里假设你dump出来的是10bit raw存成uint16_t数组宽高已知排布RGGB。// 假设raw_data是按行扫描存的Bayer rawwidth/height是偶数// 注意实际驱动对齐可能带padding不要直接用width*height计算strideuint16_t*bayer(uint16_t*)malloc(width*height*sizeof(uint16_t));// 从文件或驱动buf里读raw这里略过// ... 读出来的bayer里位置(0,0)坐标(x0,y0)就是R// 下面这个函数给定坐标(x,y)返回该点拜耳颜色ID// 我习惯用0R, 1Gr, 2Gb, 3B跟某些sensor寄存器定义保持一致别自创intbayer_pattern_id(intx,inty,intpattern){// pattern用RGGB0其他排布手动映射一下注意y奇偶和x奇偶的组合if(pattern0){// RGGBif(y%20x%20)return0;// Rif(y%20x%21)return1;// Grif(y%21x%20)return2;// Gbreturn3;// B}// 其他pattern类似这里不展开return-1;}这一段看着简单实际上有个超级容易踩的坑内存对齐和行跨距。我的一个兄弟公司用的某款平台DMA搬raw时每行会补齐到64字节对齐结果他们直接用width*height去读画面变成斜切方形颜色全乱。后来发现stride要单独从驱动里拿。所以你一定别这样写死// 别这样写万一驱动做了padding或crop offset这里全错size_tstridewidth;// 错误示例for(inty0;yheight;y){memcpy(buf[y*width],raw_buf[y*stride],width*sizeof(uint16_t));}正确姿势是从驱动获取真实的stride然后按stride去索引。你要是在这个环节省代码后面去马赛克出来的图“斜”到让你怀疑人生。再来看一个最简单的双线性去马赛克——只补G通道。对于R像素点(x,y)它缺G取上下左右四个G的平均floatcalc_g_hlinear(constuint16_t*bayer,intx,inty,intwidth,intstride){// 这里默认(x,y)是R或B像素因为它缺G// 上下左右必须是G否则就是边缘需要特殊处理// 别直接裸奔索引先用宏定义取像素防止指针越界#definePIXEL(_dx,_dy)bayer[(y(_dy))*stride(x(_dx))]intg_sum0;intcount0;// 上if(y0){g_sumPIXEL(0,-1);count;}// 下if(yheight-1){g_sumPIXEL(0,1);count;}// 左if(x0){g_sumPIXEL(-1,0);count;}// 右if(xwidth-1){g_sumPIXEL(1,0);count;}return(float)g_sum/count;#undefPIXEL}看到没这代码在边界处只能攒几个邻居算平均出来的值在图像边缘会明显偏暗。ISP里的demosaic一般会预留边框或者用镜像扩展否则整幅图的四边色彩就是烂的。如果你在应用层做验证别忘了裁剪掉边界或者做边缘复制。别只盯着插值黑电平、坏点、降噪都搅在一起很多人以为去马赛克就是“插值”其实错了。raw数据进入demosaic之前你至少得处理三件事黑电平减除、坏点校正、降噪。黑电平不对你插值出来的颜色整体偏灰或者偏绿。坏点不除demosaic会把坏点当作真实高频信号结果产生同心圆的伪彩扩散——那画面怎么说呢像一滴墨水滴到宣纸里而且是带颜色的。我调过一块Sensor有个坏点在屏幕正中偏左用最小亮度的正常画面测试那个坏点附近的红蓝杂色像“鬼火”一样。后来在demosaic之前加了坏点表校正立马消失。所以你在调试笔记里要记住看到彩噪先调查黑电平和坏点别急着调demosaic参数。ISP里的demosaic可不只是算法还带“鸡血”效果硬件ISP里demosaic之后往往紧跟着Color Correction MatrixCCM、Gamma、色调映射。也就是说你在屏幕上看到的“彩色边缘”可能不是demosaic本身造成的而是demosaic放大了某个通道的高频误差然后CCM又把这种误差映射成更刺眼的色差。有一次我调一块Sensor发现红色文字边缘有青色镶边怎么调demosaic的边缘权重都没用。后来把CCM的副对角线数值往里收镶边立刻减轻。这告诫我们调问题不要只盯一个模块数据流后面每个模块都可能放大前级的误差。换句话说demosaic和CCM是情侣闹分手了先想想是谁惹的祸。经验性建议不是教科书那套做Camera HAL/driver有一阵了掏心窝子讲几个实用习惯。第一验证Bayer排布一定要用“单色光源”“单色目标”的组合。拿红绿蓝三只手电筒或三张纯色图片照在纯白墙上分别采集raw看哪个坐标位置的通道响应高。不要用彩色条、不要用标准色卡那些东西自带高频纹理混淆判断。我有个同事用爱色丽色卡里的“饱和红”去验证结果那个色卡的红块含蓝色杂散他愣是测出了个“错”的Bayer顺序然后花了半天调一堆没用的参数。第二、别迷信“最强”的去马赛克算法。CPU上AHD效果确实好但速度太慢。你如果只是在PC端做仿真可以随便跑但在嵌入式环境得看硬件demosaic的寄存器配置。很多SoC的ISP提供多档插值强度你调的是“档位”而不是算法本身。我见过有人写了一段自定义demosaic代码想把ISP的输出替换掉结果因为DDR带宽翻倍、功耗上涨被项目经理骂了一个礼拜。能调Reg的别写死代码。第三对于调试阶段我强烈建议你在HAL层保留一个“旁路模式”直接把raw dump到文件关掉所有色彩处理然后离线在PC上跑算法。这样你能先把问题归因到“采集链路”还是“后处理链路”。在HAL源码里留个log开关配置为debug属性比如“persist.vendor.camera.rawdump”一打开就存raw这样你在产线上听到“画面发花”就知道怎么下手。第四重要的话说三遍查stride、查stride、查stride。所有raw buffer处理都要用驱动报告的stride不要用width乘bpp。我吃过这个亏而且不止一次。你可以在调试时打印首行、次行的像素值看出没有阶梯跳变。另外一个常被忽略的是raw的字节序——是高位在前还是低位在前在ARM小端上10bit raw有时候是lo8、hi2对齐到uint16有时候又是lo2、hi8。这个反了出来的画面会严重偏绿或偏品红而且所有去马赛克算法都会跟着崩。先打印一组像素的hex值对照已知亮度确认字节序后再碰算法。最后多留几份不同色温下的raw图。调试demosaic时最好有D65、A光源、混光三种典型场景的raw库。别只在办公室荧光灯下试否则你调出来的参数到室外太阳光下直接“见光死”。我自己的习惯是每颗Sensor导入前期固定采集20张raw做基线后面每次调demosaic都跑一遍回归对比。这么做看起来很土但比靠眼睛看显示器靠谱一万倍。写这篇笔记的时候我脑子里还在转上一块Sensor的“紫边”问题。虽然标题是Bayer格式与去马赛克但实际工程里你永远是在一个完整的ISP链路上调问题。记住格式是死的问题是活的。理解每个像素缺什么、怎么补再配合原始数据验证你就能在那些伪彩和锯齿面前稳住心神。好了就这些下次再聊聊CCM和Gamma怎么调肤色不发绿。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。