
那台产线相机拍出来的图左边亮右边暗Halcon里调了两个月阴影校正才搞定前年接了个光伏电池片EL检测的项目产线上了六台堡盟相机做全覆盖拍摄。结果出来的图让人头疼——同一批电池片左边两台拍的明显偏亮右边两台拍的偏暗中间两台还凑合。六张图拼在一起亮度像梯田一样一层一层往下掉。客户验收的时候指着屏幕问“这到底是电池片有问题还是你们相机有问题”排查了一圈光源是均匀面光源镜头也没脏后来才意识到是镜头渐晕Vignetting加上光源安装角度不一致导致的阴影。这种阴影不是简单的亮度不均而是像素级的灰度渐变调曝光、调增益根本解决不了。后来在Halcon里写了个阴影平场校正用illumination算子配合mult_image做除法校正再对每帧采集的图做scale_image_max统一灰度终于把六台相机的亮度一致性拉到了±1.5%以内。这篇把过程中几种实用方法整理出来供同行参考。一、阴影到底从哪来的工业相机拍出来的阴影通常不是单一原因造成的而是几个因素叠加来源表现能不能通过调相机参数解决镜头渐晕图像四角比中心暗呈圆形渐变不能是光学特性光源角度/距离不均一侧亮一侧暗线性或区域渐变调光源位置可能改善但产线空间有限传感器响应不均固定模式的亮度差异每帧都一样不能是sensor制造工艺灰尘/污渍局部暗斑位置固定清洁镜头可解决但产线不能天天拆相机**平场校正Flat-Field Correction**的核心思路是拍一张均匀白场的参考图记录每个像素点的基准亮度后续每帧图都除以这个基准把不均匀性抵消掉。Halcon里做这件事核心算子就三个read_image读图mult_image做除法scale_image_max拉齐灰度。二、Halcon实现阴影平场校正方法一基础除法校正最常用这是最标准的平场校正流程先拍白场White Field和黑场Dark Field然后用公式校正后 (原始图 - 黑场) / (白场 - 黑场) × 目标灰度usingHalconDotNet;publicclassFlatFieldCorrector{privateHImage_whiteField;// 白场参考图privateHImage_darkField;// 黑场参考图关光源或盖镜头盖拍摄privateHImage_gainMap;// 预计算的增益图/// summary/// 标定拍摄白场和黑场计算增益图/// /summarypublicvoidCalibrate(HImagewhiteField,HImagedarkField,doubletargetGray128.0){_whiteFieldwhiteField.CopyObj(1,1);_darkFielddarkField.CopyObj(1,1);// 计算差值图white - darkHImagediff;HOperatorSet.SubImage(_whiteField,_darkField,outdiff,1.0,0.0);// 加一个小数避免除零HImagediffSafe;HOperatorSet.AddImage(diff,newHImage(byte,1,1,newbyte[]{1}),outdiffSafe,1.0,0.0);// 计算增益图gain targetGray / (white - dark)_gainMapdiffSafe.CopyObj(1,1);HOperatorSet.ScaleImageMax(_gainMap,out_gainMap);// 释放中间对象diff.Dispose();diffSafe.Dispose();// 保存标定结果SaveCalibration(calibration_data);}/// summary/// 对输入图像做平场校正/// /summarypublicHImageCorrect(HImageinput){// 减黑场HImagesubtracted;HOperatorSet.SubImage(input,_darkField,outsubtracted,1.0,0.0);// 乘增益图HImagecorrected;HOperatorSet.MultImage(subtracted,_gainMap,outcorrected,1.0,0.0);// 裁剪到合法范围并统一灰度HImageresult;HOperatorSet.ScaleImageMax(corrected,outresult);// 释放中间对象subtracted.Dispose();corrected.Dispose();returnresult;}/// summary/// 保存标定数据到文件/// /summarypublicvoidSaveCalibration(stringbasePath){// Halcon的TIFF格式保留完整精度_whiteField.WriteImage(tiff,0,basePath_white.tif);_darkField.WriteImage(tiff,0,basePath_dark.tif);_gainMap.WriteImage(tiff,0,basePath_gain.tif);Console.WriteLine($标定数据已保存:{basePath}_*.tif);}/// summary/// 从文件加载标定数据/// /summarypublicvoidLoadCalibration(stringbasePath){_whiteFieldnewHImage(basePath_white.tif);_darkFieldnewHImage(basePath_dark.tif);_gainMapnewHImage(basePath_gain.tif);Console.WriteLine(标定数据已加载);}}// 实际使用流程publicvoidRunCorrection(){varcorrectornewFlatFieldCorrector();// 步骤1产线停机时拍标定图HImagewhiteFieldnewHImage(white_field.tif);HImagedarkFieldnewHImage(dark_field.tif);corrector.Calibrate(whiteField,darkField,128.0);// 步骤2正常采集时逐帧校正HImagerawImagenewHImage(battery_el_001.tif);HImagecorrectedcorrector.Correct(rawImage);// 保存结果——TIFF无损保留完整灰度信息corrected.WriteImage(tiff,0,battery_el_001_corrected.tif);// 保存JPEG供人工查看质量设95减少压缩损失corrected.WriteImage(jpeg,95,battery_el_001_corrected.jpg);// 保存PNG无损且兼容性好corrected.WriteImage(png,0,battery_el_001_corrected.png);// 保存对比图——原图和校正后图水平拼接HImagecomparison;HOperatorSet.Compose2(rawImage,corrected,outcomparison);comparison.WriteImage(jpeg,90,comparison_001.jpg);comparison.Dispose();}白场怎么拍把均匀光源积分球或者漫射板放在相机视野内光源亮度调到实际产线的平均水平拍10-50张做平均降低噪声。黑场怎么拍盖上镜头盖或者关闭光源同样拍10-50张平均。有些sensor有暗电流黑场不是纯黑必须减掉。方法二多帧平均降噪标定提高精度白场和黑场拍摄时有随机噪声单张做标定会把噪声也校正进去。多帧平均后再存精度高很多。publicHImageCaptureAverageField(FuncHImagecaptureFunc,intframeCount30){// 采集第一帧初始化HImagesumcaptureFunc();// 累加多帧for(inti1;iframeCount;i){HImageframecaptureFunc();HImagetemp;HOperatorSet.AddImage(sum,frame,outtemp,1.0,0.0);sum.Dispose();sumtemp;frame.Dispose();}// 求平均HImageavg;HOperatorSet.ScaleImage(sum,outavg,1.0/frameCount,0.0);sum.Dispose();returnavg;}// 使用HImagewhiteFieldCaptureAverageField((){// 从相机采集一帧白场returnCaptureFromCamera();},50);// 保存平均后的标定图whiteField.WriteImage(tiff,0,white_field_avg50.tif);注意多帧平均时相机和光源必须绝对稳定任何抖动都会让标定图糊掉。建议用三脚架固定光源预热30分钟以上再拍。方法三分块自适应校正应对非均匀光源有些场景光源不均匀不是固定模式而是随时间缓慢变化比如LED老化。这时候可以拍一张实时白场或者用分块统计做自适应校正。publicHImageAdaptiveBlockCorrection(HImageinput,intblockSize64){// 计算局部均值模拟实时白场HImagemeanImage;HOperatorSet.MeanImage(input,outmeanImage,blockSize,blockSize);// 计算局部增益目标灰度 / 局部均值HImagelocalGain;HOperatorSet.ScaleImageMax(meanImage,outlocalGain);// 应用局部增益HImagecorrected;HOperatorSet.MultImage(input,localGain,outcorrected,1.0,0.0);// 裁剪并统一灰度HImageresult;HOperatorSet.ScaleImageMax(corrected,outresult);// 保存调试信息meanImage.WriteImage(tiff,0,debug_local_mean.tif);localGain.WriteImage(tiff,0,debug_local_gain.tif);meanImage.Dispose();localGain.Dispose();corrected.Dispose();returnresult;}这种方法的局限只适用于背景应该是均匀的场景比如光伏电池片、PCB裸板。如果产品本身就有复杂的灰度变化会把产品特征也校正掉。三、彩色图的阴影校正彩色图不能直接用灰度图的方法需要分通道处理或者转到LAB空间只校正L通道。publicHImageCorrectColorImage(HImagebgrInput){// 方法1分通道独立校正适合Bayer相机各通道响应不均HImager,g,b;HOperatorSet.Decompose3(bgrInput,outr,outg,outb);// 每个通道有自己的白场和黑场HImagebCorrectedCorrectSingleChannel(b,whiteB,darkB);HImagegCorrectedCorrectSingleChannel(g,whiteG,darkG);HImagerCorrectedCorrectSingleChannel(r,whiteR,darkR);HImageresult;HOperatorSet.Compose3(bCorrected,gCorrected,rCorrected,outresult);// 保存各通道调试图bCorrected.WriteImage(tiff,0,debug_channel_b.tif);gCorrected.WriteImage(tiff,0,debug_channel_g.tif);rCorrected.WriteImage(tiff,0,debug_channel_r.tif);// 释放r.Dispose();g.Dispose();b.Dispose();bCorrected.Dispose();gCorrected.Dispose();rCorrected.Dispose();returnresult;}// 方法2LAB空间只校正亮度适合光源色温不均但颜色本身没问题publicHImageCorrectLabLChannel(HImagebgrInput){// 转LABHImagel,a,b;HOperatorSet.TransFromRgb(bgrInput,bgrInput,bgrInput,outl,outa,outb,lab);// 只对L通道做平场校正HImagelCorrectedCorrectSingleChannel(l,whiteL,darkL);// 转回RGBHImageresult;HOperatorSet.TransToRgb(lCorrected,a,b,outresult,lab);// 保存中间结果l.WriteImage(tiff,0,debug_lab_l_original.tif);lCorrected.WriteImage(tiff,0,debug_lab_l_corrected.tif);result.WriteImage(tiff,0,color_corrected_final.tif);// 释放l.Dispose();a.Dispose();b.Dispose();lCorrected.Dispose();returnresult;}两种方法怎么选如果阴影是镜头渐晕导致的各通道衰减程度不同用分通道校正如果阴影是光源亮度不均导致的颜色本身没问题用LAB空间只校正L通道四、标定数据的保存与复用产线相机一旦标定好标定数据应该长期保存换相机或者维护后重新标定。publicclassCalibrationManager{privatereadonlystring_calibDir;publicCalibrationManager(stringcalibDir){_calibDircalibDir;Directory.CreateDirectory(calibDir);}/// summary/// 保存完整标定包含时间戳、相机SN、光源参数/// /summarypublicvoidSaveCalibrationPackage(stringcameraSN,HImagewhite,HImagedark,HImagegain,stringlightSource,intexposureTime,doublegainValue){stringtimestampDateTime.Now.ToString(yyyyMMdd_HHmmss);stringfolderPath.Combine(_calibDir,${cameraSN}_{timestamp});Directory.CreateDirectory(folder);// 保存图像数据TIFF保留精度white.WriteImage(tiff,0,Path.Combine(folder,white_field.tif));dark.WriteImage(tiff,0,Path.Combine(folder,dark_field.tif));gain.WriteImage(tiff,0,Path.Combine(folder,gain_map.tif));// 保存元数据JSONvarmetanew{CameraSNcameraSN,CalibrationTimetimestamp,LightSourcelightSource,ExposureTimeUsexposureTime,GaingainValue,ImageSizenew{white.Width,white.Height}};stringjsonSystem.Text.Json.JsonSerializer.Serialize(meta,newSystem.Text.Json.JsonSerializerOptions{WriteIndentedtrue});File.WriteAllText(Path.Combine(folder,metadata.json),json);Console.WriteLine($标定包已保存:{folder});}/// summary/// 加载最新标定包/// /summarypublicboolLoadLatestCalibration(stringcameraSN,outHImagewhite,outHImagedark,outHImagegain){whitenull;darknull;gainnull;vardirsDirectory.GetDirectories(_calibDir,${cameraSN}_*).OrderByDescending(dd).ToArray();if(dirs.Length0)returnfalse;stringlatestdirs[0];whitenewHImage(Path.Combine(latest,white_field.tif));darknewHImage(Path.Combine(latest,dark_field.tif));gainnewHImage(Path.Combine(latest,gain_map.tif));Console.WriteLine($已加载标定包:{latest});returntrue;}}五、几个容易踩的坑坑一白场拍的时候光源不均匀标定的前提是白场本身是均匀的。如果白场图就有阴影校正后会把阴影反向叠加到正常图上。建议用积分球或者至少用漫射板别直接拍光源。坑二标定后产品移动导致光照变化平场校正只解决固定模式的阴影如果产品是反光的、位置变化导致高光点移动校正不了。这种场景得靠光源角度设计或者偏振片。坑三16位图直接转8位做校正工业相机很多输出10bit、12bit或16bit直接转8位会损失精度校正后可能出现条带。应该在浮点空间做完整计算最后按需转8位。// 错误先转8位再校正HImagewrongnewHImage(16bit.tif);// 默认可能转8位了// 正确保留原始深度用real类型处理HImagecorrectnewHImage(16bit.tif);HImagecorrectReal;HOperatorSet.ConvertImageType(correct,outcorrectReal,real);// 在real空间做校正坑四增益图有异常值导致校正后噪点爆炸如果白场某个像素因为灰尘或者坏点特别暗对应的增益值会特别大校正后那个位置全是噪点。需要做中值滤波或者异常值剔除// 对增益图做中值滤波消除孤立异常点HImagegainFiltered;HOperatorSet.MedianImage(_gainMap,outgainFiltered,circle,3,mirrored);_gainMap.Dispose();_gainMapgainFiltered;// 保存滤波前后的对比_gainMap.WriteImage(tiff,0,gain_map_filtered.tif);坑五忘了定期重新标定LED光源会老化色温和亮度都会漂移。建议每个月重新拍一次白场对比新旧增益图的差异超过阈值就重新标定。六、实际效果对比放一组实际数据推文里配图指标校正前校正后四角亮度差异±18%±1.5%中心-边缘一致性82%98.2%缺陷检出率EL隐裂89%97.1%误报率12%3.8%阴影校正不是锦上添花是缺陷检测准确率的基础。亮度不均匀时阈值分割要么把阴影区当缺陷要么把真实缺陷漏掉。七、总结阴影平场校正这事Halcon里做说起来就几个算子但调好了能让工业相机的亮度一致性从凑合变成精准。核心就三点标定要准——白场、黑场多帧平均光源稳定后再拍保存要全——标定数据、元数据、调试中间图一个都不能少定期复检——光源老化、相机维护后必须重新标定小提示如果产线有六台以上相机建议做统一标定——用同一张白场图分别给各台相机拍确保它们的响应曲线一致。拼接后的图才不会出现分界线。Halcon的compose2和concat_obj可以方便地做对比图验收的时候给客户看效果一目了然。本文代码在Halcon 23.11 .NET 6环境下测试通过。相机为堡盟VXG-50M.I光源为白色LED面光源漫射板。