一句话先理解:
Depth From Focus(DFF)就是:让同一个物体在不同焦距/不同 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
当: z=z0 的时候,这一点正好处于最佳焦平面。
于是 清晰度在 z=z0 附近达到最大
所以: z0≈argzmaxF(z)
a、光学解释
一个理想图像可以表示为: I(x,y)
经过光学系统以后: Iimage=Iobject∗PSF
其中: PSF
是点扩散函数。
b、完全聚焦
当: Δz=0
PSF 比较集中:
清晰:
│█│ │█│ │█│ │█│ │█│
c、离焦
当: Δz=0
PSF 扩散:
PSF
↑
_____│_____
__/ \__
______/ \______
原来的高频结构被平均:
原始:
│█│ │█│ │█│ │█│
离焦:
████████████████
所以:
离焦⇒PSF变宽⇒高频衰减⇒图像变模糊
因此: 清晰度⟷高频信息
而 DFF 就是利用这个关系寻找最佳焦点。
d、Laplacian Focus Measure
Focus Measure(聚焦评价函数)中的 Laplacian-CSDN博客

其中 W 是一个局部窗口。
为什么它可以判断清晰度?
因为: Laplacian≈二阶高频响应

e、Tenengrad 做 DFF

R
DFF
│
▼
Focus Measure
│
┌─────────┼─────────┐
▼ ▼ ▼
Laplacian Sobel Variance
│ │ │
▼ ▼ ▼
高频 梯度 局部对比度
f、DFF 的完整数学模型
加入我们采集了N个图像

R
Focus
↑
│
│ ●
│ ● ●
│ ● ●
│ ● ●
│ ●
│ ●
└────────────────────────────→ Z
↑
z*
那么: z∗(x,y)=argimaxFi(x,y)
最终: D(x,y)=z∗(x,y)
得到深度图:
sql
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,⋯
这些离散位置。比如: z=0,10,20,30,40 μm
实际最佳焦点可能是: 17.3μm
但你的扫描只有: 10μm,20μm
所以: argmaxF
只能告诉你: z∗=20μm
精度只有扫描步长级别。
所以真正的 DFF 会做亚步长焦点估计这是非常关键的一步。
假设: Fi−1,Fi,Fi+1
分别对应: zi−1,zi,zi+1
而 Fi 最大,可以用二次函数拟合 Focus Curve:

三、数学逻辑
cpp
depth_from_focus (
Image,
Depth,
Confidence,
'highpass',
'next_maximum')
四、案例说明
halcon 案例说明
* ============================================================
* Extract depth using multiple focus levels
* 基于多个焦平面图像进行 Depth From Focus(DFF)深度提取
*
* 核心思想:
*
* 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)
把这段程序真正理解成一张"算法流程图"
R
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
某一个像素附近得到:
| Z | Focus Measure |
|---|---|
| 0 | 12 |
| 10 | 28 |
| 20 | 50 |
| 30 | 35 |
| 40 | 16 |
那么 argmax F =20
所以初始估计 z*=20μm
假设在 10,20,30μm 三点进行抛物线拟合:
