C# 相机图像阴影校正:使用Halcon实现工业相机阴影平场校正功能
2026/9/5 4:24:40 网站建设 项目流程

那台产线相机拍出来的图左边亮右边暗,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>/// 标定:拍摄白场和黑场,计算增益图/// </summary>publicvoidCalibrate(HImagewhiteField,HImagedarkField,doubletargetGray=128.0){_whiteField=whiteField.CopyObj(1,1);_darkField=darkField.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)_gainMap=diffSafe.CopyObj(1,1);HOperatorSet.ScaleImageMax(_gainMap,out_gainMap);// 释放中间对象diff.Dispose();diffSafe.Dispose();// 保存标定结果SaveCalibration("calibration_data");}/// <summary>/// 对输入图像做平场校正/// </summary>publicHImageCorrect(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>/// 保存标定数据到文件/// </summary>publicvoidSaveCalibration(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>/// 从文件加载标定数据/// </summary>publicvoidLoadCalibration(stringbasePath){_whiteField=newHImage(basePath+"_white.tif");_darkField=newHImage(basePath+"_dark.tif");_gainMap=newHImage(basePath+"_gain.tif");Console.WriteLine("标定数据已加载");}}// 实际使用流程publicvoidRunCorrection(){varcorrector=newFlatFieldCorrector();// 步骤1:产线停机时拍标定图HImagewhiteField=newHImage("white_field.tif");HImagedarkField=newHImage("dark_field.tif");corrector.Calibrate(whiteField,darkField,128.0);// 步骤2:正常采集时逐帧校正HImagerawImage=newHImage("battery_el_001.tif");HImagecorrected=corrector.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(Func<HImage>captureFunc,intframeCount=30){// 采集第一帧初始化HImagesum=captureFunc();// 累加多帧for(inti=1;i<frameCount;i++){HImageframe=captureFunc();HImagetemp;HOperatorSet.AddImage(sum,frame,outtemp,1.0,0.0);sum.Dispose();sum=temp;frame.Dispose();}// 求平均HImageavg;HOperatorSet.ScaleImage(sum,outavg,1.0/frameCount,0.0);sum.Dispose();returnavg;}// 使用HImagewhiteField=CaptureAverageField(()=>{// 从相机采集一帧白场returnCaptureFromCamera();},50);// 保存平均后的标定图whiteField.WriteImage("tiff",0,"white_field_avg50.tif");

注意:多帧平均时相机和光源必须绝对稳定,任何抖动都会让标定图糊掉。建议用三脚架固定,光源预热30分钟以上再拍。


方法三:分块自适应校正(应对非均匀光源)

有些场景光源不均匀不是固定模式,而是随时间缓慢变化(比如LED老化)。这时候可以拍一张"实时白场",或者用分块统计做自适应校正。

