depth_from_focus 详解 一句话先理解Depth From FocusDFF就是让同一个物体在不同焦距/不同 Z 位置下成像比较每个像素在不同位置的“清晰度”哪个位置最清晰就认为物体表面就在那个深度。它本质上是焦点搜索清晰度评价深度映射​1. Depth From Focus 是什么Depth From Focus简称DFF中文一般叫基于聚焦的深度估计 / 离焦深度恢复它解决的问题是只有一台普通相机通过改变相机/物镜与物体之间的焦距或 Z 位置从一组不同焦平面的图像中恢复物体的 3D 深度。例如假设我们拍了 5 张图它和 Shape From Focus 是什么关系可以简单理解成Shape From Focus强调从焦点变化恢复物体表面形状。Depth From Focus强调从焦点变化恢复每个像素对应的深度。工程上两者高度相关。你可以把它们统一成二、核心“清晰度”得到深度为什么“清晰度”可以得到深度这才是 DFF 的核心。假设显微镜沿 Z 轴扫描 z1​,z2​,⋯,zN​物体表面某一点真实高度为 z0​当 zz0​ 的时候这一点正好处于最佳焦平面。于是清晰度在 zz0​ 附近达到最大​所以 z0​≈argzmax​F(z)a、光学解释一个理想图像可以表示为 I(x,y)经过光学系统以后 Iimage​Iobject​∗PSF其中 PSF是点扩散函数。b、完全聚焦当 Δz0PSF 比较集中清晰│█│ │█│ │█│ │█│ │█│c、离焦当 Δz0PSF 扩散PSF ↑ _____│_____ __/ \__ ______/ \______原来的高频结构被平均原始 │█│ │█│ │█│ │█│ 离焦 ████████████████所以离焦⇒PSF变宽⇒高频衰减⇒图像变模糊​因此 清晰度⟷高频信息​而 DFF 就是利用这个关系寻找最佳焦点。d、Laplacian Focus MeasureFocus Measure聚焦评价函数中的 Laplacian-CSDN博客其中 W 是一个局部窗口。为什么它可以判断清晰度因为 Laplacian≈二阶高频响应​e、Tenengrad 做 DFFDFF │ ▼ Focus Measure │ ┌─────────┼─────────┐ ▼ ▼ ▼ Laplacian Sobel Variance │ │ │ ▼ ▼ ▼ 高频 梯度 局部对比度f、DFF 的完整数学模型加入我们采集了N个图像Focus ↑ │ │ ● │ ● ● │ ● ● │ ● ● │ ● │ ● └────────────────────────────→ Z ↑ z*那么 z∗(x,y)argimax​Fi​(x,y)​最终 D(x,y)z∗(x,y)得到深度图D(x,y) ┌─────────────────────┐ │ 0.52 0.53 0.54 │ │ 0.51 0.52 0.55 │ │ 0.50 0.51 0.54 │ │ 0.49 0.50 0.52 │ └─────────────────────┘如果直接z∗argmaxF那么深度只能得到 z1​,z2​,z3​,⋯这些离散位置。比如 z0,10,20,30,40 μm实际最佳焦点可能是 17.3μm但你的扫描只有 10μm,20μm所以 argmaxF只能告诉你 z∗20μm精度只有扫描步长级别。所以真正的 DFF 会做亚步长焦点估计这是非常关键的一步。假设 Fi−1​,Fi​,Fi1​分别对应 zi−1​,zi​,zi1​而 Fi​ 最大可以用二次函数拟合 Focus Curve三、数学逻辑depth_from_focus ( Image, Depth, Confidence, highpass, next_maximum)四、案例说明halcon 案例说明* * Extract depth using multiple focus levels * 基于多个焦平面图像进行 Depth From FocusDFF深度提取 * * 核心思想 * * 10张不同Z位置/不同焦距下采集的图像 * ↓ * depth_from_focus * ↓ * 每个像素寻找最佳焦点 * ↓ * Depth * ↓ * 得到物体表面高度 * * 同时输出 * Depth 每个像素估计得到的深度/焦点位置 * Confidence 该深度估计结果的可信程度 * * ------------------------------------------------------------ * Names保存10张不同焦平面的图像文件名 * ------------------------------------------------------------ Names : [] * 关闭当前所有显示窗口 dev_close_window () * ------------------------------------------------------------ * 生成10张图像的文件名 * * i 1,2,...,10 * * dff/focus_ (i$.2) * * 其中 * * i$.2 * * 表示把整数格式化成两位数字 * * 1 - 01 * 2 - 02 * ... * 9 - 09 * 10 - 10 * * 最终 * * dff/focus_01 * dff/focus_02 * ... * dff/focus_10 * * 这些图像分别对应不同的焦平面。 * ------------------------------------------------------------ for i : 1 to 10 by 1 Names : [Names,dff/focus_ (i$.2)] endfor * ------------------------------------------------------------ * read_image * * 读取上面 Names 中指定的10张图像。 * * HALCON 会将多张图像作为一个多通道图像对象处理。 * * 可以理解成 * * Image * * Channel 1 → focus_01 * Channel 2 → focus_02 * ... * Channel 10 → focus_10 * * 这里的10个channel并不是RGB意义上的颜色通道 * 而是10个不同焦点位置采集的灰度图像。 * ------------------------------------------------------------ read_image (Image, Names) * ------------------------------------------------------------ * channels_to_image * * 将当前的图像对象重新组织成一个多通道图像。 * * 此时可以把 Image 理解为 * * Image * │ * ┌───────────┼───────────┐ * ↓ ↓ ↓ * Channel 1 Channel 2 ... Channel 10 * focus_01 focus_02 focus_10 * * 每一个channel对应一个不同的焦点位置。 * ------------------------------------------------------------ channels_to_image (Image, Image) * ------------------------------------------------------------ * 获取图像宽度和高度 * * Width 图像宽度 * Height 图像高度 * * 后面主要用于创建显示窗口。 * ------------------------------------------------------------ get_image_size (Image, Width, Height) * ------------------------------------------------------------ * 创建一个显示窗口 * * 参数 * * 0,0 * 窗口左上角位置 * * Width, Height * 窗口尺寸 * * black * 窗口背景颜色 * * WindowHandle * 输出窗口句柄 * ------------------------------------------------------------ dev_open_window (0, 0, Width, Height, black, WindowHandle) * * Access a channel of a multichannel image * 访问多通道图像中的某一个通道 * for i : 1 to 10 by 1 * -------------------------------------------------------- * access_channel * * 从多通道图像 Image 中提取第 i 个 channel。 * * Image1 * 当前第 i 个焦点位置对应的图像 * * 例如 * * i 1 * Image1 focus_01 * * i 2 * Image1 focus_02 * * ... * * i 10 * Image1 focus_10 * * -------------------------------------------------------- access_channel (Image, Image1, i) endfor * * Depth From Focus * * ------------------------------------------------------------ * depth_from_focus * * 这是整个程序最核心的算子。 * * 输入 * * Image * 包含10个不同焦点位置的图像。 * * 输出 * * Depth * 每一个像素对应的最佳聚焦深度。 * * Confidence * 深度估计的可信度。 * * highpass * 使用高通滤波/高频信息评价图像清晰度。 * * next_maximum * 使用相邻最大值附近的信息确定最佳焦点位置。 * * * ------------------------------------------------------------ * * 数学上可以理解为 * * 对于一个像素位置 (x,y)存在10个不同Z位置 * * Z1 Z2 Z3 ... Z10 * * 对应10张图像 * * I1 I2 I3 ... I10 * * 对每张图像计算Focus Measure * * F1 F2 F3 ... F10 * * 然后寻找 * * Z* argmax F(x,y,Z) * * 也就是说 * * 哪一个Z位置最清晰 * * 就认为 * * 这个像素的物体表面位于这个Z位置。 * * ------------------------------------------------------------ * * 例如某个像素 * * Z Focus * ---------------- * 1 12 * 2 20 * 3 35 * 4 52 ← 最大 * 5 41 * 6 25 * * 那么 * * Depth(x,y) ≈ Z4 * * ------------------------------------------------------------ depth_from_focus (Image, Depth, Confidence, \ highpass, next_maximum) * * 对Depth进行局部平均 * * ------------------------------------------------------------ * mean_image * * 对 Depth 图像进行平均滤波。 * * 11 × 11 * * 表示使用11×11的邻域进行均值计算。 * * 对于每个像素 * * DepthHighConf(x,y) * * 大约等于周围11×11像素深度的平均值。 * * * 为什么要这样做 * * Depth From Focus 得到的深度图可能存在 * * 1. 噪声 * 2. 局部错误峰值 * 3. 单个像素的深度跳变 * 4. Focus Measure不稳定 * * 均值滤波可以让深度图更加平滑。 * * 注意 * * 这里实际上没有使用 Confidence 去筛选 * 变量名 DepthHighConf 容易让人误以为已经进行了 * confidence filtering。 * * 它实际上只是 * * Depth * ↓ * 11×11 Mean * ↓ * DepthHighConf * * ------------------------------------------------------------ mean_image (Depth, DepthHighConf, 11, 11) * * 将深度图归一化到最大显示范围 * * ------------------------------------------------------------ * scale_image_max * * 将图像的灰度范围缩放使最大值映射到255。 * * 主要目的是方便后续 * * threshold * * 使用固定灰度范围进行分割。 * * * 假设Depth原始范围 * * 20 ~ 80 * * 经过scale_image_max以后可能变成 * * 63 ~ 255 * * 这里需要特别注意 * * Depth 原始值的物理意义不能简单认为就是 * 0~255灰度值。 * * 经过 scale_image_max 后 * * ImageScaleMax * * 是一个用于后续图像处理/阈值分割的 * 归一化深度图。 * * ------------------------------------------------------------ scale_image_max (DepthHighConf, ImageScaleMax) * * 再次进行大尺度平均 * * ------------------------------------------------------------ * 51×51均值滤波。 * * 前面 * * 11×11 * * 主要用于降低局部噪声。 * * 这里 * * 51×51 * * 是更加大范围的平滑。 * * 目的主要是 * * 1. 去除局部深度变化 * 2. 得到比较平滑的深度区域 * 3. 方便后续按照深度范围进行区域分割 * * 代价是 * * 空间分辨率下降 * * 小尺寸的深度结构可能被抹掉。 * * ------------------------------------------------------------ mean_image (ImageScaleMax, DepthMean, 51, 51) * * 根据深度范围提取一个区域 * * ------------------------------------------------------------ * threshold * * 将 DepthMean 中灰度值 * * 158 Gray 255 * * 的像素提取出来。 * * 输出 * * Resistor * * 这里可以理解为 * * Resistor 深度较高区域 * * 因为之前已经通过 scale_image_max * 将深度图进行了归一化。 * * ------------------------------------------------------------ threshold (DepthMean, Resistor, 158, 255) * ------------------------------------------------------------ * connection * * 将二值区域 Resistor 中相互连接的像素 * 分成不同的连通区域。 * * 例如 * * ███ ████ * ███ ████ * * 会被分成 * * Region1 * Region2 * * ConnectedRes 保存所有连通区域。 * ------------------------------------------------------------ connection (Resistor, ConnectedRes) * ------------------------------------------------------------ * select_shape_std * * 从所有连通区域中选择面积最大的区域。 * * max_area * * 表示按照面积选择最大区域。 * * 这里的 * * FinalRes * * 就是最终认为属于 Resistor 的最大区域。 * * ------------------------------------------------------------ select_shape_std (ConnectedRes, FinalRes, max_area, 0) * * 提取另外一个深度范围 * * ------------------------------------------------------------ * 提取 * * 125 Gray 158 * * 的区域。 * * 也就是说 * * 这里选择的是另外一个深度范围。 * * 可以理解为 * * Resistor * ↓ * 深度较高区域 * * Solder * ↓ * 中等深度区域 * * 具体哪个区域对应哪个物理结构 * 要根据这个示例中的实际物体来确定。 * ------------------------------------------------------------ threshold (DepthMean, Solder, 125, 158) * ------------------------------------------------------------ * 将Solder二值区域进行连通域分割。 * ------------------------------------------------------------ connection (Solder, ConnectedSol) * ------------------------------------------------------------ * 从所有Solder连通区域中选择面积最大的区域。 * * SelectedSol * * 最大的Solder区域 * ------------------------------------------------------------ select_shape_std (ConnectedSol, SelectedSol, max_area, 0) * * 填充区域内部孔洞 * * ------------------------------------------------------------ * fill_up * * 将 SelectedSol 区域内部的孔洞填充。 * * 例如 * * ███████ * █ ○ █ * █ █ * ███████ * * 填充之后 * * ███████ * ███████ * ███████ * ███████ * * 输出 * * FinalSol * * ------------------------------------------------------------ fill_up (SelectedSol, FinalSol) * * 设置显示参数 * * 设置区域边界线宽度为3 dev_set_line_width (3) * ------------------------------------------------------------ * 设置绘制方式 * * margin * * 表示只显示区域的边界而不是填充整个区域。 * ------------------------------------------------------------ dev_set_draw (margin) * * 显示原始多焦点图像 * dev_display (Image) * 设置后续显示区域的颜色为红色 dev_set_color (red) * ------------------------------------------------------------ * 显示FinalSol * * FinalSol * * 经过 * * Depth * ↓ * 平滑 * ↓ * 深度阈值 * ↓ * 连通域 * ↓ * 最大区域 * ↓ * 填洞 * * 得到的最终Solder区域。 * ------------------------------------------------------------ dev_display (FinalSol) * 设置后续显示区域颜色为蓝色 dev_set_color (blue) * ------------------------------------------------------------ * 显示FinalRes * * FinalRes * * 经过 * * Depth * ↓ * 平滑 * ↓ * 深度阈值 158~255 * ↓ * 连通域 * ↓ * 最大面积区域 * * 得到的最终Resistor区域。 * ------------------------------------------------------------ dev_display (FinalRes) 把这段程序真正理解成一张“算法流程图”10张不同焦平面图像 │ │ ▼ ┌───────────────────┐ │ depth_from_focus │ │ │ │ Focus Measure │ │ ↓ │ │ 找最佳焦点 │ └─────────┬─────────┘ │ ├──────────────► Confidence │ ▼ Depth │ ▼ 11×11 Mean │ ▼ scale_image_max │ ▼ 51×51 Mean │ ▼ DepthMean │ ├──────────────┐ │ │ ▼ ▼ 158~255 125~158 │ │ ▼ ▼ Resistor Solder │ │ ▼ ▼ 连通域 连通域 │ │ ▼ ▼ 最大区域 最大区域 │ │ │ ▼ │ fill_up │ │ └──────┬───────┘ ▼ 最终显示 蓝色 Resistor 红色 Solder举例说明假设 Z 扫描 0, 10, 20, 30, 40 μm某一个像素附近得到ZFocus Measure0121028205030354016那么 argmax F 20所以初始估计 z*20μm假设在 10,20,30μm 三点进行抛物线拟合