070、人脸AE实战:基于人脸检测的亮度优先与背光补偿算法
070、人脸AE实战基于人脸检测的亮度优先与背光补偿算法一、一个让我加了两周班的bug去年做某款旗舰机的前摄调试遇到一个诡异现象用户在逆光环境下自拍人脸区域始终过曝背景倒是正常了。我盯着示波器看了三天发现AE统计权重分配出了问题——传统AE把画面分成若干区块人脸区域只占了其中一小块权重不够算法直接“牺牲”了人脸去保背景。这个坑让我意识到人脸AE不是简单的“检测到人脸就提亮”而是要在亮度优先和背光补偿之间做动态博弈。今天把这套实战经验拆开揉碎讲清楚代码都是踩过坑的版本。二、人脸AE的核心矛盾传统AE的亮度统计基于整帧或固定权重网格遇到逆光场景就抓瞎。人脸AE要解决两个问题亮度优先确保人脸区域亮度落在目标区间通常Y值80-1208bit背光补偿当人脸区域亮度低于背景时主动提亮人脸同时避免背景过曝这两个目标天然冲突——提亮人脸必然导致背景更亮。关键在于找到那个“动态平衡点”。三、实战算法框架3.1 人脸区域亮度提取// 这里踩过坑直接用检测框的ROI提取亮度框太大包含背景框太小丢失边缘// 正确做法是取检测框内缩20%的核心区域staticintget_face_luminance(cam_frame_t*frame,face_rect_t*face){intcrop_xface-xface-width*0.2;// 内缩20%别写死根据人脸大小动态调整intcrop_yface-yface-height*0.2;intcrop_wface-width*0.6;intcrop_hface-height*0.6;// 别这样写直接遍历所有像素太慢// 用隔点采样步长4精度够用uint32_ty_sum0;intcount0;for(intycrop_y;ycrop_ycrop_h;y4){for(intxcrop_x;xcrop_xcrop_w;x4){y_sumframe-y_buffer[y*frame-stridex];count;}}returny_sum/count;}3.2 背光场景判定背光判定的核心不是看人脸暗不暗而是看人脸与背景的亮度差。我见过有人直接用人脸亮度阈值就判定背光结果在正常光照下也触发补偿画面变得惨白。// 背光判定逻辑踩过坑的版本typedefstruct{intface_luma;// 人脸平均亮度intbg_luma;// 背景平均亮度排除人脸区域intluma_diff;// 亮度差intis_backlight;// 是否背光floatconfidence;// 置信度用于平滑过渡}backlight_status_t;staticbacklight_status_tdetect_backlight(cam_frame_t*frame,face_rect_t*face){backlight_status_tstatus{0};// 提取人脸亮度status.face_lumaget_face_luminance(frame,face);// 提取背景亮度取画面四周区域避开人脸框// 别这样写直接取整帧平均会把脸也算进去intbg_luma0;intbg_count0;// 取四个角落区域每个区域占画面1/16for(intregion0;region4;region){intstart_x,start_y,end_x,end_y;get_corner_region(frame,region,start_x,start_y,end_x,end_y);// 跳过与人脸框重叠的区域if(is_overlap(start_x,start_y,end_x,end_y,face))continue;for(intystart_y;yend_y;y4){for(intxstart_x;xend_x;x4){bg_lumaframe-y_buffer[y*frame-stridex];bg_count;}}}status.bg_lumabg_luma/bg_count;status.luma_diffstatus.bg_luma-status.face_luma;// 背光判定亮度差30且人脸亮度80// 阈值需要根据sensor特性调整这里给的是典型值if(status.luma_diff30status.face_luma80){status.is_backlight1;// 置信度根据亮度差线性映射避免突变status.confidenceclamp((status.luma_diff-30)/50.0f,0.0f,1.0f);}returnstatus;}3.3 亮度优先与背光补偿的融合这是整个算法的核心。不能简单地在背光时提亮人脸而是要根据场景动态调整AE的目标亮度和权重分布。// 人脸AE控制逻辑踩坑无数版typedefstruct{inttarget_luma;// AE目标亮度floatface_weight;// 人脸区域权重floatcompensation_gain;// 补偿增益intsmooth_target;// 平滑后的目标亮度}face_ae_params_t;staticface_ae_params_tcalculate_face_ae_params(cam_frame_t*frame,face_rect_t*face,ae_stats_t*stats){face_ae_params_tparams{0};backlight_status_tbl_statusdetect_backlight(frame,face);// 基础目标亮度正常场景用80背光场景动态调整// 别这样写背光时直接设目标亮度为120会导致背景过曝if(!bl_status.is_backlight){params.target_luma80;// 正常场景params.face_weight0.3;// 人脸权重30%params.compensation_gain1.0;}else{// 背光补偿目标亮度在80-110之间动态调整// 补偿量 亮度差 * 置信度 * 补偿系数floatcompensationbl_status.luma_diff*bl_status.confidence*0.3;params.target_lumaclamp(80(int)compensation,80,110);// 人脸权重根据置信度提升最高到70%params.face_weight0.3bl_status.confidence*0.4;// 补偿增益限制最大1.5倍避免噪声放大params.compensation_gain1.0bl_status.confidence*0.5;}// 平滑处理防止亮度突变导致闪烁// 这里踩过坑直接用新目标值替换画面会跳变staticintprev_target80;params.smooth_targetprev_target(params.target_luma-prev_target)*0.3;prev_targetparams.smooth_target;returnparams;}3.4 权重映射到AE统计有了人脸参数需要把它映射到AE统计的权重网格上。这里有个关键点权重网格的分辨率要和硬件AE统计模块匹配。// 将人脸权重映射到AE统计网格// 假设AE统计是15x15的网格staticvoidapply_face_weight_to_ae_grid(ae_grid_t*grid,face_rect_t*face,floatface_weight){// 计算人脸框在网格中的位置intgrid_x_startface-x*15/frame_width;intgrid_x_end(face-xface-width)*15/frame_width;intgrid_y_startface-y*15/frame_height;intgrid_y_end(face-yface-height)*15/frame_height;// 别这样写直接把人脸区域权重设为1其他区域设为0// 会导致背景完全不受控for(inty0;y15;y){for(intx0;x15;x){if(xgrid_x_startxgrid_x_endygrid_y_startygrid_y_end){// 人脸区域权重提升grid-weights[y][x]face_weight;}else{// 背景区域权重降低但不能为0grid-weights[y][x](1.0f-face_weight)/(1.0f-face_weight_area);}}}// 归一化权重确保总和为1normalize_weights(grid);}四、实战中的坑与填坑4.1 人脸检测延迟问题人脸检测通常需要几十毫秒而AE每帧都要更新。如果直接等人脸检测结果AE会滞后。填坑方案用上一帧的人脸位置做当前帧的权重映射同时用运动预测修正位置。如果人脸移动太快降级为全局AE。// 预测人脸位置减少延迟影响staticface_rect_tpredict_face_position(face_rect_t*current,face_rect_t*prev){face_rect_tpredicted;// 简单线性预测predicted.xcurrent-x(current-x-prev-x);predicted.ycurrent-y(current-y-prev-y);predicted.widthcurrent-width;predicted.heightcurrent-height;returnpredicted;}4.2 多张人脸的处理遇到多人场景不能只照顾最大人脸。我的做法是取所有人脸亮度的加权平均权重按人脸大小分配。// 多张人脸亮度融合staticintget_multi_face_luma(cam_frame_t*frame,face_rect_t*faces,intface_count){inttotal_luma0;inttotal_weight0;for(inti0;iface_count;i){intface_lumaget_face_luminance(frame,faces[i]);intface_areafaces[i].width*faces[i].height;total_lumaface_luma*face_area;total_weightface_area;}returntotal_luma/total_weight;}4.3 场景切换的平滑过渡从背光场景切换到正常场景或者反过来亮度不能突变。我用了两级平滑目标亮度平滑和实际曝光平滑。// 两级平滑防止闪烁staticintsmooth_exposure(inttarget_luma,intcurrent_luma){// 第一级目标亮度平滑帧间staticintsmooth_target80;smooth_target(target_luma-smooth_target)*0.2;// 第二级实际曝光平滑帧内intstep(smooth_target-current_luma)*0.1;returncurrent_lumastep;}五、调试经验总结干了十几年调试人脸AE这块有几个血泪教训别迷信人脸检测精度检测框稍微偏一点没关系权重映射是软性的不是硬切割。我见过有人非要精确到像素级结果调试周期翻倍效果还没提升。背光补偿要留余量补偿到人脸亮度90-100就够了别追求120。因为补偿后背景会亮用户看着背景过曝更难受。人脸暗一点可以后期调背景过曝就救不回来了。权重分配要动态人脸权重不是越高越好。极端场景下比如人脸占画面80%权重太高会导致背景完全失控。我一般限制人脸权重不超过70%。平滑参数要场景自适应室内场景平滑系数可以大一些0.3-0.5室外场景要小一些0.1-0.2因为室外光照变化快。留一个手动模式自动算法再牛也有翻车的时候。给用户留一个“人脸优先”开关关键时刻能救命。最后说一句人脸AE没有银弹。不同sensor的动态范围不同不同镜头的flare特性不同甚至不同肤色的人脸反射率都不同。这套算法框架给你了具体参数还得在你的硬件上慢慢磨。调试的时候多看看直方图少看主观效果数据不会骗人。