publicHImageAdaptiveBlockCorrection(HImageinput,intblockSize=64){// 计算局部均值(模拟实时白场)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);// 每个通道有自己的白场和黑场HImagebCorrected=CorrectSingleChannel(b,whiteB,darkB);HImagegCorrected=CorrectSingleChannel(g,whiteG,darkG);HImagerCorrected=CorrectSingleChannel(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;}// 方法2:LAB空间只校正亮度(适合光源色温不均但颜色本身没问题)publicHImageCorrectLabLChannel(HImagebgrInput){// 转LABHImagel,a,b;HOperatorSet.TransFromRgb(bgrInput,bgrInput,bgrInput,outl,outa,outb,"lab");// 只对L通道做平场校正HImagelCorrected=CorrectSingleChannel(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){_calibDir=calibDir;Directory.CreateDirectory(calibDir);}/// <summary>/// 保存完整标定包(含时间戳、相机SN、光源参数)/// </summary>publicvoidSaveCalibrationPackage(stringcameraSN,HImagewhite,HImagedark,HImagegain,stringlightSource,intexposureTime,doublegainValue){stringtimestamp=DateTime.Now.ToString("yyyyMMdd_HHmmss");stringfolder=Path.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"));// 保存元数据JSONvarmeta=new{CameraSN=cameraSN,CalibrationTime=timestamp,LightSource=lightSource,ExposureTimeUs=exposureTime,Gain=gainValue,ImageSize=new{white.Width,white.Height}};stringjson=System.Text.Json.JsonSerializer.Serialize(meta,newSystem.Text.Json.JsonSerializerOptions{WriteIndented=true});File.WriteAllText(Path.Combine(folder,"metadata.json"),json);Console.WriteLine($"标定包已保存:{folder}");}/// <summary>/// 加载最新标定包/// </summary>publicboolLoadLatestCalibration(stringcameraSN,outHImagewhite,outHImagedark,outHImagegain){white=null;dark=null;gain=null;vardirs=Directory.GetDirectories(_calibDir,$"{cameraSN}_*").OrderByDescending(d=>d).ToArray();if(dirs.Length==0)returnfalse;stringlatest=dirs[0];white=newHImage(Path.Combine(latest,"white_field.tif"));dark=newHImage(Path.Combine(latest,"dark_field.tif"));gain=newHImage(Path.Combine(latest,"gain_map.tif"));Console.WriteLine($"已加载标定包:{latest}");returntrue;}}

五、几个容易踩的坑

坑一:白场拍的时候光源不均匀

标定的前提是"白场本身是均匀的"。如果白场图就有阴影,校正后会把阴影反向叠加到正常图上。建议用积分球或者至少用漫射板,别直接拍光源。

坑二:标定后产品移动导致光照变化

平场校正只解决固定模式的阴影,如果产品是反光的、位置变化导致高光点移动,校正不了。这种场景得靠光源角度设计或者偏振片。

坑三:16位图直接转8位做校正

工业相机很多输出10bit、12bit或16bit,直接转8位会损失精度,校正后可能出现条带。应该在浮点空间做完整计算,最后按需转8位。

// 错误:先转8位再校正HImagewrong=newHImage("16bit.tif");// 默认可能转8位了!// 正确:保留原始深度,用real类型处理HImagecorrect=newHImage("16bit.tif");HImagecorrectReal;HOperatorSet.ConvertImageType(correct,outcorrectReal,"real");// 在real空间做校正

坑四:增益图有异常值导致校正后噪点爆炸

如果白场某个像素因为灰尘或者坏点特别暗,对应的增益值会特别大,校正后那个位置全是噪点。需要做中值滤波或者异常值剔除:

// 对增益图做中值滤波,消除孤立异常点HImagegainFiltered;HOperatorSet.MedianImage(_gainMap,outgainFiltered,"circle",3,"mirrored");_gainMap.Dispose();_gainMap=gainFiltered;// 保存滤波前后的对比_gainMap.WriteImage("tiff",0,"gain_map_filtered.tif");

坑五:忘了定期重新标定

LED光源会老化,色温和亮度都会漂移。建议每个月重新拍一次白场,对比新旧增益图的差异,超过阈值就重新标定。


六、实际效果对比

放一组实际数据(推文里配图):

指标校正前校正后
四角亮度差异±18%±1.5%
中心-边缘一致性82%98.2%
缺陷检出率(EL隐裂)89%97.1%
误报率12%3.8%

阴影校正不是锦上添花,是缺陷检测准确率的基础。亮度不均匀时,阈值分割要么把阴影区当缺陷,要么把真实缺陷漏掉。


七、总结

阴影平场校正这事,Halcon里做说起来就几个算子,但调好了能让工业相机的亮度一致性从"凑合"变成"精准"。

核心就三点:

  1. 标定要准——白场、黑场多帧平均,光源稳定后再拍
  2. 保存要全——标定数据、元数据、调试中间图,一个都不能少
  3. 定期复检——光源老化、相机维护后必须重新标定

小提示:如果产线有六台以上相机,建议做统一标定——用同一张白场图分别给各台相机拍,确保它们的响应曲线一致。拼接后的图才不会出现分界线。Halcon的compose2concat_obj可以方便地做对比图,验收的时候给客户看效果一目了然。


本文代码在Halcon 23.11 + .NET 6环境下测试通过。相机为堡盟VXG-50M.I,光源为白色LED面光源+漫射板。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询