简介:面向机器视觉与自动化检测领域的开发者,这份资源聚焦 Halcon 景深合成技术,通过整合不同焦距下的多幅图像生成清晰且具有深度信息的合成图,尤其适合产品缺陷识别、精密装配等需要高精度检测的场景,也解决了单一焦距下景深不足导致成像模糊的问题。压缩包内共六个文件,以 cpp 源码、Halcon 工程文件(hdev)、Qt 界面文件(ui)以及工程配置类文件为主,总体积约 26KB,属于可直接阅读和二次开发的轻量级源码包。已有 2317 人学习浏览,适合正在研究景深合成流程的算法工程师,以及尝试将 Halcon 与 Qt 结合开发工具的入门者。其中封装了 dFF3D 测量相关的 Halcon 工程,并配有 Qt 界面程序,涉及图像预处理、深度信息提取、多图融合等关键步骤。同时保留测试数据与项目配置,便于对照运行、理解完整实现链路并快速改造适配自己的图像采集与检测需求。
1. 景深合成不是“堆图”:halcon 景深合成把一个老大难变成三行算子
高倍率检测的人都躲不开一个尴尬:镜头倍率一上去,景深就掉到零点几毫米,一个两毫米高的小零件拍出来永远有一半是糊的。halcon 景深合成解决的就是这个——把一组对焦位置不同的图像合成一张全清晰图,同时输出一张逐像素的深度图。它不是简单把几张图叠在一起加权平均,而是先给每一层算清晰度响应,再沿深度方向找极值、做插值。搞检测的、做显微测量的、拍电路板连接器的工程师都用得上。这篇笔记按我自己拆过的流程写:算子怎么选、参数怎么设、哪些地方最容易翻车。
2. 从焦平面到深度图:聚焦测度怎么算,以及为什么选 Halcon
2.1 景深合成的本质:逐像素找“最清晰的那一层”
先纠正一个常见误解:景深合成不是把 N 张图做加权平均,那只会得到一张处处都糊的图。真正的做法是逐像素求极值——同一场景拍 N 张,每张对应一个不同的对焦平面,那么对任意一个像素来说,它在第 k 张最清晰,就说明这个像素对应的物点恰好落在第 k 张的对焦平面上。把所有像素的 k 收集起来,就是一张相对深度图。
Halcon 在这个思路上把管线拆成四步。第一步,对每张图计算聚焦测度(focus measure),得到一个清晰度响应图像,相当于给每个像素打分;第二步,把 N 张响应图按深度方向叠起来,对每个像素找到响应最大的图层序号;第三步,在最大值附近做亚层插值,把离散的层号细化成小数,这一步决定了深度分辨率的极限;第四步,用插值结果重新采样各层图像,合成全聚焦图。depth_from_focus这个算子把四步全部封装了,输入是一组图像对象,输出是深度图、置信度和全聚焦图三件套。
这里最值得理解的是第二步和第三步的区别。如果你只找最大层号,深度分辨率就只有一层:步距是 10 微米,深度就只能按 10 微米一跳,做高度测量根本没有意义。Halcon 默认会在峰值附近做高斯或抛物线拟合,实际深度分辨率能做到步距的几十分之一,但前提是相邻两张的响应曲线必须有交叠。换句话说,步距本身不能超过单张景深的二分之一到三分之一,否则插值出来的峰值位置是错的,深度图就会断层。很多新手上来就把所有图一股脑丢给算子,结果深度图一塌糊涂,其实问题出在采集步距而不是算法。
另外要提醒一点:整条链路里没有一步是做“图像融合”的,AllInFocus 全聚焦图本质上是在深度图的指导下对原图做逐像素重采样。所以深度图的质量直接决定合成图的质量,这就是为什么后面第 4 章的参数调整那么关键。
2.2 聚焦测度选哪种:gvar、tenengrad、fmeasure、fariance 的取舍
聚焦测度本质上是一个清晰度评分函数:对聚焦良好的纹理,相邻像素灰度差大,梯度或方差就大;对离焦区域,高频信息被抹掉,评分就低。Halcon 的depth_from_focus支持多种测度,工程上我常对比的是下面四种。
| 测度 | 原理 | 相对速度 | 抗噪 | 适合场景 |
|---|---|---|---|---|
| gvar | 局部灰度方差 | 最快 | 一般 | 表面有纹理的金属件、PCB、塑料件 |
| tenengrad | Sobel 梯度能量 | 快 | 较好 | 边缘清晰、层次分明的零件 |
| fmeasure | 多尺度局部对比度 | 慢 | 好 | 弱纹理表面、要求深度平滑的场合 |
| fariance | 基于二阶导数的响应方差 | 中 | 较差 | 高倍显微下的细刻线、微结构 |
选型没有绝对答案,我的习惯是先用 gvar 把整个流程跑通,确认采集、步距、照明都没问题之后,再换 fmeasure 对比深度图的毛刺数量。对大多数工业零件,gvar 配一个 5x5 的滤波尺寸已经够用;只有当你发现深度图在平坦区域出现大量椒盐噪点时,才值得牺牲速度换 fmeasure。
还有一个容易被忽略的点:聚焦测度是在灰度图上算的,所以光源角度、表面反光会直接影响评分。同一颗螺丝,用同轴光拍和用环形光拍,深度图台阶边缘的位置能差出好几个像素。这个问题一度被同事当成玄学,后来拿测厚仪一比才发现是照明角度改变了纹理的梯度方向。做景深合成之前,先固定照明方案,不要再换来换去。
2.3 为什么是 Halcon:内置算子之外的三个理由
自己用 OpenCV 写景深合成并不是不行——梯度能量、极值搜索、插值,满打满算几百行代码。但落到工程上,真正花时间的不是算法本身,而是那些边界情况:图像边缘的窗口效应怎么处理、响应曲线出现双峰时取哪个峰、深度图上的小孔洞要不要填、填了会不会把真实台阶抹掉。Halcon 的depth_from_focus把这些封装成了内部逻辑,你只用边界处理参数控制行为就行,不用自己维护一套图像栈的数据结构。
第二个理由是 Score 输出。这个置信度图是算子自带的“后悔药”——它能告诉你每个像素的深度值有多可信。高反光区域、遮挡区域、完全离焦的死区,得分都会掉下来。做测量时用阈值把低分区域挖掉,比事后做中值滤波可靠得多,因为中值滤波会把台阶边缘的真实高度差一起磨平。
第三个理由是调试体验。HDevelop 里可以在运行中直接断点,把 Depth 和 Score 用伪彩色叠加在原图上看,哪一层出了问题一眼定位;同样的流程在 OpenCV 里得写一堆 imshow 辅助代码。我自己在某个连接器检测项目里,就是用 HDevelop 反复看 Score 图找出反光点的,换回通用语言环境估计要花三倍时间。
提示:
depth_from_focus并不是所有 license 档位默认都包含。最稳的验证方法是在 HDevelop 里直接搜这个算子,能搜到、能执行,说明当前授权可用;如果只有calculate_depth_from_focus,那是旧版算子,参数名和调用方式都不一样,拿到资源后先按 F1 看签名,不要直接照抄网上的代码。
3. 一套能跑通的流程:从多张对焦图到全清晰图像
3.1 采集约束:步距、照明和命名规则
景深合成的成败,一半在图采,一半在参数。先说采集,这是最容易让新手翻车的地方。
第一是步距。单张图像的景深是固定的,比如 5 倍镜头在 0.2 数值孔径下景深大约 10 微米,那么步距就该取 3 到 5 微米,保证相邻两张的清晰区域至少有一半重叠。步距太大,两张图的响应曲线没有交叠,深度图会断层;步距太小,图片数量翻倍,计算时间线性上涨,深度精度却不会同步提升。工程上一般先粗测总行程,除以预估步距得到张数,再留 30% 余量。
第二是照明和曝光。手动曝光、手动增益、关掉自动白平衡,光源亮度在整个采集过程中不许动。原因很简单:聚焦测度基于灰度梯度,如果相邻两张图因为自动曝光产生亮度跳变,算法会把亮度变化误判成清晰度变化,深度图上会多出一层层的假轮廓。这个坑我见过不止一次,症状是合成图出现条纹,很多人以为算法有问题,其实是采集时曝光在飘。
第三是运动方式。优先移动载物台也就是 Z 轴,不动相机和镜头;如果只能拧调焦环,尽量让镜头做单轴运动,避免俯仰晃动。半毫米行程里哪怕有 0.01 毫米的横向漂移,合成图的边缘都会发虚。
第四是命名。Halcon 读序列最顺手的方式是按序号补零命名:img_01.tif、img_02.tif……img_30.tif,然后用循环read_image再concat_obj合成一个对象集。如果文件名里带了项目名和日期,记得在循环里做字符串拼接,别图省事直接用通配符。
3.2 读取图像序列并调用 depth_from_focus
下面这段是我在 HDevelop 里实际跑通过的流程,包含读取序列、调用算子、后处理、保存结果。新版本用depth_from_focus,旧版本参数名有差异,代码里按新版本写法:
* 第一步:把按聚焦位置排序的图片读成对象集 gen_empty_obj (ImageSet) for Index := 1 to 30 by 1 read_image (Image, 'stack/img_' + Index$'02') concat_obj (ImageSet, Image, ImageSet) endfor * 第二步:景深合成,输出深度图、置信度图、全聚焦图 depth_from_focus (ImageSet, Depth, Score, AllInFocus, ... ['Method', 'FilterSize', 'SmoothSigma'], ... ['gvar', 5, 0.3]) * 第三步:把深度图线性拉伸到 0~255 便于观察和存图 min_max_gray (Depth, Depth, 0, MinVal, MaxVal, Range) scale_image (Depth, DepthVis, 255.0 / (MaxVal - MinVal + 1e-6), ... -255.0 * MinVal / (MaxVal - MinVal + 1e-6)) * 第四步:用置信度图挖掉低分区域,留着做测量 threshold (Score, ScoreOk, 0.5, 1.0) reduce_domain (Depth, ScoreOk, DepthOk) write_image (AllInFocus, 'png', 0, 'result_allinfocus.png') write_image (DepthVis, 'png', 0, 'result_depth.png')逻辑说明:gen_empty_obj先建一个空对象集,循环里read_image读单张、concat_obj追加,30 张图最终合成一个 ImageSet 传给算子。depth_from_focus返回三个图:Depth 是相对深度,Score 是 0 到 1 的置信度,AllInFocus 是全聚焦合成图。第三、第四步是后处理:min_max_gray取深度图的最小最大值,scale_image做线性映射;threshold把 score 低于 0.5 的像素丢掉,reduce_domain把深度图限制在高置信区域,这一步是给后面测量用的。
参数说明:Method 选'gvar',用灰度方差做聚焦测度,速度快、对大多数纹理表面够用;FilterSize 等于 5 表示评分窗口是 5x5,窗口越大越抗噪,但深度边缘越模糊;SmoothSigma 等于 0.3 是深度图的高斯平滑系数,只平滑深度图不动原图,对细节影响很小。这三个是起步值,第 4 章讲怎么针对工件调整。
3.3 三路输出分别怎么用
Depth 是一张相对深度图,灰度值大的像素表示它更靠近某个方向的对焦位置。注意它是相对值,不是物理高度,想得到毫米单位的 Z 坐标必须做标定,这块在第 6 章展开。肉眼观察时建议做线性拉伸,否则深度图对比度很低,看不出断层和噪声。
Score 是置信度图,相当于整条链路的质检报告。平坦低纹理区域的 score 通常很高,因为聚焦测度没有争议;真正低分的是边缘、反光点、遮挡边界。测量时不要直接用原始深度图,先threshold掉低分区域,再用剩余像素统计,你会发现标准差明显下降。在我经手的项目里,同一个台阶高度,加上 Score 掩码之后重复性标准差能从 0.05 毫米压到 0.01 毫米以内。
AllInFocus 才是给检测算法用的主图。OCR、二维码、尺寸测量都可以在它上面做,比在单张原图上做稳得多——你不需要挑一张“最不糊”的图,因为每一张原图都只有局部清晰区,全聚焦图把所有清晰区拼在了一起。我之前做连接器针脚检测时,全聚焦图直接替代了原来对 8 张图分别检测再合并结果的流程,尺寸测量的标准差大约降了一半,节拍也省下来了。
4. 参数怎么调:六个关键参数与一张调参对照表
4.1 六个参数逐个说清楚
depth_from_focus的完整参数比文档里能看到的还多,但落到实际项目,值得花时间调的就六个:Method、FilterSize、SmoothSigma,以及边界处理方式、插值方式和置信度阈值。后三个一般用默认值,前三个决定 90% 的效果,所以我先盯着前三个讲。
Method 是聚焦测度,前面 2.2 已经对比过。这里补充一条经验:如果工件表面有规律性纹理,比如网纹、拉丝,gvar 可能在纹理周期和离焦模糊尺度接近时产生双峰响应,深度图出现条纹。遇到这种情况别纠结,直接换 fmeasure,多花一倍计算时间,但深度图干净很多。
FilterSize 是评分窗口尺寸,取值范围通常是 3 到 11 的奇数。窗口小,深度方向分辨率高,但对噪声敏感;窗口大,平坦区域的椒盐噪点明显减少,代价是深度边缘被圆角化——两个高度不同的台阶,过渡带可能从 3 像素变成 8 像素。如果测量目标本身就是台阶高度,过大的窗口会让你测出来的高度偏小,这是个很隐蔽的系统误差。
SmoothSigma 是深度图平滑系数,0 表示不平滑,0.5 以上算强平滑。它和 FilterSize 的分工要分清:FilterSize 作用于清晰度评分阶段,SmoothSigma 作用于最终深度图。平滑太强会把 0.1 毫米的小台阶抹成斜坡,所以我的习惯是 SmoothSigma 不超过 0.5,深度噪声用 Score 掩码处理,而不是靠平滑硬压。
边界处理方式控制图像边缘像素怎么参与评分,默认是镜像反射,一般不用改;插值方式决定峰值拟合用高斯还是抛物线,默认高斯在大多数场景下更稳;置信度阈值是后处理参数,取值 0.3 到 0.6 都见过,建议看 Score 直方图再定,不要拍脑袋。
4.2 调参顺序:先用 ROI 把单次迭代压到 5 秒内
调参最大的敌人是迭代太慢。30 张 1200 万像素的图,跑一次depth_from_focus可能要几十秒,盲调一轮一下午就没了。我的做法是先裁 ROI:
* 读入序列后,先框一个包含台阶或纹理的代表性区域 gen_rectangle1 (ROI, 600, 800, 800, 1200) for Index := 0 to 29 by 1 select_obj (ImageSet, ImageSingle, Index + 1) reduce_domain (ImageSingle, ROI, ImageCrop) replace_obj (ImageSet, ImageCrop, ImageSet, Index + 1) endfor * 之后执行与第 3 章完全相同的 depth_from_focus 调用逻辑说明:select_obj从对象集里取单张,reduce_domain把图像域限制到 ROI,replace_obj把裁剪后的图放回原位置。循环结束后 ImageSet 里每张图都只剩 ROI 区域,计算量能小一个数量级。注意这一步只是验证参数用,正式跑之前保留一份原始 ImageSet,别把裁剪结果覆盖回去。
调参顺序我固定为:先把 Method 设为 gvar、FilterSize 设为 3,跑一遍看深度图轮廓;再把 FilterSize 从 3 加到 7,对比台阶边缘的变化;然后试一档 fmeasure,看平坦区域噪点是否改善;最后调 SmoothSigma,从 0 开始每次加 0.1,观察台阶顶部平台是否变平。整个过程控制在十次迭代以内,每次只看两个指标:深度图的台阶边缘锐度、Score 图的平均置信度。
4.3 调参对照表:现象往左看,参数往右调
| 你看到的异常 | 优先调整 | 调整方向 |
|---|---|---|
| 深度图平坦区全是椒盐噪点 | FilterSize | 3 增到 7 或 9 |
| 台阶边缘过渡带太宽,高度偏小 | FilterSize / SmoothSigma | 都往小调 |
| 深度图出现条纹状假轮廓 | Method | gvar 换 fmeasure |
| 边缘发黑、有振铃 | 边界处理方式 | 镜像反射改裁剪,或加大 FilterSize |
| 整体深度噪声大,但纹理正常 | SmoothSigma | 0 调到 0.3~0.5 |
| 运行太慢 | Method / ROI | gvar 打底,先裁 ROI |
这张表我贴在工位旁边,新产品第一次做景深合成时先对照症状查表,而不是凭感觉乱调。多数项目最后的参数组合都落在 gvar 或 fmeasure 加 5 到 7 的 FilterSize、0.3 的 SmoothSigma 这个小范围里,差别只在细节,不用每次都从头试。
5. 常见问题与排查:五条踩坑记录
下面五条是我印象最深的踩坑记录,每一条都是先看到现象、再查原因、最后改方案的真实路径。不要把这个算子当黑匣子用,出了问题先从采集端找,再到参数端找。
5.1 合成图出现横向断层,深度图呈台阶状跳变
现象:AllInFocus 图里能看到一条条横向接缝,深度图上原本应该连续过渡的斜面变成一格一格的台阶。
原因:步距超过景深的一半,相邻两张图的清晰区没有重叠。每个像素只在最近的某一层附近有响应,插值算法在两个独立峰值之间找不到公共的极值位置,只能退回到离散层号。
解决:重新测量单张景深,用标定板或分辨率板沿 Z 方向移动观察清晰范围,把步距压到景深的三分之一以内。总行程不变的情况下,张数要增加到原来的 1.5 到 2 倍,采集时间变长,但深度图会立刻连贯起来。
5.2 全聚焦图边缘发黑,出现类似振铃的暗边
现象:物体轮廓外侧有一圈暗色光晕,像是被描了边,在金属件和镜面边缘尤其明显。
原因:高反光边缘在评分阶段产生伪峰值,像素被分配到了错误的图层;边界处理在图像边缘附近的计算也不稳定,默认的镜像反射会把边缘外的假灰度反射回来参与评分。
解决:先把边界处理方式从镜像反射改成裁剪,如果依旧发黑,把 FilterSize 加大到 7,让评分窗口平滑掉单个异常像素。注意不要为了消暗边把 SmoothSigma 调大,那会模糊深度边缘,属于治标不治本。
5.3 合成图亮度出现“阴阳脸”,深度图有大面积伪轮廓
现象:图像亮度在某个深度位置突然跳变,深度图上出现一大片区域的假轮廓,看起来像工件表面本身不平。
原因:采集时开了自动曝光或自动增益,光源亮度也有波动。聚焦测度把亮度梯度当成了清晰度信号,于是照明变化被翻译成了深度变化。
解决:把所有自动参数关掉,固定曝光时间、增益、白平衡;光源关闭自动调光功能。采集前先让光源预热 30 分钟再拍,LED 冷启动阶段的亮度漂移非常明显,这是很多人忽略的细节。
5.4 深度图整体都是噪点,Score 平均灰度很低
现象:合成图勉强能看,但深度图几乎是一团噪声,Score 图整体偏黑。
原因:工件表面是低纹理甚至镜面,灰度上没有可被评分的梯度,焦点测度没有信息可用。评分在噪声上随机起伏,深度图自然就是噪声。
解决:先换照明方式,试试暗场照明增强表面划痕和颗粒的对比度;工程上临时打一束散射光也能改善,但稳定方案是做主动纹理投射。这个限制不是算法能突破的,没有梯度就没有聚焦信号,换什么测度都一样。
5.5 一模一样的参数,换了批次工件就失效
现象:A 批次工件参数调试得很好,B 批次材料表面处理一换,深度图立刻变差,合成图出现新的糊区。
原因:工件反射率和纹理密度变了,焦点测度的响应曲线也跟着变,原来合适的 FilterSize 可能过小或过大,Method 也可能不再匹配。
解决:把参数调整视为按批次验收,而不是一次性调好。每次换料后跑一组快速验证 stack,保存深度图和 Score 图做对比;Score 均值掉到 0.5 以下就该重新调参。这一条要写进项目验收清单,能省掉很多售后沟通成本。
6. 进阶技巧:把深度图当测量工具,做 Z 向标定与重复性验证
前面几章解决了怎么把图拍清楚,这一章讲一个更值钱的用法:把 Depth 图换算成物理高度。深度图只是相对灰度,要变成毫米必须标定。最实用的标定物是一组已知高度差的量块或精密台阶,比如 0.5mm、1.0mm、1.5mm 的三级标准台阶。
标定流程分三步:先用第 3 章的流程跑出深度图,在每级台阶上框 ROI 统计灰度均值;再把均值和实测高度做线性拟合得到 k 和 b;最后用 Height = k * Depth + b 换算。代码很简单:
* 统计两级台阶区域的深度均值(灰度均值) intensity (ROI_Step0, Depth, MeanDepth0, Dev0) intensity (ROI_Step1, Depth, MeanDepth1, Dev1) * 已知台阶高度:台阶0 = 0.5mm,台阶1 = 1.0mm * 线性模型 Height = k * Depth + b k := (1.0 - 0.5) / (MeanDepth1 - MeanDepth0) b := 0.5 - k * MeanDepth0逻辑说明:intensity返回区域内的灰度均值和标准差,均值就是该台阶对应的深度响应。两级台阶的已知高度差除以深度均值差,就得到灰度到物理高度的比例系数 k。实际工程里建议用三到五个台阶做多点拟合,把单点误差分摊掉,回归出的 k 更稳。
标定做完就是重复性验证。我习惯对同一工件重复采集 5 次,测量同一台阶高度,统计标准差;标准差大于 0.02mm 就先查载物台回程误差和照明漂移,而不是怀疑算法。测量区域先按第 3 章方式用 Score 掩码过滤低置信像素,重复性通常能改善一个数量级。另一个收益很大的组合是采集一次同时拿全聚焦图和深度图:前者喂给 2D 检测做 OCR、尺寸、缺陷,后者喂给 Z 向定位和高度测量。之前在某个跨平台系统里,原来需要两次分别测量的工序合并成一次采集,节拍直接省了一半。
从那以后我每次拿到新工件,都强制自己先拍一组 20 张的测试 stack,把 Method、FilterSize、Score 阈值定下来再写正式流程。这个习惯帮我挡掉了不少“换料就翻车”的售后电话。这份 halcon 景深合成资源里带的例程和调参表,建议你照着第 3 章的流程先完整跑一遍,再动手改参数,希望帮到你。
本文还有配套的精品资源,点击获取