搞机器视觉的基本都绕不开白平衡这道坎尤其是做彩色图像处理的项目里OpenCV和Halcon两套工具我都常用到。前阵子有个项目产线上要检测彩色塑料工件的表面缺陷光源换了批新的LED之后同一块区域拍出来的图像明显偏黄模板匹配和颜色分类全都乱了。当时我就意识到这类问题靠调整光源不是长久之计必须在图像预处理阶段做一个稳定可控的白平衡校正。这篇文章我就把用OpenCV和Halcon分别实现彩色图像白平衡的完整思路写出来。内容会覆盖白平衡的算法原理、两套工具的具体实现代码、关键参数怎么调、以及我在实际项目里踩过的坑。适合刚入门做图像处理的同学也适合在产线视觉项目里被颜色一致性折腾过的朋友。1. 先把白平衡的原理聊透算法选择才有依据1.1 色偏是怎么来的传感器没有人眼的“色彩恒常性”人眼看一张白纸不管是在暖黄灯光下还是在阴天室外我们都会觉得它是白色的。这是大脑做了色彩恒常性补偿。但CMOS或CCD传感器没这个本事它只会老老实实记录进入镜头的光线强度不同色温的光源打在物体上RGB三个通道的响应比例就不一样。比如白炽灯色温偏低红光成分多拍出来的图像就偏暖偏黄阴天的散射光色温高蓝光成分多图像就会偏冷偏蓝。很多相机和手机都有自动白平衡AWB功能但AWB是厂家调好的黑盒算法在不同场景下经常抽风而且在工业视觉里AWB偶尔还会导致同一工位不同时段拍出来的图像颜色漂移。所以项目对颜色敏感时稳定的做法是关掉自动白平衡固定色温值然后在软件层面做白平衡校正。1.2 灰度世界假设与完美反射假设白平衡算法有很多种OpenCV和Halcon里都没有一个叫“白平衡”的现成函数直接给你搞定一切多数要靠自己组合算子。最常用的两个基础算法是灰度世界假设和完美反射假设。灰度世界假设的核心思想是一张色彩足够丰富的彩色图像RGB三个通道的平均值应该趋近于同一个灰色值。也就是说如果图像偏黄蓝色通道均值肯定偏低那我们给蓝色通道乘一个大于1的增益把均值拉回去。数学表达式为R_gain avg_gray / avg_RG_gain avg_gray / avg_GB_gain avg_gray / avg_B其中avg_gray通常取三个通道均值的平均。这个算法的优点是简单、快速、鲁棒性不错缺点是当画面被大面积单一颜色占据时假设就不成立了。比如蓝天下拍白色建筑整个画面偏蓝灰度世界会把蓝色强行压暗结果天空变成灰色建筑也偏黄整体观感反而更怪。完美反射假设则换了个角度图像中最亮的像素应该接近白色。算法先找到图像里的高光区域或最亮的那一小撮像素统计这些像素的RGB均值然后以最大通道值为基准计算增益。这个算法对高光区域真实存在的场景效果很好但对金属反光、彩色灯光这类“最亮的点不是白色”的情况很敏感。实际项目中我很少把单一算法用到死更多是结合两者或者只在ROI区域内做统计这部分到后面实操环节再展开。2. OpenCV 端白平衡自己动手写算法2.1 先说环境OpenCV 4.x Python 最顺手OpenCV实现白平衡我一般用Python来验证算法因为改参数、看效果都非常快。环境安装不复杂直接用pip安装opencv-python就好。如果在Anaconda里安装更简单。pip install opencv-python numpy这里有个小细节opencv-python和opencv-contrib-python是两个包前者包含核心模块后者还包含很多扩展模块。白平衡用到的函数都在核心模块里装基础版就够。读取图像时OpenCV默认按BGR顺序加载这个很多人第一次处理彩色图像都会踩坑。import cv2 import numpy as np img_bgr cv2.imread(sample.jpg) if img_bgr is None: print(图像读取失败检查路径)读进来之后图像是uint8类型的数组范围是0到255。做通道运算前强烈建议先转成float32否则整数除法、通道均值计算都很容易出问题。2.2 灰度世界白平衡的 Python 实现灰度世界算法的实现流程很清晰先把图像转成浮点数然后分别算B、G、R三个通道的像素均值再用全局均值除以各通道均值得到增益系数最后每个通道乘上对应增益并截断到0到255范围内。def gray_world_white_balance(img_bgr): # 转成浮点型避免uint8运算截断 result img_bgr.astype(np.float32) avg_b np.mean(result[:, :, 0]) avg_g np.mean(result[:, :, 1]) avg_r np.mean(result[:, :, 2]) gray (avg_b avg_g avg_r) / 3.0 gain_b gray / avg_b gain_g gray / avg_g gain_r gray / avg_r result[:, :, 0] * gain_b result[:, :, 1] * gain_g result[:, :, 2] * gain_r result np.clip(result, 0, 255) return result.astype(np.uint8)这个代码里有两个关键点。第一均值计算用np.mean它操作的是整个通道的所有像素如果分辨率大建议先转成灰度图或缩小图像再统计否则速度会有点慢。第二np.clip这一步不能省增益系数在逆光或极端光源下可能达到1.5甚至2.0以上如果不截断结果图会直接溢出变成白花花一片。实测下来灰度世界算法对室内混合光源的图很管用。我之前处理一张偏黄严重的工业相机图用这个函数之后白色工件和黑色材料区域的颜色都正了很多模板匹配的分数从0.71直接提到了0.93。2.3 完美反射白平衡实现与高光截断处理完美反射算法我通常用在有明确白色参照物或高光区域比较明显的场景。它的第一步是做一个亮度图然后按亮度排序找出前1%到5%的像素把这些像素当作“参考白点”。def perfect_reflector_white_balance(img_bgr, top_percent0.01): result img_bgr.astype(np.float32) gray cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY).astype(np.float32) # 找出亮度排名前 top_percent 的阈值 threshold np.percentile(gray, (1.0 - top_percent) * 100) mask gray threshold avg_b np.mean(result[:, :, 0][mask]) avg_g np.mean(result[:, :, 1][mask]) avg_r np.mean(result[:, :, 2][mask]) max_val max(avg_b, avg_g, avg_r) gain_b max_val / avg_b gain_g max_val / avg_g gain_r max_val / avg_r result[:, :, 0] * gain_b result[:, :, 1] * gain_g result[:, :, 2] * gain_r result np.clip(result, 0, 255) return result.astype(np.uint8)top_percent这个参数很值得玩味。取1%的像素作为参考白点适合高光明确的情况取5%的话抗噪能力更强但有可能把一些浅色区域也纳入统计导致增益偏小。我一般先跑一组测试图观察不同参数下的颜色还原效果再定值。如果图像里的最亮区域本身是带颜色的比如红色LED灯珠那完美反射算法会把红色当白色来还原整体图像反而偏青。遇到这种情况可以在mask里加上饱和度过滤只保留饱和度较低的亮像素这样能排除掉彩色光源的干扰。2.4 直方图拉伸等其他思路除了灰度世界和完美反射OpenCV里还有一种常见的做法是在LAB颜色空间对L通道做直方图均衡化再转回BGR。但这个操作本质上改变的是对比度不是真正的色彩平衡。对于对比度过低的图像我会在完成白平衡之后再叠加一步CLAHE或者直方图均衡化增强细节顺序不能反过来否则增强过的对比度会影响白平衡的均值统计。3. Halcon 端白平衡工业场景下的标准姿势3.1 Halcon 图像处理的典型链路从通道分离开始Halcon在机器视觉领域的地位不需要多讲它的强大在于算子封装得非常完善而且HDevelop开发环境里可以实时拖拽图像查看中间结果这在调试白平衡时非常舒服。在Halcon里做白平衡思路和OpenCV是一致的都是先分离通道、统计均值、计算增益、再合成图像。区别在于Halcon里处理的是HObject类型的图像对象操作习惯跟Python不太一样。如果是第一次接触Halcon建议先熟悉三个最关键的算子decompose3用于把三通道图像拆成单通道intensity用于计算每个通道的均值和方差compose3用于把处理后的三个通道重新合成彩色图。3.2 灰度世界白平衡的 HDevelop 实现直接上HDevelop代码这段代码基本就是灰度世界算法的直译。* 读取彩色图像 read_image (Image, color_sample.png) * 把图像分离成 R、G、B 三个通道 decompose3 (Image, R, G, B) * 统计每个通道的灰度均值 intensity (R, RMean, RDev) intensity (G, GMean, GDev) intensity (B, BMean, BDev) * 计算全局平均灰度 GrayMean : (RMean GMean BMean) / 3.0 * 计算三个通道的增益 RScale : GrayMean / RMean GScale : GrayMean / GMean BScale : GrayMean / BMean * 用增益系数缩放每个通道 scale_image (R, RCorrected, RScale, 0) scale_image (G, GCorrected, GScale, 0) scale_image (B, BCorrected, BScale, 0) * 合成白平衡后的彩色图像 compose3 (RCorrected, GCorrected, BCorrected, WhiteBalancedImage) * 显示和保存结果 write_image (WhiteBalancedImage, png, white_balanced_result)这里有几个要点需要特别提醒。scale_image算子的签名是scale_image(Image, ImageScaled, Mult, Add)内部计算是输出 输入 × Mult Add。如果你用Mult1.0、Add0那就是原样输出。白平衡场景里Mult就是增益系数Add一般用0。halcon里scale_image输出的图像类型可能变成real或者超过byte范围这时候要记得做类型转换和截断。用convert_image_type算子把图像转回byte类型同时配合阈值截断不然合成彩色图时HDevelop会报类型不匹配的错。一种更稳妥的做法是用fit_type或者直接手写截断逻辑。我自己的习惯是* 把缩放后的图像限制在0到255之间 clip_image (RCorrected, RCorrectedClipped, 0, 255) clip_image (GCorrected, GCorrectedClipped, 0, 255) clip_image (BCorrected, BCorrectedClipped, 0, 255) * 转成byte类型 convert_image_type (RCorrectedClipped, RCorrectedByte, byte) convert_image_type (GCorrectedClipped, GCorrectedByte, byte) convert_image_type (BCorrectedClipped, BCorrectedByte, byte) compose3 (RCorrectedByte, GCorrectedByte, BCorrectedByte, WhiteBalancedImage)3.3 在 C# / Qt 项目里调用 Halcon 白平衡模块实际项目里HDevelop脚本只是验证算法的地方最终还是要集成到上位机软件里。Halcon支持导出成C#、C等工程也支持通过HDevEngine在程序中动态调用脚本。如果是C#项目我通常把HDevelop脚本导出成类然后在VS里引用HalconDotNet。白平衡函数本身不复杂导出的代码会包含HOperatorSet.Decompose3、HOperatorSet.Intensity这些调用注意在程序初始化时检查Halcon许可证是否正常加载否则运行时会报License错误。如果是Qt项目思路也一样用C调用Halcon算子库。需要注意Halcon的图像对象HObject和Qt里的QImage之间需要做数据拷贝不能直接强转一般通过HalconCpp里的转换函数搞定。网上很多人在问“Qt怎么调用Halcon”实际上只要把Halcon的include目录和lib目录配好调用链路跟C#差不多。3.4 白平衡在缺陷检测与测量前处理中的位置很多人觉得白平衡只是摄影修图用的工业视觉里不重要其实恰恰相反。在彩色缺陷检测项目里如果图像整体偏色传统的灰度阈值分割可能还能撑住但基于颜色空间的检测比如HSV空间里的色相判断就会受到严重影响。Halcon里的HSI、HSV空间转换算子会直接用到R、G、B通道值一旦白平衡不正色相分量就会整体漂移。我做过一个印刷品检测项目目标是检测彩色标签上的印错区域。光源是白色环形LED但使用一段时间后色温会漂移标签的底色从纯白慢慢变成浅黄。如果不做白平衡Halcon的测量工具测量出的尺寸倒是影响不大但基于color_histo算子的缺陷检测就会不断误报。后来在采集端塞了一个白平衡预处理问题立刻缓解。所以白平衡在这种链路里不是锦上添花是实打实的稳定性保障。4. OpenCV 和 Halcon 的白平衡方案怎么选4.1 开发效率与调试体验Halcon的IDE确实香就白平衡这一个小功能而言OpenCV的代码量和Halcon差不多但开发调试的体验差别很大。在HDevelop里你可以一边运行一边双击查看每个中间变量图像显示窗口可以放大缩小、切换通道通道甚至直接在图上划线看灰度值。这种可视化能力在调试算法、确认参数时能省不少时间。OpenCV的优势在于灵活。你可以在Jupyter Notebook里反复改代码实时出结果也可以结合Matplotlib一次对比多组参数的处理效果。不过OpenCV本身没有可视化调试界面图像需要手动用imshow或者保存到本地查看灰度值采样也要自己写稍微繁琐一点。4.2 运行效率和部署成本开源 vs 商业授权性能方面同样一个灰度世界白平衡图像分辨率在500万像素级别时OpenCV的Python实现大概在20到40毫秒左右如果用C能压到10毫秒内。Halcon的算子经过高度优化底层很多是并行处理单帧处理时间通常比OpenCV Python快但具体要看算子组合和机器配置。部署成本是更大的考量。OpenCV是开源的商业项目里随便用不用担心License。Halcon是商业软件需要一个有效的License才能运行发布到客户现场还得带上运行时和对应授权这块成本不低。不过Halcon有完善的License管理机制在大批量设备部署时价格可以谈但小项目就要掂量一下了。从功能完整性角度讲Halcon里还有很多强力的工具比如形状匹配、测量、深度学习等如果项目里不只是做白平衡还涉及一系列复杂机器视觉功能那Halcon的整套方案反而更省事。4.3 选型建议算法验证用 OpenCV产线落地用 Halcon我自己的项目习惯是这样的算法验证、参数摸排阶段一定先用OpenCV写一个Python脚本快速对一批现场图片做批处理通过调整增益、截断阈值、ROI范围来找到最优参数组合。等到参数确定再把这套逻辑用Halcon搭成固定的预处理流程放进产线视觉软件里。这样做的好处很明显OpenCV阶段可以白嫖成本随便造Halcon阶段是正儿八经的产线运行环境稳定性和一致性最重要。如果项目本身就是纯数据集处理比如离线分析图片不一定非要上HalconOpenCV足够。反过来如果项目是标准视觉工作站建议直接上Halcon毕竟后续跟其他视觉工具链结合更方便。5. 实际调试中的高频问题与避坑清单5.1 大面积同色场景灰度世界算法失效怎么补救最常见的翻车现场是拍摄对象本身颜色比较单一。比如拍一块蓝色塑料面板画面里大片都是蓝色灰度世界算法会认为蓝色通道均值过高于是拼命降低蓝色增益结果把画面里的白色部分都生生拉成了黄色。这种场景我一般用两个办法。一个是只在ROI区域内做统计手动框出包含白色参照物的区域只让该区域的均值参与增益计算这样能绕开大面积主色干扰。另一个是给灰度世界算法加一个概念限制增益系数的变化范围比如增益只能落在0.8到1.25之间超过就按边界处理这样能防止算法过度校正。5.2 通道溢出与断层为什么白平衡后图像发灰或伪影增益系数大于1时乘以原像素值很容易超过255这时候如果不做clip图像会出现高光溢出变成纯白色区域更隐蔽的问题是三个通道的增益不一致原本中性的灰色部分可能会因为截断程度不同而出现色偏伪影也就是边缘出现红边或蓝边。解决思路是尽量在RGB空间完成增益计算后再做一次gamma校正让高光区域的过渡更平滑。或者用带柔化效果的处理方式把增益图做高斯模糊让增益值在空间上平滑变化避免因为增益突变在物体边界产生伪影。这个方法在增强图像对比度时很实用。5.3 Halcon 与 OpenCV 图像类型、通道顺序的转换坑如果项目里同时使用OpenCV和Halcon图像数据必然要来回转换。Halcon的byte类型对应OpenCV的uint8Halcon的real类型对应OpenCV的float32。OpenCV里图像默认是BGR顺序Halcon里compose3出来的是R、G、B通道顺序如果不转换直接显示红色和蓝色的显示结果会完全颠倒。我每次做转换都强迫自己先查看一下图像类型和通道数再动手转换。C#里更麻烦Halcon的HObject转成Bitmap时注意像素格式最好用Interop的API封装一层避免因像素格式不匹配导致图像拉伸变形。5.4 相机采集链路中的白平衡误区有一次我在OpenCV里用VideoCapture读取USB摄像头拍到的图像颜色偏暖于是我直接在采集回调里套了一层白平衡函数结果颜色正常了但整体亮度又降了一截。最后排查发现摄像头驱动层已经做过自动白平衡我又做一次软件白平衡相当于二次校正增益叠加后过度补偿了。所以在做软件白平衡之前一定要先确认图像来源。如果用工业相机SDK里通常能直接关闭相机内部的AWB设置一个固定的色温这样软件层的白平衡才有意义。如果是普通USB摄像头尽量在驱动层面禁用自动白平衡比如OpenCV里设置CAP_PROP_AUTO_WB为0。如果无法关闭就要对采集图像做一次色偏评估偏差小的时候就不启用软件白平衡偏差大了再启用。我在实际使用中最大的感受是白平衡这类预处理算法本身原理不复杂代码也不长真正考验经验的是判断什么时候该用、什么时候不该用、以及参数怎么选。很多人一上来就套算法结果越调越花根子在于没有理解算法背后的假设条件。建议读者自己把灰度世界和完美反射两个函数各实现一遍找几张偏色严重的图反复测试亲手调几次增益和截断参数比看十篇教程都管用。最后再分享一个小技巧不管用OpenCV还是Halcon白平衡的中间结果和原图一定要同时保存方便后期对比。这个习惯救了我好几次。
阅读完成 · 觉得有帮助?