ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA图像采集自动曝光快速收敛:加权灰度均值与动态步长的工程实现

FPGA图像采集自动曝光快速收敛:加权灰度均值与动态步长的工程实现 最近在做一个FPGA图像采集项目画面亮度来回震荡的问题让我连续加了好几天班窗外有云飘过时整幅画面不是过曝就是欠曝自动曝光迭代十几帧才勉强稳定偶尔还会在目标亮度附近反复横跳。动手排查之后才发现问题根源不在传感器也不在镜头而在自动曝光那条反馈链路——我一开始用的是全图平均亮度再用固定步长去调曝光时间。后来我把评估模型换成加权灰度均值再把收敛策略改成动态步长加前馈预估最终在FPGA上跑通了一套快速收敛的自动曝光模块。这篇文章就是那次从踩坑到落地的完整记录适合正在用FPGA做摄像头图像处理、ISP或工业视觉的朋友参考。1. 画面忽明忽暗的背后自动曝光评估模型为什么需要改造1.1 全图平均亮度在真实场景中的失灵很多初做自动曝光的人第一步都会选全图平均亮度。这个指标实现太简单了整帧的灰度值累加除以像素总数得到平均亮度然后跟目标亮度比较。我也一样代码写完上板对着均匀光照的测试卡收敛效果很不错。可一到真实场景就露馅画面里有一大片白墙或者大片蓝天、大片阴影的时候全图平均亮度会迅速被大面积背景“拉走”主体反而曝光不准。举个例子一个会议室场景左侧是明亮的落地窗右侧是坐在工位上的人。窗户区域占画面40%亮度值可能到240人物区域占30%亮度值只有60剩下的桌面和墙壁占30%亮度120。全图平均算下来是(240×0.4 60×0.3 120×0.3) 96 18 36 150。如果目标平均亮度是128算法会觉得画面偏亮于是去缩短曝光时间结果人物区域越调越暗。这就是评估模型失真带来的连锁反应统计错了后面控制得再快也没有意义。人眼在判断一张照片“亮不亮”的时候并不是把所有像素一视同仁地取平均而是更关注画面中央、或者画面中的主要目标。所以自动曝光的评估模型不应该追求数学上的平均而应该追求“感知上的平均”。这就引出了加权灰度均值。1.2 FPGA平台为自动曝光带来的硬件边界为什么一定要在FPGA上重新算一遍而不是用现成的软件方案用过的都知道CPU或者DSP上跑自动曝光通常拿到一帧图像后做完整统计再通过I2C写寄存器一轮反馈的延迟往往有几毫秒甚至十几毫秒。对画面变化不敏感的安防监控可能还能忍但在工业视觉、高速检测这类场景目标物体快速移动光照条件每秒都在变等CPU慢慢算完再调画面早就跟不上了。FPGA的优势在流水线和并行性。像素数据从传感器进来经过去马赛克、色彩校正等ISP链路时可以顺路做亮度统计整个统计过程不占用额外帧时间。等到一帧结束统计结果直接交给控制模块算好下一帧需要的曝光时间和增益在下一帧开始前写入传感器寄存器。这是一个“帧内统计、帧间决策、下一帧生效”的闭环结构。但FPGA也带来了限制。首先是统计逻辑必须跟着像素时钟走每来一个有效像素就要完成一次乘累加不能像软件那样把一帧数据存下来慢慢算。其次浮点运算在FPGA里很贵乘法器和DSP资源有限我不能动不动就搞一堆浮点乘法器和除法器。所以整个自动曝光模块从第一天设计起就按“定点数、查表、移位”的硬件思维来约束算法。2. 加权灰度均值的硬件实现让“平均亮度”真正反映画面主体2.1 把画面拆成区域给每个区域一个“发言权”加权灰度均值的思路很直白先把画面按网格切成若干区域每个区域统计自己的平均灰度再乘以一个权重系数最后累加并归一化。关键就是权重表怎么定。我用的是一块可配置的权重表默认配置是5行3列的网格。为什么选5×3而不是更细因为区域太多每个区域的平均灰度统计精度会下降而且权重表在FPGA里要用寄存器或BRAM存省资源起见要控制规模。5×3网格把画面分成15块中央一列权重最高两边的权重低一些。再细分一点5×5网格也可以但实际测试下来5×3已经能有效压制大面积背景干扰再增加区域数量画面主体的位置一旦偏移反而容易因为权重固定导致新的偏差。权重表的值我直接用整数定义。比如中央位置的权重是9上下次之是7左右边缘是3四角是1。这组值有什么依据其实不是严格计算出来的而是我试出来的初始值。原则只有一个权重要能反映画面主体通常出现的位置。如果应用场景固定比如车牌识别可以把权重更集中到画面下半部分如果是无人机航拍地面通常在下半部分也可以把下侧权重调大。所以权重表应该做成可配置的而不是写死在代码里。2.2 定点化与移位除法有了权重表和每个区域的平均亮度加权灰度均值的公式就是加权灰度均值 Σ(区域平均灰度 × 区域权重) / Σ(区域权重)问题在于“除以Σ(区域权重)”这个除法。在FPGA里通用除法器又慢又费资源。我的做法是直接把分母也改成整数并把分母换成2的幂次这样除法就变成了右移。更实际的做法是不直接做除法而是把每个区域的灰度值先乘以权重累加得到加权亮度和weighted_sum同时累加权重和weight_sum最后用一次定点除法。如果嫌除法器麻烦可以预先计算好“1 / weight_sum”的定点近似值存成查找表然后把除法变成一次乘法加移位。以5×3网格为例权重表里3、7、9这些数字累加后不一定等于2的幂。所以我干脆把权重表设计成2的幂的组合比如中央区域权重8中间区域权重2边缘区域权重1这样所有区域权重之和就有可能是16、32之类的数。比如一个对称权重表中央一列8左右各3四角1那权重总和 3×8 6×3 6×1 48还不是2的幂。更干脆一点所有区域权重都取1、2、4这种值保证总和等于2的幂次那么加权灰度均值 weighted_sum N一个右移就搞定。如果实在想用更丰富的权重值也可以保留除法。在FPGA里做一个定点除法器针对1080p分辨率也不成问题只是需要几拍流水线。我最终选择的方案是权重值还是用3、7这种非2的幂但在统计结束后用一次查表加乘法的定点除法输出加权均值。这样权重表的选择更自由适应不同场景的能力更强。2.3 整帧统计的双缓冲与行消隐处理统计逻辑在FPGA里最容易被坑的地方是时序。像素流不是永远连续的每行之间有行消隐区HBlank每帧之间有场消隐区VBlank。统计模块必须只统计有效像素de信号为高的区域不能把消隐期的垃圾数据算进去。我的统计模块工作方式是这样de为高且数据有效时根据当前像素坐标判断它属于哪个网格区域把该区域的灰度累加器加一次同时该区域的像素计数加一。到帧末尾把每个区域的累加值除以计数值得到每个区域的平均灰度再乘以对应的权重把所有加权结果累加。这里有个关键细节如果统计逻辑只有一个累加器那么上一帧的数据还没来得及输出下一帧已经开始覆盖就会造成数据混叠。我用了双缓冲结构RegionAcc[0]和RegionAcc[1]一组在统计当前帧另一组在上一帧结束后锁存给后级模块。场消隐期间切换缓冲区保证控制模块读到的始终是完整且稳定的一帧统计结果。3. 向快速收敛要体验动态步长与前馈预估的组合策略3.1 为什么固定步长会在目标附近反复横跳传统自动曝光最常见的控制方式是拿当前平均亮度和目标亮度比较如果偏暗就把曝光时间增加一个固定百分比偏亮就减少一个固定百分比。这种固定步长有两个致命问题。第一个问题是离目标远的时候太慢。如果画面突然变暗当前平均亮度只有目标亮度的1/4按照每一步调整10%的做法需要十几次调整才能接近目标。第二个问题是接近目标的时候太稳不住。步长固定10%但目标误差带只有2%那最终结果只会在目标附近来回穿越形成极限环振荡。我第一版上板实测对着一个突然反转的测试图案平均要25帧左右才能稳定而且稳定后还会看到亮度在4到5个灰度等级之间跳动。这就是退化成“彬”的极限环。3.2 对数域曝光调整与动态步长真正要解决收敛速度得先把曝光调整的“味道”做对。人眼对亮度的感知接近对数关系图像传感器在曝光时间和灰度输出之间近似线性关系。如果在线性域按固定步长调整曝光时间暗光下调整量看起来大但亮度变化小亮光下调整量小但亮度变化大。这会导致暗处调得慢亮处调得猛。正确的做法是在对数域调整曝光时间。也就是说每一步调整的不是曝光时间的固定差值而是曝光时间乘以一个系数。假设当前曝光时间是E当前加权均值为Y目标为Y_target那么最直接的目标曝光估算就是E_new E × (Y_target / Y)这个式子本身就是一个前馈估计。Y_target/Y是对亮度误差的线性映射。如果画面偏暗Y只有目标的一半E就直接翻倍如果偏亮E就减半。这个操作在FPGA里实现起来也不复杂Y_target是常数Y是统计结果E和Y都是定点数做一次定点除法或者查表求倒数即可。为了兼顾稳定我在此基础上加入了动态步长。算法先计算误差率error_ratio Y_target / Y当误差率大于1.5或小于0.67时认为误差很大允许直接逼近目标当误差率在0.85到1.15之间时只允许朝目标方向移动最多20%的曝光时间防止越过目标当误差率在0.98到1.02之间时进入死区完全不调整。这就是“大误差大步走小误差小步挪进了死区不出手”的策略。3.3 前馈预估一帧到位再微调很多人把自动曝光当成纯反馈问题其实可以把“当前帧误差”变成“下一步指令”的前馈方式。具体做法很简单统计完当前帧得到加权灰度均值Y_now当前帧实际曝光时间是E_now假设场景亮度基本不变那么达到目标亮度Y_target需要的曝光时间近似为 E_target E_now × (Y_target / Y_now)直接把传感器曝光时间设置为E_target并限制在最大最小曝光时间范围内下一帧统计后再根据残差做小幅修正。这个策略在场景光照相对稳定时几乎可以做到一帧收敛。为什么说“近似”因为传感器不是理想的线性器件增益、镜头光圈、不同曝光档位下可能有一些偏差。但作为粗调它已经能把亮度拉到目标附近剩下几帧只需要做细调。为了防止前馈预估把一个瞬时噪声或场景中主体移动导致的亮度跳变当成真实光照变化我在预估输出前加了一个限幅器每帧曝光时间的最大变化倍数不超过4倍。这样即使统计结果因为个别闪烁物体出现异常画面也不会猛闪一下。3.4 死区、滞回与收敛判定死区是自动曝光必需的。没有死区任何一点细微的噪声都会让曝光时间不停变动画面虽然整体亮度接近目标但细节一直在微振观感很差。我把死区设为目标亮度的±4%。在这个范围内不再调整曝光时间。滞回是另一个容易被忽略的点。如果没有滞回当画面亮度刚好在死区边界上下抖动时算法会频繁切入切出调整状态表现为每几帧就微调一次。我的做法是进入死区时用较窄的门限离开死区时用较宽的门限。比如误差率小于1.02进入死区误差率超过1.05才离开死区。这样既能防止频繁动作又不至于让曝光长时间偏离目标。收敛判定单独做了一个状态寄存器只有当连续3帧的误差率都位于死区以内才把status.register中的收敛标志位置1。这个标志可以给后级ISP或应用层使用方便他们判断画面是否处于稳定曝光状态。否则即使某一帧亮度恰好落在死区下一帧可能又跳出去不能算真正收敛。4. 从统计到控制FPGA模块化实现与资源优化细节4.1 模块划分与数据流整个自动曝光链路在FPGA里可以分成四个模块统计模块WeightedStat、评估与控制模块AE_Control、传感器配置接口模块Sensor_Reg以及寄存器读写模块Ctrl_Reg。模块间的逻辑要非常清晰不然调试的时候你会被各种内部信号绕晕。模块输入输出职责WeightedStatde、像素灰度、行场同步加权均值Y_now、区域平均亮度缓存完成分区域灰度累加、归一化和加权AE_ControlY_now、配置寄存器值曝光时间目标值E_target、收敛状态执行前馈预估、动态步长、死区滞回Sensor_RegE_target、触发信号传感器I2C写时序将目标曝光时间写入传感器寄存器Ctrl_Reg上位机写请求权重表、目标亮度、死区、限幅器参数提供可配置接口便于现场调参AE_Control模块是整个设计的核心。我的内部状态机从IDLE开始等待WeightedStat输出新统计结果拿到Y_now后先做限幅再计算误差率然后根据误差率决定使用前馈公式还是动态步长公式最后把E_target传给Sensor_Reg。整个计算流程大约需要15个时钟周期完全在帧消隐期间完成不影响下一帧像素流。4.2 时序设计如何不打断主通路自动曝光模块最忌讳的一点就是统计逻辑阻塞了后面ISP主通路。为了不打断主通路我采用的方案是“帧内统计帧间计算”。统计模块在帧有效期间纯粹累加不产生任何反压信号不会让主通路的像素数据等待。帧结束时统计结果锁存到输出缓冲之后控制模块和配置接口再去“消费”这个结果。这样主通路帧率不受任何影响。Sensor_Reg模块的I2C写操作往往比一帧时间还长所以必须异步处理。我这里做了两级缓冲AE_Control算完E_target后写入一个pend_regSensor_Reg根据它的空闲状态把pend_reg里的值通过I2C写进传感器。写入过程中如果来了新的E_target就把它更新到pend_reg由Sensor_Reg保证上一次写完成后再写新值。这样即使写入时间跨越了多帧也不会阻塞统计和控制逻辑。需要特别注意的是“当前帧曝光时间”的锁存。统计模块统计到的是“当前已经曝光的那一帧”的亮度而这个亮度对应的是上一帧配置给传感器的曝光时间。AE_Control里用到E_now时不能直接取传感器当前寄存器值而要从一个shadow寄存器里读取“上一帧实际写入的曝光时间”。否则自动曝光会滞后一帧导致动态场景下更容易振荡。4.3 定点数处理与亚稳态FPGA上做除法我用的不是浮点库而是定点数。所有内部变量统一Q12格式也就是12位小数位。Y_target和Y_now都左移12位后再做除法得到的结果同样保持Q12。最后写传感器前再右移回整数。这样的精度足够也不会太浪费DSP资源。使用定点数有个容易踩的坑饱和处理。如果Y_now在极端暗场下小于某个小值比如只有1或2那Y_target / Y_now会变成一个很大的数直接让曝光时间冲到上限。我内部对Y_now做了下限保护小于4时直接按4处理同时限制E_target最大只能是E_now的8倍。这个限制在避免画面猛闪的同时也防止了乘法溢出。跨时钟域信号也需要处理。传感器的配置接口时钟和像素时钟往往不是一个域。我的做法是WeightedStat输出的统计结果先通过一个异步FIFOFIFO深度只要2避免多bit信号跨时钟域采样错误。像素时钟域内的AE_Control只关心FIFO的空满状态异步FIFO的读侧时钟就是控制模块的时钟。这样多bit的Y_now就稳稳当当地进入了控制模块。另外复位信号不能忽略。FPGA里复位信号如果处理不好容易出现亚稳态尤其是外部按键复位或者上电复位经过较长走线之后。我在所有模块里都用了同步复位复位信号先打两拍再作为内部复位使用。这个习惯帮我避免过很多奇怪的时序问题建议你也用起来。4.4 资源占用与调优以1080p30fps、像素时钟148.5MHz为例整个自动曝光模块的资源占用非常有限。因为大部分运算只是加法、乘法和有限的除法器权重表用BRAM或寄存器实现。资源项占用估算说明LUT1180主要是分区域判断、控制状态机、定点除法FF640双缓冲、shadow寄存器、同步复位BRAM 18K3权重表、区域平均亮度缓存DSP48E2乘累加和定点除法辅助整个设计跑时序基本没有压力最大频率在200MHz以上。如果你用的是更小的FPGA比如只有几千个LUT那就把区域数从5×3降成3×3权重表直接用2的幂次省掉除法器资源还能再压一半。5. 上板实测与调参避坑记录5.1 三种策略收敛帧数对比我专门在相同光照跳变条件下做了对比测试从暗室切换到灯光明亮的桌面再切回暗室统计画面亮度从跳变到进入死区所经过的帧数。方案从暗到亮收敛帧数从亮到暗收敛帧数是否出现振荡全图平均 固定步长10%30帧34帧是最后4级灰度跳动加权灰度均值 固定步长10%21帧25帧是幅度变小加权灰度均值 动态步长 前馈预估3帧4帧否稳定后基本不动这个结果很直观评估模型的改进大约能节省30%的收敛时间而控制策略的改进直接提升了接近一个数量级。前馈预估把原本需要二三十帧的收敛压缩到了三五帧动态步长和死区则保证了最终不振荡。5.2 我踩过的四个坑第一个坑是权重归一化出错。第一次写加权均值时我把加权累加和算出来了但忘记对权重和做除法直接把加权亮度和当成最终均值。结果画面整体明显偏亮自动曝光一直把曝光时间往下压最终过暗。查了一会儿才意识到是归一化问题。第二个坑是截断误差在暗区被放大。使用定点数计算时Y_now在暗光下数值很小Q12定点除法后小数部分右移截断导致误差率判断错误。我后来在暗光下把Y_now做了下限保护并把死区宽度从固定值改成相对于当前亮度的百分比问题才消失。第三个坑是死区设置太宽。我一开始死区设为±8%结果画面在不同色温的物体切换时曝光时间几乎不动但人眼明显感觉画面亮度有差异。把死区收窄到±4%再配合滞回边界才平衡了“稳定”和“准确”的关系。第四个坑是统计锁存时序错位。我最初没有用双缓冲帧结束信号直接触发统计输出但下一帧像素数据已经开始进入统计模块导致读到的Y_now混入了新一帧的数据。改用双缓冲后这个随机跳变的问题彻底消失。5.3 根据个人经验的调参心得调试自动曝光首先要确定目标亮度。我习惯先截几帧参考画面在电脑上用图像工具算出主体区域的灰度均值再把它当成目标值写进去。不要凭感觉设个128不同传感器的gamma曲线和ISP处理差异都会影响最终效果。权重表的调整要跟着应用场景走。固定机位的工业检测画面主体大概率在中心权重表中心集中一点就很稳如果是机器人车载相机画面下半部分往往是路面和障碍物可以适当提高下半部分权重。权重表我建议做成上位机可配置的寄存器方便现场调不要每次改代码重新综合。这套模块上线之后画面稳定速度已经达到我的预期。后续如果再往上走可以把灰度直方图统计加进来根据直方图分布动态调整权重识别出画面中真正的过曝区域并临时压低它的权重进一步抑制高光干扰。另外还可以把自动增益和自动曝光放在同一个控制循环里曝光时间调不动的时候再动用增益这是另一个值得展开的话题以后有机会再单独写一篇。
返回列表