资讯详情 Halcon 图像清晰度评价:五种算子实现与自动对焦应用
📅 2026/10/23 6:44:47
简介这份资源围绕Halcon图像清晰度计算展开面向工业视觉、机器视觉方向的开发者与自动化工程师用于解决相机自动对焦中图像质量量化评估的问题。内容涵盖方差法、拉普拉斯能量函数法、能量梯度函数法与Brenner函数法四种评价思路并给出基于evaluate_definition自定义评判函数的实现方式通过遍历不同焦距图像、比较返回值大小定位最清晰帧可应用于工业相机对焦、视觉测量与图像预处理等场景。资源包共1个docx文件约22KB以文档形式集中整理算法原理与代码示例便于对照查阅与二次开发。目前已有6159人学习下载适合希望快速掌握清晰度评价函数选型与对焦流程的读者参考借鉴。1. 从一组失焦的 PCB 图说起Halcon 清晰度评价到底在算什么产线上有台相机拍 PCB镜头是手动变焦的操作员得拧着对焦环来回找最清楚的位置。人眼判断这事特别玄学——盯着屏幕看久了觉得哪张都差不多换个人来调结果又不一样。更麻烦的是同一批板子换了个工位、换了光照之前调好的焦距又得重来。这个项目要解决的就是把哪张最清楚从人眼主观判断变成可量化的数值用 Halcon 写一个清晰度评价函数对一组连续拍摄的失焦到合焦图像逐张打分分数最高的那张就是最清晰的。项目本身不复杂核心是一个evaluate_definition函数内部实现了方差法、拉普拉斯能量函数法、能量梯度函数法、Brenner 函数法和 Tenegrad 函数法五种评价算子通过Method参数切换。配套的主程序读取 Halcon 自带的pcb_focus序列图121 张从模糊到清晰再到模糊循环调用评价函数把每张图的清晰度值收集起来最后用直方图可视化并自动定位峰值对应的那张图。适合正在做自动对焦、图像质量筛选、或者需要给视觉系统加一道图像合格性检查的开发者参考。2. 五种评价函数的实现拆解从方差到 Tenegrad 的算子选择2.1 统一入口与预处理scale_image_max 和图像尺寸获取evaluate_definition的签名很简洁输入一张图和一个 Method 字符串输出一个 Value 数值。所有方法共用两步预处理——scale_image_max把灰度拉伸到 0-255 满量程get_image_size拿到宽高供后续裁剪用。灰度拉伸这步容易被忽略但如果输入图像本身对比度低不拉伸直接算梯度或方差数值会偏小不同方法之间的可比性也会变差。* 统一预处理灰度拉伸 获取尺寸 scale_image_max(Image, Image) get_image_size(Image, Width, Height)scale_image_max的作用是把图像灰度线性映射到最大范围相当于自动对比度增强。对于清晰度评价来说这步保证了不同曝光条件下拍到的图能在同一尺度上比较。Width和Height在后面能量梯度法和 Brenner 法里用来做crop_part的边界控制。2.2 方差法像素分布离散程度的直接度量方差法的逻辑最直白——清晰图像的边缘锐利像素值在边缘两侧跳变大整体分布更分散方差自然更大。实现上先求图像均值再算每个像素与均值差的平方最后用intensity算子取均值。if (Method Deviation) * 方差法像素值离散程度越大越清晰 region_to_mean(Image, Image, ImageMean) * 求全图均值 convert_image_type(ImageMean, ImageMean, real) * 转实数避免溢出 convert_image_type(Image, Image, real) sub_image(Image, ImageMean, ImageSub, 1, 0) * 像素减均值 mult_image(ImageSub, ImageSub, ImageResult, 1, 0) * 差值平方 intensity(ImageResult, ImageResult, Value, Deviation) * 取均值即方差 endifregion_to_mean把整张图当作一个区域求平均灰度输出一张所有像素都是均值的图。sub_image的1, 0是缩放和偏移参数这里保持原值。mult_image做逐像素平方。最后intensity返回的Deviation就是方差。这个方法计算量小但对光照变化敏感——如果图像整体偏暗方差会被拉低所以前面的scale_image_max很关键。2.3 拉普拉斯能量法边缘强度的平方和拉普拉斯算子对图像做二阶微分边缘处响应强烈。这里同时用了 4 邻域和 8 邻域两种拉普拉斯核把结果叠加后平方求和。elseif (Method laplace) * 拉普拉斯能量4邻域和8邻域核叠加后平方 laplace(Image, ImageLaplace4, signed, 3, n_4) * 4邻域拉普拉斯 laplace(Image, ImageLaplace8, signed, 3, n_8) * 8邻域拉普拉斯 add_image(ImageLaplace4, ImageLaplace4, ImageResult1, 1, 0) * 4邻域结果乘2 add_image(ImageLaplace4, ImageResult1, ImageResult1, 1, 0) * 再加一次共3倍 add_image(ImageLaplace8, ImageResult1, ImageResult1, 1, 0) * 叠加8邻域 mult_image(ImageResult1, ImageResult1, ImageResult, 1, 0) * 平方 intensity(ImageResult, ImageResult, Value, Deviation) endiflaplace的signed表示保留正负号3是滤波核尺寸n_4和n_8分别指定邻域模式。4 邻域核只考虑上下左右8 邻域核还包含对角方向。代码里 4 邻域结果被加了三次自身一次、再加两次8 邻域加一次相当于给 4 邻域更高权重。这个权重分配不是固定的实际调的时候可以根据图像纹理方向调整。平方操作放大了强边缘的贡献让清晰图像的得分和模糊图像拉开差距。2.4 能量梯度法与 Brenner 法相邻像素差值的两种用法能量梯度法用水平和垂直方向的相邻像素差来度量梯度Brenner 法只算水平方向隔一个像素的差。两者结构相似区别在差分方向和步长。elseif (Method energy) * 能量梯度水平垂直方向相邻像素差 crop_part(Image, ImagePart00, 0, 0, Width-1, Height-1) * 左上区域 crop_part(Image, ImagePart01, 0, 1, Width-1, Height-1) * 下移一行 crop_part(Image, ImagePart10, 1, 0, Width-1, Height-1) * 右移一列 convert_image_type(ImagePart00, ImagePart00, real) convert_image_type(ImagePart10, ImagePart10, real) convert_image_type(ImagePart01, ImagePart01, real) sub_image(ImagePart10, ImagePart00, ImageSub1, 1, 0) * 水平差 mult_image(ImageSub1, ImageSub1, ImageResult1, 1, 0) * 平方 sub_image(ImagePart01, ImagePart00, ImageSub2, 1, 0) * 垂直差 mult_image(ImageSub2, ImageSub2, ImageResult2, 1, 0) * 平方 add_image(ImageResult1, ImageResult2, ImageResult, 1, 0) * 合并 intensity(ImageResult, ImageResult, Value, Deviation) elseif (Method Brenner) * Brenner水平方向隔一个像素的差 crop_part(Image, ImagePart00, 0, 0, Width, Height-2) * 上半部分 convert_image_type(ImagePart00, ImagePart00, real) crop_part(Image, ImagePart20, 2, 0, Width, Height-2) * 右移两列 convert_image_type(ImagePart20, ImagePart20, real) sub_image(ImagePart20, ImagePart00, ImageSub, 1, 0) * 隔像素差 mult_image(ImageSub, ImageSub, ImageResult, 1, 0) * 平方 intensity(ImageResult, ImageResult, Value, Deviation) endifcrop_part的参数是(输入图, 输出图, 起始行, 起始列, 宽度, 高度)。能量梯度法裁出三个区域基准区、下移一行、右移一列分别做垂直和水平差分。Brenner 法只裁两个区域列偏移为 2对应隔一个像素的差分。注意 Brenner 的crop_part高度是Height-2因为列偏移了 2边界要留够。这两种方法对噪声比拉普拉斯法更敏感如果图像有椒盐噪声得分会被噪声带偏实际用的时候前面加个中值滤波会更稳。2.5 Tenegrad 法Sobel 梯度加阈值筛选Tenegrad 用 Sobel 算子算梯度幅值然后设一个阈值把弱边缘滤掉只保留强边缘做平方和。这个阈值筛选是它和其他方法最大的区别。elseif (Method Tenegrad) * TenegradSobel梯度 阈值筛选强边缘 sobel_amp(Image, EdgeAmplitude, sum_sqrt, 3) * Sobel梯度幅值 min_max_gray(EdgeAmplitude, EdgeAmplitude, 0, Min, Max, Range) threshold(EdgeAmplitude, Region1, 11.8, 255) * 阈值分割 region_to_bin(Region1, BinImage, 1, 0, Width, Height) * 区域转二值图 mult_image(EdgeAmplitude, BinImage, ImageResult4, 1, 0) * 掩膜 mult_image(ImageResult4, ImageResult4, ImageResult, 1, 0) * 平方 intensity(ImageResult, ImageResult, Value, Deviation) endifsobel_amp的sum_sqrt表示梯度幅值用平方和开根号计算3是核尺寸。threshold的阈值 11.8 是个经验值针对 PCB 图像调的——太低会把噪声算进去太高会漏掉弱边缘。region_to_bin把筛选出的区域转成二值掩膜和梯度图逐像素相乘只保留强边缘的梯度值。这个方法在纹理丰富的图像上表现好但如果图像本身边缘就少比如均匀表面阈值可能把所有边缘都滤掉得分趋近于零。3. 主程序流程从 121 张序列图到清晰度峰值定位3.1 图像读取与循环评价主程序的核心是一个 121 次的循环每次读一张图调用评价函数把返回值追加到数组里。* 主循环逐张读取并评价 Ret : [] get_image_size(Image, Width, Height) for Index : 1 to 121 by 1 read_image(Image, pcb_focus/pcb_focus_telecentric_ Index$03d) evaluate_definition(Image, Brenner, Value) * 切换Method看不同效果 dev_display(Image) Ret : [Ret, Value] endforIndex$03d是 Halcon 的格式化字符串语法把数字补零到三位拼出001到121的文件名。Ret初始化为空元组每次循环用[Ret, Value]追加。这里Method传的是Brenner改成Deviation、laplace、energy或Tenegrad就能切换评价函数。实际跑的时候建议把五种都跑一遍对比同一组图的得分曲线——不同方法对同一张图的敏感度不一样有的曲线尖锐有的平缓。3.2 归一化与直方图可视化拿到 121 个得分后做 min-max 归一化映射到 0-100再用gen_region_histo生成直方图区域。* 归一化并生成直方图 VMax : max(Ret) VMin : min(Ret) GRet : 100 * (Ret - VMin) / (VMax - VMin) * 映射到0-100 gen_region_histo(Region, Ret, 255, 255, 1) * 生成直方图区域max和min是 Halcon 的元组算子直接对数组操作。GRet算出来其实没在后面用到但归一化这步在需要跨序列比较时有用——比如不同批次的图原始得分范围可能差很多归一化后才能用同一个阈值判断。gen_region_histo的255, 255, 1控制直方图的显示范围和缩放生成的是一个区域对象可以直接dev_display叠加在原图上。3.3 峰值定位与最清晰图像提取最后用find找到最大值的位置读出对应的原图显示。* 找峰值并显示最清晰图 qxd : find(Ret, max(Ret)) * 返回最大值索引 read_image(GoodImage, pcb_focus/pcb_focus_telecentric_ qxd$03d) dev_display(GoodImage) dev_display(Region)find返回的是元组中第一个匹配值的索引。注意 Halcon 元组索引从 0 开始而文件名从 1 开始这里qxd直接拼进文件名实际对应的是第qxd1张图。如果序列图是从 1 编号的这个偏移会导致差一张——不过对于找最清晰帧来说差一张通常不影响结论但如果要做精确的焦距标定这个索引偏移得修正。dev_display先显示原图再叠加直方图区域直方图的峰值位置和图像清晰度峰值在视觉上能对应起来。4. 避坑与排查跑清晰度评价时容易翻车的几个点4.1 现象所有图的得分都很接近曲线几乎是平的原因通常是图像本身对比度太低或者scale_image_max没起作用。如果原始图像灰度集中在很窄的范围内方差法和梯度法的区分度都会下降。解决方法是先检查输入图像的灰度直方图确认scale_image_max之后的灰度范围确实被拉开了。如果还是平考虑换用对对比度更敏感的 Tenegrad 法或者手动加一个emphasize增强边缘。4.2 现象Brenner 法报错提示裁剪区域超出图像边界Brenner 法的crop_part列偏移是 2高度参数写的是Height-2但如果图像本身高度很小比如小于 3 像素或者Width和Height获取时机不对就会越界。检查get_image_size是否在scale_image_max之后调用——scale_image_max不改变尺寸但如果在循环外获取尺寸而循环内图像被替换过尺寸可能不一致。稳妥做法是在evaluate_definition内部每次重新获取尺寸。4.3 现象Tenegrad 法得分异常高或异常低阈值 11.8 是针对特定 PCB 图像调的。如果换一组图边缘梯度幅值的分布变了这个固定阈值就不适用。得分异常高通常是因为阈值太低噪声被当成边缘算进去了异常低是因为阈值太高真实边缘被滤掉了。解决方法是把threshold的阈值改成自适应——先用min_max_gray拿到梯度幅值的范围再按比例设阈值比如取最大值的 10%。4.4 现象切换 Method 后得分量级差很多没法直接比较五种方法的输出量纲不一样方差法的值可能在几百Brenner 法可能在几万。这是正常的因为它们的数学定义不同。不要试图跨方法比较绝对数值而是看同一方法下不同图像的相对趋势。如果确实需要跨方法比较先做 min-max 归一化再比。另外intensity返回的Deviation是标准差还是均方根取决于输入图像的类型real 类型下返回的是标准差这点在文档里没写清楚实测确认一下。4.5 现象峰值对应的图看起来并不是最清晰的可能原因有两个一是序列图的清晰度变化不是单调的存在多个局部峰值find只返回第一个二是评价函数对某些类型的模糊不敏感比如运动模糊和离焦模糊的梯度特征不一样。解决方法是把Ret数组和对应的图像索引一起打印出来人工核对峰值附近几张图。如果确实有多个峰值用sort排序后取前几个候选再结合其他指标比如图像熵做二次筛选。5. 二次开发把评价函数接进自己的对焦流程这个项目的评价函数是纯 Halcon 实现没有外部依赖移植到自己的项目里只需要把evaluate_definition整个复制过去然后在需要的地方调用。但直接复制粘贴只是第一步真正要接进自动对焦流程还得解决几个工程问题。第一个是实时性。121 张图离线跑没问题但产线上相机每秒出几十帧每帧都调一次评价函数计算量得控制住。方差法和 Brenner 法计算量最小适合做粗对焦拉普拉斯和 Tenegrad 计算量大但精度高适合在粗对焦找到大致范围后做精调。我一般会做一个两级策略先用 Brenner 快速扫一遍找到峰值附近再用 Tenegrad 在峰值附近的小范围内细评。第二个是评价函数的参数自适应。Tenegrad 的阈值、拉普拉斯的邻域权重、Brenner 的差分步长这些参数在不同场景下需要调整。我的习惯是把这些参数做成外部配置而不是硬编码在函数里。比如用一个字典存{Tenegrad: {threshold: 11.8}, Brenner: {step: 2}}调用时传进去。这样换产品换工位的时候不用改代码改配置就行。第三个是评价结果的稳定性。单帧评价容易受噪声影响实际用的时候会对连续几帧的得分做滑动平均或者用中值滤波去掉突变。如果相机支持硬件触发最好在曝光稳定的时间段内采样避免自动曝光调整过程中得分跳变。下面是一个把评价函数封装成可配置调用的示例用 Halcon 的字典和元组操作实现* 可配置的评价函数调用封装 * ConfigDict: 字典键为Method名值为参数字典 * 返回归一化后的清晰度得分0-100 evaluate_definition_config(Image : : Method, ConfigDict : Value) scale_image_max(Image, Image) get_image_size(Image, Width, Height) * 从配置字典读取参数缺省用默认值 get_dict_param(ConfigDict, Method, threshold, Threshold) if (Threshold []) Threshold : 11.8 endif * 根据Method分发到具体实现 if (Method Tenegrad) sobel_amp(Image, EdgeAmplitude, sum_sqrt, 3) threshold(EdgeAmplitude, Region1, Threshold, 255) region_to_bin(Region1, BinImage, 1, 0, Width, Height) mult_image(EdgeAmplitude, BinImage, ImageResult4, 1, 0) mult_image(ImageResult4, ImageResult4, ImageResult, 1, 0) intensity(ImageResult, ImageResult, Value, Deviation) elseif (Method Brenner) crop_part(Image, ImagePart00, 0, 0, Width, Height-2) convert_image_type(ImagePart00, ImagePart00, real) crop_part(Image, ImagePart20, 2, 0, Width, Height-2) convert_image_type(ImagePart20, ImagePart20, real) sub_image(ImagePart20, ImagePart00, ImageSub, 1, 0) mult_image(ImageSub, ImageSub, ImageResult, 1, 0) intensity(ImageResult, ImageResult, Value, Deviation) endif return ()这个封装的关键改动是把阈值从硬编码变成从字典读取get_dict_param在键不存在时返回空元组用if (Threshold [])兜底。实际项目里我会把配置字典从 JSON 文件加载这样调参不用重新编译 Halcon 程序。验证改造是否成功的方法很简单拿同一组序列图用原始硬编码版本和配置化版本各跑一遍对比Ret数组是否完全一致。如果一致说明封装没引入逻辑变化如果不一致检查参数读取的默认值是否和原来相同。这个对比测试我每次改评价函数都会跑一遍花不了几分钟但能避免很多改了参数忘了同步的低级错误。从那以后我每次接新的对焦项目都会先把这五种评价函数在同一组图上跑一遍看哪种方法的得分曲线最尖锐、峰值最明确再决定用哪个做主力。没有哪种方法是万能的方差法在低纹理表面会失效Tenegrad 在噪声大的图上会误判Brenner 对垂直边缘不敏感——选型这件事实测比理论推导靠谱。希望帮到你。本文还有配套的精品资源点击获取