013、Bayer 格式与去马赛克
开篇先讲个真事。去年调一块Sensor驱动,预览画面在暗光下满屏“水彩纹”,尤其是黑色文字边缘,红蓝绿噪点跟下雨似的。我看了一眼dump出来的raw图,像素值本身很干净,问题出在后面的去马赛克——ISP里那个demosaic模块没配好。当时我的第一反应是“这Sensor的Bayer排布是不是搞错了?”查驱动里reg,RGGB没错,但用标准双线性插值去解,边缘就是稀碎。后来换了保边界的插值权重,瞬间就清净了。从那天起我才算真正意识到,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 raw,width/height是偶数// 注意:实际驱动对齐可能带padding,不要直接用width*height计算strideuint16_t*bayer=(uint16_t*)malloc(width*height*sizeof(uint16_t));// 从文件或驱动buf里读raw,这里略过// ... 读出来的bayer里,位置(0,0)=坐标(x=0,y=0)就是R// 下面这个函数:给定坐标(x,y),返回该点拜耳颜色ID// 我习惯用0=R, 1=Gr, 2=Gb, 3=B,跟某些sensor寄存器定义保持一致,别自创intbayer_pattern_id(intx,inty,intpattern){// pattern用RGGB=0,其他排布手动映射一下,注意y奇偶和x奇偶的组合if(pattern==0){// RGGBif(y%2==0&&x%2==0)return0;// Rif(y%2==0&&x%2==1)return1;// Grif(y%2==1&&x%2==0)return2;// Gbreturn3;// B}// 其他pattern类似,这里不展开return-1;}这一段看着简单,实际上有个超级容易踩的坑:内存对齐和行跨距。我的一个兄弟公司用的某款平台,DMA搬raw时每行会补齐到64字节对齐,结果他们直接用width*height去读,画面变成斜切方形,颜色全乱。后来发现stride要单独从驱动里拿。所以你一定别这样写死:
// 别这样写!万一驱动做了padding或crop offset,这里全错size_tstride=width;// 错误示例for(inty=0;y<height;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_sum=0;intcount=0;// 上if(y>0){g_sum+=PIXEL(0,-1);count++;}// 下if(y<height-1){g_sum+=PIXEL(0,1);count++;}// 左if(x>0){g_sum+=PIXEL(-1,0);count++;}// 右if(x<width-1){g_sum+=PIXEL(1,0);count++;}return(float)g_sum/count;#undefPIXEL}看到没,这代码在边界处只能攒几个邻居算平均,出来的值在图像边缘会明显偏暗。ISP里的demosaic一般会预留边框或者用镜像扩展,否则整幅图的四边色彩就是烂的。如果你在应用层做验证,别忘了裁剪掉边界或者做边缘复制。
别只盯着插值,黑电平、坏点、降噪都搅在一起
很多人以为去马赛克就是“插值”,其实错了。raw数据进入demosaic之前,你至少得处理三件事:黑电平减除、坏点校正、降噪。黑电平不对,你插值出来的颜色整体偏灰或者偏绿。坏点不除,demosaic会把坏点当作真实高频信号,结果产生同心圆的伪彩扩散——那画面,怎么说呢,像一滴墨水滴到宣纸里,而且是带颜色的。我调过一块Sensor,有个坏点在屏幕正中偏左,用最小亮度的正常画面测试,那个坏点附近的红蓝杂色像“鬼火”一样。后来在demosaic之前加了坏点表校正,立马消失。所以你在调试笔记里要记住:看到彩噪先调查黑电平和坏点,别急着调demosaic参数。
ISP里的demosaic可不只是算法,还带“鸡血”效果
硬件ISP里,demosaic之后往往紧跟着Color Correction Matrix(CCM)、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怎么调肤色不发绿。